diff --git a/bun.lock b/bun.lock
index 8aeaeda..6bebd0d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -7,6 +7,7 @@
"dependencies": {
"@base-ui/react": "^1.4.1",
"@icons-pack/react-simple-icons": "^13.13.0",
+ "@kitlangton/justice": "^0.3.0",
"@lucide/lab": "^0.1.2",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
@@ -20,7 +21,10 @@
"@types/pdfjs-dist": "2.10.378",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cn": "^0.4.0",
+ "leaflet": "^1.9.4",
"lucide-react": "^1.11.0",
+ "motion": "^13.4.1",
"next": "^16.2.6",
"pdfjs-dist": "4.10.38",
"radix-ui": "^1.4.3",
@@ -36,7 +40,9 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
+ "@types/leaflet": "^1.9.22",
"@types/node": "^22.19.7",
+ "@types/react-dom": "^19.3.0",
"eslint": "9.39.2",
"eslint-config-next": "16.1.3",
"prettier": "^3.8.0",
@@ -231,6 +237,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+ "@kitlangton/justice": ["@kitlangton/justice@0.3.0", "", {}, "sha512-C6jHgoiqhlkWyYa6S30MydKxFoHvxnivTNgL59DEFgpVuMzv5gsxJWKiWep3kryfKpyI/dNVNtMyqZn8y9/Y1A=="],
+
"@lucide/lab": ["@lucide/lab@0.1.2", "", {}, "sha512-VprF2BJa7ZuTGOhUd5cf8tHJXyL63wdxcGieAiVVoR9hO0YmPsnZO0AGqDiX2/br+/MC6n8BoJcmPilltOXIJA=="],
"@mdx-js/loader": ["@mdx-js/loader@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "source-map": "^0.7.0" }, "peerDependencies": { "webpack": ">=5" }, "optionalPeers": ["webpack"] }, "sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ=="],
@@ -457,12 +465,16 @@
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
+ "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
+
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
+ "@types/leaflet": ["@types/leaflet@1.9.22", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-h3lhECYEKDasG7LFHu+GiHqAvsgLuQvlJvVZzJDGONo3sEL+wUOqSFLnwkZlK0qVxnxbuGFW8iBlJNYs5wgndA=="],
+
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
@@ -475,6 +487,8 @@
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
+ "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
+
"@types/set-cookie-parser": ["@types/set-cookie-parser@2.4.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw=="],
"@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
@@ -661,6 +675,8 @@
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+ "cn": ["cn@0.4.0", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-qOAeUhPwPzBxx6jaeQdo646XIKliKHc2j1ZF7Ngb1o0pJZKH5gq8FkA0aVdUdPFUDXNXyUCXpCtoJISo6dhizg=="],
+
"code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="],
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
@@ -895,6 +911,8 @@
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
+ "framer-motion": ["framer-motion@13.4.1", "", { "dependencies": { "motion-dom": "^13.4.1", "motion-utils": "^13.3.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-6DsbU4hsNsokXkwSp3ezdMWMiv5yWzCcdjFgakkeUC6FVIK01Py8ToSzsXvBw8cYA5hff/oXFvZFU7ltbVyUYA=="],
+
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="],
@@ -1125,6 +1143,8 @@
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
+ "leaflet": ["leaflet@1.9.4", "", {}, "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="],
+
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
@@ -1245,6 +1265,12 @@
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
+ "motion": ["motion@13.4.1", "", { "dependencies": { "framer-motion": "^13.4.1", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-9YjJ918QN6/4VSigEoqJysNPh33uTruTLbrBdGDI7pM6WTWU4kTPKzmEqJ4bXzLGYyDyZ5ubWX2tv2uTuFQugg=="],
+
+ "motion-dom": ["motion-dom@13.4.1", "", { "dependencies": { "motion-utils": "^13.3.0" } }, "sha512-qw6ILBFHrIPQd3k9Dd1fNlCLHTNzvMXW2zpeTlxn4wZXX02m0Dnsf8ycyvcjtAZtBiBnL4SfbyDIXWg+b1x/Ig=="],
+
+ "motion-utils": ["motion-utils@13.3.0", "", {}, "sha512-sgSschQp7EseHInIlR7hBbMuvet3RA0bs28KPZAXJcGKGdxHGvh1ogpYDilY3bOMtl73EqPNmp75sAKHYPU5sg=="],
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"msw": ["msw@2.13.6", "", { "dependencies": { "@inquirer/confirm": "^6.0.11", "@mswjs/interceptors": "^0.41.3", "@open-draft/deferred-promise": "^3.0.0", "@types/statuses": "^2.0.6", "cookie": "^1.1.1", "graphql": "^16.13.2", "headers-polyfill": "^5.0.1", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.7", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.1", "type-fest": "^5.5.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-GAJbQy8Ra/Ydjt0Hb2MGT2qhzd83J3+QZMHdH85uW7r/XkKc846+Ma2PLif5hGvTm5Yqa+wkcstpim0WeLZU9g=="],
diff --git a/package.json b/package.json
index 8fbc8ae..7bc81a6 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"dependencies": {
"@base-ui/react": "^1.4.1",
"@icons-pack/react-simple-icons": "^13.13.0",
+ "@kitlangton/justice": "^0.3.0",
"@lucide/lab": "^0.1.2",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
@@ -30,7 +31,10 @@
"@types/pdfjs-dist": "2.10.378",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cn": "^0.4.0",
+ "leaflet": "^1.9.4",
"lucide-react": "^1.11.0",
+ "motion": "^13.4.1",
"next": "^16.2.6",
"pdfjs-dist": "4.10.38",
"radix-ui": "^1.4.3",
@@ -46,7 +50,9 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
+ "@types/leaflet": "^1.9.22",
"@types/node": "^22.19.7",
+ "@types/react-dom": "^19.3.0",
"eslint": "9.39.2",
"eslint-config-next": "16.1.3",
"prettier": "^3.8.0",
diff --git a/public/coffee/shops/7th-st-cafe.jpg b/public/coffee/shops/7th-st-cafe.jpg
new file mode 100644
index 0000000..12064e8
Binary files /dev/null and b/public/coffee/shops/7th-st-cafe.jpg differ
diff --git a/public/coffee/shops/alee-s-cafe.jpg b/public/coffee/shops/alee-s-cafe.jpg
new file mode 100644
index 0000000..86ef5ba
Binary files /dev/null and b/public/coffee/shops/alee-s-cafe.jpg differ
diff --git a/public/coffee/shops/all-star-bagels.jpg b/public/coffee/shops/all-star-bagels.jpg
new file mode 100644
index 0000000..63973e9
Binary files /dev/null and b/public/coffee/shops/all-star-bagels.jpg differ
diff --git a/public/coffee/shops/amami-kitchen.jpg b/public/coffee/shops/amami-kitchen.jpg
new file mode 100644
index 0000000..a813ed0
Binary files /dev/null and b/public/coffee/shops/amami-kitchen.jpg differ
diff --git a/public/coffee/shops/barnes-noble-caf.jpg b/public/coffee/shops/barnes-noble-caf.jpg
new file mode 100644
index 0000000..b5c5b11
Binary files /dev/null and b/public/coffee/shops/barnes-noble-caf.jpg differ
diff --git a/public/coffee/shops/cornerstone-kitchen.jpg b/public/coffee/shops/cornerstone-kitchen.jpg
new file mode 100644
index 0000000..db19e5e
Binary files /dev/null and b/public/coffee/shops/cornerstone-kitchen.jpg differ
diff --git a/public/coffee/shops/culture-coffee.jpg b/public/coffee/shops/culture-coffee.jpg
new file mode 100644
index 0000000..90793b3
Binary files /dev/null and b/public/coffee/shops/culture-coffee.jpg differ
diff --git a/public/coffee/shops/cycleup-coffee.jpg b/public/coffee/shops/cycleup-coffee.jpg
new file mode 100644
index 0000000..4b8690b
Binary files /dev/null and b/public/coffee/shops/cycleup-coffee.jpg differ
diff --git a/public/coffee/shops/dunkin.jpg b/public/coffee/shops/dunkin.jpg
new file mode 100644
index 0000000..76b4916
Binary files /dev/null and b/public/coffee/shops/dunkin.jpg differ
diff --git a/public/coffee/shops/gram-s-eatery.jpg b/public/coffee/shops/gram-s-eatery.jpg
new file mode 100644
index 0000000..8b3affd
Binary files /dev/null and b/public/coffee/shops/gram-s-eatery.jpg differ
diff --git a/public/coffee/shops/panera-bread.jpg b/public/coffee/shops/panera-bread.jpg
new file mode 100644
index 0000000..3efbfe2
Binary files /dev/null and b/public/coffee/shops/panera-bread.jpg differ
diff --git a/public/coffee/shops/paris-bakery-caf.jpg b/public/coffee/shops/paris-bakery-caf.jpg
new file mode 100644
index 0000000..295751c
Binary files /dev/null and b/public/coffee/shops/paris-bakery-caf.jpg differ
diff --git a/public/coffee/shops/starbucks-coffee.jpg b/public/coffee/shops/starbucks-coffee.jpg
new file mode 100644
index 0000000..72f0866
Binary files /dev/null and b/public/coffee/shops/starbucks-coffee.jpg differ
diff --git a/public/coffee/shops/starbucks-giant.jpg b/public/coffee/shops/starbucks-giant.jpg
new file mode 100644
index 0000000..eca81e8
Binary files /dev/null and b/public/coffee/shops/starbucks-giant.jpg differ
diff --git a/public/coffee/shops/street-of-shops.jpg b/public/coffee/shops/street-of-shops.jpg
new file mode 100644
index 0000000..49d9a95
Binary files /dev/null and b/public/coffee/shops/street-of-shops.jpg differ
diff --git a/public/coffee/shops/tastecraft-cafe.jpg b/public/coffee/shops/tastecraft-cafe.jpg
new file mode 100644
index 0000000..c2086a4
Binary files /dev/null and b/public/coffee/shops/tastecraft-cafe.jpg differ
diff --git a/public/favicon-dot.svg b/public/favicon-dot.svg
new file mode 100644
index 0000000..3749d13
--- /dev/null
+++ b/public/favicon-dot.svg
@@ -0,0 +1 @@
+
diff --git a/public/portfolio/beenvoice-logo.svg b/public/portfolio/beenvoice-logo.svg
new file mode 100644
index 0000000..8c0afdd
--- /dev/null
+++ b/public/portfolio/beenvoice-logo.svg
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/articles/page.tsx b/src/app/articles/page.tsx
index 10031ed..c8fa53b 100644
--- a/src/app/articles/page.tsx
+++ b/src/app/articles/page.tsx
@@ -1,94 +1,57 @@
-"use client";
-
-import { ArrowUpRight, Newspaper } from "lucide-react";
-
-import Link from "next/link";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "~/components/ui/card";
-import { CardSkeleton } from "~/components/ui/skeletons";
-import { useState, useEffect } from "react";
+import { ArrowUpRight } from "lucide-react";
+import { PageHeading } from "~/components/PageHeading";
+import { ContourLines } from "~/components/ContourLines";
import { articles } from "~/lib/data";
-export default function ArticlesPage() {
- const [loading, setLoading] = useState(true);
+export const metadata = { title: "In the media" };
- useEffect(() => {
- const fetchArticles = async () => {
- await new Promise((resolve) => setTimeout(resolve, 0));
- setLoading(false);
- };
- void fetchArticles();
- }, []);
+export default function ArticlesPage() {
+ const [lead, ...more] = articles;
return (
-
-
-
-
- I have been lucky enough to have a few wonderful articles written
- about me and my work. Go check them out!
-
-
+
+
-
- {loading ? (
- <>
-
-
-
- >
- ) : (
- articles.map((article, index) => (
-
-
-
-
-
{article.title}
-
- Read Article
-
-
-
-
- Written by {article.author}, found in {article.source}.
-
-
-
-
- {article.description}
-
-
- Read Article
-
-
-
-
+ {lead && (
+
+
+
+ 01 / Featured story
+ {lead.source}
+
+
+
+ )}
+
+
+ {more.map((article, index) => (
+
+
+
0{index + 2} / {article.source}
+
- ))
- )}
+
+ {article.description}
+
+
+ ))}
);
diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx
index 1b9e260..9037bcc 100644
--- a/src/app/blog/[slug]/page.tsx
+++ b/src/app/blog/[slug]/page.tsx
@@ -2,7 +2,7 @@ import { notFound } from "next/navigation";
import { Badge } from "~/components/ui/badge";
import fs from "fs";
import path from "path";
-import { BreadcrumbUpdater } from "~/components/BreadcrumbUpdater";
+import Link from "next/link";
interface BlogPostMetadata {
title: string;
@@ -58,22 +58,19 @@ export default async function BlogPost({ params }: PageProps) {
}
return (
-
-
-
- {/*
-
-
- Back to Blog
-
- */}
+
+
+ ← All writing
+
+
+ From the notebook
+ {metadata.title}
+ {metadata.summary && {metadata.summary}
}
- {metadata.title}
-
-
+
{metadata.publishedAt}
{metadata.tags && (
-
+
{metadata.tags.map((tag: string) => (
{tag}
@@ -82,11 +79,12 @@ export default async function BlogPost({ params }: PageProps) {
)}
-
+
-
+
+
← More writing
);
}
diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx
index 22e23b6..045bea4 100644
--- a/src/app/blog/page.tsx
+++ b/src/app/blog/page.tsx
@@ -1,13 +1,5 @@
+import { PageHeading } from "~/components/PageHeading";
import Link from "next/link";
-import {
- Card,
- CardContent,
- CardHeader,
- CardTitle,
- CardDescription,
-} from "~/components/ui/card";
-import { Badge } from "~/components/ui/badge";
-import { BookOpen } from "lucide-react";
import fs from "fs";
import path from "path";
@@ -57,49 +49,28 @@ export default async function BlogPage() {
const posts = await getBlogPosts();
return (
-
-
-
-
- Deep dives into my projects, tutorials, and thoughts on technology.
-
-
+
+
-
+
{posts.map((post) => (
-
-
-
-
- {post.title}
-
- {post.publishedAt}
-
-
-
- {post.summary}
-
-
-
-
- {post.tags?.map((tag: string) => (
-
- {tag}
-
- ))}
-
-
-
-
+
+
+ {post.publishedAt}
+
+
+
{post.title}
+
{post.summary}
+
{post.tags?.join(" · ")}
+
+ Read article
↗
+
+
+
))}
diff --git a/src/app/coffee/coffee.css b/src/app/coffee/coffee.css
new file mode 100644
index 0000000..3969cb9
--- /dev/null
+++ b/src/app/coffee/coffee.css
@@ -0,0 +1,83 @@
+.coffee-site-main { position: relative; z-index: 0; isolation: isolate; width: 100%; flex: 1; min-height: calc(100dvh - 72px); padding: 0; margin: 0; max-width: none; }
+body:has(.coffee-mirror) { overflow: hidden; }
+body:has(.coffee-mirror) .skip-link { z-index: 3000; }
+.coffee-mirror { position: relative; width: 100%; height: calc(100dvh - 72px); overflow: hidden; background: #eceeea; color: #161b1b; font-family: Georgia, "Times New Roman", serif; --coffee-glass: rgba(255,255,255,.82); --coffee-border: rgba(0,0,0,.12); --coffee-text: #161b1b; --coffee-subtle: #5e6363; --coffee-hover: rgba(0,0,0,.07); }
+.coffee-mirror * { box-sizing: border-box; }
+.coffee-mirror button { cursor: pointer; }
+.coffee-mirror-map { position: absolute; inset: 0; z-index: 0; }
+.coffee-mirror-map-error { position: absolute; top: 45%; left: 50%; transform: translate(-50%,-50%); z-index: 3; padding: 16px; color: white; background: #35281e; border-radius: 10px; text-align: center; }
+.coffee-mirror .leaflet-container { background: #eceeea; font-family: Georgia, "Times New Roman", serif; }
+.coffee-mirror .leaflet-control-attribution { display: none; }
+.coffee-mirror-pin { border: 2px solid white; border-radius: 50%; background: #8b4513; box-shadow: 0 2px 10px #0008; }
+.coffee-mirror-pin span { display: block; width: 100%; height: 100%; border-radius: 50%; }
+.coffee-mirror-pin.is-selected { background: #efa83f; box-shadow: 0 0 0 7px #efa83f55, 0 2px 10px #0008; }
+.coffee-mirror-header { position: absolute; top: 16px; left: 16px; right: 16px; z-index: 1000; min-height: 58px; display: flex; align-items: center; justify-content: space-between; padding: 7px 10px; border: 1px solid var(--coffee-border); border-radius: 13px; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(22px); box-shadow: 0 8px 28px #0003; }
+.coffee-mirror-icon-button { display: grid; place-items: center; width: 42px; height: 42px; border: 0; border-radius: 9px; color: var(--coffee-text); background: transparent; }
+.coffee-mirror-icon-button:hover, .coffee-mirror-icon-button.is-active { color: #f5ab45; background: var(--coffee-hover); }
+.coffee-mirror-brand { display: flex; align-items: center; gap: 12px; position: absolute; left: 50%; transform: translateX(-50%); border: 0; background: transparent; color: inherit; text-align: left; white-space: nowrap; }
+.coffee-mirror-brand > svg { color: #f1a22e; flex: none; }
+.coffee-mirror-brand strong, .coffee-mirror-brand small { display: block; }
+.coffee-mirror-brand strong { font-size: 17px; line-height: 1.1; }
+.coffee-mirror-brand small { color: var(--coffee-subtle); font-size: 12px; margin-top: 3px; }
+.coffee-mirror-header-spacer { width: 42px; }
+.coffee-mirror-drawer { position: absolute; z-index: 900; top: 92px; bottom: 32px; left: 16px; width: min(400px, calc(100vw - 32px)); overflow: hidden; border: 1px solid var(--coffee-border); border-radius: 13px; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(25px); box-shadow: 0 16px 38px #0004; transform: translateX(calc(-100% - 34px)); opacity: 0; pointer-events: none; transition: transform .3s ease, opacity .3s ease; }
+.coffee-mirror-drawer.is-open { transform: translateX(0); opacity: 1; pointer-events: auto; }
+.coffee-mirror-discovery, .coffee-mirror-detail { height: 100%; display: flex; flex-direction: column; }
+.coffee-mirror-discovery-top { flex: none; padding: 24px 22px 15px; }
+.coffee-mirror-discovery h2 { display: flex; align-items: center; gap: 9px; margin: 0 0 17px; color: var(--coffee-text); font-size: 23px; line-height: 1.2; }
+.coffee-mirror-discovery h2 svg { color: #efa83f; }
+.coffee-mirror-search { display: flex; align-items: center; gap: 8px; padding: 9px 12px; border: 1px solid var(--coffee-border); border-radius: 8px; background: var(--coffee-hover); color: var(--coffee-subtle); }
+.coffee-mirror-search input { width: 100%; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--coffee-text); font: 14px Arial, sans-serif; }
+.coffee-mirror-search input::placeholder { color: var(--coffee-subtle); }
+.coffee-mirror-list { overflow-y: auto; min-height: 0; padding: 0 14px 24px; scrollbar-width: thin; }
+.coffee-mirror-list-label { color: #eeb05c; text-transform: uppercase; letter-spacing: .12em; font: 700 11px Arial, sans-serif; padding: 8px 12px; }
+.coffee-mirror-list hr { border: 0; border-top: 1px solid var(--coffee-border); margin: 12px; }
+.coffee-mirror-row { display: flex; align-items: center; gap: 5px; width: 100%; border: 1px solid transparent; border-radius: 9px; margin: 3px 0; padding: 8px; }
+.coffee-mirror-row:hover, .coffee-mirror-row:focus-within { background: var(--coffee-hover); border-color: var(--coffee-border); }
+.coffee-mirror-row-main { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; text-align: left; color: var(--coffee-text); border: 0; background: transparent; padding: 0; }
+.coffee-mirror-row-main img { width: 63px; height: 63px; border-radius: 6px; flex: none; object-fit: cover; }
+.coffee-mirror-row-main span { min-width: 0; }
+.coffee-mirror-row-main strong, .coffee-mirror-row-main small { display: block; }
+.coffee-mirror-row-main strong { font-size: 15px; line-height: 1.25; }
+.coffee-mirror-row-main small { margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--coffee-subtle); font: 12px Arial, sans-serif; }
+.coffee-mirror-favorite { flex: none; border: 0; background: transparent; color: var(--coffee-subtle); padding: 8px; }
+.coffee-mirror-favorite:hover, .coffee-mirror-favorite:has(svg[fill="currentColor"]) { color: #eb7d79; }
+.coffee-mirror-empty { padding: 20px; text-align: center; color: var(--coffee-subtle); }
+.coffee-mirror-photo { position: relative; height: 235px; flex: none; overflow: hidden; mask-image: linear-gradient(#000 75%, transparent 100%); }
+.coffee-mirror-photo img { width: 100%; height: 100%; object-fit: cover; }
+.coffee-mirror-detail { overflow-y: auto; }
+.coffee-mirror-detail-actions { position: absolute; right: 13px; top: 13px; display: flex; gap: 7px; }
+.coffee-mirror-detail-actions button { display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid var(--coffee-border); border-radius: 50%; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(8px); }
+.coffee-mirror-detail-copy { padding: 4px 24px 30px; }
+.coffee-mirror-detail-copy h2 { margin: 0 0 12px; font-size: 29px; line-height: 1.15; }
+.coffee-mirror-detail-copy p { color: var(--coffee-text); font: 14px/1.55 Arial, sans-serif; }
+.coffee-mirror-detail-copy .coffee-mirror-address { display: flex; gap: 7px; align-items: flex-start; color: var(--coffee-subtle); }
+.coffee-mirror-address svg { flex: none; color: #f1a22e; }
+.coffee-mirror-directions { display: block; margin-top: 24px; padding: 12px; border-radius: 8px; background: #151515; color: white; text-align: center; font: 600 14px Arial, sans-serif; }
+.coffee-mirror-detail-links { display: flex; gap: 10px; margin-top: 12px; }
+.coffee-mirror-detail-links a { flex: 1; padding: 10px; border: 1px solid var(--coffee-border); border-radius: 8px; color: var(--coffee-text); text-align: center; font: 13px Arial, sans-serif; }
+.coffee-mirror-controls { position: absolute; right: 16px; bottom: 72px; z-index: 1000; display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
+.coffee-mirror-controls > button, .coffee-mirror-style-wrap > button { display: grid; place-items: center; width: 43px; height: 43px; border: 1px solid var(--coffee-border); border-radius: 12px; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(20px); box-shadow: 0 4px 14px #0003; }
+.coffee-mirror-controls button:hover { background: var(--coffee-hover); }
+.coffee-mirror-style-wrap { position: relative; }
+.coffee-mirror-style-menu { position: absolute; bottom: 0; right: 52px; min-width: 115px; padding: 5px; border: 1px solid var(--coffee-border); border-radius: 10px; background: var(--coffee-glass); backdrop-filter: blur(20px); }
+.coffee-mirror-style-menu button { display: block; width: 100%; padding: 9px; border: 0; border-radius: 6px; background: transparent; color: var(--coffee-text); text-align: left; font: 13px Arial, sans-serif; }
+.coffee-mirror-style-menu button:hover, .coffee-mirror-style-menu button[aria-pressed="true"] { background: var(--coffee-hover); }
+.coffee-mirror-location-error { max-width: 180px; padding: 8px; border-radius: 7px; background: var(--coffee-glass); color: var(--coffee-text); font: 12px Arial, sans-serif; }
+.coffee-mirror-footer { position: absolute; z-index: 800; left: 16px; right: 16px; bottom: 8px; display: flex; justify-content: space-between; color: var(--coffee-text); text-shadow: 0 1px 4px #000; font: 11px Arial, sans-serif; opacity: .75; pointer-events: none; }
+.coffee-mirror-modal-backdrop { position: absolute; z-index: 2000; inset: 0; display: grid; place-items: center; padding: 16px; background: #0009; backdrop-filter: blur(5px); }
+.coffee-mirror-modal { position: relative; width: min(100%, 440px); padding: 32px; border: 1px solid var(--coffee-border); border-radius: 15px; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(25px); box-shadow: 0 22px 70px #0007; text-align: center; }
+.coffee-mirror-modal > svg { color: #efa83f; margin: 0 auto 12px; }
+.coffee-mirror-modal h2 { font-size: 27px; line-height: 1.2; margin: 0 0 12px; }
+.coffee-mirror-modal p { margin: 0; color: var(--coffee-subtle); }
+.coffee-mirror-modal small { display: block; color: #efa83f; margin-top: 7px; }
+.coffee-mirror-modal ul { margin: 22px 0; padding: 18px 20px 18px 35px; border: 1px solid var(--coffee-border); border-radius: 9px; background: var(--coffee-hover); color: var(--coffee-subtle); text-align: left; font: 13px/1.8 Arial, sans-serif; }
+.coffee-mirror-start { width: 100%; border: 0; border-radius: 8px; padding: 12px; background: #111; color: #fff; font: 600 14px Arial, sans-serif; }
+.coffee-mirror-modal-close { position: absolute; right: 12px; top: 12px; border: 0; background: transparent; color: var(--coffee-text); }
+@media (prefers-color-scheme: dark) {
+ .coffee-mirror { color-scheme: dark; background: #182021; color: #f6f5f1; --coffee-glass: rgba(15, 18, 19, .76); --coffee-border: rgba(255,255,255,.16); --coffee-text: #f6f5f1; --coffee-subtle: #bec1c1; --coffee-hover: rgba(255,255,255,.12); }
+ .coffee-mirror .leaflet-container { background: #1c2526; }
+ .coffee-mirror:not(.is-satellite) .leaflet-tile-pane { filter: grayscale(1) invert(1) brightness(.8) contrast(1.1) hue-rotate(180deg); }
+}
+@media (max-width: 700px) { .coffee-site-main { min-height: calc(100dvh - 64px); } .coffee-mirror { height: calc(100dvh - 64px); } }
+@media (max-width: 639px) { .coffee-mirror-header { top: 12px; left: 12px; right: 12px; }.coffee-mirror-brand { gap: 7px; }.coffee-mirror-brand strong { font-size: 14px; }.coffee-mirror-brand small { font-size: 10px; }.coffee-mirror-brand > svg { width: 21px; }.coffee-mirror-drawer { top: 82px; bottom: 16px; left: 12px; width: calc(100vw - 24px); }.coffee-mirror-controls { bottom: 58px; right: 12px; }.coffee-mirror-footer { left: 12px; right: 12px; } }
diff --git a/src/app/coffee/page.tsx b/src/app/coffee/page.tsx
new file mode 100644
index 0000000..977ab85
--- /dev/null
+++ b/src/app/coffee/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+import { CoffeeMirror } from "~/components/CoffeeMirror";
+import "./coffee.css";
+
+export const metadata: Metadata = {
+ title: "Lewisburg Coffee Map",
+ description: "Explore coffee shops in Lewisburg, Pennsylvania.",
+};
+
+export default function CoffeePage() {
+ return
;
+}
diff --git a/src/app/cv/page.tsx b/src/app/cv/page.tsx
index 8048bbe..aa395b6 100644
--- a/src/app/cv/page.tsx
+++ b/src/app/cv/page.tsx
@@ -1,525 +1,17 @@
-"use client";
+import { PageHeading } from "~/components/PageHeading";
+import { DocumentReader } from "~/components/DocumentReader";
-import { useState, useRef, useEffect } from "react";
-import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
-import {
- Card,
- CardHeader,
- CardContent,
- CardTitle,
- CardDescription,
-} from "~/components/ui/card";
-import { Button } from "~/components/ui/button";
-
-import {
- Download,
- ExternalLink,
- FileText,
- ZoomIn,
- ZoomOut,
- RotateCw,
- ChevronLeft,
- ChevronRight,
- AlertCircle,
- Loader2,
- Eye,
-} from "lucide-react";
-import Link from "next/link";
-import type { PDFDocumentProxy, PDFPageProxy } from "pdfjs-dist";
-
-// Local PDF file URLs
-const CV_URL = "/publications/cv.pdf";
-const RESUME_URL = "/publications/resume.pdf";
-
-interface PDFViewerProps {
- url: string;
- title: string;
- type: "cv" | "resume";
-}
-
-function PDFViewer({ url, title, type }: PDFViewerProps) {
- const canvasRef = useRef
(null);
- const [pdfDoc, setPdfDoc] = useState(null);
- const [pageNum, setPageNum] = useState(1);
- const [pageCount, setPageCount] = useState(0);
- const [scale, setScale] = useState(1.2);
- const [rotation, setRotation] = useState(0);
- const [isLoading, setIsLoading] = useState(false);
- const [hasError, setHasError] = useState(false);
- const [errorMessage, setErrorMessage] = useState("");
- const [isClient, setIsClient] = useState(false);
- const [pdfBlob, setPdfBlob] = useState(null);
-
- useEffect(() => {
- // eslint-disable-next-line react-hooks/set-state-in-effect
- setIsClient(true);
- }, []);
-
- // Download PDF and convert to blob for PDF.js
- const downloadPDF = async (pdfUrl: string): Promise => {
- try {
- // Try direct fetch first
- const response = await fetch(pdfUrl, {
- mode: "cors",
- headers: {
- Accept: "application/pdf",
- },
- });
-
- if (!response.ok) {
- throw new Error(`Failed to download PDF: ${response.status}`);
- }
-
- const arrayBuffer = await response.arrayBuffer();
- return new Uint8Array(arrayBuffer);
- } catch (error) {
- console.error("Failed to download PDF:", error);
- throw error;
- }
- };
-
- useEffect(() => {
- if (!isClient) return;
-
- const loadPDF = async () => {
- try {
- setIsLoading(true);
- setHasError(false);
-
- // Download the PDF first
- const pdfData = await downloadPDF(url);
- setPdfBlob(pdfData);
-
- // Dynamically import PDF.js
- const pdfjsLib = await import("pdfjs-dist");
-
- // Set worker path
- pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.js";
-
- // Load PDF from the downloaded data
- const loadingTask = pdfjsLib.getDocument({ data: pdfData });
- const pdf = await loadingTask.promise;
- setPdfDoc(pdf);
- setPageCount(pdf.numPages);
- setPageNum(1);
- } catch (error) {
- console.error("Error loading PDF:", error);
- setHasError(true);
- setErrorMessage(
- error instanceof Error
- ? error.message
- : "Failed to load PDF. Please try downloading it directly.",
- );
- } finally {
- setIsLoading(false);
- }
- };
-
- void loadPDF();
- }, [url, isClient]);
-
- useEffect(() => {
- const renderPage = async () => {
- if (!pdfDoc || !canvasRef.current) return;
-
- try {
- const page: PDFPageProxy = await pdfDoc.getPage(pageNum);
- const canvas = canvasRef.current;
- const context = canvas.getContext("2d");
-
- if (!context) return;
-
- // Get container width for responsive scaling
- const container = canvas.parentElement;
- const containerWidth = container
- ? container.clientWidth - 32
- : window.innerWidth - 32; // Account for padding
-
- // Calculate responsive scale
- const baseViewport = page.getViewport({ scale: 1, rotation });
- const responsiveScale = Math.min(
- containerWidth / baseViewport.width,
- scale,
- );
-
- const viewport = page.getViewport({ scale: responsiveScale, rotation });
-
- // Fix blurriness by using device pixel ratio
- const devicePixelRatio = window.devicePixelRatio || 1;
- const outputScale = devicePixelRatio;
-
- canvas.width = Math.floor(viewport.width * outputScale);
- canvas.height = Math.floor(viewport.height * outputScale);
- canvas.style.width = Math.floor(viewport.width) + "px";
- canvas.style.height = Math.floor(viewport.height) + "px";
-
- // Enable anti-aliasing and smoothing
- context.imageSmoothingEnabled = true;
- context.imageSmoothingQuality = "high";
-
- const transform =
- outputScale !== 1
- ? [outputScale, 0, 0, outputScale, 0, 0]
- : undefined;
-
- const renderContext = {
- canvasContext: context,
- viewport: viewport,
- transform: transform,
- };
-
- await page.render(renderContext).promise;
- } catch (error) {
- console.error("Error rendering page:", error);
- setHasError(true);
- setErrorMessage("Failed to render PDF page.");
- }
- };
-
- void renderPage();
- }, [pdfDoc, pageNum, scale, rotation]);
-
- const nextPage = () => {
- if (pageNum < pageCount) {
- setPageNum(pageNum + 1);
- }
- };
-
- const prevPage = () => {
- if (pageNum > 1) {
- setPageNum(pageNum - 1);
- }
- };
-
- const zoomIn = () => {
- if (scale < 3) {
- setScale(scale + 0.2);
- }
- };
-
- const zoomOut = () => {
- if (scale > 0.5) {
- setScale(scale - 0.2);
- }
- };
-
- const rotate = () => {
- setRotation((rotation + 90) % 360);
- };
-
- const downloadBlob = () => {
- if (!pdfBlob) return;
-
- const blob = new Blob([pdfBlob as BlobPart], { type: "application/pdf" });
- const url = window.URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = `${title.toLowerCase().replace(/\s+/g, "_")}.pdf`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- window.URL.revokeObjectURL(url);
- };
-
- if (!isClient) {
- return (
-
- {/* Action Bar */}
-
-
-
-
-
{title}
-
- {type === "cv"
- ? "Academic curriculum vitae"
- : "Professional resume"}
-
-
-
-
-
-
- Download
-
-
-
- Open
-
-
-
-
- {/* Loading State */}
-
-
- );
- }
-
- if (hasError) {
- return (
-
- {/* Action Bar */}
-
-
-
-
{title}
-
-
-
-
- Download
-
-
-
-
- View PDF
-
-
-
-
-
-
-
-
- {errorMessage}
-
-
-
-
-
- Download {title}
-
-
-
-
- View PDF in New Tab
-
-
-
-
-
- PDF preview not available. Please use the download or open
- buttons above.
-
-
-
-
-
- );
- }
-
- return (
-
- {/* Action Bar */}
-
-
-
-
{title}
-
-
-
-
- Download
-
-
-
-
- View PDF
-
-
-
-
-
- {/* PDF Controls */}
- {!isLoading && pageCount > 0 && (
-
-
-
-
-
-
- {pageCount > 0 ? `${pageNum} / ${pageCount}` : "Loading..."}
-
- = pageCount}
- >
-
-
-
-
-
-
-
-
-
- {Math.round(scale * 100)}%
-
- = 3}
- >
-
-
-
-
-
-
-
- )}
-
- {/* PDF Canvas */}
-
- {isLoading && (
- <>
-
-
-
-
- Loading {title.toLowerCase()}...
-
-
-
- {/* Loading Skeleton */}
-
- >
- )}
-
-
-
-
-
-
- {/* Mobile Notice */}
-
-
-
-
-
- Better on larger screens
-
-
- For the best viewing experience, try opening this on a tablet or
- desktop. You can also download the PDF directly.
-
-
-
-
-
- );
-}
+export const metadata = { title: "Résumé & CV" };
export default function CVPage() {
- const [activeTab, setActiveTab] = useState("cv");
-
return (
-
-
-
-
-
-
Curriculum Vitae
-
-
-
- My academic and professional experience in computer science, robotics,
- and engineering.
-
-
-
-
-
-
-
-
- Academic CV
-
-
-
- Resume
-
-
-
-
-
-
-
-
-
-
-
-
+
);
}
diff --git a/src/app/experience/page.tsx b/src/app/experience/page.tsx
index 429135c..bc39101 100644
--- a/src/app/experience/page.tsx
+++ b/src/app/experience/page.tsx
@@ -1,21 +1,5 @@
-import {
- FlaskConical,
- GraduationCap,
- Building,
- Users,
- Calendar,
- MapPin,
- Award,
- ExternalLink,
-} from "lucide-react";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "~/components/ui/card";
-import { Badge } from "~/components/ui/badge";
+import Link from "next/link";
+import { PageHeading } from "~/components/PageHeading";
import {
experiences,
awards,
@@ -24,257 +8,129 @@ import {
relevantCoursework,
} from "~/lib/data";
+const sections = [
+ {
+ type: "professional",
+ title: "Professional",
+ id: "professional-experience",
+ },
+ { type: "teaching", title: "Teaching", id: "teaching-experience" },
+ { type: "research", title: "Research", id: "research-experience" },
+ { type: "leadership", title: "Leadership", id: "leadership-and-activities" },
+];
+export const metadata = { title: "Experience" };
export default function ExperiencePage() {
- const researchExperience = experiences.filter(
- (exp) => exp.type === "research",
- );
- const teachingExperience = experiences.filter(
- (exp) => exp.type === "teaching",
- );
- const professionalExperience = experiences.filter(
- (exp) => exp.type === "professional",
- );
- const leadershipExperience = experiences.filter(
- (exp) => exp.type === "leadership",
- );
-
- const getIcon = (type: string) => {
- switch (type) {
- case "research":
- return FlaskConical;
- case "teaching":
- return GraduationCap;
- case "professional":
- return Building;
- case "leadership":
- return Users;
- default:
- return Building;
- }
- };
-
- const renderExperienceSection = (
- title: string,
- experiences: typeof researchExperience,
- delay = 1,
- ) => (
-
- {title}
-
- {experiences.map((exp, index) => {
- const IconComponent = getIcon(exp.type);
- return (
-
-
-
-
-
-
-
-
-
- {exp.title}
-
-
- {exp.organization}
-
-
-
-
- {exp.location}
-
-
-
- {exp.period}
-
-
-
-
-
-
-
-
-
- {exp.description.map((item, itemIndex) => (
-
-
-
- {item}
-
-
- ))}
-
-
-
-
- );
- })}
-
-
- );
-
return (
-
- {/* Header */}
-
-
-
- Building products, running systems, and helping people use them.
- My path through business, engineering, teaching, and research.
-
-
-
- {renderExperienceSection("Professional Experience", professionalExperience, 1)}
- {renderExperienceSection("Teaching Experience", teachingExperience, 2)}
- {renderExperienceSection("Research Experience", researchExperience, 3)}
-
- {/* Leadership & Activities */}
- {renderExperienceSection(
- "Leadership & Activities",
- leadershipExperience,
- 4,
- )}
-
- {/* Technical Skills */}
-
- Technical Skills
-
- {Object.entries(technicalSkills).map(([category, skills], index) => (
-
-
-
- {category}
-
-
-
- {skills.map((skill) => (
-
- {skill}
-
- ))}
-
-
-
-
+
+
+
-
-
- {/* Conferences & Presentations */}
-
- Conferences & Presentations
-
- {conferences.map((conf, index) => (
-
-
-
-
-
-
- {conf.title}
-
-
- {conf.location} • {conf.date}
-
-
-
-
-
-
- {conf.presentation}
- {conf.youtubeUrl && (
-
-
- Watch talk
-
+ Skills
+ Recognition
+
+
+ Résumé & CV ↗
+
+
+ {sections.map((section) => (
+
+ {section.title}
+ {experiences
+ .filter((entry) => entry.type === section.type)
+ .map((entry) => (
+
+
+
{entry.period}
+
{entry.location}
+
+
+
{entry.organization}
+
{entry.title}
+
{entry.description[0]}
+ {entry.description.length > 1 && (
+
+ More about this role
+
+ {entry.description.slice(1).map((line) => (
+ {line}
+ ))}
+
+
)}
-
-
+
+
+ ))}
+
+ ))}
+
+ Tools & skills
+
+ {Object.entries(technicalSkills).map(([category, skills]) => (
+
+
{category}
+
{skills.join(" · ")}
))}
-
- {/* Awards & Recognition */}
-
- Awards & Recognition
-
- {awards.map((award, index) => (
-
-
-
-
-
-
-
- {award.title}
-
- {award.organization && (
-
- {award.organization} • {award.year}
-
- )}
-
-
-
- {award.description && (
-
-
- {award.description}
-
-
- )}
-
+
+ Talks & recognition
+ {conferences.map((item) => (
+
+
+
{item.date}
+
{item.location}
+
+
{item.title}
+
{item.presentation}
+ {item.youtubeUrl && (
+
+ Watch talk ↗
+
+ )}
+
+
+ ))}
+
+
+ Awards & recognition {awards.length}
+
+ {awards.map((item) => (
+
+ {item.title}
+
+ {item.organization} · {item.year}
+
+ {item.description}
+
))}
-
-
-
- {/* Relevant Coursework */}
-
- Relevant Coursework
-
-
-
-
- {relevantCoursework.map((course, index) => (
-
-
-
- {course}
-
-
- ))}
-
-
-
-
+
+
+ Relevant coursework
+
+ {relevantCoursework.map((course) => (
+ {course}
+ ))}
+
+
);
diff --git a/src/app/identity/page.tsx b/src/app/identity/page.tsx
new file mode 100644
index 0000000..bbe4f9a
--- /dev/null
+++ b/src/app/identity/page.tsx
@@ -0,0 +1,19 @@
+import type { Metadata } from "next";
+import { PageHeading } from "~/components/PageHeading";
+import { IdentityChoices } from "~/components/IdentityChoices";
+export const metadata: Metadata = {
+ title: "Identity directions",
+ robots: { index: false, follow: false },
+};
+export default function IdentityPage() {
+ return (
+
+ );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 2123b86..60c516e 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -4,9 +4,8 @@ import { env } from "~/env";
import { Footer } from "~/components/Footer";
import { Navigation } from "~/components/Navigation";
import { SiteFrame } from "~/components/SiteFrame";
-import { BreadcrumbProvider } from "~/context/BreadcrumbContext";
-import { manrope, instrument } from "~/lib/fonts";
+import { instrument } from "~/lib/fonts";
import { description, name } from "~/lib/data";
import "~/styles/globals.css";
import { cn } from "~/lib/utils";
@@ -14,14 +13,14 @@ import { cn } from "~/lib/utils";
export const metadata: Metadata = {
title: `${name[0]?.first} ${name[0]?.last}`,
description: description,
- icons: [{ rel: "icon", url: "/monogram.svg", type: "image/svg+xml" }],
+ icons: [{ rel: "icon", url: "/favicon-dot.svg", type: "image/svg+xml" }],
};
export default function RootLayout({ children }: React.PropsWithChildren) {
return (
-
-
- {children}
-
+
+ {children}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index df0cca1..635468e 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,4 +1,6 @@
+import { ContourLines } from "~/components/ContourLines";
import Image from "next/image";
+import { JusticeParagraph } from "~/components/JusticeParagraph";
import { ManyanglesPreview } from "~/components/ManyanglesPreview";
import { BeenvoicePreview } from "~/components/BeenvoicePreview";
import Link from "next/link";
@@ -55,13 +57,14 @@ export default function HomePage() {
return (
+
Software engineer & founder
Boston, MA
-
+
Sean O’Connor.
@@ -130,13 +133,13 @@ export default function HomePage() {
sizes="(max-width: 700px) 100vw, 50vw"
/>
)}
- {project.name === "Racetix" && (
-
+ {(project.name === "Racetix" || project.name === "beenvoice") && (
+
)}
@@ -149,7 +152,7 @@ export default function HomePage() {
{project.category}
-
{project.description}
+
{project.stack}
))}
@@ -159,12 +162,8 @@ export default function HomePage() {
My company
-
Hadlock Technologies
-
- I started Hadlock in 2026 to bring my software and infrastructure
- work together. I work directly with businesses, from figuring out
- what they need to building it and keeping it running.
-
+
+
hadlockCMS
@@ -199,24 +198,12 @@ export default function HomePage() {
-
- I started working on technology at Riverhead Raceway in 2020. Over
- time that grew from IT into building the website, live race systems,
- and software used around the track.
-
-
- At Bucknell, I studied Computer Science and Engineering, built tools
- for robotics research, and helped other students learn to build
- things. Now I’m at BU, aiming to finish my master’s in a year while
- continuing my own projects.
-
+
+
Outside of software, I’m into specialty coffee, Formula One, and
- travel. I cofounded Bucknell’s Coffee Society—and, naturally, made a
-
+ travel. I cofounded Bucknell’s Coffee Society—and, naturally, made a{" "}
+
website for finding coffee in Lewisburg
.
@@ -225,22 +212,64 @@ export default function HomePage() {
Experience
-
- Trips & interests
+
+ Résumé & CV
+
+ Writing
+ Publications
+ In the media
+
-
-
-
Get in touch
-
Always happy to talk.
+
+
+
);
}
diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx
index 67496be..2099007 100644
--- a/src/app/projects/page.tsx
+++ b/src/app/projects/page.tsx
@@ -1,11 +1,6 @@
-import Image from "next/image";
-import { ManyanglesPreview } from "~/components/ManyanglesPreview";
-import { BeenvoicePreview } from "~/components/BeenvoicePreview";
-import Link from "next/link";
-import { ArrowUpRight } from "lucide-react";
-import type { Metadata } from "next";
+import { ProjectExplorer } from "~/components/ProjectExplorer";
import { projects } from "~/lib/data";
-
+import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Work — Sean O’Connor",
description:
@@ -32,71 +27,18 @@ const orderedProjects = [...projects].sort((a, b) => {
export default function ProjectsPage() {
return (
-
+
- The portfolio
+ Work / Products & experiments
Things I’ve built.
- Products, client work, and a few explorations. From the interface to
- the infrastructure, I like taking an idea all the way through.
+ From the first sketch to the parts that run it. Products, client work,
+ and a few experiments along the way.
-
- {orderedProjects.map((project, index) => (
-
- {project.image && (
-
- {project.title === "beenvoice" ? (
-
- ) : project.title === "Manyangles" ? (
-
- ) : (
-
- )}
-
- )}
-
-
{project.title}
-
{project.longDescription ?? project.description}
-
- {project.tags.map((tag) => (
- {tag}
- ))}
-
-
-
-
- ))}
-
+
);
}
diff --git a/src/app/publications/page.tsx b/src/app/publications/page.tsx
index bbdd923..83baf8c 100644
--- a/src/app/publications/page.tsx
+++ b/src/app/publications/page.tsx
@@ -1,238 +1,92 @@
-"use client";
-
-import {
- ArrowUpRight,
- BookOpenText,
- FileText,
- Presentation,
- Monitor,
-} from "lucide-react";
-import Link from "next/link";
-import { useEffect, useState } from "react";
-import { Badge } from "~/components/ui/badge";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "~/components/ui/card";
-import { CardSkeleton } from "~/components/ui/skeletons";
-import type { Publication } from "~/lib/bibtex";
+import { readFile } from "node:fs/promises";
+import path from "node:path";
+import { PageHeading } from "~/components/PageHeading";
import { parseBibtex } from "~/lib/bibtex";
-// No custom tracking needed - Vercel automatically tracks page views
-
-export default function PublicationsPage() {
- const [publications, setPublications] = useState
([]);
- const [loading, setLoading] = useState(true);
-
-
- useEffect(() => {
- void fetch("/publications.bib")
- .then((res) => res.text())
- .then((text) => {
- const pubs = parseBibtex(text);
- setPublications(pubs);
- setLoading(false);
- });
- }, []);
-
- const downloadBibtex = (pub: Publication) => {
- const {
- title,
- authors,
- venue,
- year,
- doi,
- abstract,
- type,
- citationType,
- citationKey,
- } = pub;
- let bibtexEntry = `@${citationType}{${citationKey},\n`;
- bibtexEntry += ` title = {${title}},\n`;
- bibtexEntry += ` author = {${authors.join(" and ")}},\n`;
- bibtexEntry += ` year = {${year}},\n`;
- if (type === "conference" || type === "workshop") {
- bibtexEntry += ` organization = {${venue}},\n`;
- } else if (type === "journal") {
- bibtexEntry += ` journal = {${venue}},\n`;
- } else if (type === "thesis") {
- bibtexEntry += ` school = {${venue}},\n`;
- }
- if (doi) {
- bibtexEntry += ` doi = {${doi}},\n`;
- }
- if (abstract) {
- bibtexEntry += ` abstract = {${abstract}},\n`;
- }
- bibtexEntry += `}\n`;
-
- const blob = new Blob([bibtexEntry], { type: "text/plain" });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = `${citationKey}.bib`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- };
-
- // No custom click handlers needed - Vercel automatically tracks API route access
+export const metadata = { title: "Publications" };
+export default async function PublicationsPage() {
+ const source = await readFile(
+ path.join(process.cwd(), "public/publications.bib"),
+ "utf8",
+ );
+ const publications = parseBibtex(source).sort((a, b) => b.year - a.year);
+ const entries = source.split(/(?=@\w+\s*\{)/);
return (
-
-
-
-
-
-
- Peer-Reviewed Publications
-
-
-
-
- My research publications in human-robot interaction and robotics.
-
-
-
-
- {loading ? (
- <>
-
-
-
- >
- ) : (
- publications.map((pub, index) => (
-
-
-
-
-
- {pub.title}
-
- {pub.paperUrl && (
-
-
-
- )}
-
-
- {pub.authors.join(", ")}
-
-
- {pub.venue} ({pub.year})
-
- {pub.address && (
-
- {pub.address}
-
+
+
+
+
+ {publications.map((pub) => {
+ const bib = entries.find((entry) =>
+ entry.includes(`{${pub.citationKey},`),
+ );
+ return (
+
+
+
{pub.year}
+
{pub.type}
+
+
+
{pub.venue}
+
{pub.title}
+
{pub.authors.join(", ")}
+ {pub.notes &&
{pub.notes}
}
+
+ {pub.paperUrl && (
+
+ Paper ↗
+
)}
- {pub.notes && (
-
-
- {pub.notes}
-
-
+ {pub.posterUrl && (
+
+ Poster ↗
+
)}
-
-
- {pub.abstract && (
-
- {pub.abstract}
-
+ {pub.slidesUrl && (
+
+ Slides ↗
+
)}
-
-
- {pub.type}
-
- {pub.doi && (
-
-
-
- DOI
-
-
- )}
- {pub.paperUrl && (
-
-
-
- Paper
-
-
- )}
- {pub.posterUrl && (
-
-
-
- Poster
-
-
- )}
- {pub.slidesUrl && (
-
-
-
- Slides
-
-
- )}
-
downloadBibtex(pub)}
- className="flex h-6 cursor-pointer items-center capitalize"
- variant="secondary"
+ DOI ↗
+
+ )}
+ {bib && (
+
-
- BibTeX
-
-
-
-
-
- ))
- )}
+ BibTeX ↓
+
+ )}
+
+ {pub.abstract && (
+
+ Read abstract
+ {pub.abstract}
+
+ )}
+
+
+ );
+ })}
);
diff --git a/src/app/travel/page.tsx b/src/app/travel/page.tsx
index 5c1974e..9e43714 100644
--- a/src/app/travel/page.tsx
+++ b/src/app/travel/page.tsx
@@ -1,102 +1,47 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import {
- Card,
- CardHeader,
- CardTitle,
- CardContent,
- CardDescription,
-} from "~/components/ui/card";
-import { CardSkeleton } from "~/components/ui/skeletons";
-import { ImageWithSkeleton } from "~/components/ui/image-with-skeleton";
-import { Badge } from "~/components/ui/badge";
-import { Plane } from "lucide-react";
+import Image from "next/image";
+import { PageHeading } from "~/components/PageHeading";
+import { JusticeParagraph } from "~/components/JusticeParagraph";
import { travel } from "~/lib/data";
export default function TripsPage() {
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- const timer = setTimeout(() => {
- setLoading(false);
- }, 1000);
- return () => clearTimeout(timer);
- }, []);
-
return (
-
-
-
-
-
-
My Trips & Events
-
-
-
- A collection of memorable trips and events I've attended.
-
-
-
-
- {loading ? (
- <>
-
-
-
-
-
-
- >
- ) : (
- travel.map((trip, index) => (
+
+
+
+ {travel.map((trip, index) => (
+
-
-
-
-
- {trip.images.map((image, imgIndex) => (
-
-
-
- ))}
-
-
-
-
-
-
- {trip.title}
-
- {trip.description}
-
-
- {/* Show badges for tags */}
-
- {trip.tags.map((tag, tagIndex) => (
-
- {tag}
-
- ))}
-
-
-
-
+ {trip.images.map((src, i) => (
+
+ ))}
- ))
- )}
+
+
+ {String(index + 1).padStart(2, "0")} / {trip.tags.join(" · ")}
+
+
{trip.title}
+
+
Scroll through the photos ↔
+
+
+ ))}
);
diff --git a/src/app/type-study/page.tsx b/src/app/type-study/page.tsx
new file mode 100644
index 0000000..2e8f5c3
--- /dev/null
+++ b/src/app/type-study/page.tsx
@@ -0,0 +1,23 @@
+import type { Metadata } from "next";
+import { JusticeComparison } from "~/components/JusticeComparison";
+
+export const metadata: Metadata = {
+ title: "Typography comparison — Sean O’Connor",
+ robots: { index: false, follow: false },
+};
+
+export default function TypeStudyPage() {
+ return (
+
+
+ Typography study
+ A little more considered.
+
+ Native wrapping and Justice, side by side with the same font, size,
+ and copy. This is a preview; the main site is unchanged.
+
+
+
+
+ );
+}
diff --git a/src/components/CoffeeMirror.tsx b/src/components/CoffeeMirror.tsx
new file mode 100644
index 0000000..af9cc6c
--- /dev/null
+++ b/src/components/CoffeeMirror.tsx
@@ -0,0 +1,216 @@
+"use client";
+
+import { useEffect, useMemo, useRef, useState } from "react";
+import Image from "next/image";
+import type { Map as LeafletMap, Marker, TileLayer } from "leaflet";
+import {
+ ChevronLeft, Coffee, Heart, Layers, LocateFixed, MapPin,
+ Minus, PanelLeft, Plus, Search, X,
+} from "lucide-react";
+import { COFFEE_SHOPS, type CoffeeShop } from "~/lib/coffee-shops";
+import "leaflet/dist/leaflet.css";
+
+type MapStyle = "street" | "satellite";
+
+const images = [
+ "amami-kitchen", "culture-coffee", "7th-st-cafe", "tastecraft-cafe",
+ "paris-bakery-caf", "cycleup-coffee", "cornerstone-kitchen", "gram-s-eatery",
+ "starbucks-coffee", "barnes-noble-caf", "starbucks-giant", "dunkin",
+ "alee-s-cafe", "all-star-bagels", "panera-bread", "street-of-shops",
+];
+
+function shopImage(shop: CoffeeShop) {
+ return `/coffee/shops/${images[shop.id - 1]}.jpg`;
+}
+
+const tiles: Record
= {
+ street: {
+ url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
+ attribution: '© OpenStreetMap contributors',
+ },
+ satellite: {
+ url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
+ attribution: "Tiles © Esri",
+ },
+};
+
+export function CoffeeMirror() {
+ const [selected, setSelected] = useState(null);
+ const [drawerOpen, setDrawerOpen] = useState(true);
+ const [query, setQuery] = useState("");
+ const [favorites, setFavorites] = useState([]);
+ const [style, setStyle] = useState("street");
+ const [styleOpen, setStyleOpen] = useState(false);
+ const [welcomeOpen, setWelcomeOpen] = useState(false);
+ const [mapFailed, setMapFailed] = useState(false);
+ const [locationError, setLocationError] = useState("");
+ const mapElement = useRef(null);
+ const map = useRef(null);
+ const tileLayer = useRef(null);
+ const markers = useRef(new Map());
+
+ useEffect(() => {
+ const timeout = window.setTimeout(() => {
+ setDrawerOpen(window.innerWidth >= 640);
+ try {
+ const saved = JSON.parse(localStorage.getItem("lewisburg-coffee-favorites") ?? "[]") as unknown;
+ if (Array.isArray(saved)) setFavorites(saved.filter((id): id is number => typeof id === "number"));
+ setWelcomeOpen(!localStorage.getItem("hasSeenWelcome"));
+ } catch {
+ setFavorites([]);
+ }
+ }, 0);
+ return () => window.clearTimeout(timeout);
+ }, []);
+
+ useEffect(() => {
+ let cancelled = false;
+ let resizeObserver: ResizeObserver | undefined;
+ const savedMarkers = markers.current;
+ import("leaflet").then((L) => {
+ if (cancelled || !mapElement.current) return;
+ const instance = L.map(mapElement.current, {
+ center: [40.9645, -76.8845], zoom: 15, zoomControl: false,
+ });
+ map.current = instance;
+ tileLayer.current = L.tileLayer(tiles.street.url, {
+ attribution: tiles.street.attribution, maxZoom: 19,
+ }).addTo(instance);
+ COFFEE_SHOPS.forEach((shop) => {
+ const marker = L.marker([shop.lat, shop.lng], {
+ title: shop.name,
+ icon: L.divIcon({ className: "coffee-mirror-pin", html: " ", iconSize: [18, 18], iconAnchor: [9, 9] }),
+ }).addTo(instance);
+ marker.bindTooltip(shop.name);
+ marker.on("click", () => {
+ setSelected(shop);
+ setDrawerOpen(true);
+ });
+ markers.current.set(shop.id, marker);
+ });
+ resizeObserver = new ResizeObserver(() => instance.invalidateSize());
+ resizeObserver.observe(mapElement.current);
+ requestAnimationFrame(() => instance.invalidateSize());
+ }).catch(() => { if (!cancelled) setMapFailed(true); });
+ return () => {
+ cancelled = true;
+ resizeObserver?.disconnect();
+ map.current?.remove();
+ map.current = null;
+ tileLayer.current = null;
+ savedMarkers.clear();
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!map.current || !tileLayer.current) return;
+ const L = tileLayer.current;
+ L.setUrl(tiles[style].url);
+ L.options.attribution = tiles[style].attribution;
+ }, [style]);
+
+ useEffect(() => {
+ if (!selected || !map.current) return;
+ const instance = map.current;
+ const target = instance.project([selected.lat, selected.lng], 16);
+ const offset = window.innerWidth >= 640 && drawerOpen ? 200 : 0;
+ instance.flyTo(instance.unproject(target.subtract([offset, 0]), 16), 16, { duration: 0.8 });
+ markers.current.forEach((marker, id) => marker.getElement()?.classList.toggle("is-selected", id === selected.id));
+ }, [selected, drawerOpen]);
+
+ const filtered = useMemo(() => COFFEE_SHOPS.filter((shop) =>
+ `${shop.name} ${shop.address}`.toLowerCase().includes(query.toLowerCase().trim()),
+ ), [query]);
+ const favoriteShops = filtered.filter((shop) => favorites.includes(shop.id));
+ const otherShops = filtered.filter((shop) => !favorites.includes(shop.id));
+
+ function toggleFavorite(id: number) {
+ setFavorites((current) => {
+ const next = current.includes(id) ? current.filter((item) => item !== id) : [...current, id];
+ localStorage.setItem("lewisburg-coffee-favorites", JSON.stringify(next));
+ return next;
+ });
+ }
+
+ function closeWelcome() {
+ setWelcomeOpen(false);
+ localStorage.setItem("hasSeenWelcome", "true");
+ }
+
+ function locate() {
+ setLocationError("");
+ if (!navigator.geolocation) { setLocationError("Location is unavailable in this browser."); return; }
+ navigator.geolocation.getCurrentPosition(
+ ({ coords }) => map.current?.flyTo([coords.latitude, coords.longitude], 16),
+ () => setLocationError("Couldn’t get your location."),
+ );
+ }
+
+ return (
+
+
+ {mapFailed &&
The map could not load. You can still browse the coffee shops.
}
+
+
+ setDrawerOpen((open) => !open)} aria-label="Toggle coffee shops" aria-expanded={drawerOpen}>
+ setWelcomeOpen(true)}>
+
+ Lewisburg Coffee Map Find your perfect brew
+
+
+
+
+
+ {selected ? (
+
+
+
+ toggleFavorite(selected.id)} aria-label={favorites.includes(selected.id) ? "Remove favorite" : "Add favorite"}>
+ setSelected(null)} aria-label="Back to coffee shops">
+ { setSelected(null); setDrawerOpen(false); }} aria-label="Close panel">
+
+
+
{selected.name}
+
{selected.address}
+
{selected.description}
+
Get Directions ↗
+
+
+
+ ) : (
+
+
+
Coffee Shops
+ setQuery(event.target.value)} aria-label="Search coffee shops" />
+
+
+ {favoriteShops.length > 0 && <>
♥ Favorites
{favoriteShops.map((shop) =>
)}
>}
+ {favoriteShops.length > 0 &&
All Shops
}
+ {otherShops.map((shop) =>
)}
+ {filtered.length === 0 &&
No shops found matching “{query}”
}
+
+
+ )}
+
+
+
+ {locationError &&
{locationError} }
+
+
map.current?.zoomIn()} aria-label="Zoom in">
+
map.current?.zoomOut()} aria-label="Zoom out">
+
+
setStyleOpen((open) => !open)} aria-label="Change map style" aria-expanded={styleOpen}>
+ {styleOpen &&
{(["street", "satellite"] as const).map((option) => { setStyle(option); setStyleOpen(false); }}>{option.charAt(0).toUpperCase() + option.slice(1)} )}
}
+
+
+
+
© {new Date().getFullYear()} Sean O’Connor Map data © OpenStreetMap contributors
+
+ {welcomeOpen &&
event.stopPropagation()}>
Welcome to the Lewisburg Coffee Map Discover the best coffee spots in Lewisburg, PA.
Created by Sean O’Connor Explore the map and select a coffee shop. Search the list to find your next cup. Use the location button to see what’s nearby. Start Exploring }
+
+ );
+}
+
+function ShopRow({ shop, favorite, onSelect, onFavorite }: { shop: CoffeeShop; favorite: boolean; onSelect: (shop: CoffeeShop) => void; onFavorite: (id: number) => void }) {
+ return onSelect(shop)}>{shop.name} {shop.address} onFavorite(shop.id)} aria-label={favorite ? `Remove ${shop.name} from favorites` : `Add ${shop.name} to favorites`}>
;
+}
diff --git a/src/components/ContourLines.tsx b/src/components/ContourLines.tsx
new file mode 100644
index 0000000..6c31d9b
--- /dev/null
+++ b/src/components/ContourLines.tsx
@@ -0,0 +1,19 @@
+export function ContourLines() {
+ return (
+
+ {[0, 1, 2, 3, 4, 5].map((line) => (
+
+ ))}
+
+ );
+}
diff --git a/src/components/DocumentReader.tsx b/src/components/DocumentReader.tsx
new file mode 100644
index 0000000..7e80622
--- /dev/null
+++ b/src/components/DocumentReader.tsx
@@ -0,0 +1,274 @@
+"use client";
+import { useEffect, useRef, useState } from "react";
+import type {
+ PDFDocumentProxy,
+ PDFDocumentLoadingTask,
+ RenderTask,
+} from "pdfjs-dist";
+
+const documents = [
+ { id: "resume", title: "Résumé", url: "/publications/resume.pdf" },
+ { id: "cv", title: "Academic CV", url: "/publications/cv.pdf" },
+];
+
+function DocumentPage({
+ pdf,
+ page,
+ width,
+ zoom,
+ title,
+}: {
+ pdf: PDFDocumentProxy;
+ page: number;
+ width: number;
+ zoom: number;
+ title: string;
+}) {
+ const canvas = useRef(null);
+ const [error, setError] = useState(false);
+ useEffect(() => {
+ if (!width) return;
+ let disposed = false;
+ let render: RenderTask | undefined;
+ void pdf
+ .getPage(page)
+ .then(async (sheet) => {
+ if (disposed || !canvas.current) return;
+ const target = canvas.current;
+ const context = target.getContext("2d");
+ if (!context) return;
+ const base = sheet.getViewport({ scale: 1 });
+ const viewport = sheet.getViewport({
+ scale: (Math.min(width, 900) / base.width) * zoom,
+ });
+ const ratio = window.devicePixelRatio || 1;
+ target.width = Math.floor(viewport.width * ratio);
+ target.height = Math.floor(viewport.height * ratio);
+ target.style.width = viewport.width + "px";
+ target.style.height = viewport.height + "px";
+ render = sheet.render({
+ canvasContext: context,
+ viewport,
+ transform: [ratio, 0, 0, ratio, 0, 0],
+ });
+ await render.promise;
+ })
+ .catch((reason: unknown) => {
+ if (
+ !disposed &&
+ !(
+ reason instanceof Error &&
+ reason.name === "RenderingCancelledException"
+ )
+ )
+ setError(true);
+ });
+ return () => {
+ disposed = true;
+ render?.cancel();
+ };
+ }, [pdf, page, zoom, width]);
+
+ return (
+
+ {error ? (
+
+ Page {page} couldn’t render. Use Open PDF above to read it.
+
+ ) : (
+
+ )}
+
+ );
+}
+
+function Preview({ url, title }: { url: string; title: string }) {
+ const frame = useRef(null);
+ const [pdf, setPdf] = useState(null);
+ const [error, setError] = useState(false);
+ const [page, setPage] = useState(1);
+ const [zoom, setZoom] = useState(1);
+ const [width, setWidth] = useState(0);
+
+ useEffect(() => {
+ let disposed = false;
+ let task: PDFDocumentLoadingTask | undefined;
+ void import("pdfjs-dist")
+ .then(async (lib) => {
+ if (disposed) return;
+ lib.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.js";
+ task = lib.getDocument(url);
+ const document = await task.promise;
+ if (!disposed) setPdf(document);
+ })
+ .catch(() => {
+ if (!disposed) setError(true);
+ });
+ return () => {
+ disposed = true;
+ void task?.destroy();
+ };
+ }, [url]);
+
+ useEffect(() => {
+ if (!frame.current) return;
+ const observer = new ResizeObserver(([entry]) => {
+ if (entry) setWidth(Math.max(1, entry.contentRect.width - 32));
+ });
+ observer.observe(frame.current);
+ return () => observer.disconnect();
+ }, []);
+
+ const updateCurrentPage = () => {
+ const container = frame.current;
+ if (!container) return;
+ const readingLine =
+ container.getBoundingClientRect().top +
+ Math.min(120, container.clientHeight / 4);
+ const sheets = Array.from(
+ container.querySelectorAll("[data-page]"),
+ );
+ const current = sheets.find(
+ (sheet) => sheet.getBoundingClientRect().bottom > readingLine,
+ );
+ if (current) setPage(Number(current.dataset.page));
+ };
+
+ const goToPage = (next: number) => {
+ const container = frame.current;
+ const sheet = container?.querySelector(
+ `[data-page="${next}"]`,
+ );
+ if (!container || !sheet) return;
+ container.scrollTo({
+ top:
+ container.scrollTop +
+ sheet.getBoundingClientRect().top -
+ container.getBoundingClientRect().top -
+ 16,
+ behavior: "auto",
+ });
+ setPage(next);
+ };
+
+ return (
+ <>
+
+
+ goToPage(page - 1)}
+ >
+ ←
+
+
+ {pdf ? `Page ${page} of ${pdf.numPages}` : "Loading PDF…"}
+
+ = pdf.numPages}
+ onClick={() => goToPage(page + 1)}
+ >
+ →
+
+
+
+ setZoom((n) => Math.max(0.75, n - 0.25))}
+ >
+ −
+
+ setZoom(1)} aria-label="Fit PDF to width">
+ {zoom === 1 ? "Fit width" : Math.round(zoom * 100) + "%"}
+
+ = 2}
+ onClick={() => setZoom((n) => Math.min(2, n + 0.25))}
+ >
+ +
+
+
+
+
+ {error ? (
+
+ The preview couldn’t load. You can still open or download the PDF
+ above.
+
+ ) : (
+ <>
+ {pdf &&
+ Array.from({ length: pdf.numPages }, (_, index) => (
+
+ ))}
+ {!pdf && (
+
+ Preparing your preview…
+
+ )}
+ >
+ )}
+
+ >
+ );
+}
+
+export function DocumentReader() {
+ const [active, setActive] = useState("resume");
+ const document = documents.find((item) => item.id === active)!;
+ return (
+
+
+
+ {documents.map((item) => (
+ setActive(item.id)}
+ >
+ {item.title}
+
+ ))}
+
+
+
+
+
+ Open the PDF for text selection, search, and printing.
+
+
+ );
+}
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
index 3c79c84..a8d5f69 100644
--- a/src/components/Footer.tsx
+++ b/src/components/Footer.tsx
@@ -1,19 +1,41 @@
+"use client";
+
import Link from "next/link";
-import { Monogram } from "~/components/Monogram";
+import { usePathname } from "next/navigation";
+import { ArrowUpRight } from "lucide-react";
+import { ContourLines } from "~/components/ContourLines";
export function Footer() {
+ const pathname = usePathname();
+ if (pathname === "/coffee") return null;
return (
+
+ {pathname === "/" && (
+
+ )}
-
Sean O’Connor
+
Projects
+
Writing
Experience
Research
In the press
-
Elsewhere
+
Coffee map
+
Trips & interests
+
October
+
Résumé & CV
GitHub
LinkedIn
diff --git a/src/components/IdentityChoices.tsx b/src/components/IdentityChoices.tsx
new file mode 100644
index 0000000..d70e683
--- /dev/null
+++ b/src/components/IdentityChoices.tsx
@@ -0,0 +1,120 @@
+"use client";
+
+import { useState } from "react";
+
+type Direction = "linked" | "roundel" | "name";
+function Mark({ direction }: { direction: Direction }) {
+ if (direction === "name")
+ return (
+
+ Sean O’Connor.
+
+ );
+ return (
+
+ {direction === "linked" ? (
+
+
+
+
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+ );
+}
+const choices = [
+ {
+ id: "linked" as const,
+ letter: "A",
+ title: "Linked initials",
+ description:
+ "A bolder S and O sharing a curve. Compact, readable, and less formal.",
+ },
+ {
+ id: "roundel" as const,
+ letter: "B",
+ title: "The roundel",
+ description:
+ "An S cut into an O-shaped disc. A distinct little symbol, even at favicon size.",
+ },
+ {
+ id: "name" as const,
+ letter: "C",
+ title: "Name first",
+ description:
+ "A clean typographic signature. Your name becomes the identity.",
+ },
+];
+export function IdentityChoices() {
+ const [selected, setSelected] = useState
("linked");
+ return (
+
+
+
+
+ {selected !== "name" && Sean O’Connor }
+
+
+ Projects Writing About
+
+
+
+ Previewing {choices.find((c) => c.id === selected)?.title}. Choose an
+ option below to see it in the header.
+
+
+ {choices.map((choice) => (
+
setSelected(choice.id)}
+ >
+
+
+
+
+
+ {choice.letter} {choice.title}
+
+
{choice.description}
+
+
+
+ ))}
+
+
+ My preference is A for the site, with B worth considering if you want a
+ stronger standalone icon. These are previews; the current site mark
+ hasn’t changed.
+
+
+ );
+}
diff --git a/src/components/JusticeComparison.tsx b/src/components/JusticeComparison.tsx
new file mode 100644
index 0000000..2b26a24
--- /dev/null
+++ b/src/components/JusticeComparison.tsx
@@ -0,0 +1,79 @@
+"use client";
+
+import { useState } from "react";
+import { JusticeParagraph } from "~/components/JusticeParagraph";
+
+const samples = [
+ {
+ label: "About",
+ paragraphs: [
+ "I started working on technology at Riverhead Raceway in 2020. Over time that grew from IT into building the website, live race systems, and software used around the track.",
+ "At Bucknell, I studied Computer Science and Engineering, built tools for robotics research, and helped other students learn to build things. Now I’m at BU, aiming to finish my master’s in a year while continuing my own projects.",
+ ],
+ },
+ {
+ label: "Project description · beenvoice",
+ paragraphs: [
+ "Manage clients, track time, send recurring invoices, and follow payments across a web app and mobile companion. The Expo app includes iOS widgets, while shared domain logic and a background job worker support the product across platforms.",
+ ],
+ },
+];
+
+export function JusticeComparison() {
+ const [width, setWidth] = useState(440);
+ return (
+
+
+
+ Column width {width}px
+
+
setWidth(Number(event.target.value))}
+ />
+
+ Both columns use Manrope at 15px. Drag to compare line breaks; columns
+ shrink to fit smaller screens.
+
+
+ {samples.map((sample) => (
+
+ {sample.label}
+
+
+
+ Native wrapping Current site
+
+
+ {sample.paragraphs.map((text) => (
+
+ {text}
+
+ ))}
+
+
+
+
+ Justice Justified · conservative spacing
+
+
+ {sample.paragraphs.map((text) => (
+
+ ))}
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/components/JusticeParagraph.tsx b/src/components/JusticeParagraph.tsx
new file mode 100644
index 0000000..d285043
--- /dev/null
+++ b/src/components/JusticeParagraph.tsx
@@ -0,0 +1,135 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { lineText, prepare, solve } from "@kitlangton/justice";
+
+type DisplayLine = {
+ text: string;
+ spacing: number;
+ tracking: number;
+ opening: number;
+};
+
+export function JusticeParagraph({
+ text,
+ className = "",
+}: {
+ text: string;
+ className?: string;
+}) {
+ const ref = useRef(null);
+ const [lines, setLines] = useState(null);
+
+ useEffect(() => {
+ const paragraph = ref.current;
+ if (!paragraph) return;
+ let disposed = false;
+ let observer: ResizeObserver | undefined;
+ let frame = 0;
+
+ void document.fonts.ready.then(() => {
+ if (disposed) return;
+ let prepared: ReturnType | null = null;
+ let measuredFont = "";
+ const update = () => {
+ if (disposed) return;
+ const width = paragraph.getBoundingClientRect().width;
+ if (width <= 0) return;
+ const style = getComputedStyle(paragraph);
+ const fontKey = [
+ style.fontFamily,
+ style.fontSize,
+ style.fontWeight,
+ style.fontStyle,
+ style.fontStretch,
+ ].join("|");
+ if (!prepared || measuredFont !== fontKey) {
+ const probe = document.createElement("span");
+ Object.assign(probe.style, {
+ position: "absolute",
+ visibility: "hidden",
+ whiteSpace: "pre",
+ fontFamily: style.fontFamily,
+ fontSize: style.fontSize,
+ fontWeight: style.fontWeight,
+ fontStyle: style.fontStyle,
+ fontStretch: style.fontStretch,
+ fontKerning: "none",
+ fontVariantLigatures: "none",
+ letterSpacing: "0px",
+ wordSpacing: "0px",
+ });
+ probe.setAttribute("aria-hidden", "true");
+ document.body.appendChild(probe);
+ prepared = prepare(text, (fragment) => {
+ probe.textContent = fragment;
+ return probe.getBoundingClientRect().width;
+ });
+ prepared.hyphenation = undefined;
+ probe.remove();
+ measuredFont = fontKey;
+ }
+ const measured = prepared;
+ const layout = solve(measured, width, {
+ mode: "balanced",
+ tracking: 0.3,
+ stretch: 0.6,
+ shrink: 0.25,
+ hanging: 0,
+ opening: 0,
+ });
+ setLines(
+ layout.lines.length &&
+ layout.lines.every((line) => line.residual >= -0.5)
+ ? layout.lines.map((line) => ({
+ text: lineText(measured, line),
+ spacing: line.wordSpacing,
+ tracking: line.tracking,
+ opening: line.opening,
+ }))
+ : null,
+ );
+ };
+ let previousWidth = -1;
+ observer = new ResizeObserver(([entry]) => {
+ const width = entry?.contentRect.width ?? 0;
+ if (Math.abs(width - previousWidth) < 0.25) return;
+ previousWidth = width;
+ cancelAnimationFrame(frame);
+ frame = requestAnimationFrame(update);
+ });
+ observer.observe(paragraph);
+ update();
+ });
+ return () => {
+ disposed = true;
+ observer?.disconnect();
+ cancelAnimationFrame(frame);
+ };
+ }, [text]);
+
+ return (
+
+ {lines
+ ? lines.map((line, i) => (
+
+ {line.text}
+ {i < lines.length - 1 ? " " : ""}
+
+ ))
+ : text}
+
+ );
+}
diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx
index 48a51c5..6b06665 100644
--- a/src/components/Navigation.tsx
+++ b/src/components/Navigation.tsx
@@ -1,83 +1,142 @@
"use client";
-import { ArrowUpRight, Menu, X } from "lucide-react";
-import { usePathname } from "next/navigation";
import Link from "next/link";
-import { Monogram } from "~/components/Monogram";
-import { useRef, useState } from "react";
+import { usePathname } from "next/navigation";
+import { useState } from "react";
+import { createPortal } from "react-dom";
+import { ChevronDown, Menu, X, Home, Book, Newspaper, FolderGit2, BookOpenText, Briefcase, Plane, FileText } from "lucide-react";
+import { AnimatePresence, motion, useReducedMotion } from "motion/react";
+import { Button } from "~/components/ui/button";
+import {
+ NavigationMenu, NavigationMenuList, NavigationMenuItem, NavigationMenuLink,
+} from "~/components/ui/navigation-menu";
+import {
+ DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
+ DropdownMenuGroup, DropdownMenuItem,
+} from "~/components/ui/dropdown-menu";
-const items = [
- { href: "/projects", label: "Work" },
- { href: "/#about", label: "About" },
- { href: "/blog", label: "Writing" },
- { href: "/cv", label: "Résumé" },
+const backgroundPages = [
+ { href: "/experience", label: "Experience" },
+ { href: "/publications", label: "Publications" },
+ { href: "/articles", label: "In the media" },
];
+const mainPages = [
+ { href: "/#work", label: "Work" },
+ { href: "/#about", label: "About" },
+ { href: "/#contact", label: "Contact" },
+];
+
+// Restored from the September 10 header (52a2177).
+const mobilePages = [
+ { href: "/", label: "About", icon: Home },
+ { href: "/blog", label: "Blog", icon: Book },
+ { href: "/articles", label: "Articles", icon: Newspaper },
+ { href: "/projects", label: "Projects", icon: FolderGit2 },
+ { href: "/publications", label: "Publications", icon: BookOpenText },
+ { href: "/experience", label: "Experience", icon: Briefcase },
+ { href: "/travel", label: "Travel", icon: Plane },
+ { href: "/cv", label: "Résumé & CV", icon: FileText },
+];
+
+function HeaderDropdown({ mobile = false }: { mobile?: boolean }) {
+ const [open, setOpen] = useState(false);
+ const pathname = usePathname();
+ const reducedMotion = useReducedMotion();
+ const items = (pages: typeof mainPages) => pages.map((item) => (
+
+
+ {item.label}
+
+
+ ));
+
+ return (
+ <>
+ {mobile && typeof document !== "undefined" && createPortal(
+
+ {open && (
+ setOpen(false)}
+ />
+ )}
+ ,
+ document.body,
+ )}
+
+
+
+ {mobile ? (open ? : ) : "Background"}
+ {!mobile &&
+
+ }
+
+
+
+
+
+ {mobile ? mobilePages.map((item) => (
+
+
+
+ {item.label}
+
+
+ )) : items(backgroundPages)}
+
+
+
+
+ >
+ );
+}
export function Navigation() {
const pathname = usePathname();
- const [open, setOpen] = useState(false);
- const toggle = useRef(null);
return (
- {
- if (event.key === "Escape" && open) {
- setOpen(false);
- toggle.current?.focus();
- }
- }}
- >
-
- setOpen(false)}
- aria-label="Sean O'Connor home"
- >
-
+
+
+
Sean O’Connor
-
-
setOpen(!open)}
- >
- {open ? : }
-
-
- {open && (
-
- )}
+
+
+ {mainPages.slice(0, 2).map((item) => (
+
+ {item.label}
+
+ ))}
+
+
+
+ Résumé & CV
+
+
+
+ Contact
+
+
+
+
+
+ Résumé & CV
+
+
+
+
);
}
diff --git a/src/components/PageHeading.tsx b/src/components/PageHeading.tsx
new file mode 100644
index 0000000..e2f8893
--- /dev/null
+++ b/src/components/PageHeading.tsx
@@ -0,0 +1,20 @@
+export function PageHeading({
+ eyebrow,
+ title,
+ description,
+}: {
+ eyebrow: string;
+ title: string;
+ description: string;
+}) {
+ return (
+
+ {eyebrow}
+
+ {title}
+ .
+
+ {description}
+
+ );
+}
diff --git a/src/components/ProjectExplorer.tsx b/src/components/ProjectExplorer.tsx
new file mode 100644
index 0000000..fb5e409
--- /dev/null
+++ b/src/components/ProjectExplorer.tsx
@@ -0,0 +1,169 @@
+"use client";
+
+import { useState } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { ArrowUpRight, Search } from "lucide-react";
+import { BeenvoicePreview } from "~/components/BeenvoicePreview";
+import { ManyanglesPreview } from "~/components/ManyanglesPreview";
+import { JusticeParagraph } from "~/components/JusticeParagraph";
+import type { Project } from "~/lib/data";
+
+const productNames = new Set([
+ "Racetix", "beenvoice", "Puter", "Manyangles", "hadlockCMS",
+ "Real-time Racing Statistics Platform",
+]);
+
+function ProjectCard({
+ project,
+ index,
+ compact = false,
+}: {
+ project: Project;
+ index: number;
+ compact?: boolean;
+}) {
+ return (
+
+
+ {project.image ? (
+ project.title === "beenvoice" ? (
+
+ ) : project.title === "Manyangles" ? (
+
+ ) : (
+
+ )
+ ) : (
+
{project.title}
+ )}
+ {(project.title === "Racetix" || project.title === "beenvoice") && (
+
+
+
+ )}
+
+
+
+
{project.title}
+ {String(index + 1).padStart(2, "0")}
+
+
{project.description}
+
+ {project.tags.slice(0, compact ? 3 : 4).map((tag) => {tag} )}
+
+
+ {project.longDescription && (
+
+ Project details
+
+
+ )}
+
+
+ );
+}
+
+export function ProjectExplorer({ projects }: { projects: Project[] }) {
+ const [query, setQuery] = useState("");
+ const [filter, setFilter] = useState("All");
+ const filtered = projects.filter(
+ (project) =>
+ (filter === "All" ||
+ (filter === "Products" ? productNames.has(project.title) : !productNames.has(project.title))) &&
+ `${project.title} ${project.description} ${project.tags.join(" ")}`
+ .toLowerCase()
+ .includes(query.toLowerCase()),
+ );
+ const showSpotlights = filter === "All" && query.trim() === "";
+ const spotlights = showSpotlights ? filtered.slice(0, 4) : [];
+ const remaining = showSpotlights ? filtered.slice(4) : filtered;
+
+ return (
+
+
+
+ Find a project
+
+ setQuery(event.target.value)}
+ />
+
+
+ {["All", "Products", "Research & experiments"].map((label) => (
+ setFilter(label)}>
+ {label}
+
+ ))}
+
+
{filtered.length} {filtered.length === 1 ? "project" : "projects"}
+
+ {filtered.length === 0 && (
+
+
No projects match that search.
+
{ setQuery(""); setFilter("All"); }}>Show all projects
+
+ )}
+ {spotlights.length > 0 && (
+
+
+ 01 / Selected work
+
A few things in use.
+
+
+ {spotlights.map((project, index) =>
)}
+
+
+ )}
+ {remaining.length > 0 && (
+
+
+ {showSpotlights ? "02 / The rest" : "The archive"}
+
{showSpotlights ? "More work & experiments." : "Projects."}
+
+
+ {remaining.map((project, index) =>
)}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 4371427..5d02924 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -1,138 +1,50 @@
"use client";
+import Link from "next/link";
import { usePathname } from "next/navigation";
-import { name, contact, location } from "~/lib/data";
-
-import { ImageWithSkeleton } from "~/components/ui/image-with-skeleton";
+import { ArrowUpRight } from "lucide-react";
+import { siteGroups } from "~/lib/site-navigation";
export function Sidebar() {
const pathname = usePathname();
- const isHomePage = pathname === "/";
-
return (
- <>
- {/* Mobile layout - horizontal intro bar only on home page */}
- {isHomePage && (
-
-
-
-
+
+
+ {siteGroups.map((group) => (
+
+
{group.label}
+ {group.items.map((item) => (
+
+ {item.label}
+
↗
+
+ ))}
-
-
-
- )}
-
- {/* Desktop layout - pinned sidebar */}
-
-
- {/* Profile Section */}
-
-
-
-
-
- {name[0]?.first} {name[0]?.last}
-
- {location.map((item) => (
-
- {item.label}
-
- ))}
-
-
-
-
- {/* Contact Links Section */}
-
-
- {/* Footer */}
-
-
- © {new Date().getFullYear()} {name[0]?.first} {name[0]?.last}
-
-
-
+ ))}
+
+
- >
+
);
}
diff --git a/src/components/SiteFrame.tsx b/src/components/SiteFrame.tsx
index 7d4e80e..e5556a7 100644
--- a/src/components/SiteFrame.tsx
+++ b/src/components/SiteFrame.tsx
@@ -1,17 +1,17 @@
"use client";
import { usePathname } from "next/navigation";
-import { BreadcrumbWrapper } from "~/components/BreadcrumbWrapper";
export function SiteFrame({ children }: React.PropsWithChildren) {
const pathname = usePathname();
return (
-
- {pathname !== "/" && }
- {children}
-
+ <>
+
+ {children}
+
+ >
);
}
diff --git a/src/components/ui/navigation-menu.tsx b/src/components/ui/navigation-menu.tsx
new file mode 100644
index 0000000..3987e42
--- /dev/null
+++ b/src/components/ui/navigation-menu.tsx
@@ -0,0 +1,165 @@
+"use client"
+
+import * as React from "react"
+import { cva } from "class-variance-authority"
+import { cn } from "~/lib/utils"
+import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui"
+import { ChevronDownIcon } from "lucide-react"
+
+function NavigationMenu({
+ className,
+ children,
+ viewport = true,
+ ...props
+}: React.ComponentProps
& {
+ viewport?: boolean
+}) {
+ return (
+
+ {children}
+ {viewport && }
+
+ )
+}
+
+function NavigationMenuList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function NavigationMenuItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+const navigationMenuTriggerStyle = cva(
+ "group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center rounded-lg px-2.5 py-1.5 text-xs/relaxed font-medium transition-all outline-none hover:bg-muted focus:bg-muted focus-visible:ring-2 focus-visible:ring-ring/30 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted data-open:bg-muted/50 data-open:hover:bg-muted data-open:focus:bg-muted"
+)
+
+function NavigationMenuTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}{" "}
+
+
+ )
+}
+
+function NavigationMenuContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function NavigationMenuViewport({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function NavigationMenuLink({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function NavigationMenuIndicator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export {
+ NavigationMenu,
+ NavigationMenuList,
+ NavigationMenuItem,
+ NavigationMenuContent,
+ NavigationMenuTrigger,
+ NavigationMenuLink,
+ NavigationMenuIndicator,
+ NavigationMenuViewport,
+ navigationMenuTriggerStyle,
+}
diff --git a/src/lib/coffee-shops.ts b/src/lib/coffee-shops.ts
new file mode 100644
index 0000000..5e2c525
--- /dev/null
+++ b/src/lib/coffee-shops.ts
@@ -0,0 +1,192 @@
+// Shop listings carried over from the original Lewisburg Coffee map.
+
+export interface CoffeeShop {
+ id: number;
+ name: string;
+ description: string;
+ lat: number;
+ lng: number;
+ address: string;
+ phone: string;
+ website: string;
+}
+
+export const COFFEE_SHOPS: CoffeeShop[] = [
+ {
+ id: 1,
+ name: "Amami Kitchen",
+ description:
+ "A beloved culinary spot offering a blend of high-quality Italian espresso and American coffee bar options, alongside mouthwatering baked goods and paninis.",
+ lat: 40.96112008888467,
+ lng: -76.88692162812505,
+ address: "103 S 6th St, Lewisburg, PA 17837",
+ phone: "(570) 490-7857",
+ website: "https://www.amamiespresso.com",
+ },
+ {
+ id: 2,
+ name: "Culture Coffee",
+ description:
+ "A modern coffee shop focusing on unique brewing methods for specialty coffee and traditional techniques for matcha. Known for its 'Red Velvet Latte' and croissants.",
+ lat: 40.966214038235066,
+ lng: -76.88549177840001,
+ address: "216 St. John St, Lewisburg, PA 17837",
+ phone: "(570) 601-7903",
+ website: "https://www.instagram.com/culturecoffeee_",
+ },
+ {
+ id: 3,
+ name: "7th St Cafe",
+ description:
+ "Bucknell's ultimate on-campus dessert and coffee shop. A cozy spot for students and visitors to enjoy milkshakes, ice cream, and specialty espresso drinks.",
+ lat: 40.95773659979501,
+ lng: -76.88483933057073,
+ address: "420 S 7th St, Lewisburg, PA 17837",
+ phone: "(570) 577-1240",
+ website:
+ "https://www.bucknell.edu/life-bucknell/dining-services/places-eat",
+ },
+ {
+ id: 4,
+ name: "Tastecraft Cafe",
+ description:
+ "Downtown cafe known for its in-house roasted coffee and over 20 flavors of French macarons. A perfect spot to relax with a fresh brew and a sweet treat.",
+ lat: 40.962987142339166,
+ lng: -76.88753610710032,
+ address: "512 Market St, Lewisburg, PA 17837",
+ phone: "(570) 768-5340",
+ website: "https://www.tastecraftcafe.com",
+ },
+ {
+ id: 5,
+ name: "Paris Bakery & Café",
+ description:
+ "Authentic French bakery offering legendary croissants, pain au chocolat, and artisanal breads. Experience a taste of France in the heart of Lewisburg.",
+ lat: 40.96380135223123,
+ lng: -76.88561321931094,
+ address: "335 Market St, Lewisburg, PA 17837",
+ phone: "(570) 884-2138",
+ website: "https://parisbakery.cafe",
+ },
+ {
+ id: 6,
+ name: "CycleUp Coffee",
+ description:
+ "A unique bike shop cafe serving as a community hub. Enjoy coffee, light bites, and bike services in a cozy, cycling-themed atmosphere.",
+ lat: 40.96325677827358,
+ lng: -76.88663292782749,
+ address: "429 Market St, Lewisburg, PA 17837",
+ phone: "(570) 413-1705",
+ website: "https://www.cycleupcafe.com",
+ },
+ {
+ id: 7,
+ name: "Cornerstone Kitchen",
+ description:
+ "Located in the Miller Center, offering fresh, healthy, and delicious cafe food. A place to connect over real food and a shared table.",
+ lat: 40.96125715212586,
+ lng: -76.89719763291119,
+ address: "120 Hardwood Dr, Lewisburg, PA 17837",
+ phone: "(570) 556-4191",
+ website: "https://millercenterlewisburg.com/cornerstone-kitchen",
+ },
+ {
+ id: 8,
+ name: "Gram's Eatery",
+ description:
+ "A local favorite for homestyle breakfast and brunch. Known for generous portions, locally sourced ingredients, and a welcoming atmosphere.",
+ lat: 40.9648823532259,
+ lng: -76.88542268394536,
+ address: "21 N 3rd St, Lewisburg, PA 17837",
+ phone: "(570) 522-0230",
+ website: "https://www.facebook.com/gramseaterylewisburg",
+ },
+ {
+ id: 9,
+ name: "Starbucks Coffee",
+ description:
+ "Seattle-based coffeehouse chain known for its signature roasts, light bites and WiFi availability.",
+ lat: 40.98121817592506,
+ lng: -76.88452326979083,
+ address: "7431 Westbranch Hwy, Lewisburg, PA 17837",
+ phone: "(570) 452-2370",
+ website: "https://www.starbucks.com/store-locator/store/1010326/",
+ },
+ {
+ id: 10,
+ name: "Barnes & Noble Café",
+ description:
+ "Barnes & Noble Café at Bucknell University. Serving Starbucks coffee and a variety of pastries and sandwiches in a comfortable bookstore setting.",
+ lat: 40.963756201655016,
+ lng: -76.88622019668863,
+ address: "400 Market St, Lewisburg, PA 17837",
+ phone: "(570) 577-3960",
+ website: "https://bucknell.bncollege.com",
+ },
+ {
+ id: 11,
+ name: "Starbucks (Giant)",
+ description:
+ "Conveniently located inside Giant Food Store. Perfect for grabbing your favorite coffee while grocery shopping.",
+ lat: 40.96169263125371,
+ lng: -76.89522292676229,
+ address: "224 Hardwood Dr, Lewisburg, PA 17837",
+ phone: "(570) 523-3200",
+ website: "https://www.starbucks.com",
+ },
+ {
+ id: 12,
+ name: "Dunkin'",
+ description:
+ "Long-running chain serving signature breakfast items & a variety of coffee drinks.",
+ lat: 40.969956373480606,
+ lng: -76.89248274516008,
+ address: "600 N Derr Dr, Lewisburg, PA 17837",
+ phone: "(570) 524-4900",
+ website: "https://www.dunkindonuts.com",
+ },
+ {
+ id: 13,
+ name: "Alee's Cafe",
+ description:
+ "A cozy local spot offering a variety of coffee drinks and light fare in the heart of downtown.",
+ lat: 40.964708961176,
+ lng: -76.8846770451187,
+ address: "232 Market St, Lewisburg, PA 17837",
+ phone: "(570) 523-1234",
+ website: "https://www.aleescafe.com",
+ },
+ {
+ id: 14,
+ name: "All Star Bagels",
+ description:
+ "Freshly baked bagels and coffee. A popular morning stop for locals and students.",
+ lat: 40.96445033682396,
+ lng: -76.88506518736742,
+ address: "300 Market St, Lewisburg, PA 17837",
+ phone: "(570) 524-1000",
+ website: "https://www.allstarbagels.com",
+ },
+ {
+ id: 15,
+ name: "Panera Bread",
+ description:
+ "Counter-serve bakery/cafe chain serving sandwiches, salads & more, known for its bread & free WiFi.",
+ lat: 40.97531008164084,
+ lng: -76.8897556844414,
+ address: "6951 Westbranch Hwy, Lewisburg, PA 17837",
+ phone: "(570) 524-0000",
+ website: "https://www.panerabread.com",
+ },
+ {
+ id: 16,
+ name: "Street of Shops Restaurant",
+ description:
+ "Part of a shopping village, this retro eatery serves down-home American fare & breakfast.",
+ lat: 40.96779250919243,
+ lng: -76.88264400489504,
+ address: "100 N Water St, Lewisburg, PA 17837",
+ phone: "(570) 523-0420",
+ website: "https://streetofshops.net",
+ },
+];
diff --git a/src/lib/data.tsx b/src/lib/data.tsx
index c322596..434e978 100644
--- a/src/lib/data.tsx
+++ b/src/lib/data.tsx
@@ -159,7 +159,7 @@ export const contact = [
export const articles = [
{
title: "Positively Innovative: Robotics for Good",
- link: "https://magazine.bucknell.edu/issue/fall-2024/robotics-for-good/",
+ link: "https://magazine.bucknell.edu/issue/bucknell/fall-2024/robotics-for-good#p14",
author: "Kate Willard",
description:
"Sean O'Connor '26 is using his interest in robotics to fuel forward-thinking research and lead important conversations about the impact robots can have on society.",
@@ -175,7 +175,7 @@ export const articles = [
},
{
title: "Shaping the Future: Exploring the Social Impact of Robots",
- link: "https://magazine.bucknell.edu/college-of-engineering/2024-college-report/",
+ link: "https://publications.bucknell.edu/issue/bucknellpublications/engineering-report-2024/distinctive-by-design?cs=redirect#p10",
author: "Bucknell Publications",
description:
"RoboLab provides an environment for scholarly and creative conversations.",
@@ -577,7 +577,7 @@ export const projects: Project[] = [
longDescription:
"Aggregates coffee shop data from various sources and displays it in a user-friendly interface. Use it to find your next brew!",
tags: ["Coffee", "Lewisburg, PA", "Bucknell"],
- websiteLink: "https://lewisburgcoffee.soconnor.dev",
+ websiteLink: "/coffee",
gitLink: "https://github.com/soconnor0919/lewisburg-coffee",
image: "/images/lewisburg-coffee.webp",
imageAlt: "Screenshot of the Lewisburg, PA Coffee Map website",
diff --git a/src/lib/fonts.ts b/src/lib/fonts.ts
index 5046aae..31a6e7a 100644
--- a/src/lib/fonts.ts
+++ b/src/lib/fonts.ts
@@ -1,10 +1,4 @@
-import { Instrument_Serif, Manrope } from "next/font/google";
-
-export const manrope = Manrope({
- subsets: ["latin"],
- variable: "--font-sans",
- display: "swap",
-});
+import { Instrument_Serif } from "next/font/google";
export const instrument = Instrument_Serif({
subsets: ["latin"],
diff --git a/src/lib/site-navigation.ts b/src/lib/site-navigation.ts
new file mode 100644
index 0000000..51867ea
--- /dev/null
+++ b/src/lib/site-navigation.ts
@@ -0,0 +1,27 @@
+export const siteGroups = [
+ {
+ label: "Make & share",
+ items: [
+ { href: "/", label: "Home" },
+ { href: "/projects", label: "Projects" },
+ { href: "/blog", label: "Writing" },
+ ],
+ },
+ {
+ label: "Background",
+ items: [
+ { href: "/experience", label: "Experience" },
+ { href: "/publications", label: "Publications" },
+ { href: "/articles", label: "In the media" },
+ { href: "/cv", label: "Résumé & CV" },
+ ],
+ },
+ {
+ label: "Beyond work",
+ items: [
+ { href: "/coffee", label: "Coffee map" },
+ { href: "/travel", label: "Trips & interests" },
+ { href: "/october", label: "October, forever" },
+ ],
+ },
+];
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 51b7552..4940d1e 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -302,7 +302,9 @@ button {
color: var(--studio-blue);
}
.wordmark-name {
- font-size: 14px;
+ font-size: 21px;
+ font-weight: 650;
+ letter-spacing: -0.055em;
}
.desktop-nav {
display: flex;
@@ -487,14 +489,14 @@ button {
height: 100% !important;
object-fit: cover;
}
-.studio-racetix-logo {
+.studio-product-logo {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: #0004;
}
-.studio-racetix-logo img {
+.studio-product-logo img {
width: 43%;
height: auto;
}
@@ -895,11 +897,7 @@ button {
}
/* Studio identity: expressive display type, quiet working typography. */
-.wordmark-name {
- font-size: 13px;
- font-weight: 600;
- letter-spacing: -0.025em;
-}
+
.intro-main h1,
.archive-header h1,
.studio-about h2,
@@ -1031,3 +1029,2350 @@ button {
letter-spacing: -0.04em;
line-height: 1.1;
}
+
+.type-study-controls {
+ padding: 22px 0;
+ border-block: 1px solid var(--studio-rule);
+}
+.type-study-controls label {
+ display: flex;
+ justify-content: space-between;
+ max-width: 350px;
+ font-size: 13px;
+}
+.type-study-controls output {
+ color: var(--studio-blue);
+}
+.type-study-controls input {
+ display: block;
+ width: min(100%, 350px);
+ accent-color: var(--studio-blue);
+ margin: 16px 0;
+}
+.type-study-controls p {
+ font-size: 12px;
+ color: var(--studio-muted);
+}
+.type-study-sample {
+ margin: 34px 0 48px;
+}
+.type-study-sample h2 {
+ font-size: 21px;
+ margin-bottom: 22px;
+}
+.type-study-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 50px;
+}
+.type-study-grid h3 {
+ font-size: 13px;
+ margin-bottom: 24px;
+}
+.type-study-grid h3 span {
+ display: block;
+ color: var(--studio-muted);
+ font-size: 11px;
+ margin-top: 7px;
+}
+.type-study-column {
+ width: 100%;
+ border-right: 1px dashed #cdd3e0;
+}
+.type-study-paragraph {
+ margin-bottom: 20px;
+ font-family: var(--font-sans), sans-serif;
+ font-size: 15px;
+ font-weight: 400;
+ line-height: 1.9;
+ letter-spacing: 0;
+ word-spacing: 0;
+ font-kerning: none;
+ font-variant-ligatures: none;
+ color: var(--studio-muted);
+}
+.justice-line {
+ display: block;
+ white-space: nowrap;
+}
+@media (max-width: 700px) {
+ .type-study-grid {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 25px;
+ }
+}
+
+.type-study-grid > div {
+ min-width: 0;
+}
+
+/* A compact personal workspace, across every section. */
+:root {
+ --background: 42 30% 96%;
+ --card: 45 35% 99%;
+ --border: 35 15% 82%;
+ --studio-rule: #ddd8cd;
+}
+.site-header {
+ background: #f8f6f0f5;
+}
+.nav-inner {
+ height: 76px;
+ max-width: 1480px;
+ padding-inline: 30px;
+}
+.site-rail {
+ display: none;
+}
+.studio-intro {
+ padding: 36px 0 40px;
+}
+.intro-main {
+ margin-top: 20px;
+ gap: 30px;
+ grid-template-columns: 1fr 145px;
+}
+.intro-main h1 {
+ font-size: clamp(55px, 6vw, 82px);
+ margin-bottom: 18px;
+}
+.intro-portrait {
+ width: 145px;
+ height: 175px;
+}
+.intro-links {
+ margin-top: 20px;
+}
+.studio-grid {
+ gap: 30px 24px;
+}
+.studio-project-image {
+ aspect-ratio: 1.65;
+ border-radius: 10px;
+}
+.studio-project-title {
+ margin-top: 14px;
+}
+.studio-project-title h3 {
+ font-size: 22px;
+}
+.studio-stack {
+ margin-top: 10px;
+}
+.studio-hadlock {
+ margin: 45px 0;
+ padding: 28px;
+ gap: 28px;
+ background: #e7eada;
+}
+.studio-about {
+ gap: 35px;
+ padding-bottom: 36px;
+}
+.archive-header {
+ padding: 12px 0 25px;
+}
+.archive-header h1 {
+ font-family: var(--font-heading), Georgia, serif;
+ font-weight: 400;
+ font-size: clamp(40px, 5vw, 60px);
+ margin: 12px 0;
+}
+.archive-header > p:last-child {
+ font-size: 15px;
+}
+.interior-page [data-slot="card"] {
+ border-radius: 9px;
+ background: hsl(var(--card));
+ backdrop-filter: none;
+}
+.interior-page [data-slot="card-title"] {
+ font-family: var(--font-sans), sans-serif;
+ font-weight: 600;
+}
+.interior-page > article > div > h1 {
+ font-family: var(--font-heading), Georgia, serif;
+ font-weight: 400;
+ font-size: clamp(36px, 5vw, 60px);
+}
+.justice-paragraph {
+ font-kerning: none;
+ font-variant-ligatures: none;
+ letter-spacing: 0;
+ word-spacing: 0;
+ min-width: 0;
+}
+.personal-corners {
+ margin: 10px 0 38px;
+}
+.corners-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 18px;
+}
+.corner-card {
+ display: block;
+ background: #fffdf7;
+ border: 1px solid var(--studio-rule);
+ padding: 9px 9px 15px;
+ border-radius: 4px;
+ transition: transform 0.2s;
+}
+.corner-card:nth-child(1) {
+ transform: rotate(-1.5deg);
+}
+.corner-card:nth-child(2) {
+ transform: rotate(1deg);
+}
+.corner-card:hover {
+ transform: translateY(-5px) rotate(0);
+}
+.corner-card > img {
+ width: 100%;
+ height: 150px;
+ object-fit: cover;
+}
+.corner-card > span {
+ display: block;
+ font-weight: 600;
+ font-size: 15px;
+ margin: 14px 7px 7px;
+}
+.corner-card > p {
+ font-size: 12px;
+ line-height: 1.7;
+ color: var(--studio-muted);
+ margin-inline: 7px;
+}
+.october-stamp {
+ height: 150px;
+ background: #efc9a1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ color: #6c3518;
+}
+.october-stamp > span {
+ letter-spacing: 0.2em;
+ font-size: 12px;
+}
+.october-stamp strong {
+ font-family: var(--font-heading), Georgia, serif;
+ font-size: 60px;
+ font-weight: 400;
+ line-height: 1.05;
+}
+.october-stamp small {
+ font-size: 10px;
+}
+.project-tools {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ gap: 18px;
+ margin-bottom: 24px;
+ padding: 18px 0;
+ border-block: 1px solid var(--studio-rule);
+}
+.project-tools label {
+ font-size: 11px;
+ flex: 1;
+ min-width: 200px;
+}
+.project-tools input {
+ display: block;
+ width: 100%;
+ min-width: 0;
+ border: 1px solid var(--studio-rule);
+ background: #fffdf8;
+ padding: 10px 12px;
+ border-radius: 6px;
+ margin-top: 8px;
+ font-size: 13px;
+}
+.project-filters {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+.project-filters button,
+.project-empty button {
+ padding: 9px 12px;
+ border: 1px solid var(--studio-rule);
+ border-radius: 6px;
+ font-size: 12px;
+}
+.project-filters button[aria-pressed="true"] {
+ background: var(--studio-blue);
+ color: white;
+ border-color: var(--studio-blue);
+}
+.project-tools > p {
+ font-size: 11px;
+ color: var(--studio-muted);
+ padding-bottom: 10px;
+}
+.archive-grid {
+ align-items: start;
+ gap: 22px;
+}
+.archive-body {
+ padding: 22px;
+}
+.archive-body h2 {
+ font-size: 21px;
+}
+.project-detail {
+ margin-top: 15px;
+}
+.project-detail summary {
+ cursor: pointer;
+ font-size: 12px;
+ color: var(--studio-blue);
+}
+.archive-body .project-detail p {
+ font-size: 14px;
+}
+.project-empty {
+ padding: 35px;
+ border: 1px dashed var(--studio-rule);
+}
+.project-empty button {
+ margin-top: 15px;
+}
+.travel-notes {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 25px;
+}
+.travel-note {
+ min-width: 0;
+ border: 1px solid var(--studio-rule);
+ border-radius: 8px;
+ overflow: hidden;
+ background: #fffdf8;
+}
+.travel-strip {
+ display: flex;
+ overflow-x: auto;
+ scroll-snap-type: x mandatory;
+}
+.travel-strip img {
+ flex: 0 0 90%;
+ width: 90%;
+ height: 260px;
+ object-fit: cover;
+ scroll-snap-align: start;
+}
+.travel-caption {
+ padding: 22px;
+}
+.travel-caption h2 {
+ font-size: 22px;
+ letter-spacing: -0.04em;
+ margin: 10px 0 15px;
+}
+.travel-caption .justice-paragraph,
+.press-list .justice-paragraph {
+ font-size: 14px;
+ line-height: 1.85;
+ color: var(--studio-muted);
+}
+.gallery-hint,
+.press-byline {
+ font-size: 11px;
+ margin-top: 18px;
+ color: var(--studio-muted);
+}
+.press-list {
+ max-width: 800px;
+}
+.press-list article {
+ padding: 25px 0;
+ border-top: 1px solid var(--studio-rule);
+}
+.press-list h2 {
+ font-size: 25px;
+ line-height: 1.4;
+ margin: 12px 0;
+}
+.press-list h2 svg {
+ display: inline;
+}
+.mobile-menu {
+ max-height: calc(100dvh - 76px);
+ overflow-y: auto;
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 15px 25px;
+}
+.mobile-nav-group > p {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--studio-muted);
+ padding-top: 10px;
+}
+.mobile-menu a {
+ font-size: 14px;
+ padding: 10px 0;
+}
+.mobile-menu a[aria-current] {
+ color: var(--studio-blue);
+}
+@media (min-width: 1100px) {
+ .site-rail {
+ display: block;
+ position: fixed;
+ top: 76px;
+ bottom: 0;
+ left: 0;
+ width: 224px;
+ overflow-y: auto;
+ padding: 25px 18px;
+ border-right: 1px solid var(--studio-rule);
+ background: #f1eee5;
+ }
+ .rail-group {
+ margin-top: 19px;
+ }
+ .rail-group:first-child {
+ margin-top: 0;
+ }
+ .rail-group p {
+ margin: 0 10px 7px;
+ font-size: 9px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--studio-muted);
+ }
+ .rail-group a {
+ display: flex;
+ justify-content: space-between;
+ font-size: 12px;
+ padding: 8px 10px;
+ border-radius: 5px;
+ }
+ .rail-group a span {
+ opacity: 0;
+ }
+ .rail-group a:hover {
+ background: #e6e4d9;
+ }
+ .rail-group a:hover span {
+ opacity: 1;
+ }
+ .rail-group a[aria-current] {
+ background: #dce3f9;
+ color: #2849d4;
+ font-weight: 600;
+ }
+ .rail-elsewhere {
+ border-top: 1px solid var(--studio-rule);
+ padding: 16px 10px 0;
+ margin-top: 22px;
+ }
+ .rail-elsewhere a {
+ display: flex;
+ justify-content: space-between;
+ font-size: 10px;
+ margin-bottom: 12px;
+ color: var(--studio-muted);
+ }
+ .site-main {
+ margin-left: 224px;
+ width: calc(100% - 224px);
+ }
+ .site-main.interior-page {
+ max-width: 1340px;
+ padding: 25px 32px 55px;
+ }
+ .studio-home {
+ padding-inline: 32px;
+ }
+ .site-footer {
+ margin-left: 224px;
+ width: calc(100% - 224px);
+ max-width: none;
+ padding: 0 32px 20px;
+ }
+}
+@media (max-width: 700px) {
+ .intro-main {
+ display: block;
+ }
+ .intro-portrait {
+ width: 72px;
+ height: 84px;
+ }
+ .intro-main h1 {
+ font-size: clamp(46px, 12vw, 70px);
+ }
+ .studio-intro {
+ padding: 26px 0 32px;
+ }
+ .studio-hadlock {
+ padding: 23px;
+ margin: 32px 0;
+ }
+ .corners-grid,
+ .travel-notes {
+ grid-template-columns: 1fr;
+ }
+ .corner-card {
+ display: grid;
+ grid-template-columns: 105px 1fr;
+ column-gap: 12px;
+ transform: none !important;
+ }
+ .corner-card > img,
+ .october-stamp {
+ grid-row: span 2;
+ height: 120px;
+ }
+ .corner-card > span {
+ align-self: end;
+ }
+ .corner-card > p {
+ margin-bottom: 8px;
+ }
+ .october-stamp strong {
+ font-size: 43px;
+ }
+ .project-tools {
+ align-items: stretch;
+ flex-direction: column;
+ gap: 12px;
+ }
+ .project-tools > p {
+ padding: 0;
+ }
+ .studio-project-title {
+ flex-wrap: wrap;
+ gap: 5px;
+ }
+}
+@media (min-width: 701px) and (max-width: 1099px) {
+ .mobile-toggle {
+ display: block;
+ }
+ .mobile-menu {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+ .desktop-nav {
+ margin-left: auto;
+ margin-right: 20px;
+ }
+}
+.studio-about > div,
+.studio-hadlock > div {
+ min-width: 0;
+}
+.justice-line {
+ max-width: 100%;
+}
+@media (prefers-reduced-motion: reduce) {
+ .corner-card {
+ transform: none !important;
+ }
+}
+.section-jumps {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+.section-jumps a {
+ font-size: 12px;
+ padding: 8px 13px;
+ border: 1px solid var(--studio-rule);
+ border-radius: 5px;
+ background: #fffdf8;
+ color: var(--studio-blue);
+}
+
+.mark-live-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 20px;
+ border: 1px solid var(--studio-rule);
+ background: #fffdf8;
+ padding: 16px 24px;
+ border-radius: 8px;
+}
+.mark-live-brand {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ font-size: 13px;
+ font-weight: 600;
+}
+.mark-live-brand > svg {
+ width: 42px;
+ height: 42px;
+ color: var(--studio-blue);
+}
+.mark-example-links {
+ display: flex;
+ gap: 24px;
+ font-size: 12px;
+ color: var(--studio-muted);
+}
+.mark-helper {
+ font-size: 12px;
+ line-height: 1.8;
+ color: var(--studio-muted);
+ margin: 16px 0 24px;
+ max-width: 730px;
+}
+.mark-choice-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 20px;
+}
+.mark-choice {
+ min-width: 0;
+ text-align: left;
+ border: 1px solid var(--studio-rule);
+ border-radius: 9px;
+ overflow: hidden;
+ background: #fffdf8;
+}
+.mark-choice[aria-pressed="true"] {
+ border-color: var(--studio-blue);
+ box-shadow: 0 0 0 1px var(--studio-blue);
+}
+.mark-large {
+ height: 200px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: #eeeee6;
+ padding: 20px;
+}
+.mark-large svg {
+ width: 120px;
+ height: 120px;
+ color: var(--studio-blue);
+}
+.name-only-mark {
+ font-family: var(--font-sans), sans-serif;
+ font-weight: 650;
+ letter-spacing: -0.055em;
+ white-space: nowrap;
+}
+.name-only-mark > span {
+ color: var(--studio-blue);
+}
+.mark-large .name-only-mark {
+ font-size: clamp(18px, 2vw, 28px);
+}
+.mark-choice-body {
+ padding: 21px;
+}
+.mark-choice-body h2 {
+ font-size: 17px;
+ font-weight: 600;
+}
+.mark-choice-body h2 > span {
+ color: var(--studio-blue);
+ margin-right: 7px;
+}
+.mark-choice-body > p {
+ font-size: 12px;
+ line-height: 1.8;
+ color: var(--studio-muted);
+ margin-top: 12px;
+ min-height: 65px;
+}
+.mark-samples {
+ display: flex;
+ align-items: center;
+ gap: 18px;
+ margin-top: 20px;
+}
+.mark-samples > div {
+ display: grid;
+ place-items: center;
+ min-width: 40px;
+ height: 40px;
+}
+.mark-samples svg {
+ width: 30px;
+ height: 30px;
+}
+.mark-samples .name-only-mark {
+ font-size: 11px;
+}
+.mark-reversed {
+ --mark-paper: #2849d4;
+ padding: 4px 8px;
+ background: #2849d4;
+ color: #fff;
+ border-radius: 5px;
+}
+.mark-reversed .name-only-mark > span {
+ color: #fff;
+}
+@media (max-width: 800px) {
+ .mark-choice-grid {
+ grid-template-columns: 1fr;
+ }
+ .mark-large {
+ height: 150px;
+ }
+ .mark-choice-body > p {
+ min-height: 0;
+ }
+ .mark-example-links {
+ display: none;
+ }
+}
+
+/* Coffee lives alongside the rest of the personal site. */
+.coffee-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ margin-bottom: 20px;
+ flex-wrap: wrap;
+}
+.coffee-toolbar label {
+ font-weight: 650;
+}
+.coffee-toolbar input {
+ flex: 1;
+ min-width: 180px;
+ padding: 12px 15px;
+ background: var(--studio-paper, #fffdf8);
+ border: 1px solid var(--studio-rule);
+ border-radius: 6px;
+}
+.coffee-toolbar button {
+ font-size: 13px;
+ text-decoration: underline;
+ text-underline-offset: 4px;
+ padding: 10px 0;
+}
+.coffee-layout {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 290px;
+ border: 1px solid var(--studio-rule);
+ border-radius: 8px;
+ overflow: hidden;
+}
+.coffee-map-panel {
+ min-width: 0;
+ position: relative;
+ background: #e8e9df;
+}
+.coffee-map {
+ height: 520px;
+ z-index: 0;
+ font-family: inherit;
+}
+.coffee-map-status {
+ position: absolute;
+ top: 55px;
+ left: 60px;
+ right: 25px;
+ padding: 15px;
+ background: #fffdf8;
+}
+.coffee-pin {
+ background: var(--studio-blue);
+ border: 2px solid #fff;
+ color: white;
+ border-radius: 50%;
+ display: grid;
+ place-items: center;
+ font-size: 12px;
+ font-weight: 700;
+ box-shadow: 0 2px 6px #0003;
+}
+.coffee-pin.is-selected {
+ background: #26251f;
+ outline: 3px solid #3949c655;
+}
+.coffee-shop-list {
+ max-height: 520px;
+ overflow: auto;
+ background: #fffdf8;
+ padding: 18px 12px;
+}
+.coffee-shop-list > .studio-label {
+ padding: 0 10px 12px;
+}
+.coffee-shop {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ text-align: left;
+ padding: 14px 10px;
+ border-top: 1px solid var(--studio-rule);
+ border-radius: 3px;
+}
+.coffee-shop:hover,
+.coffee-shop[aria-pressed="true"] {
+ background: #eeeee6;
+}
+.coffee-shop > span:nth-child(2) {
+ flex: 1;
+}
+.coffee-shop strong {
+ display: block;
+ font-size: 14px;
+ font-weight: 650;
+}
+.coffee-shop small {
+ display: block;
+ color: var(--studio-muted);
+ margin-top: 4px;
+ font-size: 11px;
+}
+.coffee-shop-number {
+ color: var(--studio-blue);
+ font-size: 12px;
+}
+.coffee-detail {
+ background: #fffdf8;
+ padding: 22px;
+ border-top: 1px solid var(--studio-rule);
+}
+.coffee-detail-heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+}
+.coffee-detail h2 {
+ font-size: 22px;
+ font-weight: 650;
+ letter-spacing: -0.035em;
+}
+.coffee-detail-heading button {
+ min-width: 40px;
+ min-height: 40px;
+ font-size: 26px;
+}
+.coffee-detail p {
+ font-size: 14px;
+ line-height: 1.7;
+ margin: 8px 0;
+}
+.coffee-detail .coffee-address {
+ color: var(--studio-muted);
+ font-size: 12px;
+}
+.coffee-detail-links {
+ display: flex;
+ gap: 22px;
+ color: var(--studio-blue);
+ margin-top: 16px;
+ font-size: 13px;
+}
+.coffee-map-note {
+ color: var(--studio-muted);
+ font-size: 12px;
+ line-height: 1.6;
+ margin-top: 18px;
+}
+.coffee-empty {
+ padding: 12px;
+ font-size: 14px;
+}
+@media (max-width: 800px) {
+ .coffee-layout {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ .coffee-map {
+ height: 340px;
+ }
+ .coffee-shop-list {
+ max-height: 340px;
+ border-top: 1px solid var(--studio-rule);
+ }
+ .coffee-toolbar label {
+ width: 100%;
+ }
+}
+.coffee-pin > span {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ place-items: center;
+}
+
+.parent-page-link {
+ display: inline-flex;
+ align-items: center;
+ min-height: 32px;
+ font-size: 13px;
+ color: var(--studio-muted);
+}
+.parent-page-link:hover {
+ color: var(--studio-blue);
+}
+
+/* Shared editorial rhythm for the background and archive pages. */
+.record-intro {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 16px;
+ margin-bottom: 28px;
+}
+.record-intro .section-jumps {
+ margin: 0;
+}
+.record-section {
+ margin-bottom: 48px;
+ scroll-margin-top: 110px;
+}
+.record-section > h2 {
+ font-family: var(--font-heading), serif;
+ font-size: 34px;
+ margin-bottom: 20px;
+ letter-spacing: -0.025em;
+}
+.record-row {
+ display: grid;
+ grid-template-columns: 180px minmax(0, 1fr);
+ gap: 32px;
+ padding: 28px 0;
+ border-top: 1px solid var(--studio-rule);
+}
+.record-meta {
+ font-size: 12px;
+ line-height: 1.7;
+ color: var(--studio-muted);
+}
+.record-meta p + p {
+ margin-top: 8px;
+}
+.record-number {
+ display: block;
+ color: var(--studio-blue);
+ margin-bottom: 12px;
+ font-size: 15px;
+}
+.record-body {
+ max-width: 780px;
+ min-width: 0;
+}
+.record-body h2,
+.record-body h3 {
+ font-family: inherit;
+ font-size: 23px;
+ line-height: 1.3;
+ font-weight: 600;
+ letter-spacing: -0.035em;
+ margin: 0 0 12px;
+ text-wrap: balance;
+}
+.record-body h2 a:hover {
+ color: var(--studio-blue);
+}
+.record-body > p,
+.record-body details p {
+ font-size: 14px;
+ line-height: 1.8;
+ color: var(--studio-muted);
+ margin: 10px 0;
+}
+.record-body .record-kicker,
+.record-kicker {
+ font-size: 12px;
+ line-height: 1.6;
+ margin: 0 0 10px;
+ color: var(--studio-muted);
+}
+.record-body details {
+ margin-top: 16px;
+ font-size: 14px;
+}
+.record-body summary,
+.record-disclosure summary {
+ cursor: pointer;
+ color: var(--studio-blue);
+ padding: 8px 0;
+}
+.record-body ul,
+.record-disclosure ul {
+ list-style: disc;
+ padding-left: 20px;
+ color: var(--studio-muted);
+ font-size: 14px;
+ line-height: 1.8;
+}
+.record-body li + li {
+ margin-top: 8px;
+}
+.record-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px 24px;
+ font-size: 13px;
+ color: var(--studio-blue);
+ margin: 18px 0 8px;
+}
+.record-links a:hover {
+ text-decoration: underline;
+ text-underline-offset: 4px;
+}
+.skills-index {
+ border-top: 1px solid var(--studio-rule);
+}
+.skills-index > div {
+ display: grid;
+ grid-template-columns: 180px minmax(0, 1fr);
+ gap: 32px;
+ padding: 18px 0;
+ border-bottom: 1px solid var(--studio-rule);
+}
+.skills-index h3 {
+ font-size: 13px;
+ font-weight: 650;
+}
+.skills-index p {
+ font-size: 14px;
+ line-height: 1.8;
+ color: var(--studio-muted);
+}
+.record-disclosure {
+ border-top: 1px solid var(--studio-rule);
+ padding: 16px 0;
+}
+.record-disclosure summary {
+ font-size: 16px;
+}
+.record-disclosure summary span {
+ color: var(--studio-muted);
+ font-size: 12px;
+ margin-left: 8px;
+}
+.record-disclosure article {
+ padding: 18px 0;
+ max-width: 780px;
+ font-size: 14px;
+ line-height: 1.8;
+}
+.record-disclosure h3 {
+ font-weight: 650;
+}
+.document-reader {
+ min-width: 0;
+}
+.document-topbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 16px;
+ padding-bottom: 20px;
+}
+.document-topbar .record-links {
+ margin: 0;
+}
+.document-switch {
+ display: flex;
+ gap: 4px;
+ padding: 4px;
+ border: 1px solid var(--studio-rule);
+ border-radius: 8px;
+}
+.document-switch button {
+ padding: 10px 18px;
+ border-radius: 5px;
+ font-size: 14px;
+}
+.document-switch button[aria-pressed="true"] {
+ background: var(--studio-ink, #292923);
+ color: #fffdf8;
+}
+.document-controls {
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 10px;
+ padding: 12px;
+ border: 1px solid var(--studio-rule);
+ border-radius: 8px 8px 0 0;
+ background: #f0efe8;
+}
+.control-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+}
+.control-group button {
+ min-width: 36px;
+ min-height: 36px;
+ padding: 6px 10px;
+ border: 1px solid var(--studio-rule);
+ border-radius: 4px;
+ background: #fffdf8;
+}
+.control-group button:disabled {
+ opacity: 0.35;
+ cursor: not-allowed;
+}
+.control-group span {
+ min-width: 94px;
+ text-align: center;
+ font-variant-numeric: tabular-nums;
+}
+.document-paper {
+ position: relative;
+ height: min(76vh, 1050px);
+ min-height: 420px;
+ overflow: auto;
+ padding: 16px;
+ background: #e9e8e1;
+ border: 1px solid var(--studio-rule);
+ border-top: 0;
+}
+.document-paper canvas {
+ display: block;
+ margin: 0 auto;
+ max-width: none;
+ background: white;
+ box-shadow: 0 3px 20px #24231f12;
+}
+.document-message {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ place-content: center;
+ padding: 30px;
+ text-align: center;
+ color: var(--studio-muted);
+ font-size: 14px;
+}
+.reader-note {
+ margin-top: 12px;
+ font-size: 12px;
+ color: var(--studio-muted);
+}
+@media (max-width: 700px) {
+ .record-row {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 12px;
+ padding: 24px 0;
+ }
+ .record-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 16px;
+ }
+ .record-meta p + p {
+ margin: 0;
+ }
+ .record-number {
+ margin: 0;
+ }
+ .record-body h2,
+ .record-body h3 {
+ font-size: 21px;
+ }
+ .skills-index > div {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 6px;
+ }
+ .document-topbar {
+ align-items: flex-start;
+ }
+ .document-paper {
+ min-height: 360px;
+ height: 65vh;
+ }
+}
+
+.document-sheet + .document-sheet {
+ margin-top: 24px;
+}
+.document-sheet {
+ width: max-content;
+ min-width: 100%;
+}
+
+/* A centered site with one consistent, compact navigation. */
+.site-header {
+ background: #f8f6f0f5;
+ backdrop-filter: blur(18px);
+ border-bottom: 1px solid var(--studio-rule);
+}
+.nav-inner {
+ max-width: 1160px;
+ height: 72px;
+ padding-inline: 40px;
+ border-bottom: 0;
+}
+.primary-links {
+ display: flex;
+ align-items: center;
+ gap: 28px;
+ font-size: 13px;
+}
+.primary-links a:hover {
+ color: var(--studio-blue);
+}
+.primary-links a {
+ padding-block: 14px;
+}
+.site-main {
+ margin-left: 0;
+ width: 100%;
+}
+.site-main.interior-page {
+ width: 100%;
+ max-width: 1160px;
+ margin-inline: auto;
+ padding: 36px 40px 60px;
+}
+.studio-home {
+ max-width: 1160px;
+ padding-inline: 40px;
+}
+.studio-home .studio-intro {
+ padding: 44px 0 40px;
+}
+.studio-home .intro-topline {
+ justify-content: space-between;
+ padding-right: 0;
+}
+.site-footer {
+ width: 100%;
+ max-width: 1160px;
+ margin-inline: auto;
+ padding: 0 40px 24px;
+}
+.studio-home section[id] {
+ scroll-margin-top: 96px;
+}
+.background-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px 24px;
+ margin-top: 20px;
+ font-size: 12px;
+ color: var(--studio-muted);
+}
+.background-links a:hover {
+ color: var(--studio-blue);
+ text-decoration: underline;
+ text-underline-offset: 4px;
+}
+@media (max-width: 700px) {
+ .nav-inner {
+ height: 64px;
+ padding-inline: 20px;
+ }
+ .nav-inner .wordmark-name {
+ font-size: 18px;
+ }
+ .primary-links {
+ gap: 16px;
+ font-size: 12px;
+ }
+ .studio-home {
+ padding-inline: 23px;
+ }
+ .studio-home .studio-intro {
+ padding-top: 28px;
+ }
+ .site-main.interior-page {
+ padding: 28px 23px 44px;
+ }
+ .site-footer {
+ padding: 0 23px 20px;
+ }
+ .studio-home section[id] {
+ scroll-margin-top: 84px;
+ }
+}
+@media (max-width: 360px) {
+ .nav-inner {
+ padding-inline: 16px;
+ }
+ .nav-inner .wordmark-name {
+ font-size: 16px;
+ }
+ .primary-links {
+ gap: 12px;
+ }
+}
+
+.header-background {
+ position: relative;
+}
+.header-background summary {
+ display: flex;
+ gap: 7px;
+ align-items: center;
+ cursor: pointer;
+ list-style: none;
+ padding-block: 14px;
+}
+.header-background summary::-webkit-details-marker {
+ display: none;
+}
+.header-background summary[data-active="true"],
+.header-background[open] summary {
+ color: var(--studio-blue);
+}
+.header-background summary .menu-chevron {
+ transition: transform 150ms ease;
+}
+.header-background[open] summary .menu-chevron {
+ transform: rotate(180deg);
+}
+.header-dropdown {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ width: 190px;
+ padding: 6px;
+ border: 1px solid var(--studio-rule);
+ background: #fffdf8;
+ border-radius: 8px;
+ box-shadow: 0 10px 25px #24231f12;
+}
+.primary-links .header-dropdown a {
+ display: block;
+ padding: 12px;
+ border-radius: 4px;
+}
+.header-dropdown a:hover,
+.header-dropdown a[aria-current="page"] {
+ background: #eeeee6;
+ color: var(--studio-blue);
+}
+.primary-links .header-resume {
+ color: var(--studio-blue);
+ font-weight: 650;
+ white-space: nowrap;
+}
+@media (max-width: 700px) {
+ .nav-inner {
+ position: relative;
+ height: 102px;
+ flex-wrap: wrap;
+ align-content: center;
+ row-gap: 2px;
+ }
+ .nav-inner > .wordmark {
+ height: 42px;
+ }
+ .primary-links {
+ width: 100%;
+ justify-content: space-between;
+ gap: 12px;
+ }
+ .primary-links .header-resume {
+ position: absolute;
+ top: 12px;
+ right: 20px;
+ padding: 10px 0;
+ }
+ .primary-links > a,
+ .header-background summary {
+ padding-block: 12px;
+ }
+ .header-dropdown {
+ right: -30px;
+ }
+ .studio-home section[id],
+ .record-section {
+ scroll-margin-top: 122px;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ .header-background summary .menu-chevron {
+ transition: none;
+ }
+}
+
+/* Hadlock palette, with soft paper surfaces and lake-blue contour lines. */
+:root {
+ --font-sans: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ --background: 40 31% 94%;
+ --foreground: 205 34% 16%;
+ --card: 48 100% 99%;
+ --card-foreground: 205 34% 16%;
+ --primary: 196 100% 31%;
+ --ring: 196 100% 31%;
+ --muted-foreground: 207 10% 40%;
+ --studio-blue: #0076a0;
+ --studio-muted: #616a6e;
+ --studio-rule: #d9d3c6;
+ --studio-ink: #173441;
+}
+body {
+ background:
+ radial-gradient(ellipse at 95% 5%, #dce8e7 0, transparent 38rem),
+ radial-gradient(ellipse at 0 65%, #e9e4d5 0, transparent 40rem), #f4f1ea;
+ font-family: var(--font-sans);
+}
+::selection {
+ background: #c7e5ed;
+ color: #163542;
+}
+.site-header {
+ background: #f4f1eaed;
+ border-color: #d9d3c680;
+}
+.wordmark-name {
+ font-weight: 600;
+ letter-spacing: -0.045em;
+}
+.primary-links {
+ font-size: 14px;
+}
+.header-dropdown {
+ background: #fffefa;
+ box-shadow: 0 14px 40px #17344116;
+}
+.header-dropdown a:hover,
+.header-dropdown a[aria-current="page"] {
+ background: #e4eff0;
+}
+.primary-links .header-resume {
+ padding: 9px 14px;
+ border: 1px solid #0076a030;
+ border-radius: 6px;
+ background: #0076a009;
+}
+.studio-home {
+ padding-top: 28px;
+}
+.studio-home .studio-intro {
+ position: relative;
+ isolation: isolate;
+ overflow: hidden;
+ padding: 38px 44px 42px;
+ margin-bottom: 44px;
+ border: 1px solid #245565;
+ border-radius: 22px;
+ color: #fff8e9;
+ background:
+ radial-gradient(ellipse at 100% 110%, #0076a073, transparent 65%),
+ linear-gradient(115deg, #102e3c, #1c4b5b);
+ box-shadow:
+ 0 14px 40px #1734410c,
+ inset 0 1px 0 #ffffff10;
+}
+.contour-lines {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: -1;
+ opacity: 0.45;
+}
+.contour-lines path {
+ stroke: #92b8bc;
+ stroke-width: 1.3;
+ vector-effect: non-scaling-stroke;
+}
+.studio-intro .intro-topline {
+ color: #c0d4d7;
+ font-size: 11px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+.studio-intro .intro-main {
+ grid-template-columns: minmax(0, 1fr) 145px;
+ gap: 32px;
+ margin-top: 25px;
+}
+.studio-intro h1 {
+ color: #fff8e9;
+ font-size: clamp(54px, 6vw, 84px);
+ font-weight: 400;
+ letter-spacing: -0.04em;
+ margin-bottom: 22px;
+}
+.studio-intro h1 span {
+ color: #91c2ce;
+}
+.studio-intro .intro-statement {
+ color: #fff8e9;
+ font-size: 23px;
+ line-height: 1.45;
+ letter-spacing: -0.025em;
+}
+.studio-intro .intro-statement a {
+ color: #b3dfe7;
+ text-decoration-color: #8bbacb65;
+}
+.studio-intro .intro-detail {
+ color: #c0d1d6;
+ font-size: 15px;
+ line-height: 1.8;
+ max-width: 590px;
+}
+.studio-intro .intro-links {
+ color: #fff8e9;
+ margin-top: 25px;
+}
+.studio-intro .intro-links a {
+ font-size: 13px;
+}
+.studio-intro .intro-links a:first-child {
+ border: 1px solid #b9d4d45c;
+ border-radius: 5px;
+ padding: 9px 13px;
+ background: #ffffff09;
+}
+.studio-intro .intro-portrait {
+ transform: none;
+ width: 145px;
+ height: 175px;
+ border: 6px solid #fff8e9;
+ border-radius: 12px;
+ box-shadow: 0 14px 30px #071c2630;
+}
+.studio-label,
+.eyebrow {
+ letter-spacing: 0.12em;
+ color: #54717c;
+ font-size: 10px;
+ font-weight: 500;
+}
+.studio-section-heading {
+ margin-bottom: 22px;
+}
+.studio-section-heading h2 {
+ font-family: var(--font-heading), serif;
+ font-size: 32px;
+ font-weight: 400;
+ letter-spacing: -0.025em;
+}
+.studio-section-heading h2 span {
+ font-family: var(--font-sans);
+ font-size: 11px;
+}
+.studio-grid > article {
+ background: #fffefa;
+ padding: 12px 12px 20px;
+ border: 1px solid #d9d3c6;
+ border-radius: 16px;
+ box-shadow: 0 4px 12px #17344104;
+}
+.studio-project-image {
+ border-radius: 9px;
+}
+.studio-project-title,
+.studio-project-description,
+.studio-stack {
+ margin-left: 8px;
+ margin-right: 8px;
+}
+.studio-project-title h3 {
+ font-size: 23px;
+ letter-spacing: -0.035em;
+ font-weight: 500;
+}
+.studio-grid .justice-paragraph {
+ margin-inline: 8px;
+}
+.studio-hadlock {
+ background: linear-gradient(120deg, #e3eceb, #eef0e6);
+ border: 1px solid #ccd8d5;
+ border-radius: 18px;
+ padding: 32px;
+}
+.studio-hadlock > img {
+ border-radius: 10px;
+}
+.studio-about {
+ padding-top: 20px;
+}
+.corner-card {
+ background: #fffefa;
+ border: 1px solid #d9d3c6;
+ box-shadow: 0 5px 16px #17344106;
+ border-radius: 10px;
+}
+.october-corner {
+ background: linear-gradient(145deg, #eee6d4, #f9f5e9);
+}
+.studio-contact {
+ position: relative;
+ isolation: isolate;
+ overflow: hidden;
+ padding: 36px;
+ margin: 40px 0 44px;
+ border: 1px solid #245565;
+ border-radius: 18px;
+ background:
+ radial-gradient(ellipse at 90% 120%, #0076a065, transparent 70%), #173b49;
+ color: #fff8e9;
+}
+.studio-contact .studio-label {
+ color: #bdd3d6;
+}
+.studio-contact h2 {
+ color: #fff8e9;
+}
+.studio-contact > a {
+ color: #bee2e8;
+ font-size: 22px;
+}
+.archive-header {
+ position: relative;
+ padding: 22px 0 30px;
+ margin-bottom: 26px;
+ border-bottom: 1px solid var(--studio-rule);
+}
+.archive-header h1 {
+ color: #173b49;
+}
+.record-body h2,
+.record-body h3 {
+ font-weight: 500;
+}
+.document-controls {
+ background: #e4ebe8;
+}
+.document-paper {
+ background: linear-gradient(135deg, #e4e8e2, #eeebe2);
+}
+.document-switch button[aria-pressed="true"] {
+ background: #173b49;
+}
+.site-footer {
+ color: #616a6e;
+}
+@media (max-width: 700px) {
+ .studio-home {
+ padding-top: 18px;
+ }
+ .studio-home .studio-intro {
+ padding: 26px 24px 30px;
+ border-radius: 16px;
+ margin-bottom: 32px;
+ }
+ .studio-intro .intro-main {
+ display: block;
+ margin-top: 20px;
+ }
+ .studio-intro h1 {
+ font-size: clamp(42px, 10.5vw, 65px);
+ }
+ .studio-intro .intro-statement {
+ font-size: 20px;
+ }
+ .studio-intro .intro-detail {
+ font-size: 14px;
+ }
+ .studio-intro .intro-portrait {
+ width: 70px;
+ height: 84px;
+ margin-top: 24px;
+ border-width: 4px;
+ border-radius: 8px;
+ }
+ .studio-intro .intro-topline {
+ font-size: 9px;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+ .studio-grid {
+ gap: 24px;
+ }
+ .studio-project-title {
+ flex-wrap: wrap;
+ }
+ .studio-hadlock {
+ padding: 24px;
+ }
+ .studio-contact {
+ padding: 28px 24px;
+ }
+ .studio-contact > a {
+ font-size: 19px;
+ }
+ .primary-links .header-resume {
+ top: 10px;
+ padding: 9px 12px;
+ }
+}
+.contour-lines {
+ opacity: 0.3;
+ mask-image: linear-gradient(120deg, transparent 25%, #000 75%);
+}
+
+/* The hero is a full-width backdrop, not an inset card. */
+.studio-home {
+ padding-top: 0;
+}
+.studio-home .studio-intro {
+ overflow: visible;
+ isolation: isolate;
+ background: none;
+ border: 0;
+ border-radius: 0;
+ box-shadow: none;
+ padding: 44px 0 48px;
+}
+.studio-home .studio-intro::before {
+ content: "";
+ position: absolute;
+ inset-block: 0;
+ left: calc((100% - 100vw) / 2);
+ width: 100vw;
+ z-index: -2;
+ background:
+ radial-gradient(ellipse at 100% 110%, #0076a073, transparent 65%),
+ linear-gradient(115deg, #102e3c, #1c4b5b);
+}
+.studio-intro > .contour-lines {
+ left: calc((100% - 100vw) / 2);
+ width: 100vw;
+}
+@media (max-width: 700px) {
+ .studio-home .studio-intro {
+ padding: 30px 0 36px;
+ border-radius: 0;
+ }
+}
+
+/* Shared product branding over the existing product imagery. */
+.studio-product-logo {
+ background: radial-gradient(ellipse at center, #071c2660, #071c261c 75%);
+}
+.studio-product-logo img {
+ filter: drop-shadow(0 2px 3px #0009) drop-shadow(0 8px 18px #0008);
+}
+.beenvoice-logo img {
+ width: 55%;
+ filter: brightness(0) invert(1) drop-shadow(0 2px 3px #0009) drop-shadow(0 8px 18px #0008);
+}
+.mobile-menu-label,
+.primary-links .header-dropdown .mobile-menu-link {
+ display: none;
+}
+.nav-inner { flex-wrap: nowrap; }
+@media (max-width: 700px) {
+ .nav-inner {
+ height: 64px;
+ gap: 12px;
+ flex-wrap: nowrap;
+ }
+ .nav-inner > .wordmark { flex-shrink: 0; }
+ .primary-links {
+ width: auto;
+ gap: 12px;
+ }
+ .primary-links > a:not(.header-resume),
+ .desktop-menu-label { display: none; }
+ .mobile-menu-label { display: inline; }
+ .primary-links .header-resume {
+ position: static;
+ order: -1;
+ padding: 9px 0;
+ border: 0;
+ background: none;
+ }
+ .header-background summary {
+ min-height: 44px;
+ gap: 5px;
+ }
+ .header-dropdown { right: 0; }
+ .primary-links .header-dropdown .mobile-menu-link { display: block; }
+ .studio-home section[id], .record-section { scroll-margin-top: 84px; }
+}
+@media (max-width: 360px) {
+ .nav-inner, .primary-links { gap: 8px; }
+}
+
+/* Brand marks share a centered, shadowed treatment. */
+.manyangles-lockup {
+ inset: 0;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+ background: radial-gradient(ellipse at center, #071c2660, #071c261c 75%);
+}
+.manyangles-lockup svg,
+.manyangles-lockup span {
+ filter: drop-shadow(0 2px 3px #0009) drop-shadow(0 8px 18px #0008);
+}
+.hadlock-brand img { width: min(100%, 340px); height: auto; }
+
+/* Personal postcards: photographs, editorial captions, a playful calendar. */
+.personal-corners { padding-block: 20px 32px; }
+.corners-grid { gap: 22px; align-items: stretch; }
+.corner-card {
+ position: relative;
+ isolation: isolate;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ min-height: 350px;
+ padding: 26px;
+ overflow: hidden;
+ border: 1px solid #ffffff40;
+ border-radius: 14px;
+ background: #173b49;
+ box-shadow: 0 12px 26px #17344116;
+ color: #fff8e9;
+}
+.corner-card > img {
+ position: absolute;
+ inset: 0;
+ height: 100%;
+ z-index: -2;
+ transition: transform 400ms ease;
+}
+.corner-card::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ background: linear-gradient(180deg, #102e3c00 15%, #102e3c55 45%, #102e3cf5);
+}
+.corner-card::after {
+ content: "↗";
+ position: absolute;
+ top: 18px;
+ right: 18px;
+ display: grid;
+ place-items: center;
+ width: 34px;
+ height: 34px;
+ border: 1px solid #fff8e960;
+ border-radius: 50%;
+ background: #102e3c30;
+}
+.corner-card:hover > img { transform: scale(1.04); }
+.corner-card:focus-visible { outline: 3px solid var(--studio-blue); outline-offset: 5px; }
+.corner-card > span {
+ margin: 0 0 10px;
+ font-family: var(--font-heading), Georgia, serif;
+ font-size: 32px;
+ line-height: 1.1;
+ font-weight: 400;
+}
+.corner-card > p { margin: 0; color: #e2e9e6; font-size: 13px; line-height: 1.65; }
+.october-corner {
+ background: radial-gradient(ellipse at 100% 0, #f0d9a4, transparent 70%), #e5b88b;
+ color: #623719;
+}
+.october-corner::before { background: repeating-linear-gradient(0deg, transparent 0 31px, #6237190a 31px 32px); }
+.october-corner::after { border-color: #62371950; background: #fff8e930; }
+.october-corner > p { color: #744f32; }
+.october-stamp { background: none; height: auto; flex: 1; min-height: 170px; padding-bottom: 24px; }
+.october-stamp strong { font-size: 82px; }
+.october-stamp > span { font-size: 13px; letter-spacing: .3em; }
+.october-stamp small { margin-top: 8px; }
+
+/* The closing band echoes the full-bleed hero. */
+.studio-contact {
+ overflow: visible;
+ border: 0;
+ border-radius: 0;
+ background: none;
+ padding: 48px 0;
+ margin: 24px 0 0;
+}
+.site-footer::before {
+ content: "";
+ position: absolute;
+ inset-block: 0;
+ left: calc((100% - 100vw) / 2);
+ width: 100vw;
+ z-index: -2;
+ background: radial-gradient(ellipse at 100% 110%, #0076a073, transparent 65%), linear-gradient(115deg, #102e3c, #1c4b5b);
+}
+.site-footer > .contour-lines { left: calc((100% - 100vw) / 2); width: 100vw; }
+.site-footer { position: relative; isolation: isolate; color: #fff8e9; padding-top: 0; padding-bottom: 28px; }
+.site-footer .footer-top { border-top-color: #b9d4d438; padding-block: 28px; align-items: flex-start; }
+.site-footer .wordmark { flex-shrink: 0; }
+.site-footer .footer-links { max-width: 660px; gap: 12px 22px; color: #c0d1d6; line-height: 1.8; }
+.site-footer .footer-bottom { color: #a5bec6; padding-top: 8px; }
+.site-footer a:hover { color: #fff; }
+@media (max-width: 700px) {
+ .corners-grid { gap: 18px; }
+ .corner-card { display: flex; min-height: 310px; padding: 26px; }
+ .corner-card > img { height: 100%; }
+ .corner-card > span { align-self: auto; }
+ .corner-card > p { margin: 0; }
+ .october-stamp { height: auto; min-height: 150px; }
+ .october-stamp strong { font-size: 76px; }
+ .studio-contact { padding: 36px 0; }
+ .site-footer .footer-top { flex-direction: column; gap: 20px; }
+}
+@media (prefers-reduced-motion: reduce) {
+ .corner-card, .corner-card > img { transition: none; }
+}
+.hadlock-brand img { filter: brightness(0) saturate(100%) invert(16%) sepia(20%) saturate(1477%) hue-rotate(154deg) brightness(91%) contrast(89%); }
+
+/* Responsive composition of the shadcn navigation primitives. */
+.header-desktop-navigation { flex: 0 1 auto; }
+.header-mobile-navigation { display: none; align-items: center; gap: 4px; }
+@media (max-width: 700px) {
+ .header-desktop-navigation { display: none; }
+ .header-mobile-navigation { display: flex; }
+}
+/* Tailwind 3 equivalents for the registry's Tailwind 4 menu tokens. */
+[data-slot="dropdown-menu-content"] {
+ max-height: var(--radix-dropdown-menu-content-available-height);
+ transform-origin: var(--radix-dropdown-menu-content-transform-origin);
+ background: hsl(var(--popover) / .98);
+}
+[data-slot="dropdown-menu-item"] { min-height: 44px; font-size: 13px; outline: none; }
+[data-slot="dropdown-menu-item"][data-highlighted],
+[data-slot="dropdown-menu-item"][aria-current="page"] {
+ background: hsl(var(--accent));
+ color: hsl(var(--accent-foreground));
+}
+:root {
+ --popover: 48 100% 99%;
+ --popover-foreground: 205 34% 16%;
+ --accent: 190 20% 91%;
+ --accent-foreground: 199 47% 20%;
+}
+
+/* One proportional logo height, independent of each wordmark's width. */
+.studio-project-image {
+ container-type: inline-size;
+ --project-logo-height: 8cqw;
+}
+.studio-product-logo img,
+.beenvoice-logo img {
+ width: auto;
+ height: var(--project-logo-height);
+ max-width: 84%;
+ object-fit: contain;
+}
+.manyangles-lockup { gap: 2cqw; }
+.manyangles-lockup svg {
+ width: var(--project-logo-height);
+ height: var(--project-logo-height);
+ max-width: none;
+}
+.manyangles-lockup span {
+ /* Match visible capital height to the SVG wordmarks. */
+ font-size: 10.5cqw;
+ line-height: 1;
+}
+
+/* Contact and footer use a single shared backdrop. */
+.site-footer .studio-contact { margin: 0; scroll-margin-top: 84px; }
+
+/* The page heading already separates the writing list from its introduction. */
+.writing-page .record-row:first-child { border-top: 0; padding-top: 0; }
+
+/* Archive previews use the same proportional logo system as the homepage. */
+.archive-image { container-type: inline-size; --project-logo-height: 8cqw; }
+.archive-image > img { object-fit: contain; object-position: center; }
+.archive-image--photo > img { object-fit: cover; }
+
+/* September 10's full-width mobile header panel. */
+.restored-header-menu {
+ width: 100vw;
+ border-radius: 0;
+ padding: 16px 20px;
+ background: hsl(var(--background) / .96);
+ backdrop-filter: blur(18px);
+ border-top: 1px solid hsl(var(--border) / .5);
+}
+.restored-header-menu [data-slot="dropdown-menu-item"] {
+ gap: 12px;
+ padding-inline: 12px;
+}
+.restored-header-menu [data-slot="dropdown-menu-item"] svg { width: 16px; height: 16px; }
+
+.mobile-menu-backdrop {
+ position: fixed;
+ inset: 64px 0 0;
+ z-index: 40;
+ background: linear-gradient(180deg, #f4f1ea10, #f4f1ea8c 70%);
+ -webkit-backdrop-filter: blur(14px);
+ backdrop-filter: blur(14px);
+ -webkit-mask-image: linear-gradient(180deg, transparent, #000 42%);
+ mask-image: linear-gradient(180deg, transparent, #000 42%);
+}
+@media (min-width: 701px) {
+ .mobile-menu-backdrop { display: none; }
+}
+
+/* Portrait beside the introduction on tablets, above the name on phones. */
+@media (min-width: 541px) and (max-width: 900px) {
+ .studio-intro .intro-main {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 110px;
+ align-items: start;
+ gap: 24px;
+ }
+ .studio-intro .intro-portrait {
+ width: 110px;
+ height: 133px;
+ margin-top: 0;
+ border-width: 4px;
+ }
+}
+@media (max-width: 540px) {
+ .studio-intro .intro-main {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 18px;
+ }
+ .studio-intro .intro-portrait {
+ order: -1;
+ width: 64px;
+ height: 77px;
+ margin: 0;
+ border-width: 3px;
+ }
+}
+
+/* Editorial treatment for the media, writing, and work archives. */
+.media-page .archive-header,
+.writing-page .archive-header,
+.portfolio-page .archive-header {
+ border-bottom: 0;
+ margin-bottom: 28px;
+ padding-bottom: 8px;
+}
+.media-page .archive-header > p:last-child,
+.writing-page .archive-header > p:last-child,
+.portfolio-page .archive-header > p:last-child {
+ max-width: 700px;
+ font-size: 17px;
+ line-height: 1.6;
+}
+
+.media-feature {
+ position: relative;
+ isolation: isolate;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ min-height: 400px;
+ overflow: hidden;
+ padding: 32px 38px 36px;
+ border-radius: 16px;
+ color: #fff8e9;
+ background: radial-gradient(ellipse at 100% 100%, #0076a080, transparent 58%), linear-gradient(115deg, #102e3c, #1c4b5b);
+}
+.media-feature .contour-lines { left: 20%; width: 80%; }
+.media-feature-top,
+.media-story-top,
+.media-story-bottom {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+}
+.media-feature-top,
+.media-story-top { font-size: 11px; letter-spacing: .11em; text-transform: uppercase; }
+.media-feature-top { color: #bad2d8; }
+.media-feature-bottom { max-width: 770px; }
+.media-feature h2 {
+ font-family: var(--font-heading), Georgia, serif;
+ font-size: clamp(42px, 5.2vw, 70px);
+ font-weight: 400;
+ line-height: 1.03;
+ letter-spacing: -.035em;
+ text-wrap: balance;
+}
+.media-feature p { max-width: 610px; margin-top: 17px; font-size: 15px; line-height: 1.65; color: #d1e0e1; }
+.media-feature a {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ margin-top: 25px;
+ padding-bottom: 6px;
+ border-bottom: 1px solid #b9d4d48c;
+ font-size: 13px;
+ font-weight: 600;
+}
+.media-feature a:hover { color: #b7e1e8; }
+.media-story-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 18px; }
+.media-story {
+ display: flex;
+ flex-direction: column;
+ min-height: 335px;
+ padding: 28px 30px;
+ border: 1px solid var(--studio-rule);
+ border-radius: 14px;
+ background: #fffefa;
+}
+.media-story-top { color: #54717c; }
+.media-story-top svg { flex-shrink: 0; }
+.media-story h2 {
+ margin-top: 38px;
+ font-family: var(--font-heading), Georgia, serif;
+ font-size: clamp(27px, 3vw, 39px);
+ font-weight: 400;
+ line-height: 1.13;
+ letter-spacing: -.025em;
+ text-wrap: balance;
+}
+.media-story h2 a:hover { color: var(--studio-blue); }
+.media-story > p { margin-top: 14px; color: var(--studio-muted); font-size: 14px; line-height: 1.65; }
+.media-story-bottom {
+ margin-top: auto;
+ padding-top: 25px;
+ font-size: 12px;
+ color: var(--studio-muted);
+}
+.media-story-bottom a { color: var(--studio-blue); white-space: nowrap; }
+.media-story-bottom a:hover { text-decoration: underline; text-underline-offset: 4px; }
+
+.writing-page .record-section {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 18px;
+ margin: 0;
+}
+.writing-page .writing-entry {
+ display: flex;
+ flex-direction: column;
+ gap: 22px;
+ min-height: 275px;
+ padding: 30px;
+ border: 1px solid var(--studio-rule);
+ border-radius: 14px;
+ background: #fffefa;
+}
+.writing-page .writing-entry:first-child {
+ position: relative;
+ isolation: isolate;
+ grid-column: 1 / -1;
+ min-height: 350px;
+ padding: 38px;
+ overflow: hidden;
+ border: 0;
+ color: #fff8e9;
+ background: radial-gradient(ellipse at 100% 100%, #0076a080, transparent 62%), linear-gradient(115deg, #102e3c, #1c4b5b);
+}
+.writing-entry .record-meta { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; }
+.writing-entry:first-child .record-meta { color: #bbd1d5; }
+.writing-entry .record-body { display: flex; flex-direction: column; flex: 1; max-width: none; }
+.writing-entry .record-body h2 {
+ max-width: 630px;
+ margin: 0;
+ font-family: var(--font-heading), Georgia, serif;
+ font-size: clamp(29px, 3vw, 39px);
+ font-weight: 400;
+ line-height: 1.14;
+ letter-spacing: -.025em;
+}
+.writing-entry:first-child .record-body h2 { font-size: clamp(44px, 5vw, 65px); }
+.writing-entry .record-body h2 a:hover { color: var(--studio-blue); }
+.writing-entry:first-child .record-body h2 a:hover { color: #b7e1e8; }
+.writing-entry .record-body > p { margin: 12px 0 0; font-size: 14px; line-height: 1.7; }
+.writing-entry:first-child .record-body > p { max-width: 540px; color: #d2e1e3; }
+.writing-entry .record-body .record-kicker { margin-top: 15px; font-size: 11px; letter-spacing: .06em; text-transform: uppercase; }
+.writing-entry:first-child .record-body .record-kicker { color: #a9c7cd; }
+.writing-entry-link { display: inline-flex; align-items: center; gap: 8px; align-self: flex-start; margin-top: auto; padding-top: 22px; color: var(--studio-blue); font-size: 13px; font-weight: 600; }
+.writing-entry:first-child .writing-entry-link { color: #fff8e9; }
+.writing-entry-link:hover { text-decoration: underline; text-underline-offset: 4px; }
+
+.article-page { max-width: 820px; margin-inline: auto; }
+.article-page > .parent-page-link { margin-bottom: 35px; }
+.article-page-header { border-bottom: 1px solid var(--studio-rule); padding-bottom: 35px; margin-bottom: 42px; }
+.article-page-header h1 {
+ margin: 14px 0 16px;
+ font-family: var(--font-heading), Georgia, serif;
+ font-size: clamp(45px, 6vw, 74px);
+ font-weight: 400;
+ line-height: 1.05;
+ letter-spacing: -.035em;
+ text-wrap: balance;
+}
+.article-page-summary { max-width: 680px; font-size: 19px; line-height: 1.55; color: var(--studio-muted); }
+.article-page-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 22px; margin-top: 25px; color: var(--studio-muted); font-size: 12px; }
+.article-page-tags { display: flex; flex-wrap: wrap; gap: 7px; }
+.article-page-body { max-width: 730px; color: var(--studio-ink); font-size: 16px; line-height: 1.85; }
+.article-page-body.prose h2,
+.article-page-body.prose h3 { font-family: var(--font-heading), Georgia, serif; font-weight: 400; letter-spacing: -.02em; }
+.article-page-body.prose h2 { font-size: 36px; }
+.article-page-body.prose h3 { font-size: 29px; }
+.article-page-body pre { overflow-x: auto; border-radius: 10px; }
+.article-page-body img { max-width: 100%; height: auto; border-radius: 8px; }
+.article-page-return { display: inline-block; margin-top: 50px; padding-top: 20px; border-top: 1px solid var(--studio-rule); color: var(--studio-blue); font-size: 13px; }
+
+.portfolio-page .archive-header { margin-bottom: 14px; }
+.portfolio-page .project-tools {
+ gap: 12px;
+ margin-bottom: 42px;
+ padding: 13px;
+ border: 1px solid var(--studio-rule);
+ border-radius: 12px;
+ background: #fffefa;
+}
+.portfolio-page .project-tools label { position: relative; min-width: 260px; }
+.portfolio-page .project-tools label > svg { position: absolute; top: 50%; left: 14px; transform: translateY(-50%); color: #67808a; }
+.portfolio-page .project-tools input { margin: 0; padding: 11px 13px 11px 41px; border-color: transparent; background: #f4f5f0; }
+.portfolio-page .project-tools input:focus { outline: 2px solid var(--studio-blue); outline-offset: 2px; }
+.portfolio-page .project-tools > p { padding: 0 5px; white-space: nowrap; }
+.portfolio-page .project-filters button { border-radius: 6px; }
+.portfolio-section-heading { display: flex; align-items: end; justify-content: space-between; gap: 25px; margin: 0 0 22px; }
+.portfolio-section-heading span { order: 2; font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: #54717c; }
+.portfolio-section-heading h2 { font-family: var(--font-heading), Georgia, serif; font-size: clamp(32px, 3vw, 43px); line-height: 1.1; font-weight: 400; }
+.portfolio-feature-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
+.portfolio-page .archive-card--featured {
+ padding: 12px;
+ border-radius: 16px;
+ background: #fffefa;
+ box-shadow: 0 6px 18px #17344108;
+}
+.archive-card--featured .archive-image { border-radius: 9px; aspect-ratio: 1.62; }
+.archive-card--featured .archive-body { padding: 20px 9px 10px; }
+.archive-card-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; }
+.archive-card-heading h2 { font-family: var(--font-heading), Georgia, serif; font-size: 31px; font-weight: 400; letter-spacing: -.025em; }
+.archive-card-heading > span { font-size: 11px; color: #67808a; }
+.portfolio-page .archive-body > p { margin-top: 10px; line-height: 1.6; }
+.portfolio-page .project-tags { gap: 6px; margin-top: 19px; }
+.portfolio-page .project-tags span { padding: 5px 8px; border: 0; border-radius: 4px; background: #eef1ec; font-size: 10px; color: #54717c; }
+.portfolio-page .archive-links { display: flex; gap: 12px 25px; flex-wrap: wrap; margin-top: 22px; }
+.portfolio-page .project-detail { border-top: 1px solid var(--studio-rule); margin-top: 20px; padding-top: 10px; }
+.portfolio-page .project-detail summary { padding: 5px 0; }
+.portfolio-more { margin-top: 65px; }
+.portfolio-more-list { border-top: 1px solid var(--studio-rule); }
+.portfolio-page .archive-card--compact {
+ display: grid;
+ grid-template-columns: 174px minmax(0, 1fr);
+ gap: 25px;
+ padding: 20px 0;
+ border: 0;
+ border-bottom: 1px solid var(--studio-rule);
+ border-radius: 0;
+ background: none;
+}
+.archive-card--compact .archive-image { aspect-ratio: 1.6; align-self: start; border-radius: 8px; }
+.archive-image--fallback { display: grid; place-items: center; padding: 15px; background: radial-gradient(ellipse at 100% 100%, #0076a080, transparent 70%), #173b49; color: #fff8e9; }
+.archive-image--fallback > span { font-family: var(--font-heading), Georgia, serif; font-size: clamp(15px, 2cqw, 24px); line-height: 1.1; text-align: center; }
+.archive-card--compact .archive-body { padding: 0; }
+.archive-card--compact .archive-card-heading h2 { font-size: 27px; }
+.archive-card--compact .archive-body > p { margin-top: 6px; }
+.archive-card--compact .project-tags { margin-top: 14px; }
+.archive-card--compact .archive-links { margin-top: 15px; }
+.archive-card--compact .archive-links { padding-top: 0; }
+.archive-card--compact .project-detail { margin-top: 13px; }
+
+@media (max-width: 700px) {
+ .media-feature { min-height: 410px; padding: 25px; }
+ .media-feature-top { flex-wrap: wrap; gap: 6px 15px; }
+ .media-feature h2 { font-size: clamp(42px, 11vw, 57px); }
+ .media-story-grid,
+ .writing-page .record-section,
+ .portfolio-feature-grid { grid-template-columns: 1fr; }
+ .media-story { min-height: 310px; padding: 24px; }
+ .media-story h2 { margin-top: 28px; }
+ .writing-page .writing-entry:first-child { grid-column: auto; min-height: 360px; padding: 28px; }
+ .writing-page .writing-entry { min-height: 270px; padding: 25px; }
+ .article-page-header h1 { font-size: clamp(43px, 11vw, 62px); }
+ .article-page-summary { font-size: 17px; }
+ .article-page-body { font-size: 15px; }
+ .portfolio-page .project-tools { padding: 12px; }
+ .portfolio-page .project-tools label { min-width: 100%; }
+ .portfolio-page .project-tools > p { width: 100%; }
+ .portfolio-section-heading { align-items: flex-start; flex-direction: column; gap: 8px; }
+ .portfolio-section-heading span { order: -1; }
+ .portfolio-page .archive-card--compact { grid-template-columns: 100px minmax(0, 1fr); gap: 15px; padding: 18px 0; }
+ .archive-card--compact .archive-card-heading h2 { font-size: 22px; }
+ .archive-card--compact .archive-body > p { font-size: 13px; }
+ .archive-card--compact .project-tags { display: none; }
+ .archive-card--compact .archive-links { gap: 9px 15px; }
+ .portfolio-more { margin-top: 45px; }
+}
+@media (max-width: 380px) {
+ .portfolio-page .archive-card--compact { grid-template-columns: 78px minmax(0, 1fr); }
+ .media-story-bottom { align-items: flex-start; flex-direction: column; gap: 8px; }
+}
+
+/* The site follows the operating system's appearance, including every archive. */
+@media (prefers-color-scheme: dark) {
+ :root {
+ color-scheme: dark;
+ --background: 202 32% 9%;
+ --foreground: 43 30% 94%;
+ --card: 201 28% 14%;
+ --card-foreground: 43 30% 94%;
+ --popover: 201 27% 16%;
+ --popover-foreground: 43 30% 94%;
+ --primary: 192 65% 66%;
+ --primary-foreground: 202 35% 11%;
+ --secondary: 201 23% 21%;
+ --secondary-foreground: 43 30% 94%;
+ --muted: 201 23% 21%;
+ --muted-foreground: 194 18% 70%;
+ --accent: 198 28% 25%;
+ --accent-foreground: 43 30% 94%;
+ --border: 198 19% 30%;
+ --input: 198 19% 30%;
+ --ring: 192 65% 66%;
+ --studio-blue: #83d1e5;
+ --studio-muted: #afc0c5;
+ --studio-rule: #38505a;
+ --studio-ink: #f2eee4;
+ }
+
+ body {
+ background:
+ radial-gradient(ellipse at 95% 5%, #1a3540 0, transparent 38rem),
+ radial-gradient(ellipse at 0 65%, #243039 0, transparent 40rem), #101d25;
+ color: hsl(var(--foreground));
+ }
+ ::selection { background: #28586b; color: #f8f4e9; }
+ .skip-link { background: #1c303a; color: var(--studio-blue); }
+ .site-header { background: #142832f0; border-color: var(--studio-rule); }
+ .site-header .wordmark, .site-header a { color: hsl(var(--foreground)); }
+ .site-header a:hover, .site-header a[aria-current="page"] { color: var(--studio-blue); }
+ .restored-header-menu, .header-dropdown { background: #1c303a; color: hsl(var(--foreground)); border-color: var(--studio-rule); }
+ .restored-header-menu [data-slot="dropdown-menu-item"] { color: hsl(var(--foreground)); }
+ .restored-header-menu [data-slot="dropdown-menu-item"][data-highlighted],
+ .header-dropdown a:hover, .header-dropdown a[aria-current="page"] { background: #294451; color: var(--studio-blue); }
+ .mobile-menu-backdrop { background: linear-gradient(180deg, #10263010, #102630b8 70%); }
+
+ .studio-grid > article, .studio-hadlock, .media-story,
+ .writing-page .writing-entry:not(:first-child),
+ .portfolio-page .project-tools, .portfolio-page .archive-card--featured,
+ .travel-note, .mark-live-header, .mark-choice,
+ .section-jumps a, .project-tools input, .control-group button,
+ .project-filters button:not([aria-pressed="true"]), .project-empty button {
+ background: #1a2b34;
+ color: hsl(var(--foreground));
+ border-color: var(--studio-rule);
+ }
+ .studio-hadlock { background: linear-gradient(120deg, #1d3942, #263734); }
+ .hadlock-brand img { filter: brightness(0) invert(1); }
+ .archive-header h1, .archive-card-heading h2, .article-page-body,
+ .record-body h2, .record-body h3 { color: hsl(var(--foreground)); }
+ .media-story-top, .portfolio-section-heading span,
+ .archive-card-heading > span, .portfolio-page .project-tools label > svg,
+ .portfolio-page .project-tags span { color: var(--studio-muted); }
+ .portfolio-page .project-tools input { background: #12242e; }
+ .portfolio-page .project-tools input::placeholder { color: var(--studio-muted); }
+ .portfolio-page .project-tags span { background: #29434c; }
+ .mark-large { background: #253842; }
+ .mark-reversed { --mark-paper: #16323e; background: #83d1e5; color: #112630; }
+ .mark-reversed .name-only-mark > span { color: #112630; }
+ .october-corner { background: radial-gradient(ellipse at 100% 0, #774e31, transparent 70%), #573e31; color: #fff1dc; }
+ .october-corner::before { background: repeating-linear-gradient(0deg, transparent 0 31px, #fff1dc12 31px 32px); }
+ .october-corner::after { border-color: #fff1dc60; }
+ .october-corner > p { color: #eed6bd; }
+ .document-controls, .document-switch { background: #1a2b34; }
+ .document-paper { background: linear-gradient(135deg, #1c2c34, #293640); }
+ .document-switch button[aria-pressed="true"] { background: #385d6c; color: #fff; }
+ .article-page-body.prose { color: hsl(var(--foreground)); }
+ .article-page-body.prose :is(h2, h3, h4, strong, a, blockquote) { color: hsl(var(--foreground)); }
+ .article-page-body.prose a { color: var(--studio-blue); }
+ .article-page-body.prose code:not(pre code) { color: #cbeaf0; }
+ .article-page-body.prose pre { background: #091821; }
+ .site-footer::before { background: radial-gradient(ellipse at 100% 110%, #006b8773, transparent 65%), linear-gradient(115deg, #0b202b, #143746); }
+ .site-footer .footer-links, .site-footer .footer-bottom { color: #b9cbd0; }
+}