Protocol Obsidian: Building a Design System for Galain.dev
A walkthrough of the design system used on Galain.dev, including tokens, typography, layout primitives, code blocks, and light mode.
Protocol Obsidian is the design system behind this site. It is intentionally lightweight: CSS custom properties for tokens, a small set of layout primitives, and a few component conventions.
This post covers the core decisions: the token palette, typography, the HUD frame, and the approach to code blocks and light mode.
Design goals
The design system is guided by a few constraints:
- Technical UI influences: terminal and HUD-inspired interfaces, with a focus on readability
- Dark-first theme: designed for dark surfaces, with a complete light theme
- Small token surface area: a limited palette and a single accent color to keep the UI consistent
Color tokens
The palette is intentionally small. These are the core tokens used throughout the site:
@theme { --color-void: #09090b; /* Canvas (near black) */ --color-void-elevated: #0f0f11; /* Raised surfaces */ --color-structure: #27272a; /* Borders and dividers */ --color-structure-subtle: #18181b; /* Faint grid lines */ --color-text-primary: #d4d4d8; /* Primary text */ --color-text-secondary: #a1a1aa; /* Secondary text */ --color-text-muted: #71717a; /* Metadata text */ --color-ignition: #ea580c; /* Accent color */ --color-ignition-glow: rgba(234, 88, 12, 0.15); /* Hover background */ --color-ignition-hover: #f97316; /* Hover state */}The base background
--color-void is near-black rather than pure black. That gives headings and borders a little more separation without pushing contrast too hard.
--color-void-elevated is used for panels and cards so that elevation is expressed through tone instead of heavy shadows.
Accent color
A single accent color (--color-ignition) is used for links, selection, and interactive highlights. Keeping the accent surface area small makes the UI feel consistent and reduces the need for one-off colors.
Structure and borders
Neutral grays define boundaries, dividers, and the background grid. This keeps the interface readable while staying visually understated.
Typography
Four fonts are used, each with a clear role:
| Font | Token | Role |
|---|---|---|
| Bebas Neue | --font-display | Hero titles, the nav logo |
| Space Grotesk | --font-brutalist | Section labels and small headings |
| JetBrains Mono | --font-mono | Code, navigation, labels, timestamps |
| Inter | --font-sans | Body text and descriptions |
--font-display: 'Bebas Neue', ui-sans-serif, sans-serif;--font-brutalist: 'Space Grotesk', ui-sans-serif, sans-serif;--font-mono: 'JetBrains Mono', ui-monospace, monospace;--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;Bebas Neue is used for large display headings to keep the hero section compact even at very large sizes. JetBrains Mono is used for system UI elements and for code, which ties the interface back to the terminal metaphor.
The HUD frame
The main structural element is a fixed frame that stays in place while content scrolls:
.hud-frame { position: fixed; inset: var(--spacing-hud); /* 16px from all edges */ border: 1px solid var(--color-structure); pointer-events: none; z-index: 100;}The important detail is pointer-events: none. The frame is decorative and should never block interaction with content underneath it.
Why a fixed frame?
A fixed frame provides consistent context across pages and helps the site feel like one interface rather than a set of disconnected templates.
The blueprint grid
Behind the content is a subtle grid pattern:
.blueprint-grid { background-image: linear-gradient(var(--color-structure-subtle) 1px, transparent 1px), linear-gradient(90deg, var(--color-structure-subtle) 1px, transparent 1px); background-size: var(--spacing-grid) var(--spacing-grid); /* 40px */ background-position: -1px -1px;}The grid is deliberately low-contrast so it adds texture without competing with the content.
Navigation
The navigation bar is fixed, uses the token palette, and keeps spacing aligned with the HUD frame:
.site-nav { position: fixed; top: var(--spacing-hud); left: var(--spacing-hud); right: var(--spacing-hud); z-index: 102; height: 44px; background: rgba(9, 9, 11, 0.97); backdrop-filter: blur(16px); border-bottom: 1px solid var(--color-structure);}A small React island (HudMeta.tsx) provides a live clock. Keeping it isolated avoids pushing JavaScript into the rest of the page.
Code blocks
For code blocks, I use Expressive Code (the Astro integration) for syntax highlighting and copy-to-clipboard. The integration is configured with both dark and light themes:
expressiveCode({ emitExternalStylesheet: false, themes: ['vitesse-dark', 'vitesse-light'], themeCssSelector: (theme) => { if (theme.type === 'light') return '[data-theme="light"]'; return ':root'; }, styleOverrides: { borderRadius: '0px', codeFontFamily: "'JetBrains Mono', ui-monospace, monospace", codeFontSize: '0.8125rem', codePaddingBlock: '1.25rem', codePaddingInline: '1.5rem', frames: { frameBoxShadowCssValue: 'none', editorActiveTabBorderColor: 'transparent', editorActiveTabIndicatorBottomColor: 'transparent', editorActiveTabIndicatorTopColor: 'transparent', }, },});Header styling
The default Expressive Code header styles are reset and rebuilt in CSS. The core idea is to reset the .title element and reapply only the styles needed for the layout:
.prose :global(.expressive-code .frame.has-title .title) { all: unset; display: flex !important; align-items: center; gap: 0.6rem; font-family: var(--font-mono); font-size: 0.75rem; font-weight: 500; color: var(--color-text-primary) !important; padding: 0.55rem 1rem 0.55rem 1.25rem !important; letter-spacing: 0.03em;}Interactive patterns
Interactive states use the accent color consistently. For example, borders and text switch to --color-ignition on hover:
.stack-tag:hover { border-color: var(--color-ignition); color: var(--color-ignition);}
.nav-link:hover { border-color: var(--color-ignition); color: var(--color-ignition); background: var(--color-ignition-glow);}Selection also uses the accent color so it matches the rest of the interaction language:
::selection { background: var(--color-ignition); color: var(--color-void);}Lessons
A few takeaways from building the system:
- Constraints help consistency. A small palette and a single accent color reduce accidental variation.
- Dark-first needs an elevation system. Layering should be expressed through tones and borders, not just inversion.
- Small details add up. Grid texture, consistent typography, and predictable spacing make the UI feel cohesive.
- CSS custom properties are effective tokens. For a static site, they are simple, flexible, and widely supported.
- Documentation is part of the system. Writing down the conventions makes future changes easier.