diff --git a/generate.scm b/generate.scm
index 1f7a9a3..429bb24 100644
--- a/generate.scm
+++ b/generate.scm
@@ -18,16 +18,20 @@
;; ─── Individual cards (all become flat children of .columns) ──────────────────
+;; Project card — optionally wraps in an anchor if url is set
(define (render-project proj)
- `(div (@ (class "project-card"))
- (div (@ (class "project-header"))
- (span (@ (class "project-name")) ,(field proj 'name))
- ,(date-range (field proj 'date-start) (field proj 'date-end)))
- (div (@ (class "project-summary"))
- ,(bullets (field proj 'summary-bullets)))
- (div (@ (class "project-details"))
- ,(bullets (field proj 'detail-bullets)))
- ,(tech-list (field proj 'technologies))))
+ (let* ((url (field proj 'url))
+ (card `(div (@ (class ,(string-append "project-card" (if url " clickable" "")))
+ ,@(if url `((onclick ,(string-append "window.open('" url "','_blank')"))) '()))
+ (div (@ (class "project-header"))
+ (span (@ (class "project-name")) ,(field proj 'name))
+ ,(date-range (field proj 'date-start) (field proj 'date-end)))
+ (div (@ (class "project-summary"))
+ ,(bullets (field proj 'summary-bullets)))
+ (div (@ (class "project-details"))
+ ,(bullets (field proj 'detail-bullets)))
+ ,(tech-list (field proj 'technologies)))))
+ card))
;; Title card: blurb only, no projects nested inside.
;; Projects are emitted as siblings after the title card.
@@ -64,6 +68,15 @@
(span (@ (class "interest-title")) ,(field item 'title))
(span (@ (class "interest-description")) ,(field item 'description))))
+(define (render-interests sec)
+ `(div (@ (class "interests"))
+ ,@(map render-interest (field sec 'items))))
+
+(define (render-favorite-software sec)
+ `(div (@ (class "favorite-software"))
+ ,@(map (lambda (s) `(span (@ (class "sw-item")) ,s))
+ (field sec 'items))))
+
;; ─── Section → flat list of column children ───────────────────────────────────
(define (section->items sec)
@@ -75,37 +88,44 @@
((hobby-projects)
(map render-project (field sec 'projects)))
((interests)
- (map render-interest (field sec 'items)))
+ (list (render-interests sec)))
+ ((favorite-software)
+ (list (render-favorite-software sec)))
(else '())))
(define (section-label sec)
(case (car sec)
- ((job) "Work Experience")
- ((education) "Education")
- ((hobby-projects) "Projects")
- ((interests) "Interests")
- (else "")))
+ ((job) "Work Experience")
+ ((education) "Education")
+ ((hobby-projects) "Projects")
+ ((interests) "Interests")
+ ((favorite-software) "Favorite Software")
+ (else "")))
;; Group consecutive sections of same type, emit one heading per group
(define (sections->flat-items sections)
(if (null? sections) '()
- (let loop ((rest (cdr sections))
- (cur-type (caar sections))
+ (let loop ((rest (cdr sections))
+ (cur-type (caar sections))
(cur-label (section-label (car sections)))
(cur-items (section->items (car sections)))
- (acc '()))
+ (acc '()))
(cond
((null? rest)
- (append (reverse acc)
+ ;; flush the last group — acc is already in forward order
+ (append acc
(list `(h2 (@ (class "sec-heading")) ,cur-label))
cur-items))
((eq? (caar rest) cur-type)
- ;; same type: accumulate items without a new heading
+ ;; same section type: merge items under one heading
(loop (cdr rest) cur-type cur-label
(append cur-items (section->items (car rest)))
acc))
(else
- (loop (cdr rest) (caar rest) (section-label (car rest))
+ ;; new section type: flush current group into acc, start new
+ (loop (cdr rest)
+ (caar rest)
+ (section-label (car rest))
(section->items (car rest))
(append acc
(list `(h2 (@ (class "sec-heading")) ,cur-label))
@@ -118,7 +138,6 @@
(h1 (@ (class "fm-name")) ,(field fm 'name))
(div (@ (class "fm-contact"))
(span ,(field fm 'location))
- (span ,(field fm 'phone))
(span ,(field fm 'email)))))
;; ─── Document ─────────────────────────────────────────────────────────────────
@@ -132,7 +151,29 @@
(meta (@ (charset "UTF-8")))
(meta (@ (name "viewport") (content "width=device-width, initial-scale=1")))
(title ,(field fm 'name))
- (link (@ (rel "stylesheet") (href "style.css"))))
+ (link (@ (rel "stylesheet") (href "style.css")))
+ ;; Measure the front-matter height after every resize/zoom and
+ ;; set .columns height to exactly fill the remaining space.
+ (script "
+function setColHeight() {
+ var fm = document.querySelector('.front-matter');
+ var col = document.querySelector('.columns');
+ if (!fm || !col) return;
+ var used = fm.getBoundingClientRect().height
+ + parseInt(getComputedStyle(document.body).paddingTop) * 2;
+ col.style.height = (window.innerHeight - used) + 'px';
+}
+window.addEventListener('load', setColHeight);
+window.addEventListener('resize', setColHeight);
+// zoom via ctrl+/- fires resize in most browsers, but also poll as fallback
+var _lastW = window.innerWidth, _lastH = window.innerHeight;
+setInterval(function() {
+ if (window.innerWidth !== _lastW || window.innerHeight !== _lastH) {
+ _lastW = window.innerWidth; _lastH = window.innerHeight;
+ setColHeight();
+ }
+}, 200);
+"))
(body
,(render-front-matter fm)
(main (@ (class "columns"))
diff --git a/resume.html b/resume.html
index db909c4..33f16f8 100644
--- a/resume.html
+++ b/resume.html
@@ -1,2 +1,21 @@
-
Jane Doe- A 600-line Lisp interpreter in Python supporting tail-call optimisation.
- Implemented environments as linked frames, closures, and a macro expander.
- Added a REPL with readline history and pretty-printing.
- A resume generator written in Scheme that produces HTML via SXML.
- Defined a small DSL for resume data in resume.scm.
- generate.scm walks the data and emits SXML serialised to HTML.
- CSS handles all layout including the two-column split.
Projects
- Delivered a live analytics dashboard processing 50k events/s.
- Replaced polling with WebSocket fan-out, cutting server load by 60%.
- Implemented client-side time-series aggregation using D3.js.
- Added user-configurable alert thresholds stored in IndexedDB.
- React
- Node.js
- WebSockets
- D3.js
- AWS
Startup XYZ • Athens, GA
- Built product features across the React frontend and Node.js backend.
- Maintained CI/CD pipelines and AWS infrastructure.
- Collaborated directly with design and product teams.
- Implemented token-bucket rate limiting across all public API endpoints.
- Built as a gRPC interceptor; config hot-reloaded from the config service.
- Reduced abusive traffic incidents by 80% in the first quarter after launch.
- Added per-tenant override rules managed via an internal admin UI.
- Rewrote legacy PHP auth service in Go; cut p99 latency from 800 ms to 40 ms.
- Migrated 12 M users with zero downtime using a dual-write/dark-read strategy.
- Introduced PKCE OAuth 2.0 flow replacing home-grown session tokens.
- Added comprehensive integration test suite with 92% coverage.
- Go
- PostgreSQL
- Redis
- OAuth2
- Docker
Acme Corp • Remote
- Owned the authentication service end-to-end.
- Reduced API latency by 35% through caching improvements.
- Unified internal service catalogue and runbook search into one portal.
- Indexed 300+ services via OpenAPI scraping and custom metadata YAML.
- Built a fuzzy-search backend in Go backed by Bleve.
- Reduced mean time-to-runbook from ~8 min to <30 s.
- Built a Raft-based distributed config store used by 40+ services.
- Designed the consensus layer using a custom Raft implementation in Go.
- Added multi-region replication with conflict-free merge semantics.
- Reduced config propagation time from ~30 s to <500 ms p99.
- Go
- Raft
- gRPC
- Kubernetes
- Prometheus
Acme Corp • Remote
- Led a team of 4 engineers on the core platform.
- Drove adoption of internal developer tooling.
- Reviewed architecture proposals and mentored junior engineers.
Work Experience
The University of Georgia • Athens, GA
- Graduated December 2016
- ICPC Participant and Meeting Coordinator
Education
Interests
Open-SourceRegular contributor to Guile and Chicken Scheme ecosystem libraries.
Rock ClimbingSport climber; currently projecting 5.12a outdoors.
Technical WritingBlog at janedoe.dev — compilers, distributed systems, functional programming.
+Jane DoeEducation
The University of Georgia • Athens, GA
- Graduated December 2016
- ICPC Participant and Meeting Coordinator
Work Experience
Acme Corp • Remote
- Led a team of 4 engineers on the core platform.
- Drove adoption of internal developer tooling.
- Reviewed architecture proposals and mentored junior engineers.
- Built a Raft-based distributed config store used by 40+ services.
- Designed the consensus layer using a custom Raft implementation in Go.
- Added multi-region replication with conflict-free merge semantics.
- Reduced config propagation time from ~30 s to <500 ms p99.
- Go
- Raft
- gRPC
- Kubernetes
- Prometheus
- Unified internal service catalogue and runbook search into one portal.
- Indexed 300+ services via OpenAPI scraping and custom metadata YAML.
- Built a fuzzy-search backend in Go backed by Bleve.
- Reduced mean time-to-runbook from ~8 min to <30 s.
Acme Corp • Remote
- Owned the authentication service end-to-end.
- Reduced API latency by 35% through caching improvements.
- Rewrote legacy PHP auth service in Go; cut p99 latency from 800 ms to 40 ms.
- Migrated 12 M users with zero downtime using a dual-write/dark-read strategy.
- Introduced PKCE OAuth 2.0 flow replacing home-grown session tokens.
- Added comprehensive integration test suite with 92% coverage.
- Go
- PostgreSQL
- Redis
- OAuth2
- Docker
- Implemented token-bucket rate limiting across all public API endpoints.
- Built as a gRPC interceptor; config hot-reloaded from the config service.
- Reduced abusive traffic incidents by 80% in the first quarter after launch.
- Added per-tenant override rules managed via an internal admin UI.
Startup XYZ • Athens, GA
- Built product features across the React frontend and Node.js backend.
- Maintained CI/CD pipelines and AWS infrastructure.
- Collaborated directly with design and product teams.
- Delivered a live analytics dashboard processing 50k events/s.
- Replaced polling with WebSocket fan-out, cutting server load by 60%.
- Implemented client-side time-series aggregation using D3.js.
- Added user-configurable alert thresholds stored in IndexedDB.
- React
- Node.js
- WebSockets
- D3.js
- AWS
Projects
- A resume generator written in Scheme that produces HTML via SXML.
- Defined a small DSL for resume data in resume.scm.
- generate.scm walks the data and emits SXML serialised to HTML.
- CSS handles all layout including the two-column split.
- A 600-line Lisp interpreter in Python supporting tail-call optimisation.
- Implemented environments as linked frames, closures, and a macro expander.
- Added a REPL with readline history and pretty-printing.
Interests
Open-SourceRegular contributor to Guile and Chicken Scheme ecosystem libraries.
Rock ClimbingSport climber; currently projecting 5.12a outdoors.
Technical WritingBlog at janedoe.dev — compilers, distributed systems, functional programming.
Favorite Software
Guile SchemeEmacsNeovimNixGuixTailscaleWireGuardmpvzathuratmuxAlacrittySyncthingBspwmRofi
diff --git a/resume.scm b/resume.scm
index db194e8..1331a45 100644
--- a/resume.scm
+++ b/resume.scm
@@ -2,21 +2,21 @@
;; ─── Data constructors ────────────────────────────────────────────────────────
-(define (make-front-matter name email phone location)
+(define (make-front-matter name email location)
(list 'front-matter
(cons 'name name)
(cons 'email email)
- (cons 'phone phone)
(cons 'location location)))
-(define (make-project name date-start date-end summary-bullets detail-bullets technologies)
+(define (make-project name date-start date-end summary-bullets detail-bullets technologies . rest)
(list 'project
(cons 'name name)
(cons 'date-start date-start)
(cons 'date-end date-end)
(cons 'summary-bullets summary-bullets)
(cons 'detail-bullets detail-bullets)
- (cons 'technologies technologies)))
+ (cons 'technologies technologies)
+ (cons 'url (if (null? rest) #f (car rest)))))
;; Title card: lives inside a job card
;; blurb – responsibility bullets
@@ -62,6 +62,12 @@
(cons 'title title)
(cons 'description description)))
+;; Favorite software section: flat list of names, displayed in N columns
+(define (make-favorite-software heading items)
+ (list 'favorite-software
+ (cons 'heading heading)
+ (cons 'items items)))
+
(define (make-interests heading items)
(list 'interests
(cons 'heading heading)
@@ -77,7 +83,6 @@
(make-front-matter
"Jane Doe"
"jane@example.com"
- "(555) 000-0000"
"Springfield, USA"))
(define job-acme
@@ -163,13 +168,15 @@
'("Defined a small DSL for resume data in resume.scm."
"generate.scm walks the data and emits SXML serialised to HTML."
"CSS handles all layout including the two-column split.")
- '("Scheme" "SXML" "HTML" "CSS"))
+ '("Scheme" "SXML" "HTML" "CSS")
+ "https://github.com/janedoe/schemeresume")
(make-project
"Tiny Lisp Interpreter" "Jan 2025" "Mar 2025"
'("A 600-line Lisp interpreter in Python supporting tail-call optimisation.")
'("Implemented environments as linked frames, closures, and a macro expander."
"Added a REPL with readline history and pretty-printing.")
- '("Python" "Lisp")))))
+ '("Python" "Lisp")
+ "https://github.com/janedoe/tinylisp"))))
(define my-interests
(make-interests
@@ -179,6 +186,12 @@
(make-interest "Rock Climbing" "Sport climber; currently projecting 5.12a outdoors.")
(make-interest "Technical Writing" "Blog at janedoe.dev — compilers, distributed systems, functional programming."))))
+(define my-favorite-software
+ (make-favorite-software
+ "Favorite Software"
+ '("Guile Scheme" "Emacs" "Neovim" "Nix" "Guix" "Tailscale" "WireGuard"
+ "mpv" "zathura" "tmux" "Alacritty" "Syncthing" "Bspwm" "Rofi")))
+
;; Flat ordered section list; CSS handles column placement
(define resume
(list 'resume
@@ -187,4 +200,5 @@
job-acme
job-startup
my-hobby-projects
- my-interests))))
+ my-interests
+ my-favorite-software))))
diff --git a/style.css b/style.css
index 2ebd4f1..5b3938f 100644
--- a/style.css
+++ b/style.css
@@ -16,7 +16,7 @@
}
/* ─── Base ───────────────────────────────────────────────────────────────────── */
-html { font-size: 13px; }
+html, body { height: 100%; margin: 0; }
body {
font-family: var(--font);
@@ -25,12 +25,10 @@ body {
line-height: 1.54;
background: var(--bg);
color: var(--text);
- padding: 32px 36px;
- max-width: 1000px;
- margin: 0 auto;
- /* Don't let the page grow beyond one screen */
- height: 100vh;
- overflow: hidden;
+ padding: 28px 32px;
+ box-sizing: border-box;
+ overflow-y: auto;
+ overflow-x: hidden;
}
ul { list-style: none; }
@@ -39,9 +37,11 @@ a { color: var(--accent); text-decoration: none; }
/* ─── Front matter ───────────────────────────────────────────────────────────── */
.front-matter {
text-align: center;
- padding-bottom: 16px;
- margin-bottom: 16px;
+ padding-bottom: 14px;
+ margin-bottom: 14px;
border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ width: 100%;
}
.fm-name {
@@ -64,45 +64,30 @@ a { color: var(--accent); text-decoration: none; }
margin: 0 6px;
}
-/* ─── Columns: flex column-wrap ──────────────────────────────────────────────── */
-/*
- * All cards and headings are direct children of .columns.
- * flex-flow: column wrap makes them stack vertically, and once they
- * exceed the container height they wrap into the next column —
- * exactly like text wrapping in a newspaper column.
- * Any card, at any level, can be the wrap point.
- *
- * max-height is the remaining viewport after front-matter.
- * The front-matter is roughly 80px tall with the padding above.
- * JS sets this precisely via --col-height, but the CSS default is close.
- */
+/* ─── Column layout ──────────────────────────────────────────────────────────── */
.columns {
- display: flex;
- flex-flow: column wrap;
- align-content: space-between;
- max-height: calc(100vh - 130px);
- gap: 0;
+ column-width: 22em;
+ column-gap: 2em;
+ column-rule: 1px solid var(--border);
+ column-fill: balance;
}
-/* Every direct child is half the container width */
-.columns > * {
- width: calc(50% - 20px);
+/* Prevent any card from being split across a column boundary */
+.sec-heading,
+.title-card,
+.project-card,
+.education-card,
+.interests,
+.favorite-software {
+ break-inside: avoid;
}
-/* Dividing rule: pseudo-element positioned in the middle */
-.columns::before {
- content: "";
- position: absolute;
- left: 50%;
- top: 0;
- bottom: 0;
- width: 1px;
- background: var(--border);
- pointer-events: none;
+/* Keep each section heading attached to whatever follows it */
+.sec-heading {
+ break-after: avoid;
}
-.columns { position: relative; }
-/* Section headings */
+/* ─── Section headings ───────────────────────────────────────────────────────── */
.sec-heading {
font-size: 0.7rem;
font-weight: 800;
@@ -180,6 +165,14 @@ a { color: var(--accent); text-decoration: none; }
border-left-color: var(--accent);
}
+.project-card.clickable { cursor: pointer; }
+.project-card.clickable:hover .project-name::after {
+ content: " ↗";
+ font-size: 0.7rem;
+ color: var(--accent);
+ opacity: 0.7;
+}
+
.project-name { font-size: 0.83rem; }
.project-summary ul { padding-left: 12px; margin-top: 2px; }
@@ -261,20 +254,50 @@ a { color: var(--accent); text-decoration: none; }
}
.edu-extras li::before { content: "- "; }
-/* ─── Interests ──────────────────────────────────────────────────────────────── */
-.interest-card {
- font-size: 0.82rem;
- line-height: 1.54;
- padding: 2px 0;
+/* ─── Interests — 2-column micro-card grid ───────────────────────────────────── */
+.interests {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 4px;
+}
+
+.interest-card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 3px;
+ padding: 5px 8px;
+}
+.interest-title {
+ display: block;
+ font-weight: 700;
+ font-size: 0.78rem;
+ color: var(--text);
+}
+.interest-description {
+ display: block;
+ font-size: 0.73rem;
+ color: var(--muted);
+ line-height: 1.4;
+}
+
+/* ─── Favorite software — 4-column tag grid ─────────────────────────────────── */
+.favorite-software {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 4px;
+}
+
+.sw-item {
+ background: var(--tag-bg);
+ color: var(--tag-text);
+ font-size: 0.72rem;
+ padding: 3px 6px;
+ border-radius: 2px;
+ text-align: center;
}
-.interest-title { font-weight: 700; }
-.interest-title::after { content: ": "; color: var(--muted); font-weight: normal; }
-.interest-description { color: var(--muted); }
/* ─── Responsive ─────────────────────────────────────────────────────────────── */
-@media (max-width: 640px) {
- body { height: auto; overflow: auto; padding: 20px 16px; }
- .columns { flex-flow: column nowrap; max-height: none; }
- .columns > * { width: 100%; }
- .columns::before { display: none; }
+@media (max-width: 400px) {
+ html, body { height: auto; overflow: auto; }
+ .columns { column-width: auto; columns: 1; }
}