# levineuwirth.org — Design Specification v11 **Author:** Levi Neuwirth **Date:** March 2026 (v11: 21 March 2026) **Status:** LIVING DOCUMENT — Updated as implementation progresses. --- ## I. Vision & Philosophy This website is an **intellectual home** — the permanent residence of a mind that moves freely between computer science, music composition, poetry, fiction, and whatever else catches fire. ### Commitments 1. **Long content over disposable content.** Essays are living documents. 2. **Semantic zoom.** Title → abstract → headers → body → sidenotes → citations → sources. 3. **Earned ornament.** Every decorative element serves a purpose. 4. **The site is the proof.** Entirely FOSS. No tracking. No analytics. No fingerprinting. 5. **Reader > Author.** 6. **Configuration is code.** The build system is a Haskell program. 7. **No homepage epigraph.** 8. **Extensible metadata.** Future-proofed for semantic embeddings via external JSON injection. --- ## II. All Resolved Decisions ### Typography | Role | Font | License | Notes | |------|------|---------|-------| | **Body** | **Spectral** | SIL OFL | Screen-first serif. True smallcaps (`smcp`), four figure styles, ligatures, seven weights + italics. Self-hosted from source — Google Fonts strips OT features. | | **UI / Headers** | **Fira Sans** | SIL OFL | Humanist sans-serif. Complements Spectral. | | **Code** | **JetBrains Mono** | SIL OFL | Ligatures, excellent legibility. | Font pairing has been tested across screens and confirmed. **Self-hosting workflow:** ```bash pyftsubset Spectral-Regular.ttf \ --output-file=spectral-regular.woff2 \ --flavor=woff2 \ --layout-features='liga,dlig,smcp,c2sc,onum,lnum,pnum,tnum,frac,ordn,sups,subs,ss01,ss02,ss03,ss04,ss05,kern' \ --unicodes='U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD' \ --no-hinting --desubroutinize ``` ### LaTeX Math Client-side KaTeX (not pure build-time SSR — see Implementation Notes): - Pandoc outputs math spans with `class="math inline"` / `class="math display"` - KaTeX renders client-side from a deferred script - KaTeX CSS/fonts loaded conditionally only on pages with math (`$if(math)$` in head template) ### Navigation ``` Home | Me | Current | New | Links | Search [⚙] ─────────────────────────────────────────────── ▼ Portals AI | Fiction | Miscellany | Music | Nonfiction | Poetry | Research | Tech ``` - **Primary row (always visible):** Home, Me, Current (now-page), New (changelog), Links, Search; settings gear (⚙) on the right - **Settings panel** (⚙ button): Theme (Light/Dark), Text size (A−/A+), Focus Mode, Reduce Motion, Print — managed by `settings.js`; state persisted via `localStorage` - **Expandable portal row:** AI, Fiction, Miscellany, Music, Nonfiction, Poetry, Research, Tech - Portal row collapsed by default; expansion state persisted via `localStorage` - Fira Sans smallcaps for primary row ### Layout - **Left margin:** Interactive sticky TOC (`IntersectionObserver`). Collapses on narrow screens. - **Center column:** Body text in Spectral. 650–700px max-width. - **Right margin:** Sidenotes only (right column). ### Color Pure monochrome. No accent color. Light mode default (`#faf8f4` background, `#1a1a1a` text). Dark mode via `[data-theme="dark"]` + `prefers-color-scheme`. ### Content Systems - **Tag system:** Hierarchical, slash-separated (`research/mathematics`). Hakyll `buildTags` + custom hierarchy. Tag pages at `//` with no `/tags/` namespace prefix. - **Pagination:** Blog index 20/page, tag pages 20/page. Essay index all on one page. - **RSS:** Atom feed at `/feed.xml` (all content types, sorted by `date`) and `/music/feed.xml` (compositions only). - **Citations:** Numbered superscript markers `[1]` linked to a bibliography section. Hover preview via `citations.js`. Further Reading section separate from cited works. `data/bibliography.bib` + Chicago Author-Date CSL. - **Collapsible sections:** h2/h3 headings toggle their content via `collapse.js`. Smooth `max-height` transition. State persisted in `localStorage`. ### Gwern Codebase: Selective Adoption | Component | Action | Actual outcome | |-----------|--------|----------------| | `sidenotes.js` | Adopt directly (Said Achmiz, MIT) | **Written from scratch** — purpose-built for our HTML structure | | `popups.js` | Fork and simplify (Said Achmiz, MIT) | Exists in `static/js/popups.js`; Phase 3 | | CSS typographic foundations | Extract and refactor | Done | | Pandoc AST filters | Write from scratch | Done | | Hakyll architecture | Rewrite, informed by gwern | Done | | Everything else | Ignore | — | ### Metadata Extensible YAML frontmatter. Hakyll strips frontmatter before passing to Pandoc, so all frontmatter access goes through Hakyll's metadata API (`lookupStringList`, `getMetadataField`, etc.), not through Pandoc `Meta`. **Frontmatter keys in use:** ```yaml title: # page title date: # ISO date (YYYY-MM-DD) — used for sorting, feed, reading-time abstract: # short description (1–3 sentences) tags: # hierarchical tag list authors: # list of author names (defaults to Levi Neuwirth) further-reading: # list of BibTeX keys for the Further Reading section bibliography: # path to .bib file (optional; defaults to data/bibliography.bib) csl: # path to .csl file (optional; defaults to data/chicago-notes.csl) # Epistemic profile (all optional; section shown only if `status` is present) status: # Draft | Working model | Durable | Refined | Superseded | Deprecated confidence: # 0–100 integer (%) importance: # 1–5 integer (rendered as filled/empty dots) evidence: # 1–5 integer (rendered as filled/empty dots) scope: # personal | local | average | broad | civilizational novelty: # conventional | moderate | idiosyncratic | innovative practicality: # abstract | low | moderate | high | exceptional stability: # volatile | revising | fairly stable | stable | established # (auto-computed from git history; use IGNORE.txt to pin) last-reviewed: # ISO date — overrides git-derived date when in IGNORE.txt confidence-history: # list of integers — trend derived from last two entries (↑↓→) # Version history (optional; falls back to git log, then to date-created/date-modified) history: - date: "2026-03-01" # ISO date string (quote to prevent YAML date parsing) note: Initial draft # human-readable annotation - date: "2026-03-14" note: Expanded typography section; added citations ``` Auto-computed at build time: `word-count`, `reading-time`. Auto-derived at build time: `stability` (from `git log --follow`), `last-reviewed` (most recent commit date), `confidence-trend` (from `confidence-history`). **`IGNORE.txt`:** A file in the project root listing content paths (one per line) whose `stability` and `last-reviewed` should not be recomputed. Cleared automatically after every `make build`. Useful for pinning manually-set stability labels on pages whose git history is misleading. **Top metadata block:** 1. **Tags** — hierarchical tag list with links to tag index pages 2. **Description** — the `abstract` field, rendered in italic 3. **Authors** — `authors` list 4. **Page info** — jump links to bottom metadata sections (Epistemic/Bibliography/Backlinks shown conditionally) **Bottom metadata footer:** - **Version history** — three-tier priority: (1) frontmatter `history` list with authored notes → (2) git log dates (date-only) → (3) `date-created` / `date-modified` fallback. `make build` auto-commits `content/` before building, keeping git history current. - **Epistemic** (if `status` set) — compact: status chip · confidence % · importance dots · evidence dots; expanded `
`: stability · scope · novelty · practicality · last reviewed · confidence trend - **Bibliography** — formatted citations + Further Reading - **Backlinks** — auto-generated; each entry shows source title (link) + collapsible context paragraph ### Licensing - **Content:** CC BY-SA-NC 4.0 - **Code:** MIT --- ## III. Deployment & Infrastructure ### Deployment Pipeline ``` [Local machine] [Arch Linux VPS / DreamHost] content/*.md ↓ cabal run site -- build nginx serving ↓ /var/www/levineuwirth.org/ pagefind --site _site ↓ rsync -avz --delete \ _site/ \ vps:/var/www/levineuwirth.org/ ──→ Live site ``` ```makefile build: @git add content/ @git diff --cached --quiet || git commit -m "auto: $(date -u +%Y-%m-%dT%H:%M:%SZ)" @date +%s > data/build-start.txt @./tools/convert-images.sh # WebP conversion (skipped if cwebp absent) cabal run site -- build pagefind --site _site @if [ -d .venv ]; then \ uv run python tools/embed.py || echo "Warning: embedding failed"; \ fi > IGNORE.txt # clear stability pins after each build @BUILD_END=$(date +%s); BUILD_START=$(cat data/build-start.txt); \ echo $((BUILD_END - BUILD_START)) > data/last-build-seconds.txt sign: @./tools/sign-site.sh # detach-sign every _site/**/*.html; requires passphrase cached via preset-signing-passphrase.sh deploy: build sign rsync -avz --delete _site/ vps:/var/www/levineuwirth.org/ watch: cabal run site -- watch clean: cabal run site -- clean download-model: @./tools/download-model.sh # fetch quantized ONNX model to static/models/ (once per machine) convert-images: @./tools/convert-images.sh # manual trigger; also runs in build ``` ### Hosting Timeline 1. **Immediate:** Deploy to DreamHost (rsync static files) 2. **Phase 5:** Provision Arch VPS (Hetzner), configure nginx + certbot, migrate DNS ### VPS: nginx config (Arch Linux) ```nginx server { listen 443 ssl http2; server_name levineuwirth.org www.levineuwirth.org; root /var/www/levineuwirth.org; # TLS (managed by certbot) ssl_certificate /etc/letsencrypt/live/levineuwirth.org/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/levineuwirth.org/privkey.pem; # cdn.jsdelivr.net required for transformers.js (semantic search library). # Model weights served same-origin from /models/ — connect-src stays 'self'. add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self';" always; gzip on; gzip_types text/html text/css application/javascript application/json image/svg+xml; location ~* \.(woff2|css|js|svg|png|jpg|webp)$ { expires 1y; add_header Cache-Control "public, immutable"; } location ~* \.html$ { expires 1h; add_header Cache-Control "public, must-revalidate"; } try_files $uri $uri.html $uri/ =404; error_page 404 /404.html; } server { listen 80; server_name levineuwirth.org www.levineuwirth.org; return 301 https://$host$request_uri; } ``` --- ## IV. Repository Structure ``` levineuwirth.org/ ├── content/ │ ├── essays/ │ │ └── test-essay.md # Feature test document │ ├── blog/ │ ├── music/ │ │ └── {slug}/ │ │ ├── index.md # Composition frontmatter + program notes │ │ ├── scores/ # LilyPond SVG pages + PDF │ │ └── audio/ # Per-movement MP3s │ └── *.md # Standalone pages (me, colophon, etc.) ├── static/ │ ├── css/ │ │ ├── base.css # CSS variables, palette, dark mode │ │ ├── typography.css # Spectral OT features, dropcaps, smallcaps, link icons │ │ ├── layout.css # 3-column layout, responsive breakpoints │ │ ├── sidenotes.css # Sidenote positioning │ │ ├── popups.css # Link preview popup styles │ │ ├── syntax.css # Monochrome code highlighting (JetBrains Mono) │ │ ├── components.css # Nav (incl. settings panel), TOC, metadata, citations, collapsibles │ │ ├── viz.css # Visualization figure layout (.viz-figure, .vega-container, .viz-caption) │ │ ├── gallery.css # Exhibit system + annotation callouts │ │ ├── selection-popup.css # Text-selection toolbar │ │ ├── annotations.css # User highlight marks + annotation tooltip │ │ ├── images.css # Figure layout, captions, lightbox overlay │ │ ├── score-reader.css # Full-page score reader layout │ │ ├── catalog.css # Music catalog page (`/music/`) │ │ └── print.css # Print stylesheet (media="print") │ ├── js/ │ │ ├── theme.js # Dark/light toggle (sync, not deferred) │ │ ├── sidenotes.js # Written from scratch — collision avoidance, hover/focus │ │ ├── toc.js # Sticky TOC + scroll tracking + animated collapse │ │ ├── nav.js # Portal row expand/collapse + localStorage │ │ ├── collapse.js # Section collapsing with localStorage persistence │ │ ├── citations.js # Citation hover previews │ │ ├── gallery.js # Exhibit overlay + annotation toggle │ │ ├── popups.js # Link preview popups (internal, Wikipedia, citations) │ │ ├── settings.js # Settings panel (theme, text size, focus mode, reduce motion, print) │ │ ├── selection-popup.js # Context-aware text-selection toolbar │ │ ├── annotations.js # localStorage highlight/annotation engine (UI deferred) │ │ ├── score-reader.js # Score reader: page-turn, movement jumps, deep linking │ │ ├── viz.js # Vega-Lite render + dark mode re-render via MutationObserver │ │ ├── semantic-search.js # Client-side semantic search: transformers.js + Float32Array cosine ranking │ │ ├── search.js # Pagefind UI init + ?q= pre-fill + search timing (#search-timing) │ │ └── prism.min.js # Syntax highlighting │ ├── fonts/ # Self-hosted WOFF2 (subsetted with OT features) │ ├── gpg/ │ │ └── pubkey.asc # Ed25519 signing subkey public key (master: CD90AE96…; subkey: C9A42A6F…) │ ├── models/ # Self-hosted ONNX model (gitignored; run: make download-model) │ │ └── all-MiniLM-L6-v2/ # ~22 MB quantized — served at /models/ for semantic-search.js │ └── images/ │ └── link-icons/ # SVG icons for external link classification │ ├── external.svg │ ├── wikipedia.svg │ ├── github.svg │ ├── arxiv.svg │ └── doi.svg ├── templates/ │ ├── default.html # Outer shell: nav, head, footer JS │ ├── essay.html # 3-column layout with TOC │ ├── composition.html # Music landing page (metadata block, movements, body, recording player) │ ├── music-catalog.html # Music catalog index (`/music/`) │ ├── score-reader.html # Minimal score reader body (top bar + SVG stage) │ ├── score-reader-default.html # Minimal HTML shell for score reader (no nav/footer) │ ├── blog-post.html │ ├── page.html # Simple standalone pages │ ├── essay-index.html │ ├── blog-index.html │ ├── tag-index.html │ └── partials/ │ ├── head.html # CSS, conditional JS (citations, collapse) │ ├── nav.html # Two-row nav with portals │ ├── footer.html │ ├── metadata.html # Essay metadata block (top) │ └── page-footer.html # Essay footer (bibliography, backlinks) ├── build/ │ ├── Main.hs # Entry point │ ├── Site.hs # Hakyll rules (all routes + Atom feed) │ ├── Compilers.hs # Pandoc compiler wrappers │ ├── Contexts.hs # Template contexts (word-count, reading-time, bibliography) │ ├── Citations.hs # citeproc pipeline: Cite→superscript + bibliography HTML │ ├── Filters.hs # Re-exports all filter modules │ ├── Filters/ │ │ ├── Typography.hs # Smart quotes, dashes │ │ ├── Sidenotes.hs # Footnote → sidenote conversion │ │ ├── Dropcaps.hs # Decorative first-letter drop caps │ │ ├── Smallcaps.hs # Smallcaps via smcp OT feature │ │ ├── Wikilinks.hs # [[wikilink]] syntax │ │ ├── Links.hs # External link classification + data-link-icon attributes │ │ ├── Math.hs # Simple LaTeX → Unicode conversion │ │ ├── Code.hs # Prepend language- prefix for Prism.js │ │ ├── Images.hs # Lazy loading, lightbox data-attributes, WebP wrapper for local raster images │ │ ├── Score.hs # Score fragment SVG inlining + currentColor replacement │ │ └── Viz.hs # Visualization IO filter: runs Python scripts, inlines SVG / Vega-Lite JSON │ ├── Authors.hs # Author-as-tag system (slugify, authorLinksField, author pages) │ ├── Backlinks.hs # Two-pass build-time backlinks with context paragraph extraction │ ├── Catalog.hs # Music catalog: featured works + grouped-by-category HTML rendering │ ├── Stability.hs # Git-based stability auto-calculation + last-reviewed derivation │ ├── Metadata.hs # Stub (Phase 2+) │ ├── Tags.hs # Hierarchical tag system │ ├── Pagination.hs # 20/page for blog + tag indexes │ └── Utils.hs # Shared helpers (wordCount, readingTime) ├── data/ │ ├── bibliography.bib # BibTeX references │ ├── chicago-notes.csl # CSL style (in-text, Chicago Author-Date) │ └── (future: embeddings.json, similar-links.json) ├── tools/ │ ├── subset-fonts.sh │ ├── viz_theme.py # Matplotlib monochrome helpers (apply_monochrome, save_svg, LINESTYLE_CYCLE) │ ├── sign-site.sh # Detach-sign every _site/**/*.html → .html.sig (called by `make sign`) │ ├── preset-signing-passphrase.sh # Cache signing subkey passphrase in gpg-agent (run once per boot) │ ├── download-model.sh # Fetch quantized ONNX model to static/models/ (run once per machine) │ ├── convert-images.sh # Convert JPEG/PNG → WebP companions via cwebp (runs automatically in build) │ └── embed.py # Build-time embedding pipeline: similar-links + semantic search index ├── levineuwirth.cabal ├── cabal.project ├── cabal.project.freeze ├── Makefile └── CLAUDE.md ``` --- ## V. Implementation Phases ### Phase 1: Foundation ✓ - [x] Init Hakyll project, modular Haskell build system - [x] Font subsetting + self-hosting (Spectral, Fira Sans, JetBrains Mono) - [x] CSS: base (palette, variables, dark mode), typography (Spectral features), layout (3-column), sidenotes - [x] `sidenotes.js` — written from scratch (not adopted; see Implementation Notes) - [x] Two-row navigation with expandable portals - [x] Templates: default, essay, blog-post, index - [x] Dark/light toggle with `localStorage` + `prefers-color-scheme` - [x] Basic Pandoc pipeline (Markdown → HTML, smart typography) - [x] Deploy to DreamHost via rsync — deployed to Hetzner VPS instead ### Phase 2: Content Features ✓ - [x] Pandoc filters: sidenotes, dropcaps, smallcaps, wikilinks, typography, link classification, code, math - [x] Interactive sticky TOC — IntersectionObserver, animated expand/collapse, page-title display, auto-collapse on scroll - [x] Citation system — numbered superscript markers, hover preview, bibliography + Further Reading sections - [x] Monochrome syntax highlighting (Prism.js + `Filters.Code`) - [x] Collapsible h2/h3 sections (`collapse.js`) — `max-height` transition, localStorage persistence - [x] Hierarchical tag system + tag index pages - [x] Pagination (blog index and tag pages, 20/page) - [x] Metadata: YAML frontmatter + auto-computed word count / reading time - [x] Single Atom feed (`/feed.xml`, all content, sorted by date) - [x] External link icons (SVG mask-image, domain-classified via `Filters.Links`) - [x] Gallery / Exhibit system (`gallery.js`, `gallery.css`) — added (not in original spec) ### Phase 3: Rich Interactions - [x] Link preview popups (`popups.js`) — internal page previews (title, abstract, authors, tags, reading time), Wikipedia excerpts, citation previews; relative-URL fix for index pages - [x] Pagefind search (`/search.html`) — `search.js` pre-fills from `?q=` param; `#search-timing` shows elapsed ms (mono, faint) via `MutationObserver` on search results subtree - [x] Author system — authors treated as tags; `build/Authors.hs`; author pages at `/authors/{slug}/`; `authorLinksField` in all contexts; defaults to Levi Neuwirth - [x] Settings panel — `settings.js` + `settings.css` section in `components.css`; theme, text size (3 steps), focus mode, reduce motion, print; all state in `localStorage`; `theme.js` restores all settings before first paint - [x] Selection popup — `selection-popup.js` / `selection-popup.css`; context-aware toolbar appears 450 ms after text selection; see Implementation Notes - [x] Print stylesheet — `print.css` (media="print"); single-column, light colors, sidenotes as indented blocks, external URLs shown - [x] Current page (`/current.html`) — now-page; added to primary nav - [x] Annotations — `annotations.js` / `annotations.css`; localStorage storage, text re-anchoring, highlight marks, tooltip with delete; color-picker UI in selection popup (four swatches + optional note field) ### Phase 4: Creative Content & Polish - [x] Image handling (lazy load, lightbox, figures, WebP `` wrapper for local raster images) - [x] Homepage (replaces standalone index; gateway + curated recent content) - [x] Poetry typesetting — codex reading mode (`reading.html`, `reading.css`, `reading.js`); `poetryCompiler` with `Ext_hard_line_breaks`; narrower measure, stanza spacing, drop-cap suppressed - [x] Fiction reading mode — same codex layout; `fictionCompiler`; chapter drop caps + smallcaps lead-in via `h2 + p::first-letter`; reading mode infrastructure shared with poetry - [x] Music section — score fragment system (A): inline SVG excerpts (motifs, passages) integrated into the gallery/exhibit system; named, TOC-listed, focusable in the shared overlay alongside equations; authored via `{.score-fragment score-name="..." score-caption="..."}` fenced-div; SVG inlined at build time by `Filters.Score`; black fills/strokes replaced with `currentColor` for dark-mode; see Implementation Notes - [x] Music section — composition landing pages + full score reader (C): two-URL architecture per composition; `/music/{slug}/` (rich prose landing page with movement list, audio players, inline score fragments) and `/music/{slug}/score/` (minimal dedicated reader); Hakyll `version "score-reader"` mechanism; `compositionCtx` with `slug`, `score-url`, `has-score`, `score-page-count`, `score-pages` list, `has-movements`, `movements` list (Aeson-parsed nested YAML); `score-reader-default.html` minimal shell; `score-reader.js` (page navigation, movement jumps, `?p=` deep linking, preloading, keyboard); `score-reader.css`; dark mode via `filter: invert(1)`; see Implementation Notes - [x] Accessibility audit — skip link, TOC collapsed-link tabbing (`visibility: hidden`), section-toggle focus visibility, lightbox/gallery/settings focus restoration, popup `aria-hidden`, metadata nav wrapping, footer `onclick` removal; settings panel focus-steal bug fixed (focus only returns to toggle when it was inside the panel, preventing interference with text-selection popup) - [~] Visualization pipeline — Pandoc filter approach (`Filters.Viz`): `.figure` fenced divs run `python3