Colophon
This site is itself a work sample. Everything below it — templates, search, the theme switcher, the accessibility patterns — is hand-written for this repository: no Jekyll theme, no CSS framework, no frontend build tooling. Just Liquid templates, one plain CSS file, and a couple hundred lines of dependency-free JavaScript.
Search
The search box in the header is a from-scratch implementation, not a
bundled library. At build time Jekyll emits search-index.json
— every page's title, section, tags, summary, and body text. In the
browser, assets/js/search.js fetches that once and scores it
against the query with a small weighted substring matcher: exact title
match scores highest, then a title substring, then a tag hit, then the
summary, then the full body — no stemming or fuzzy matching, which is
plenty of precision for a site this size and keeps the whole thing
dependency-free.
The input is a full ARIA combobox: aria-expanded,
aria-controls, and aria-activedescendant track
state for assistive tech, the results list is a real listbox
with option roles, and a visually-hidden
aria-live="polite" region announces result counts as you type.
Arrow keys move the active option, Enter navigates, Escape closes it. If
JavaScript doesn't run, the input stays disabled and a plain-text note
says so rather than presenting a search box that silently does nothing.
Theme system
Light/dark mode is a single data-theme attribute on
<html>, driving every color through CSS custom
properties defined once in main.css and re-mapped under a
[data-theme="dark"] selector. The initial theme is resolved by
a small inline script in <head>, before anything paints:
it reads a stored preference from localStorage, falls back to
prefers-color-scheme, and sets the attribute immediately —
so there's no flash of the wrong theme on load. The toggle button in the
header keeps its aria-pressed and aria-label
("Switch to dark theme" / "Switch to light theme") in sync with the actual
state, and it stays hidden until the script confirms
JavaScript is running, so a no-JS visitor never sees a button that does
nothing. One more detail: the canvas-drawn demos (the dice-roller and
wave/FFT visualizers on the Visualizers page) bake colors into already-drawn
pixels, so they don't repaint automatically when the CSS variables change
— the toggle dispatches a sitethemechange custom event
and those demos listen for it to redraw in the new palette.
Accessibility
Alongside the search and theme-toggle patterns above: every page has a
skip-to-content link that's visually hidden until it receives focus; every
interactive element gets a visible :focus-visible outline
instead of relying on the browser default (or worse, suppressing it); a
prefers-reduced-motion media query collapses all animation and
transition durations for anyone who's set that preference at the OS level;
and navigation links mark the current page with aria-current
rather than just a CSS class, so assistive tech gets the same "you are
here" signal a sighted user gets from the visual state. This isn't
theoretical — an automated audit of the live site (Lighthouse +
axe-core across all 18 pages) is part of how this site gets checked before
changes ship, and the one real violation that audit caught — a
skipped heading level on the projects grid — got fixed because of it.
Build & deploy
This repo pins the github-pages gem in its
Gemfile, so the Jekyll version and every plugin running on my
laptop is exactly what GitHub's own infrastructure runs in production.
GitHub Pages rebuilds and redeploys automatically on every push to
main — no separate GitHub Actions workflow to maintain,
because the platform's native build already guarantees dev/prod parity.
The tradeoff is deliberate: for a static portfolio site, a custom CI
pipeline would be more infrastructure than the problem calls for.
Performance
Images are served through <picture> with a WebP source
and a compressed JPEG/PNG fallback, sized to the width they actually
render at (not the raw export from whatever tool produced them) so
retina screens stay sharp without shipping multi-megabyte originals to
everyone else. Below-the-fold figures are lazy-loaded. Video demos use
preload="metadata" so the browser fetches just enough to show
a poster frame and duration, not the whole file, until a visitor actually
presses play.