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.

← Back home