"Preprint" in the venue line directly above a PREPRINT chip was a labelling
error: the venue says what the document is, so the chip should say what the
link yields. All four paper links are the full text, hosted here, so they
are labelled that way; Code, Calculator, and Artifact are unchanged. Fixes
the same redundancy on the CV, which read "Preprint. [Preprint]" and
"Technical report … [Report]".
The venue line is now roman. Citation convention italicizes journal and book
titles but not conference names or status words, and publications.yml
already marks the journal names it wants italic — so a blanket italic on the
line flattened that distinction (italic nested in italic reads as nothing)
and set "Preprint" in a register reserved for titles. Roman lets the data
carry the emphasis, and JAMIA's full name now actually renders italic inside
a roman line.
Role and degree lines lose their italic too. The size step down from the
title and the muted ink already separate them, so it was emphasis on a line
that only labels. The two font-style resets that existed solely to escape
these italics are gone with them.
Italic on these pages is now either data-level <em> or the dagger footnote.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUGesXiMmACsLBTGG1xuEU
Two specificity collisions, both invisible in the markup and both my fault
for styling with bare class selectors on a surface that typography.css owns
through #markdownBody:
#markdownBody :is(h1..h6) (1,0,1) beat .vita-entry-title (0,1,0)
#markdownBody p + p (1,0,2) beat .vita-role etc. (0,1,0)
So entry titles rendered at 1.45rem sans with 2.475rem heading margins
instead of 1rem serif — the "too much space" and the odd text sizes — and
every second paragraph in a card picked up the 1.5em essay prose indent,
which is the strange horizontal offset on the date lines.
now.css never hits either, because Now uses no heading elements inside its
cards and emits one paragraph per card. The vita keeps its headings, since a
record page wants a real document outline, and pays with specificity: every
rule is now #markdownBody-scoped, and card paragraphs reset text-indent
outright.
Also restructured onto the layout item-card.css already establishes: title
and date share a baseline with the date flush right, and role, location and
GPA collapse onto one line beneath it, instead of stacking title / role /
lone indented date. Dates moved from <time> to <span> — half of them are
"Fall 2024", "expected 2028" or "Present", which have no valid datetime
value, and a <time> without one is worse than none.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUGesXiMmACsLBTGG1xuEU
The vita page carried a hand-typed duplicate of education, publications,
presentations, and experience, and had drifted from the PDFs in about
fifteen places — Shu Lab still one entry titled "Undergraduate
Researcher" three months after the degree, Weenix at ~7,000 lines, CHASE
still "accepted" after being presented, a preprint missing entirely, and
two different code URLs for the same paper.
build/Vita.hs reads yaml-source/data/*.yml — the same files the PDF
pipeline builds from — and renders education, experience (keeping the
CV's research/industry split), publications, presentations, and contact.
Drift is now structurally impossible rather than merely discouraged.
Notes on the implementation:
- The YAML is LaTeX-flavoured because xelatex is its first consumer, so
values pass through a small closed converter (\textbf, \textit,
\texttt, \href, $\times$, $\delta$, \#, {,}, ~, --, ---). Escaping
happens before conversion; unhandled commands pass through visibly
rather than silently dropping their argument.
- Scalars are parsed loosely: the same logical field is sometimes a YAML
number and sometimes a string (`year: 2026` vs `year: "2026--2027"`),
and the PDF pipeline does not care, so this does not force it to.
- personal.yml's phone number is deliberately not read. It is printed on
the CV, which is handed to a chosen reader; this page is crawled.
- Contact chips carry labels (ORCID, Forgejo, GitHub) rather than the
CV's full display values, which exist because paper cannot be clicked.
The page keeps only what nothing else on the site owns: the pointers and
the four research threads. In-progress work belongs to /current, the
engineering index to /cv/projects/, and the narrative to /me/ — whose
abstract already described this page as the formal record.
vita.css composes on item-card.css and mirrors now.css's headings and
chip geometry, so /about and /current read as one system. Link chips get
the only interaction polish on the page, being the one affordance a
printed CV cannot offer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUGesXiMmACsLBTGG1xuEU
positionPopup clamped horizontally but never vertically: the
flip-above branch positioned tall popups (rich layouts, lead figures)
above the visible region whenever the target sat near the top of the
screen. Placement is now: below if it fits, above if THAT fits, else
the roomier side — then clamped into the viewport on both axes.
.link-popup is additionally capped at viewport height (matching GAP,
overflow-y: auto) so the clamp always has room to work, and a
dimension-less image that loads after positioning re-clamps instead of
growing past the edge.
Verified by simulating the clamp across five viewport scenarios: the
near-top bug case moves from 470px above the viewport to fully
visible; the fits-below and flip-above cases are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reference popups (provider-rendered: arXiv, Wikipedia, CrossRef, …)
get a glanceable layout: wider container (560px), larger title and
body type, and a full-width image banner under the source label.
Internal page previews and item-card popups (new/library pages) keep
the compact layout — the shared popup element toggles
.link-popup--rich per show based on the rendered content.
- arXiv: a new best-effort enrich step fetches the paper's LaTeXML
HTML rendition and pulls the first figure as a lead image. Enrich is
time-boxed (1.8s) so the metadata popup is never held hostage; late
results refresh the cache for the next hover. Figures letterbox with
object-fit: contain (plots must not crop); Wikipedia photos
cover-crop with an upper focal point. width/height attrs reserve
aspect ratio so positioning is stable before the image loads.
- Wikipedia thumbnails request 480px for the banner width.
- nginx: new ^~ /proxy/arxiv-html/ location backed by arxiv.org proper
(export.arxiv.org serves the Atom API but 429s the /html/ asset
tree); 404s cached 1d (the common no-HTML-rendition case). All four
proxy locations switched to ^~ — without it, static-assets.conf's
per-extension regex location outranks plain prefixes and serves a
local 404 for any proxied URL ending in an image extension, which is
exactly how the first figure fetch failed.
Installed and verified live: proxied page (200, 298KB), figure (200
image/png), API unchanged, no-rendition 404 path; the full client
resolution chain (relative src -> proxy path -> guard -> image)
validated against production.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The root cause of 'PDF/arXiv previews simply do not work' was twofold:
1. nginx/popup-proxy.conf was never installed on the VPS — every
/proxy/* request (arXiv, PubMed, Internet Archive) returned nginx's
default 404. Now installed (snippets + http{}-context cache/limit
zones in conf.d, included in the vhost, nginx -t verified, reloaded).
2. The snippet itself had a latent bug that only surfaced once
installed: with a VARIABLE upstream, a URI part on proxy_pass is
passed literally — every request hit the upstream's homepage
(archive.org HTML where JSON was expected, arXiv 429s, NCBI doc-page
redirects). Fixed with explicit prefix-strip rewrites; bad cached
responses purged. All three proxies verified returning real data,
including a live arXiv title resolve.
Client-side improvements:
- arXiv match covers old-style IDs (cs/9901002, math.GT/0309136,
cond-mat/...v1) alongside new-style, and .pdf-suffixed /pdf/ URLs
(regex verified against six forms)
- Wikipedia popups show the article's lead image: pageimages rides
along the existing extracts call (pithumbsize=320), rendered via a
new https-only image slot in renderPopup with float styling;
upload.wikimedia.org added to the CSP's img-src
- pdf-thumbs now walks all of static/ (pdfjs pruned), so /cv.pdf and
/resume.pdf — the most-linked internal PDFs, previously thumbnail-less
and therefore popup-less — get hover previews
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The ~33 KB traced logo moves from an inlined-per-page partial to
/logo-sprite.svg referenced with <use> — cached once instead of
shipped on every page (homepage HTML: 46 KB -> 13 KB). CSS custom
properties cascade into the use shadow tree, so the two-tone cutout
is unchanged (AUDIT §9.1)
- favicon.ico regenerated at 16/32/48 from the 512px master: 71 KB ->
15 KB; modern browsers take the SVG anyway, the .ico is the legacy
fallback (§9.2)
- link-icons/internal.svg restored to the simple 4 KB path: it renders
at 0.7-1.6 rem through a CSS mask, where the 33 KB traced detail
cannot resolve (§9.2)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The filter consumes every Pandoc Note, so the "standard Pandoc-
generated section.footnotes" its doc claimed as the no-JS fallback
never existed — below 1500px with JS disabled, footnote content was
simply invisible (AUDIT §2.3). The filter now collects consumed notes
and appends the section itself: letter labels, jump targets for the
in-text refs (which now point at the visible fallback item), and
doc-backlink returns. sidenotes.js pairs ref/note by element id and
preventDefaults clicks, so behavior with JS is unchanged.
Verified in output: per-page item count matches inline sidenote count;
refs target #fn-<label>; backlinks target #snref-<label>.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New inline logo-mark.svg partial in the nav (two-tone cutout via
--logo-ink/--logo-bg), regenerated favicon set + web-app manifest icons
from the new mark, 1200x630 og-image wired into head.html.
Known follow-ups (AUDIT-2026-06-09.md §9): the traced SVG is ~33 KB
inlined per page, favicon.ico carries 128/256px entries, and the
webmanifest dropped its maskable purpose.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Internal-page hover popups now show the source's monogram alongside
the title / abstract / metadata when one exists. Two-column grid is
gated on .has-monogram so popups for pieces without an authored mark
keep their default single-column body. The serialised SVG comes from
the rendered page's own .frontmatter-mark--monogram figure, excluded
when it is the symmetric-layout placeholder roundel so empty-slot
pieces do not get a fake mark in the preview.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tightens what gets printed and how. Reading-mode warm tints are
disabled so pages do not repaint cream; the mobile TOC bar's
screen-only body padding is reset; sidenote / footnote treatments
are reworked so the prose flows continuously instead of breaking
into a separate footnotes section; decorative link-icon glyphs
are suppressed while external links keep their underline so a
reader can follow them in the printed copy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
iOS WebKit silently degrades position: sticky to static on direct
flex/grid children of <body>, so the sticky nav was breaking on
mobile. Wrapping everything below the nav in a .page-shell flex
column keeps the sticky-footer math out of body { } and restores
sticky behaviour across browsers. The essay-frontmatter hoisted in
the Marks II commit becomes a body-level sibling of .page-shell so
its monogram and epistemic-figure columns can span viewport width.
* templates/default.html wraps $body$ + footer in .page-shell.
* static/css/layout.css moves the flex-column + min-height math from
body to .page-shell; the body > header rule now excludes
.essay-frontmatter so the essay header does not inherit nav chrome
(sticky, nav-bg, border-bottom).
* static/css/base.css drops the html/body overflow-x: clip — the
page-shell wrapper handles horizontal containment and clipping at
the viewport level was interfering with position: sticky.
* static/css/reading.css updates its #markdownBody centering selector
to match .page-shell > #markdownBody.
* static/css/components.css makes the TOC outline scrollable when
it overflows: bounded max-height tied to the sticky budget plus a
thin themed scrollbar, with overflow: hidden preserved for the
collapse transition.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extends the Phase-1 monogram mark system to every long-form content
type (essays, blog posts, poems, fiction, music) and introduces a
coverage audit so gaps are visible.
* build/Marks.hs gains hasMonogram (predicate), monogramSvgFieldFor +
hasMonogramFieldFor (for explicit-path callers like the /build/ and
/stats/ pages). Contexts.hs exports hasMonogramField as a siteCtx
boolean so templates can conditionally render the slot without
emitting an empty <div>.
* essay.html, blog-post.html, reading.html: hoist the frontmatter
block out of <main id="markdownBody"> so the monogram + epistemic
marks render as wrapper chrome rather than indexable prose; left
+ right mark slots are now unconditional (CSS handles the empty
state) so the layout is grid-stable across pieces.
* templates/partials/item-card.html: optional monogram chip on cards
(item-card--has-monogram modifier), gated on $has-monogram$ so
monogram-less pieces stay flush.
* build/Stats.hs grows a "Marks coverage" telemetry section: per-type
pieces / monogram / epistemic-figure counts + a coverage rollup,
rendered between epistemic and output on /build/.
* tools/audit-marks.py: coverage report (ASCII table) walking
content/**/*.md, plus a pre-commit hook at
tools/hooks/pre-commit-marks.sh that runs the same scan against
newly-staged .md files. New `make audit-marks` runs the report
manually; the hook gates commits.
* static/css/marks.css: layout for the new frontmatter slots and the
item-card monogram chip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Preserve external works the site cites against link rot, host them at
permanent /archive/<slug>/ URLs in site chrome, and treat them as
first-class citizens of the backlinks and similar-pages indexes.
Curated, not crawled: the author adds one line to archive/manifest.yaml
and the build fetches, hashes, snapshots, and indexes the work.
* archive/manifest.yaml + tools/archive.py (fetch / refresh / wayback /
check / gc) — PDFs downloaded directly, HTML pages snapshotted with a
vendored monolith (tools/bin/monolith @ 2.10.1) into a single
self-contained file with the archive CSP and a noarchive robots meta
injected. Per-entry PROVENANCE.json committed; gitignored .txt
sidecars regenerated from the artifact's SHA-256.
* build/Archive.hs + build/ArchiveIndex.hs + build/Filters/Archive.hs
— Hakyll rules for /archive/ and /archive/<slug>/, a body Pandoc
filter that appends an archive affordance to live citations and
flips dead ones to the local copy on archive.py check's asymmetric
hysteresis (rotted needs 3 fails over >= 14 days; one ok recovers).
* build/Backlinks.hs — keeps archived external URLs through pass 1 and
canonicalises them to /archive/<slug>/ in pass 2, producing a
"Referenced by" section grouped by the fragment each citation
targets. build/Stats.hs gains a "Link archive" telemetry block on
/build/ (count, total size, median age, by-status / by-quality /
by-visibility, orphans).
* Integrity: archive.py fetch and build/Archive.hs (via sha256sum)
both re-hash every committed artifact, so a tampered file halts the
build even with cabal invoked directly or no .venv present. refresh
refuses to replace an uncommitted prior snapshot and rolls back
atomically on any exit path. removed.yaml is honoured by fetch,
wayback, and check using canonical-form (tracking-stripped,
arXiv-canonicalised) comparison.
* visibility: private keeps an entry in-repo but undeployed.
nginx/archive.conf emits X-Robots-Tag: noindex, noarchive for raw
artifacts that cannot carry meta directives.
The full design, phase plan (1-5), and three refinement passes live
in ARCHIVE.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Scoped warm accent tokens (--library-accent and friends) defined at
:root inside library.css — page-scoped since the file only loads on
/library.html and /search.html. Section headings become Spectral
small-caps chapter markers at 1.2rem in the accent color, so the
ornament span inherits it via currentColor. Divider and "more on
this shelf" link pick up the muted variant. The leading blockquote
gets an epigraph treatment: narrower measure, italic, warm intro
ink, with the attribution line dropping to the accent-muted tone.
Card-level refinements (oldstyle figures on dates, small-caps on
item-kind) scoped via .library-section so /new.html and tag pages
retain their lining-figure UI treatment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each shelf gets a dingbat keyed by portal slug: laurel (research),
quill (nonfiction), open book (fiction), lyre (poetry), plus the
existing clef / ai / tech / trefoil glyphs for the remaining four.
Rendered via mask-image with currentColor so a single SVG per
portal inherits whatever color its heading carries. Between rendered
shelves, a centered fleuron flanked by thin rules (library-divider.svg)
sits via CSS adjacent-sibling so hidden sections leave no orphan
dividers. The template swaps its Unicode placeholder for a
data-ornament span, wires a '\$library-intro\$' slot above the shelves,
and renders a "More on this shelf →" link when has-more gates fire.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>