Opening paragraph in body text, with an inline link and some strong text.
A section heading
- A first point.
- A nested detail, a little quieter.
- A second point with
inline code.
A short quote that deserves to stand out.
npm run build
Design system
Calm paper-and-ink pages with a raspberry accent. Used by rr2.dev, notes.rr2.dev and resume.rr2.dev.
Paste this into the AGENTS.md (or CLAUDE.md) of any site that should follow this system.
## UI This site uses the Personal brand design system v1. - Before any UI work, read https://design.rr2.dev/personal/v1/DESIGN.md and follow it. - Load https://design.rr2.dev/personal/v1/brand.css and put class="pb" on <html>. - Copy component markup from DESIGN.md exactly. Use only --pb-* tokens and pb- classes. - After changing UI, run: curl -sO https://design.rr2.dev/check.py && python3 check.py site . --system personal Fix every error before handing over.
Swatches follow the theme toggle. Values are light · dark. Contrast is measured on paper.
Text on paper
--pb-ink#1f2329 · #dadadb14.3:1 · 11.9:1 on paper--pb-text#2c3038 · #c4c4c512.0:1 · 9.6:1 on paper--pb-muted#5d636d · #a8a9aa5.5:1 · 7.1:1 on paperBrand
--pb-raspberry#a8325e · #ec8db1Primary brand colour--pb-raspberry-strong#8f2950 · #f4a9c5Primary hover/pressed--pb-raspberry-soft#f8e5ed · #3a1f2bPrimary tint for tags and highlights--pb-slate#3a5a80 · #93b4daSecondary brand colour: links, focus rings, current page--pb-slate-strong#2f6690 · #82b6ddSecondary accent for sections and dates--pb-slate-soft#e2edf6 · #1c2b38Secondary tintSurfaces and lines
--pb-paper#f3f4f6 · #1d1e20Page background--pb-surface#ffffff · #2e2e33Cards, panels, bubbles--pb-surface-hover#f6f7f9 · #34353aRow and card hover--pb-sunken#e9ecf0 · #37383eInline code, icon-button hover--pb-line#e2e5ea · #3a3b3fHairline borders and dividers--pb-line-strong#cdd1d8 · #4a4b4fDashed outlines, slash in the header, inactive dots. Decoration only, never textStatus
--pb-amber#995700 · #f2a94aNew, warnings--pb-amber-soft#fff1dc · #3a2c19--pb-green#0e7553 · #7fcca2Success, ok states--pb-green-soft#ddf3ea · #1f3328--pb-red#c2302f · #ef8f8eErrors, bad states--pb-red-soft#fbe6e6 · #3a2020Use the class, not the numbers.
.pb-displayheadline-display · 52px/600.pb-h1headline-lg · 40px/600.pb-h2headline-md · 28px/600.pb-h3headline-sm · 22px/500.pb-h4title · 18px/600.pb-bodybody-lg · 18px/400.pb-uibody-md · 16px/400.pb-smallbody-sm · 14px/400.pb-labellabel-md · 13px/600.pb-taglabel-sm · 12px/600.pb-codecode · 14px/400code sample.pb-handhand · 19px/6004px spacing steps, 8px corners, hairline borders, faint shadows.
14px28px312px416px520px624px832px936px1248px1872pxradius-sm4pxradius-md8pxradius-lg12pxradius-pill999pxshadow-restshadow-liftEach one: a live preview, the exact HTML to copy, and the rules that go with it.
Text
Type classes for every text style. Use these instead of setting sizes.
Section label
A lede introduces the page in a sentence or two.
Interface text for lists, menus and forms.
Small text for summaries and captions.
Inline code and an emphasis link.
<div class="pb-stack is-tight">
<p class="pb-label">Section label</p>
<h1 class="pb-display">Display heading</h1>
<h1 class="pb-h1">Page title</h1>
<h2 class="pb-h2">Section heading</h2>
<h3 class="pb-h3">Sub-section heading</h3>
<h4 class="pb-h4">Small heading</h4>
<p class="pb-lede">A lede introduces the page in a sentence or two.</p>
<p class="pb-ui">Interface text for lists, menus and forms.</p>
<p class="pb-small">Small text for summaries and captions.</p>
<p class="pb-ui">Inline <code class="pb-code">code</code> and an <a class="pb-link" href="#">emphasis link</a>.</p>
</div>
Layout
Every page starts from this. Skip link, sticky header, main column, footer.
<!doctype html>
<html lang="en" class="pb">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page title — rr2.dev</title>
<link rel="stylesheet" href="https://design.rr2.dev/personal/v1/brand.css">
<script>try{var t=localStorage.getItem('pb-theme');if(t)document.documentElement.dataset.theme=t}catch(e){}</script>
</head>
<body>
<a class="pb-skip" href="#main">Skip to content</a>
<!-- Header component here -->
<main class="pb-wrap" id="main">
<h1 class="pb-h1">Page title</h1>
<p class="pb-lede">One or two sentences that say what this page is for.</p>
</main>
<!-- Footer component here -->
</body>
</html>
Layout
Sticky frosted bar. Avatar and "rr2.dev / section" on the left, menu and theme toggle on the right.
<div class="pb-top">
<header class="pb-nav">
<div class="pb-logo">
<a href="https://rr2.dev" aria-label="rr2.dev"><img class="pb-ico" src="https://design.rr2.dev/personal/v1/assets/avatar.webp" width="24" height="24" alt=""><span class="pb-root">rr2.dev</span></a>
<span class="pb-slash">/</span><a href="/">section</a>
</div>
<div class="pb-cluster is-tight">
<ul class="pb-menu">
<li><a href="/posts/" aria-current="page"><span>Posts</span></a></li>
<li><a href="/about/"><span>About</span></a></li>
</ul>
<button class="pb-toggle" type="button" aria-label="Switch light or dark theme" onclick="var r=document.documentElement,d=r.dataset.theme?r.dataset.theme==='dark':matchMedia('(prefers-color-scheme: dark)').matches;r.dataset.theme=d?'light':'dark';try{localStorage.setItem('pb-theme',r.dataset.theme)}catch(e){}">
<svg class="pb-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
<svg class="pb-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
</button>
</div>
</header>
</div>
Layout
Layout primitives for spacing. A stack spaces children vertically; a cluster lays them out in a wrapping row.
<div class="pb-stack">
<div class="pb-cluster is-between">
<h2 class="pb-h3">Recent posts</h2>
<a class="pb-btn is-quiet" href="#">See all</a>
</div>
<div class="pb-cluster is-tight">
<span class="pb-tag">design</span><span class="pb-tag" data-accent="slate">notes</span><span class="pb-tag" data-accent="neutral">draft</span>
</div>
</div>
Actions
Pill buttons. Primary (raspberry), secondary (outlined), quiet (text), and small.
<div class="pb-cluster">
<button class="pb-btn" type="button">Save changes</button>
<a class="pb-btn is-secondary" href="#">View details</a>
<button class="pb-btn is-secondary is-small" type="button">Small</button>
<button class="pb-btn is-quiet" type="button">Cancel</button>
<button class="pb-btn" type="button" disabled>Disabled</button>
</div>
Actions
Round 32px button holding a single 16px icon.
<div class="pb-cluster is-tight">
<button class="pb-icon-btn" type="button" aria-label="Previous"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
<button class="pb-icon-btn" type="button" aria-label="Next"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg></button>
<button class="pb-icon-btn" type="button" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
</div>
Actions
Pill links for filtering a list by section. The current one fills with its accent.
<ul class="pb-chips">
<li><a href="#" aria-current="page">All <span class="pb-n">24</span></a></li>
<li><a href="#" data-accent="slate">Notes <span class="pb-n">17</span></a></li>
<li><a href="#" data-accent="green">Guides <span class="pb-n">5</span></a></li>
<li><a href="#" data-accent="amber">Reports <span class="pb-n">2</span></a></li>
</ul>
Content
Tags are mono pills tinted by their accent. Badges are tiny uppercase status pills.
<div class="pb-cluster is-tight">
<span class="pb-tag">design</span>
<span class="pb-tag" data-accent="slate">notes</span>
<span class="pb-tag" data-accent="green">guide</span>
<span class="pb-tag" data-accent="neutral">draft</span>
<span class="pb-badge">New</span>
<span class="pb-badge" data-accent="green">Live</span>
</div>
Content
Date, reading time and tag under a title, separated by middle dots.
<div class="pb-meta">
<span class="pb-badge">New</span>
<time datetime="2026-11-02">Nov 2, 2026</time>
<span>3 min read</span>
<span class="pb-tag" data-accent="slate">notes</span>
</div>
Content
White card with a serif title, a two-line summary and a meta row. Lifts on hover when clickable.
<ul class="pb-grid">
<li class="pb-card" data-accent="slate">
<a class="pb-cover" href="#" aria-label="A short, specific title"></a>
<h3>A short, specific title</h3>
<p>One or two lines that tell the reader what they will get from this.</p>
<div class="pb-meta"><time datetime="2026-11-02">Nov 2</time><span>3 min read</span><span class="pb-tag" data-accent="slate">notes</span></div>
</li>
<li class="pb-card">
<a class="pb-cover" href="#" aria-label="Another title that runs a little longer than the first"></a>
<h3>Another title that runs a little longer than the first</h3>
<p>Summaries are clamped visually by length, so keep them short.</p>
<div class="pb-meta"><time datetime="2026-10-28">Oct 28</time><span>8 min read</span><span class="pb-tag">design</span></div>
</li>
</ul>
Content
An archive grouped by month. Each row has a calendar tile, a title and a sub-line.
<div class="pb-list">
<h3>November 2026 <span>2 posts</span></h3>
<ol>
<li data-accent="slate"><a href="#"><span class="pb-cal"><b>Nov</b><span>2</span></span><span class="pb-body"><span class="pb-t">A short, specific title</span><span class="pb-sub"><span class="pb-tag" data-accent="slate">notes</span> 3 min read</span></span></a></li>
<li><a href="#"><span class="pb-cal"><b>Nov</b><span>1</span></span><span class="pb-body"><span class="pb-t">Another post title</span><span class="pb-sub"><span class="pb-tag">design</span> 8 min read</span></span></a></li>
</ol>
</div>
Content
A boxed note with an accent edge. Dashed variant for placeholders.
<div class="pb-stack is-tight">
<div class="pb-note" data-accent="slate"><strong>Heads up.</strong> This changes how links open on every page.</div>
<div class="pb-note" data-accent="amber"><strong>Careful.</strong> This can't be undone once it is published.</div>
<div class="pb-note is-dashed">Placeholder for something that hasn't been written yet.</div>
</div>
Content
Article styles for plain HTML inside .pb-content. Headings, lists, quotes, code, tables and figures.
Opening paragraph in body text, with an inline link and some strong text.
inline code.A short quote that deserves to stand out.
npm run build
<article class="pb-content">
<p>Opening paragraph in body text, with an <a href="#">inline link</a> and some <strong>strong text</strong>.</p>
<h2>A section heading</h2>
<ul>
<li>A first point.
<ul><li>A nested detail, a little quieter.</li></ul></li>
<li>A second point with <code>inline code</code>.</li>
</ul>
<blockquote>A short quote that deserves to stand out.</blockquote>
<pre><code>npm run build</code></pre>
</article>
Forms
Label, input and help text stacked. Also textarea and select. Error state in red.
<form class="pb-stack" onsubmit="return false">
<div class="pb-field">
<label for="f-name">Name</label>
<input class="pb-input" id="f-name" name="name" autocomplete="name" placeholder="Alex Lee">
</div>
<div class="pb-field is-error">
<label for="f-email">Email</label>
<input class="pb-input" id="f-email" name="email" type="email" value="alex@" aria-invalid="true" aria-describedby="f-email-help">
<p class="pb-help" id="f-email-help">Add the part after the @, like alex@example.com.</p>
</div>
<div class="pb-field">
<label for="f-topic">Topic</label>
<select class="pb-select" id="f-topic" name="topic"><option>General</option><option>Feedback</option></select>
</div>
<div class="pb-field">
<label for="f-msg">Message <span class="pb-optional">(optional)</span></label>
<textarea class="pb-textarea" id="f-msg" name="message" aria-describedby="f-msg-help"></textarea>
<p class="pb-help" id="f-msg-help">A few lines is plenty.</p>
</div>
<div class="pb-cluster"><button class="pb-btn" type="submit">Send</button><button class="pb-btn is-quiet" type="reset">Clear</button></div>
</form>
Forms
Native checkboxes and radios with a raspberry accent, wrapped in their label.
<div class="pb-stack is-loose">
<fieldset class="pb-fieldset">
<legend>Email me about</legend>
<label class="pb-check"><input type="checkbox" checked> New posts</label>
<label class="pb-check"><input type="checkbox"><span>Project updates <small>About once a month.</small></span></label>
</fieldset>
<fieldset class="pb-fieldset">
<legend>Theme</legend>
<label class="pb-check"><input type="radio" name="theme-demo" checked> Match my system</label>
<label class="pb-check"><input type="radio" name="theme-demo"> Light</label>
<label class="pb-check"><input type="radio" name="theme-demo"> Dark</label>
</fieldset>
</div>
Navigation
A row of links with a raspberry underline on the current one. Works without JavaScript.
<nav aria-label="Sections">
<ul class="pb-tabs">
<li><a href="#" aria-current="page">Overview</a></li>
<li><a href="#">Notes</a></li>
<li><a href="#">Guides</a></li>
<li><a href="#">Archive</a></li>
</ul>
</nav>
Navigation
Small path above a page title.
<nav aria-label="Breadcrumb">
<ol class="pb-crumbs">
<li><a href="#">Home</a></li>
<li><a href="#">Notes</a></li>
<li><span aria-current="page">This page</span></li>
</ol>
</nav>
Navigation
Previous and next pills with a page count between them.
<nav class="pb-pager" aria-label="Pagination">
<a href="#" rel="prev" aria-disabled="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>Newer</a>
<span>Page 1 of 4</span>
<a href="#" rel="next">Older<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg></a>
</nav>
Data
A bordered table on a white surface, with uppercase column labels and tabular figures.
| Site | Status | Pages | Size |
|---|---|---|---|
| Home | Live | 12 | 84 KB |
| Notes | Live | 31 | 212 KB |
| Profile | New | 1 | 146 KB |
<div class="pb-table-wrap">
<table class="pb-table">
<caption>Builds this week</caption>
<thead><tr><th scope="col">Site</th><th scope="col">Status</th><th scope="col" class="is-num">Pages</th><th scope="col" class="is-num">Size</th></tr></thead>
<tbody>
<tr><th scope="row">Home</th><td><span class="pb-badge" data-accent="green">Live</span></td><td class="is-num">12</td><td class="is-num">84 KB</td></tr>
<tr><th scope="row">Notes</th><td><span class="pb-badge" data-accent="green">Live</span></td><td class="is-num">31</td><td class="is-num">212 KB</td></tr>
<tr><th scope="row">Profile</th><td><span class="pb-badge">New</span></td><td class="is-num">1</td><td class="is-num">146 KB</td></tr>
</tbody>
</table>
</div>
Data
What shows when a list has nothing in it yet. A short title, one line of why, one action.
Posts you save will show up here, newest first.
Browse posts<div class="pb-empty">
<strong>Nothing here yet</strong>
<p>Posts you save will show up here, newest first.</p>
<a class="pb-btn is-secondary is-small" href="#">Browse posts</a>
</div>
Feedback
A native <dialog> with a title, a sentence and actions in the footer.
<dialog class="pb-dialog" id="confirm" aria-labelledby="confirm-title">
<div class="pb-dialog-body">
<h2 id="confirm-title">Delete this draft?</h2>
<p>The draft and its images will be removed. Published posts aren't affected.</p>
</div>
<form class="pb-dialog-actions" method="dialog">
<button class="pb-btn is-quiet" value="cancel">Cancel</button>
<button class="pb-btn" value="delete">Delete draft</button>
</form>
</dialog>
<button class="pb-btn is-secondary" type="button" onclick="document.getElementById('confirm').showModal()">Open dialog</button>
Feedback
A short dark pill confirming something happened, with an optional undo.
<div class="pb-toast" role="status" data-accent="green"><span class="pb-dot" aria-hidden="true"></span>Link copied to clipboard<button type="button">Undo</button></div>
Signature
A stick figure in ink lines with a handwritten speech bubble. The one playful element.
Hi! Want to see the newest post?
<div class="pb-guide">
<svg class="pb-figure" viewBox="0 0 60 88" aria-hidden="true"><path class="ln wave-arm" d="M30 41 L22 52 L20 61"/><path class="ln" d="M30 41 L38 52 L40 61"/><path class="ln" d="M30 36 L30 63 M30 63 L26 75 L24 86 M30 63 L34 75 L36 86"/><circle class="hd" cx="30" cy="23" r="13"/><circle class="eye" cx="25.5" cy="22" r="1.6"/><circle class="eye" cx="34.5" cy="22" r="1.6"/><path class="mouth" d="M25.5 27 Q30 30.5 34.5 27"/><g class="wv"><path class="ln" d="M30 41 L18 38"/><path class="ln fa" d="M18 38 L12 26"/></g></svg>
<div class="pb-bubble" role="status">
<p>Hi! Want to see the newest post?</p>
<div class="pb-acts"><a class="pb-btn is-small is-secondary" href="#">Show me</a><button class="pb-btn is-quiet" type="button">Later</button></div>
</div>
</div>