Styling Guide

A portable spec for the look and behavior of this site, written so another repo can adopt the same design language. It's framework-agnostic: the source here is plain HTML/CSS/JS, but everything below is expressed as design tokens and component recipes you can drop into React/Vue/Svelte/Tailwind just as easily.

1. Design ethos


2. Fastest path (drop-in)

Two files carry the entire system:

Every page opts in with two lines in <head> (no build step, no bundler):

html
<link rel="stylesheet" href="assets/theme.css">
<script src="assets/site.js"></script>

The rest of the recommended <head> (fonts, viewport, theme-color):

html
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#222436">
<title>Page title</title>

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,900&family=Hanken+Grotesk:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">

<link rel="stylesheet" href="assets/theme.css">
<script src="assets/site.js"></script>

viewport-fit=cover plus the safe-area insets used throughout are what make it sit correctly on notched phones and installed PWAs.

On a component framework? You don't need site.js's DOM-injection approach — reimplement the bottom bar and menu as components. But keep theme.css's :root token blocks (section 3) verbatim and keep the theming mechanism (section 9). Those two are the actual "design system"; the rest is presentation you can rebuild.


3. Color system

All color is expressed as CSS custom properties on :root. Never hardcode a hex value in a component — reference a token, and light/dark comes for free.

Token reference (light is TokyoNight Day):

css
:root {
  /* Surfaces & text */
  --bg:          #e1e2e7;  /* page background */
  --surface:     #ffffff;  /* cards, sheets, raised elements */
  --surface2:    #eaebf1;  /* subtle fills: hover, table headers, code bg */
  --ink:         #2e3358;  /* primary text */
  --muted:       #63699a;  /* secondary text, captions, labels */
  --faint:       #9ba0c4;  /* tertiary / disabled text, strikethrough rules */
  --line:        #cdd0e0;  /* hairline borders */
  --line2:       #bcc0d8;  /* stronger borders (bar, sheet, emphasized) */

  /* Accent (historically named "orange"; it's now blue — name kept for back-compat) */
  --orange:      #2e7de9;  /* the accent — links, active states, focus */
  --orange-ink:  #1a63c4;  /* accent text on a wash (higher contrast) */
  --orange-soft: #5a94ee;  /* lighter accent */
  --orange-wash: #dce7fb;  /* accent tint fill (active pills, current row) */

  /* Semantic green — "happening now" / success */
  --leaf:        #587539;
  --leaf-wash:   #dde8d0;

  --on-accent:   #ffffff;  /* text/icons sitting ON an accent fill */

  --shadow: 0 1px 2px rgba(40,50,90,.06), 0 8px 24px -14px rgba(40,60,130,.30);
  --radius: 14px;
}

Dark mode (TokyoNight Moon) applies the same tokens in two selectors, so it covers both "OS is dark and the user hasn't forced light" and "user forced dark":

css
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { /* …dark values… */ }
}
:root[data-theme="dark"] { /* …same dark values… */ }

Dark values:

css
--bg:#222436; --surface:#2b2e45; --surface2:#31344f;
--ink:#c8d3f5; --muted:#828bb8; --faint:#565f89;
--line:#383b52; --line2:#454a6e;
--orange:#82aaff; --orange-ink:#a5c1ff; --orange-soft:#6f9bf0; --orange-wash:#2d3f76;
--leaf:#c3e88d; --leaf-wash:#2b3a2c;
--on-accent:#1b1d2b;
--shadow:0 1px 2px rgba(0,0,0,.3), 0 10px 30px -16px rgba(0,0,0,.7);

Rules for using color:


4. Typography

Three Google fonts, each with a distinct job. This split is the single biggest driver of the "look" — a warm display serif over a clean grotesk body, with a monospace used only for labels and numbers.

Load weights: Fraunces 400/600/900, Hanken Grotesk 400/500/600/700, Space Mono 400/700.

Type recipes:

css
/* Body */
body{
  font-family:"Hanken Grotesk",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
  line-height:1.5;               /* 1.6 for long-form prose */
  color:var(--ink); background:var(--bg);
}

/* Display h1 — the signature look: heavy Fraunces, tight tracking, fluid size */
h1{
  font-family:"Fraunces",serif; font-weight:900;
  font-size:clamp(40px,10vw,64px);   /* hero pages go up to clamp(44px,12vw,80px) */
  letter-spacing:-.03em; line-height:.95; margin:8px 0 6px;
}
h2{ font-family:"Fraunces",serif; font-weight:600; font-size:26px; letter-spacing:-.01em; }
h3{ font-family:"Fraunces",serif; font-weight:600; font-size:20px; }

/* Kicker — the small uppercase mono label above a heading */
.kick{
  font-family:"Space Mono",monospace; font-weight:700;
  font-size:12px; letter-spacing:.2em; text-transform:uppercase;
  color:var(--orange);
}

/* Secondary / caption text */
.sub{ color:var(--muted); font-size:15px; }

Conventions:


5. Space, radius, shadow, motion

Radius:

Shadow: one elevation token, --shadow, used on every raised element (cards, chips, sheets, the bar). It's a tight contact shadow plus a soft wide shadow, tuned darker for dark mode. Don't invent new shadows — reuse --shadow.

Elevation and borders: raised surfaces are background:var(--surface) plus 1px solid var(--line) (or --line2 for stronger edges) plus --shadow. That trio is the card look.

Layout spacing:

Motion:


6. Component recipes

These are the repeating building blocks. Each is theme-aware because it only uses tokens.

Card (the workhorse) — raised surface, hairline border, --shadow, lift on hover. Title in Fraunces, description in --muted, an accent "Open →" affordance.

css
a.card{
  display:block; background:var(--surface); border:1px solid var(--line);
  border-radius:16px; padding:20px 22px; text-decoration:none; color:var(--ink);
  box-shadow:var(--shadow);
  transition:transform .16s,border-color .16s;
  -webkit-tap-highlight-color:transparent;
}
a.card:hover{ border-color:var(--orange); transform:translateY(-2px); }
a.card:active{ transform:scale(.985); }
.card-title{ font-family:"Fraunces",serif; font-weight:900; font-size:22px; line-height:1.15; }
.card-desc{ color:var(--muted); font-size:14.5px; margin-top:6px; }
.card-meta{ display:flex; align-items:center; justify-content:space-between; margin-top:14px; }
.card-meta .date{ font-family:"Space Mono",monospace; font-size:11px; color:var(--muted); }
.card-meta .go{ color:var(--orange); font-weight:700; font-size:14px; margin-left:auto; }

A grid of cards (landing / section) uses display:flex; gap:14px; flex-wrap:wrap with each card flex:1 1 200px, border-radius:18px, lifting -3px on hover.

Chip (metadata / at-a-glance) — a small rounded label-over-value, or a pill toggle.

css
/* label/value stat chip */
.chip{ background:var(--surface); border:1px solid var(--line); border-radius:12px;
  padding:7px 13px; box-shadow:var(--shadow); }
.chip .k{ display:block; font-size:11px; text-transform:uppercase; letter-spacing:.08em; color:var(--muted); }
.chip .v{ display:block; font-family:"Space Mono",monospace; font-weight:700; font-size:16px; color:var(--ink); }

/* pill chip toggle — ghost, and a solid accent "on" state */
.pill{ display:inline-flex; align-items:center; gap:6px; background:var(--surface);
  border:1px solid var(--line2); color:var(--ink); border-radius:999px;
  padding:6px 12px; font-size:13px; font-weight:600; cursor:pointer; transition:.15s; }
.pill:hover{ border-color:var(--orange); color:var(--orange); }
.pill.solid{ background:var(--orange); border-color:var(--orange); color:var(--on-accent); }

Badge (status tag) — a tiny uppercase tag; solid accent for "now", outlined for "next".

css
.badge{ display:inline-flex; align-items:center; font-family:"Hanken Grotesk";
  font-size:10px; font-weight:800; letter-spacing:.07em; text-transform:uppercase;
  padding:3px 8px; border-radius:6px; }
.badge.now{ background:var(--orange); color:var(--on-accent); }
.badge.next{ background:transparent; color:var(--orange-ink); border:1px solid var(--orange); }

Custom checkbox (checklists) — square, accent when checked, drawn check via a CSS border. Checked items dim and strike through. State persists in localStorage (see section 10).

css
input[type=checkbox]{
  appearance:none; -webkit-appearance:none; width:22px; height:22px; flex:none; margin:0;
  border:2px solid var(--line); border-radius:6px; background:var(--bg);
  cursor:pointer; position:relative; transition:.12s;
}
input[type=checkbox]:checked{ background:var(--orange); border-color:var(--orange); }
input[type=checkbox]:checked::after{
  content:""; position:absolute; left:6px; top:2px; width:6px; height:11px;
  border:solid var(--on-accent); border-width:0 2.5px 2.5px 0; transform:rotate(45deg);
}
/* dim the label of a ticked row */
input:checked ~ label{ opacity:.5; text-decoration:line-through; }

Table (reference / troubleshooting) — always wrapped in a horizontally-scrolling container so it never breaks a phone layout. Header row on --surface2, first column mono.

css
.tablewrap{ overflow-x:auto; border:1px solid var(--line); border-radius:12px; box-shadow:var(--shadow); }
table{ width:100%; border-collapse:collapse; background:var(--surface); min-width:320px; }
th,td{ padding:10px 14px; text-align:left; border-bottom:1px solid var(--line); font-size:14px; vertical-align:top; }
tr:last-child td{ border-bottom:0; }
th{ background:var(--surface2); font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); }
td:first-child{ font-family:"Space Mono",monospace; font-weight:700; white-space:nowrap; }

Inline code and code blocks:

css
code{ font-family:"Space Mono",monospace; font-size:.88em; background:var(--surface2);
  border:1px solid var(--line); border-radius:5px; padding:1px 5px; }
pre{ background:var(--surface); border:1px solid var(--line); border-radius:12px;
  padding:14px 16px; overflow-x:auto; box-shadow:var(--shadow); }
pre code{ background:none; border:0; padding:0; font-size:13px; line-height:1.5; }

Blockquote and notes callout:

css
blockquote{ margin:16px 0; padding:2px 16px; border-left:4px solid var(--orange);
  color:var(--muted); background:var(--surface); border-radius:0 10px 10px 0; }
.notes{ background:var(--surface); border:1px solid var(--line); border-radius:14px;
  padding:4px 18px 14px; box-shadow:var(--shadow); }

Links (in prose) — accent color, no underline until hover, with a soft color-mix underline at rest:

css
a{ color:var(--orange); text-decoration:none;
   border-bottom:1px solid color-mix(in srgb,var(--orange) 40%,transparent); }
a:hover{ border-bottom-color:var(--orange); }

Icon button:

css
.iconbtn{ width:38px; height:38px; flex:none; border-radius:11px;
  border:1px solid var(--line2); background:var(--surface); color:var(--ink);
  font-size:17px; cursor:pointer; display:grid; place-items:center; transition:.15s; }
.iconbtn:hover{ border-color:var(--orange); color:var(--orange); }

Sticky translucent header (for dense / long pages):

css
header{ position:sticky; top:0; z-index:20;
  background:color-mix(in srgb,var(--bg) 88%,transparent);
  backdrop-filter:saturate(1.4) blur(10px); border-bottom:1px solid var(--line); }

Section divider heading — a Fraunces label with a rule that fills the remaining width:

css
.sec{ font-family:"Fraunces",serif; font-weight:600; font-size:15px; color:var(--muted);
  text-transform:uppercase; display:flex; align-items:center; gap:10px; }
.sec::after{ content:""; flex:1; height:1px; background:var(--line); }

7. The bottom navigation bar

A floating, thumb-reachable dock injected on every page by site.js. It is always exactly three equal-width pills:

  1. ⌂ Home — the site root (derived from the script's own URL, correct at any depth or domain). Shown in the active state (--orange-wash) when already home.
  2. ☰ Menu — opens the slide-up sheet (file tree, per-page tools, theme).
  3. Third button — ← Back (up one level: page to its folder to home) everywhere except the home page, where it becomes ↻ Refresh (re-primes the offline cache; see section 10). The Back label is filled with the destination page's title and fades at the right edge (a gradient mask, not an ellipsis) when too long.

Key styling (from theme.css):

css
:root{ --pages-nav-h:58px; }             /* exported so pages can offset above it */
.pages-nav{
  position:fixed; inset-inline:0; margin-inline:auto;
  width:min(640px,calc(100% - 20px));    /* full-width on phones, capped mid-page on desktop */
  bottom:calc(14px + env(safe-area-inset-bottom,0px));
  z-index:45; display:flex; gap:6px; padding:6px; border-radius:999px;
  background:var(--surface); border:1px solid var(--line2); box-shadow:var(--shadow);
}
.pages-nav__btn{ flex:1 1 0; height:46px; border-radius:999px; /* equal-width pills */
  font:600 14px/1 "Hanken Grotesk",system-ui,sans-serif; }
.pages-nav__btn.is-active{ background:var(--orange-wash); color:var(--orange-ink); }
@media (max-width:360px){ .pages-nav__txt{ display:none; } } /* icon-only on tiny screens */

Principles worth carrying over even if you rebuild it:


8. The slide-up menu sheet and per-page tools

☰ Menu opens a modal bottom sheet (.pages-sheet) that grows up from the bar. Top to bottom it contains:

  1. A file tree of the whole site (folders expand; the current page is highlighted).
  2. A "This page" section of page-specific tools — rendered only when the page registered any — as a two-column grid of pill buttons.
  3. The light/dark theme toggle, pinned at the foot. This is the single theme control for the whole site; pages must not add their own.

Sheet mechanics: full-screen scrim (rgba(16,18,30,.5)), panel max-width:520px border-radius:22px 22px 0 0, slides in with transform:translateY(101%) to 0 over .28s cubic-bezier(.32,.72,0,1), a grab handle at top, Esc / scrim / ✕ to close, and body scroll locked while open.

Per-page tools API — a page adds its own actions to the menu from its own <script>, no bar surgery:

js
Pages.addHelper({ label, icon, href });               // a link
Pages.addHelper({ label, icon, onClick, disabled });  // an action
Pages.addHelpers([ … ]);                              // several at once

This is the extension point: the shell stays fixed at three buttons, and every page contributes its own verbs into one predictable place.


9. Theming behavior

The mechanism, in order of precedence:

  1. Explicit override wins. data-theme="light" or data-theme="dark" on <html> forces a mode.
  2. Otherwise follow the OS via @media (prefers-color-scheme: dark).

site.js handles the rest:

To reproduce in a framework: keep the two :root blocks and the data-theme plus prefers-color-scheme contract exactly; back the toggle with the same localStorage['theme'] key; apply the attribute before hydration or paint.


10. Offline / PWA and persistence

js
// 🗒️ notepad favicon, crisp at any size:
'data:image/svg+xml,' + encodeURIComponent(
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🗒️</text></svg>')

11. Accessibility and interaction checklist


12. Porting checklist

To make another repo look and behave like this one:

  1. Copy the tokens. Bring theme.css's :root light block and both dark blocks over verbatim (section 3). Everything else keys off these.
  2. Wire the three fonts (Fraunces, Hanken Grotesk, Space Mono) and adopt the type roles in section 4 (serif headings, grotesk body, mono labels).
  3. Adopt the card, plus the kicker/h1/sub header, as your default page scaffold (sections 4 and 6) — that combination is most of the visual identity.
  4. Reuse --shadow and --radius for every raised surface; don't invent new elevation.
  5. Standardize interaction: .16s transitions, hover-lift, :active press, no tap-highlight (section 5).
  6. Implement the theming contract (section 9): data-theme override plus prefers-color-scheme, localStorage['theme'], applied before paint, theme-color synced.
  7. Add the shell: a fixed bottom bar of at most three thumb-reachable actions, plus one slide-up menu that holds navigation, page-specific tools, and the single theme toggle (sections 7 and 8). Either port site.js or rebuild it as components.
  8. If it should work offline or install: add the manifest, an emoji-or-PNG icon set, and a cache-first service worker with the 200-only-overwrite and network-check-before-clear safeguards (section 10).

The litmus test for any new component: does it reference only tokens (so it's theme-aware), does it lift on hover and press on tap, and does it put its actions in the shared menu rather than inventing new chrome?