diff --git a/AGENTS.md b/AGENTS.md
index 4ec9e5c..9e29365 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -116,6 +116,8 @@ The project includes a **`docs/`** directory containing specialized documentatio
- **`docs/NOSTR_DIRECT_MESSAGES.md`**: Read when implementing direct messaging features, building chat interfaces, or working with encrypted peer-to-peer communication (NIP-04 and NIP-17).
+- **`docs/DESIGN_STYLEGUIDE.md`**: Read before making any visual change — color, type, spacing, or component styling. Covers the design tokens in `src/index.css`, the "layer stack" signature motif, and the rationale behind them. The live version is the `/styleguide` route (`src/pages/Styleguide.tsx`).
+
## System Prompt Management
The AI assistant's behavior and knowledge is defined by the AGENTS.md file, which serves as the system prompt. To modify the assistant's instructions or add new project-specific guidelines:
diff --git a/docs/DESIGN_STYLEGUIDE.md b/docs/DESIGN_STYLEGUIDE.md
new file mode 100644
index 0000000..46f964c
--- /dev/null
+++ b/docs/DESIGN_STYLEGUIDE.md
@@ -0,0 +1,127 @@
+# LAYER.systems Design Styleguide
+
+Read this before making visual changes anywhere in the app. The live version of
+everything below is at [`/styleguide`](../src/pages/Styleguide.tsx).
+
+## The idea
+
+LAYER.systems is a public Nostr relay: one layer in the path between a
+person's client and the rest of the network. The brand doesn't use a logo to
+*represent* that — the signature visual **is** the stack (`Client → Relay →
+Network`, see `src/components/brand/LayerStack.tsx`), lit up at the relay
+layer, because that's literally what this product is.
+
+Everything else in the system follows from taking that stack seriously:
+layered, hairline-bordered surfaces instead of soft drop shadows; a
+monospace face used for headlines and data, not tucked away as a caption
+font; and a dark "Ink" hero that reads like an oscilloscope panel, not a
+marketing gradient.
+
+## Color
+
+Named tokens live in `src/index.css` as HSL CSS variables, consumed through
+Tailwind (`tailwind.config.ts`) and shadcn/ui components. Don't hardcode hex
+values in components — reference the token (`bg-primary`, `text-brand-cyan`,
+`hsl(var(--brand-navy))`) so light/dark mode and future palette tweaks stay
+centralized.
+
+| Name | Hex | Token | Usage |
+|---|---|---|---|
+| Paper | `#F5F6F5` | `--background` (light) | Default light-mode surface — cool, not cream |
+| Ink | `#0E1116` | `--brand-ink`, `--background` (dark) | Hero panel + dark-mode base surface |
+| Relay Amber | `#F5A623` | `--primary` | The signature signal color: primary actions, the "live" layer, focus rings |
+| Deep Signal Navy | `#1C2B45` | `--brand-navy` | Structural color for layer bands and dark-mode accents |
+| Circuit Cyan | `#3FC1CE` | `--brand-cyan`, `--chart-2` | Secondary data color — charts, hover accents |
+| Slate Ink | `#2B3140` | `--foreground` (light) | Body text on light surfaces |
+
+Both light and dark modes share the same amber accent so the brand reads
+consistently either way — only the surface (paper vs. ink) flips.
+
+**Why not the obvious defaults:** we deliberately avoided a warm-cream +
+serif look (doesn't fit infrastructure) and a pure near-black + single
+neon accent (too generic for what should feel like real hardware — a relay
+lamp, not a startup gradient). The dark hero pairs a warm accent (amber)
+with a cool structural color (navy) and a cool data color (cyan), which is
+richer than the single-accent cliché, and only the *hero* is dark — the app
+itself is light-first.
+
+## Type
+
+Two faces, doing different jobs:
+
+- **Display / data — JetBrains Mono** (`font-display` / `font-mono`,
+ `@fontsource-variable/jetbrains-mono`). Used for headlines, the eyebrow
+ labels (`.eyebrow` utility class), stat numbers, relay URLs, hex ids, and
+ timestamps. This is a wire-protocol product — a monospace headline is a
+ deliberate choice that matches the subject, not a caption font promoted
+ by accident.
+- **Body — Inter** (`font-sans`, `@fontsource-variable/inter`). Everything a
+ person reads at length: paragraphs, descriptions, terms & privacy copy.
+
+Both are self-hosted via `@fontsource-variable/*` and imported once in
+`src/main.tsx` — the CSP (`font-src 'self'`) blocks third-party font CDNs,
+so don't add a Google Fonts ``.
+
+Eyebrow labels use the `.eyebrow` utility (`src/index.css`): mono, uppercase,
+`0.14em` tracking, small size. Use them to label a section's role
+("Public Nostr relay", "Getting connected"), not as decoration.
+
+## Spacing & radius
+
+Standard Tailwind 4px scale (`--spacing: 0.25rem`). `--radius` is `0.5rem`
+site-wide — soft enough to feel modern, restrained enough to stay
+technical. Don't mix radius values ad hoc; use the `rounded-lg` /
+`rounded-md` / `rounded-sm` scale derived from the token.
+
+## Signature components
+
+- **`LayerStack`** (`src/components/brand/LayerStack.tsx`) — the hero mark.
+ Three cascading bands (Client / Relay / Network), the relay band lit
+ amber and gently pulsing. Respects `prefers-reduced-motion` via Tailwind's
+ `motion-safe:` variant — the settle-in animation and pulse simply don't
+ run when reduced motion is requested; the stack still renders in its
+ final state.
+- **`LayerDivider`** — a quiet three-line echo of the stack for breaking up
+ sections without repeating the hero verbatim.
+- **`.layer-card`** utility class — the default card treatment across the
+ marketing site: two hairline borders offset by 6px instead of a drop
+ shadow, widening slightly on hover. Use plain `border + shadow-sm` cards
+ instead inside dense data views (dashboard, explorer) where the offset
+ border would add noise against a grid of cards.
+
+## Components
+
+Built on shadcn/ui (`src/components/ui/*`) — Button, Card, Badge, Input,
+Alert, Sidebar, etc. — restyled entirely through the token layer, not by
+forking the components. See `/styleguide` for live specimens of every
+variant. When adding new UI, prefer an existing shadcn primitive over a
+bespoke one.
+
+## Motion
+
+- Page-load: the `LayerStack` bands settle in with a short staggered
+ animation (`animate-layer-settle`); the relay band pulses continuously
+ but subtly (`animate-signal-pulse`).
+- Hover: `.layer-card` widens its offset border; buttons and links use the
+ existing shadcn transition tokens. No scroll-triggered effects — this is
+ a technical, read-once page, not a scrollytelling site.
+- Everything animated is wrapped in Tailwind's `motion-safe:` variant so
+ `prefers-reduced-motion: reduce` gets the static end state.
+
+## Accessibility
+
+- Color pairs (background/foreground, primary/primary-foreground, etc.)
+ are chosen for contrast in both themes — check new token pairs with a
+ contrast checker before shipping.
+- Interactive elements rely on shadcn's built-in focus rings (`--ring`,
+ amber) — don't remove `focus-visible` outlines.
+- The `LayerStack` diagram carries a text `aria-label` describing the same
+ information conveyed visually (client → this relay → the network).
+
+## Voice
+
+Plain, direct, written from the person's side of the screen: "add the
+relay," not "leverage relay connectivity." Say what things do, not why
+they're impressive. Errors and empty states say what happened and what to
+do next — see `NotFound.tsx` for the tone ("There's no layer at
+`/whatever`") instead of a generic apology.
diff --git a/package-lock.json b/package-lock.json
index 2b607ec..bcffb7c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@fontsource-variable/inter": "^5.2.6",
+ "@fontsource-variable/jetbrains-mono": "^5.3.0",
"@getalby/sdk": "^5.1.1",
"@hookform/resolvers": "^3.9.0",
"@nostrify/nostrify": "^0.48.2",
@@ -955,6 +956,15 @@
"url": "https://github.com/sponsors/ayuhito"
}
},
+ "node_modules/@fontsource-variable/jetbrains-mono": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz",
+ "integrity": "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
"node_modules/@getalby/lightning-tools": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@getalby/lightning-tools/-/lightning-tools-5.2.0.tgz",
@@ -3260,7 +3270,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3274,7 +3283,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3288,7 +3296,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3302,7 +3309,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3316,7 +3322,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3330,7 +3335,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3344,7 +3348,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3358,7 +3361,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3372,7 +3374,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3386,7 +3387,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3400,7 +3400,6 @@
"cpu": [
"loong64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3414,7 +3413,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3428,7 +3426,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3442,7 +3439,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3456,7 +3452,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3470,7 +3465,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3484,7 +3478,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3498,7 +3491,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3512,7 +3504,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3526,7 +3517,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
diff --git a/package.json b/package.json
index 5dc7498..3264e12 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"@fontsource-variable/inter": "^5.2.6",
+ "@fontsource-variable/jetbrains-mono": "^5.3.0",
"@getalby/sdk": "^5.1.1",
"@hookform/resolvers": "^3.9.0",
"@nostrify/nostrify": "^0.48.2",
diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx
index e3bb12e..1d423c6 100644
--- a/src/AppRouter.tsx
+++ b/src/AppRouter.tsx
@@ -6,6 +6,8 @@ import { Explore } from "./pages/Explore";
import { Dashboard } from "./pages/Dashboard";
import { DashboardEvents } from "./pages/DashboardEvents";
import { DashboardExport } from "./pages/DashboardExport";
+import Messages from "./pages/Messages";
+import { Styleguide } from "./pages/Styleguide";
import { NIP19Page } from "./pages/NIP19Page";
import { Terms } from "./pages/Terms";
import { Privacy } from "./pages/Privacy";
@@ -21,6 +23,8 @@ export function AppRouter() {
} />
} />
} />
+ } />
+ } />
} />
} />
{/* NIP-19 route for npub1, note1, naddr1, nevent1, nprofile1 */}
diff --git a/src/components/brand/LayerDivider.tsx b/src/components/brand/LayerDivider.tsx
new file mode 100644
index 0000000..6d4ead2
--- /dev/null
+++ b/src/components/brand/LayerDivider.tsx
@@ -0,0 +1,16 @@
+import { cn } from '@/lib/utils';
+
+/**
+ * A quiet echo of the layer-stack device used to break up sections —
+ * three hairlines of shrinking width, same cascade logic as the hero mark,
+ * without repeating it verbatim.
+ */
+export function LayerDivider({ className }: { className?: string }) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/brand/LayerStack.tsx b/src/components/brand/LayerStack.tsx
new file mode 100644
index 0000000..6fa3da7
--- /dev/null
+++ b/src/components/brand/LayerStack.tsx
@@ -0,0 +1,93 @@
+import { Laptop, Radio, Globe2 } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+interface Layer {
+ eyebrow: string;
+ name: string;
+ detail: string;
+ icon: typeof Laptop;
+ emphasis?: boolean;
+}
+
+const layers: Layer[] = [
+ {
+ eyebrow: '01 · You',
+ name: 'Client',
+ detail: 'Damus, Amethyst, Snort, or any Nostr app',
+ icon: Laptop,
+ },
+ {
+ eyebrow: '02 · This relay',
+ name: 'LAYER.systems',
+ detail: 'Receives, stores, and serves signed events',
+ icon: Radio,
+ emphasis: true,
+ },
+ {
+ eyebrow: '03 · Everyone else',
+ name: 'Nostr network',
+ detail: 'Every other relay and client your notes reach',
+ icon: Globe2,
+ },
+];
+
+/**
+ * The site's signature device: LAYER.systems is literally one layer in a
+ * relay stack, so the brand mark is that stack, not a logo standing in
+ * for it. Each band cascades down and to the right — data moving outward
+ * from the reader's client, through this relay, into the wider network.
+ */
+export function LayerStack({ className }: { className?: string }) {
+ return (
+