Phase 5: every page of the site, the resume PDF and RSS

Landing, About, Now, Resume (with a generated PDF), Work and case studies,
Inquiries and the sent page, Library and the Shelf, Workshop and craft pages,
Tabletop, Blog with topics and posts, RSS, the Store, product lines, the
order form and the 404, all rendered from the content service's notes.

Schema: experience previousRoles, skills stack core items, page stats,
make tags and deck favorite. Icon names from notes map to brand icons in one
place, and HTML comments are dropped when Markdown renders.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
Claude
2026-10-11 21:33:49 +00:00
parent 7025e95f89
commit 99ecdc2113
83 changed files with 11681 additions and 14 deletions
+559
View File
@@ -0,0 +1,559 @@
---
/**
* The resume: at a glance with the PDF, highlights, skills and education in
* the sidebar, and the experience timeline. Every word comes from the Resume
* notes and the /resume Page text; the PDF is built from the same data.
*/
import Button from "@gk/ui/components/Button.astro";
import Chip from "@gk/ui/components/Chip.astro";
import GiltCorners from "@gk/ui/components/GiltCorners.astro";
import Icon from "@gk/ui/components/Icon.astro";
import PageShell from "@gk/ui/components/PageShell.astro";
import Seal from "@gk/ui/components/Seal.astro";
import Star from "@gk/ui/components/Star.astro";
import StarPath from "@gk/ui/components/StarPath.astro";
import TickRing from "@gk/ui/components/TickRing.astro";
import TickScale from "@gk/ui/components/TickScale.astro";
import { site } from "../lib/page.ts";
import { period, resumeData } from "../lib/resume.ts";
import { formatDate } from "../lib/site.ts";
const s = await site();
const r = resumeData(s);
const caseStudies = s.caseStudies();
const isCore = (group: { core?: string[] }, item: string) => group.core?.some((c) => c.toLowerCase() === item.toLowerCase()) ?? false;
---
<PageShell title="Resume" family="resume" current="resume" description={`${r.name}: ${r.headline}. Experience, skills and a PDF to take with you.`}>
<div slot="hero" class="hero">
<div class="intro">
<div class="plaque"><GiltCorners inset={4} size={30} />RESUME AT A GLANCE</div>
<h1>{r.name}</h1>
<p class="headline">{[r.headline, r.location].filter(Boolean).join(" · ")}</p>
{r.summary && <p class="summary">{r.summary}</p>}
</div>
<aside class="take" aria-labelledby="take-h">
<span class="take-star" aria-hidden="true"><TickRing size={36}><Star size={16} /></TickRing></span>
<span class="take-head">
<span id="take-h" class="take-label">TAKE IT WITH YOU</span>
<TickScale width={120} count={24} />
</span>
<Button href="/resume.pdf" download="Greg-Kilmer-Resume.pdf">
<Icon name="arrow-right" size={18} />Download PDF
</Button>
{r.updated && <span class="pdf-date">PDF · updated {formatDate(r.updated)}</span>}
<Button variant="secondary" href="/inquiries?type=job">Start an inquiry</Button>
{r.availability && <span class="avail"><span class="dot" aria-hidden="true"></span>{r.availability}</span>}
</aside>
</div>
{
r.stats.length > 0 && (
<section aria-label="Highlights" class="stats">
{r.stats.map((st) => (
<div class="stat">
<span class="stat-value">{st.value}</span>
<TickScale width={120} count={24} />
<span class="stat-label">{st.label}</span>
{st.detail && <span class="stat-detail">{st.detail}</span>}
</div>
))}
</section>
)
}
{r.stats.length > 0 && <div class="rule"><StarPath seed="resume" strength="calm" centerStar={false} /></div>}
<div class="columns">
<aside class="side">
{
r.skills && (
<section aria-labelledby="skills-h" class="panel">
<Seal name="mini-astrolabe" size={116} class="panel-seal" />
<div class="panel-head">
<h2 id="skills-h">SKILLS</h2>
<TickScale width={140} count={28} />
</div>
{r.skills.stack.map((g) => (
<div class="group">
<span class="group-label">{g.label}</span>
<div class="chips">
{g.items.map((item) => (
<Chip core={isCore(g, item)}>{item}</Chip>
))}
</div>
</div>
))}
{r.skills.stack.some((g) => g.core?.length) && <span class="hint">Teal outline marks the core stack.</span>}
</section>
)
}
{
r.skills?.core?.length ? (
<section aria-labelledby="strengths-h" class="panel">
<div class="panel-head">
<h2 id="strengths-h">STRENGTHS</h2>
<TickScale width={140} count={28} />
</div>
<ul class="strengths">
{r.skills.core.map((c) => (
<li>
<Star size={10} />
{c}
</li>
))}
</ul>
</section>
) : null
}
{
r.education?.degrees.length ? (
<section aria-labelledby="edu-h" class="panel">
<Seal name="mini-tome" size={116} class="panel-seal" />
<div class="panel-head">
<h2 id="edu-h">EDUCATION</h2>
<TickScale width={140} count={28} />
</div>
{r.education.degrees.map((d) => (
<div class="degree">
<span class="degree-name">{d.credential}</span>
<span class="muted">
{d.institution} · {d.period}
</span>
{d.detail && <span class="muted small">{d.detail}</span>}
</div>
))}
</section>
) : null
}
{
r.education?.certifications?.length ? (
<section aria-labelledby="certs-h" class="panel">
<Seal name="mini-layered" size={116} class="panel-seal" />
<div class="panel-head">
<h2 id="certs-h">CERTIFICATIONS</h2>
<TickScale width={140} count={28} />
</div>
{r.education.certifications.map((c) => (
<div class="cert">
<span class="cert-name">{c.name}</span>
<span class="muted">
{c.issuer} · {c.year}
</span>
</div>
))}
</section>
) : null
}
</aside>
<section aria-labelledby="exp-h" class="experience">
<div class="exp-head">
<h2 id="exp-h">Experience</h2>
<span class="gk-underline" aria-hidden="true"></span>
</div>
<div class="timeline">
<div class="grid" aria-hidden="true"></div>
<div class="spine" aria-hidden="true"></div>
{
r.roles.map((role, i) => (
<article id={role.slug} class:list={["role", { current: i === 0 && !role.data.end }]}>
{i === 0 && !role.data.end && <GiltCorners inset={4} size={30} />}
<span class="marker" aria-hidden="true">
<Star size={i === 0 ? 14 : 11} tone={i === 0 ? "gold" : "outline"} />
</span>
<div class="role-top">
<h3>{role.data.role}</h3>
<span class="dates">{period(role)}</span>
</div>
<span class="company">{role.data.company}{role.data.location ? ` · ${role.data.location}` : ""}</span>
{role.data.previousRoles?.length ? (
<span class="previously">Previously {role.data.previousRoles.join(" and ")} at {role.data.company}</span>
) : null}
{role.data.focus && <span class="focus">{role.data.focus}</span>}
<ul>
{role.data.bullets.map((b) => (
<li>
<Star size={9} />
<span>{b}</span>
</li>
))}
</ul>
{role.data.tech?.length ? (
<div class="chips">
{role.data.tech.map((t) => (
<Chip>{t}</Chip>
))}
</div>
) : null}
</article>
))
}
</div>
{caseStudies.length > 0 && <Button variant="text" href="/work" arrow>See the case studies on the Work page</Button>}
</section>
</div>
</PageShell>
<style>
.hero {
display: flex;
flex-wrap: wrap;
gap: 32px;
align-items: center;
padding-top: 8px;
}
.intro {
flex: 1 1 520px;
min-width: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
.plaque {
align-self: flex-start;
position: relative;
font-size: 11px;
letter-spacing: 0.3em;
font-weight: 600;
color: var(--gk-gold-bright);
border: 1px solid rgba(201, 162, 74, 0.55);
border-radius: 10px;
padding: 17px 60px;
line-height: 1.2;
}
h1 {
margin: 0;
font-size: clamp(34px, 6vw, 48px);
letter-spacing: 0.08em;
line-height: 1.05;
}
.headline {
margin: 0;
font-size: 18px;
color: var(--gk-soft);
}
.summary {
margin: 0;
padding-left: 14px;
border-left: 2px solid var(--gk-gold);
font-size: 16px;
line-height: 1.65;
color: var(--gk-soft);
max-width: 62ch;
}
.take {
flex: 0 1 310px;
min-width: 0;
position: relative;
background: var(--gk-midnight);
border: 1px solid rgba(201, 162, 74, 0.6);
border-radius: 12px;
box-shadow:
inset 0 0 0 5px var(--gk-midnight),
inset 0 0 0 6px rgba(201, 162, 74, 0.3);
padding: 32px 26px 24px;
display: flex;
flex-direction: column;
gap: 12px;
}
.take-star {
position: absolute;
left: 50%;
top: -18px;
transform: translateX(-50%);
border-radius: 999px;
background: var(--gk-midnight);
display: flex;
}
.take-head {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.take-label {
font-size: 11px;
letter-spacing: 0.3em;
font-weight: 600;
color: var(--gk-gold-bright);
}
.pdf-date {
margin-top: -4px;
font-size: 12px;
color: var(--gk-muted);
text-align: center;
}
.avail {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding-top: 12px;
border-top: 1px dashed rgba(244, 242, 237, 0.16);
font-size: 13px;
color: var(--gk-soft);
text-align: center;
}
.dot {
flex: 0 0 8px;
height: 8px;
border-radius: 999px;
background: var(--gk-teal-bright);
box-shadow: 0 0 8px rgba(43, 194, 183, 0.7);
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
padding: 12px 0 8px;
}
.stat {
background: var(--gk-midnight);
border: 1px solid var(--gk-lines);
border-top: 2px solid var(--gk-gold);
border-radius: 10px;
padding: 24px;
display: flex;
flex-direction: column;
gap: 6px;
}
.stat-value {
font-family: var(--gk-font-display);
font-weight: 700;
font-size: 44px;
color: var(--gk-gold-bright);
line-height: 1;
}
.stat-label {
font-size: 15px;
font-weight: 600;
}
.stat-detail {
font-size: 14px;
color: var(--gk-muted);
line-height: 1.5;
}
.rule {
padding: 28px 0 4px;
}
.columns {
display: flex;
flex-wrap: wrap;
gap: 48px;
align-items: flex-start;
padding: 32px 0 24px;
}
.side {
flex: 1 1 300px;
min-width: 0;
display: flex;
flex-direction: column;
gap: 24px;
}
.panel {
position: relative;
overflow: hidden;
background: var(--gk-midnight);
border: 1px solid var(--gk-lines);
border-radius: 10px;
padding: 24px;
display: flex;
flex-direction: column;
gap: 14px;
}
.panel :global(.panel-seal) {
position: absolute;
right: -30px;
top: -30px;
opacity: 0.12;
pointer-events: none;
}
.panel-head {
display: flex;
flex-direction: column;
gap: 6px;
}
.panel h2 {
margin: 0;
font-family: var(--gk-font-body);
font-size: 11px;
letter-spacing: 0.3em;
font-weight: 600;
color: var(--gk-gold-bright);
}
.group {
display: flex;
flex-direction: column;
gap: 8px;
}
.group-label {
font-size: 13px;
font-weight: 600;
color: var(--gk-muted);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.hint,
.muted {
font-size: 13px;
color: var(--gk-muted);
}
.small {
font-size: 13px;
line-height: 1.5;
}
.strengths {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
font-size: 14px;
}
.strengths li {
display: flex;
align-items: center;
gap: 10px;
}
.degree {
display: flex;
flex-direction: column;
gap: 4px;
}
.degree-name {
font-family: var(--gk-font-display);
font-weight: 700;
font-size: 17px;
}
.cert {
display: flex;
justify-content: space-between;
gap: 12px;
font-size: 14px;
}
.cert-name {
font-weight: 600;
}
/* Once the columns stack, the experience leads and the sidebar follows. */
@media (max-width: 960px) {
.experience {
order: -1;
}
}
.experience {
flex: 2 1 560px;
min-width: 0;
display: flex;
flex-direction: column;
gap: 20px;
}
.exp-head {
display: flex;
flex-direction: column;
gap: 8px;
}
.exp-head h2 {
margin: 0;
font-size: 28px;
letter-spacing: 0.08em;
}
.timeline {
position: relative;
display: flex;
flex-direction: column;
gap: 24px;
padding-left: 36px;
}
.grid {
position: absolute;
inset: -24px -24px -24px 0;
pointer-events: none;
background:
repeating-linear-gradient(0deg, rgba(244, 242, 237, 0.045) 0 1px, transparent 1px 40px),
repeating-linear-gradient(90deg, rgba(244, 242, 237, 0.045) 0 1px, transparent 1px 40px);
-webkit-mask-image: radial-gradient(ellipse at 40% 50%, #000 40%, transparent 80%);
mask-image: radial-gradient(ellipse at 40% 50%, #000 40%, transparent 80%);
}
.spine {
position: absolute;
left: 10px;
top: 10px;
bottom: 10px;
width: 1px;
background: repeating-linear-gradient(to bottom, var(--gk-gold) 0 1px, transparent 1px 4px);
}
.role {
position: relative;
background: var(--gk-midnight);
border: 1px solid var(--gk-lines);
border-radius: 10px;
padding: 24px;
display: flex;
flex-direction: column;
gap: 10px;
scroll-margin-top: 24px;
}
.role.current {
border-color: rgba(201, 162, 74, 0.55);
}
.marker {
position: absolute;
left: -26.5px;
top: 28px;
transform: translateX(-50%);
display: flex;
background: var(--gk-sky);
padding: 3px;
}
.role-top {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 8px;
}
.role h3 {
margin: 0;
font-size: 18px;
letter-spacing: 0.04em;
}
.role.current h3 {
font-size: 20px;
}
.dates {
font-size: 13px;
font-weight: 600;
color: var(--gk-gold-bright);
}
.company {
font-size: 15px;
font-weight: 600;
color: var(--gk-teal-bright);
}
.previously,
.focus {
font-size: 13px;
color: var(--gk-muted);
}
.role ul {
margin: 4px 0 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 6px;
font-size: 15px;
line-height: 1.55;
color: var(--gk-soft);
}
.role li {
display: flex;
gap: 10px;
}
.role li :global(svg) {
margin-top: 7px;
}
</style>