Content Updates with projects and experience

This commit is contained in:
Greg Kilmer
2026-07-04 18:03:38 -04:00
parent 1e8f5f1f01
commit 35d561525f
29 changed files with 347 additions and 148 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+1
View File
@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
+4 -4
View File
@@ -134,12 +134,12 @@ export default function AnimatedBackground({
const cy = 0;
const maxR = Math.sqrt(cx * cx + height * height) + 40;
// Dense starfield: ~3x the previous density for a richly star-packed sky.
// One star per ~1100 px^2 of the disk, capped for perf (~15% anchors).
// Ultra-dense starfield: ~5x the previous density for a richly packed sky.
// One star per ~220 px^2 of the disk, capped for perf (~17% anchors).
const diskArea = Math.PI * maxR * maxR;
const count = Math.min(1500, Math.floor(diskArea / 1100));
const count = Math.min(3000, Math.floor(diskArea / 220));
stars = Array.from({ length: count }, (_, i) => {
const anchor = i % 7 === 0; // ~15% anchors -> dense constellation backbone
const anchor = i % 6 === 0; // ~17% anchors -> very dense constellation backbone
// Uniform-ish distribution across the disk via sqrt on radius.
const rr = maxR * Math.sqrt(Math.random());
const a = Math.random() * Math.PI * 2;
+2 -2
View File
@@ -41,12 +41,12 @@ const {
} = Astro.props;
---
<section class="relative min-h-[60vh] overflow-hidden pt-28 sm:pt-32">
<section class="relative overflow-hidden pb-8 pt-28 sm:pt-32">
<!-- Full animated Milky Way field + stars (matches the Home/Interests hero).
Carries the per-page morph name (one .vt-sky per page snapshot). -->
<AnimatedBackground client:idle className="vt-sky" />
<div class="container-prose relative z-10 flex flex-col items-start gap-6 py-16">
<div class="container-prose relative z-10 flex flex-col items-start gap-6 py-8">
<Reveal>
<a
href="/interests"
+28 -28
View File
@@ -2,20 +2,15 @@
/**
* CelestialFrame.astro
* ---------------------------------------------------------------------------
* An ornamental frame for imagery: rounded image container with thin gold
* hairline + small star-burst flourishes in the corners and a faint sacred-
* geometry tint behind. Wraps an <img> (or any media) passed via the default
* slot.
* An ornamental frame for imagery: rounded image container with a gold border
* + small star-burst flourishes in the corners and a faint sacred-geometry tint
* behind. Wraps an <img> (or any media) passed via the default slot.
*
* The frame is structural (not just decorative), so it owns the rounded
* corners + border + overflow. The corner sparkles are absolutely positioned
* SVGs that inherit the accent color.
*
* Usage:
* <CelestialFrame>
* <img src="/images/hero.jpg" alt="..." class="h-full w-full object-cover" />
* </CelestialFrame>
* <CelestialFrame rounded="none"> ... </CelestialFrame>
* CORNER FIT: the gold border lives on the outer <figure>. The media container
* is INSET by exactly the border width (via --frame-bw) and its corner radius
* is reduced by the same amount, so the image nests flush inside the border at
* every corner — no gap, no mismatch. Adjust --frame-bw here in one place if
* the border width changes.
*/
import StarBurst from "./StarBurst.astro";
@@ -32,30 +27,33 @@ const {
emphasis = "subtle",
} = Astro.props;
const radius = {
none: "rounded-none",
lg: "rounded-lg",
xl: "rounded-xl",
"2xl": "rounded-2xl",
}[rounded];
const borderStyle = emphasis === "strong" ? "border-2" : "border";
// Outer radius (px) per option. The inner media radius is outer - border width
// so the image corner nests perfectly inside the border's corner.
const outerPx = { none: 0, lg: 8, xl: 12, "2xl": 16 }[rounded];
const bw = emphasis === "strong" ? 4 : 3; // border width in px (matches border-2 / 3px)
const innerPx = Math.max(0, outerPx - bw);
---
<figure
class:list={["relative", radius, borderStyle, className]}
style="border-color: color-mix(in srgb, var(--color-accent) 75%, transparent); border-width: 3px;"
class:list={["relative", className]}
style={`border-radius: ${outerPx}px; border: ${bw}px solid color-mix(in srgb, var(--color-accent) 75%, transparent); overflow: hidden;`}
>
{/* Faint sacred-geometry tint behind the image. */}
{/* Faint sacred-geometry tint behind the image (fills the full figure). */}
<div
class:list={["absolute inset-0", radius]}
style="background: color-mix(in srgb, var(--color-violet, var(--color-accent)) 14%, var(--color-surface-sunken));"
class="absolute inset-0"
style={`border-radius: ${outerPx}px; background: color-mix(in srgb, var(--color-violet, var(--color-accent)) 14%, var(--color-surface-sunken));`}
aria-hidden="true"
>
</div>
{/* The media itself, clipped to the frame. */}
<div class:list={["relative overflow-hidden", radius]}>
{/* The media itself, in normal flow (defines the figure's height via the slot's
aspect-ratio div). Its radius = outer radius - border width, so the image's
rounded corner nests PERFECTLY inside the border's rounded corner. The
figure's `overflow: hidden` clips the tint layer to the outer radius. */}
<div
class="relative overflow-hidden"
style={`border-radius: ${innerPx}px;`}
>
<slot />
</div>
@@ -79,3 +77,5 @@ const borderStyle = emphasis === "strong" ? "border-2" : "border";
style="color: var(--color-accent); opacity:.7;"
/>
</figure>
+10 -17
View File
@@ -69,9 +69,9 @@ export default function CircuitField({
canvas.height = Math.floor(height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
// Build an orthogonal circuit on a grid: place nodes at grid intersections
// (with jitter + random omissions), then connect neighbors with right-angle
// traces. This reads as a PCB / schematic, not a star map.
// Build a clean, ALIGNED orthogonal circuit on a grid: pads sit exactly at
// grid intersections (no jitter) and traces connect them in straight
// right-angle lines. Reads as an orderly schematic / PCB layout.
const step = 64;
const cols = Math.ceil(width / step) + 1;
const rows = Math.ceil(height / step) + 1;
@@ -83,9 +83,8 @@ export default function CircuitField({
// ~62% of intersections get a pad.
present[r][c] = Math.random() < 0.62;
if (present[r][c]) {
const jx = (Math.random() - 0.5) * step * 0.25;
const jy = (Math.random() - 0.5) * step * 0.25;
nodes.push({ x: c * step + jx, y: r * step + jy });
// Snapped to the grid — no jitter, so traces land flush on pads.
nodes.push({ x: c * step, y: r * step });
}
}
}
@@ -236,21 +235,15 @@ export default function CircuitField({
className={`pointer-events-none absolute inset-0 -z-10 overflow-hidden ${className}`}
aria-hidden="true"
>
{/* Base dark purple/blue gradient — the professional hero's atmosphere. */}
{/* Base gradient — IDENTICAL colors to AnimatedBackground's night sky so
the morph between star field and circuit field is color-consistent.
No nebula radials here (the user asked to keep the circuit style only);
just the clean deep-blue sky base. */}
<div
className="absolute inset-0"
style={{
background:
'linear-gradient(160deg, #0a0e2a 0%, #14123a 38%, #1a1648 64%, #0e0c28 100%)',
}}
/>
{/* Animated violet/teal nebula clouds for depth + motion. */}
<div
className="animate-gradient-shift absolute inset-0"
style={{
backgroundImage:
'radial-gradient(50% 60% at 18% 20%, rgba(139, 123, 216, 0.28), transparent 62%), radial-gradient(45% 55% at 85% 15%, rgba(111, 182, 173, 0.16), transparent 60%), radial-gradient(55% 70% at 70% 110%, rgba(139, 123, 216, 0.20), transparent 60%)',
backgroundSize: '200% 200%',
'linear-gradient(180deg, #04061a 0%, #070b24 32%, #0a1230 58%, #0e1a3e 82%, #12224a 100%)',
}}
/>
{/* Circuit traces + nodes + signal pulses. */}
+4 -5
View File
@@ -92,12 +92,11 @@ export default function ConstellationField({
const cx = width;
const cy = 0;
const maxR = Math.sqrt(cx * cx + height * height) + 40;
// Denser field for richer constellations. One star per ~4500 px^2, scaled
// ~3x density for a richly star-packed sky. One star per ~1500 px^2,
// scaled by density; ~15% anchors for a dense constellation backbone.
// Ultra-dense: ~5x. One star per ~300 px^2, scaled by density; ~17%
// anchors for a very dense constellation backbone.
const count = Math.min(
900,
Math.floor(((Math.PI * maxR * maxR) / 1500) * density),
3000,
Math.floor(((Math.PI * maxR * maxR) / 300) * density),
);
stars = Array.from({ length: count }, (_, i) => {
const anchor = i % 7 === 0;
+90 -23
View File
@@ -2,14 +2,15 @@
/**
* SectionNav.astro
* ---------------------------------------------------------------------------
* Sticky in-page sub-navigation for the Professional page. Jumps between the
* six anchor sections (Summary, Experience, Skills, Projects, Education,
* Contact) via smooth scroll. Hidden on small screens where the page reads as a
* simple scroll. Active state is computed client-side via IntersectionObserver.
* Fixed LEFT-SIDE vertical rail for the Professional page. Shows the six
* sections as a stacked icon+label list, highlights the section currently in
* view, and lets the user click any item to smooth-scroll to it. A vertical
* progress line beside the items fills as the user scrolls down the page.
*
* Sits BELOW the hero and is OPAQUE (a solid dark nebula surface) so it remains
* readable while scrolling over content. Sticky so it tracks the viewport once
* the user scrolls past it.
* Stylistically it mirrors the page's `.card` language (same raised surface,
* border, radius) so it reads as a sibling of the content cards rather than a
* separate chrome element. Icons + labels use bright tokens so they pop on the
* dark surface. Hidden below `lg`.
*/
import Icon from "@components/Icon.astro";
const sections = [
@@ -22,28 +23,79 @@ const sections = [
];
---
<nav aria-label="Professional sections" class="hidden lg:block">
<div
class="sticky top-20 z-30 flex flex-wrap gap-1 rounded-xl border p-1.5 shadow-lg"
style="border-color: color-mix(in srgb, var(--color-accent) 28%, var(--color-border)); background: linear-gradient(180deg, rgba(12, 14, 40, 0.96), rgba(8, 10, 32, 0.96)); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);"
<nav
aria-label="Professional sections"
class="pro-section-nav fixed left-4 top-1/2 z-40 hidden -translate-y-1/2 flex-col lg:flex"
data-pro-nav
>
{/* Rail container — matches the .card aesthetic: raised surface, hairline
border, 1rem radius. Bright icons + labels inside. */}
<div
class="card relative flex flex-col gap-1 p-3"
style="border-color: var(--color-border); background: var(--color-surface-raised); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); box-shadow: 0 18px 40px -24px rgba(11, 16, 38, 0.6);"
>
{/* Vertical progress track behind the dots. */}
<div
class="pointer-events-none absolute left-[1.55rem] top-6 bottom-6 w-px"
style="background: color-mix(in srgb, var(--color-border) 80%, transparent);"
aria-hidden="true"
>
</div>
{/* The fill bar — height set by JS as the user scrolls. */}
<div
class="pointer-events-none absolute left-[1.55rem] top-6 w-px origin-top"
style="background: var(--color-accent); height: 0%; box-shadow: 0 0 8px color-mix(in srgb, var(--color-accent) 60%, transparent);"
data-pro-fill
aria-hidden="true"
>
</div>
{sections.map((s) => (
<a
href={`#${s.id}`}
data-section-link={s.id}
class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 font-sans text-sm font-medium transition-colors"
style="color: var(--color-cream); opacity: 0.7;"
class="group relative flex items-center gap-3 rounded-lg px-2.5 py-2 font-sans text-sm font-semibold transition-all"
style="color: var(--color-cream); opacity: 0.85;"
>
<Icon name={s.icon} class="h-4 w-4" />
{s.label}
<span
class="relative z-10 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition-colors"
style="background: color-mix(in srgb, var(--color-accent) 14%, transparent); color: var(--color-cream);"
data-section-dot={s.id}
>
<Icon name={s.icon} class="h-[1.15rem] w-[1.15rem]" />
</span>
<span class="relative z-10 tracking-wide">{s.label}</span>
</a>
))}
</div>
</nav>
<style>
/* Active link: full opacity + accent text + subtle accent wash. */
.pro-section-nav a[data-active="true"] {
opacity: 1 !important;
color: var(--color-accent) !important;
background: color-mix(in srgb, var(--color-accent) 14%, transparent);
}
/* Active dot: filled accent, bright icon, glow ring. */
.pro-section-nav a[data-active="true"] [data-section-dot] {
background: color-mix(in srgb, var(--color-accent) 80%, transparent) !important;
color: var(--color-ink) !important;
box-shadow: 0 0 12px color-mix(in srgb, var(--color-accent) 65%, transparent);
}
/* Hover lift on inactive items. */
.pro-section-nav a:hover {
opacity: 1 !important;
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
}
</style>
<script>
// Highlight the link for whichever section is currently in view.
// Spy: highlight the link for whichever section is in view + fill the rail.
function setupSectionSpy() {
const nav = document.querySelector<HTMLElement>("[data-pro-nav]");
if (!nav) return;
const links = Array.from(
document.querySelectorAll<HTMLAnchorElement>("[data-section-link]"),
);
@@ -51,20 +103,16 @@ const sections = [
const sections = links
.map((l) => document.getElementById(l.dataset.sectionLink!))
.filter((el): el is HTMLElement => Boolean(el));
const fill = nav.querySelector<HTMLElement>("[data-pro-fill]");
const setActive = (id: string) => {
for (const l of links) {
const active = l.dataset.sectionLink === id;
l.style.color = active
? "var(--color-accent)"
: "var(--color-cream)";
l.style.opacity = active ? "1" : "0.7";
l.style.background = active
? "color-mix(in srgb, var(--color-accent) 16%, transparent)"
: "transparent";
l.setAttribute("data-active", active ? "true" : "false");
}
};
// Highlight the most-visible section.
const io = new IntersectionObserver(
(entries) => {
const visible = entries
@@ -75,6 +123,25 @@ const sections = [
{ rootMargin: "-30% 0px -55% 0px", threshold: [0, 0.25, 0.5] },
);
sections.forEach((s) => io.observe(s));
// Progress fill: fraction of the page scrolled.
const trackTop = 24; // px — matches top-6 on the track/fill
const trackBottom = 24; // px — matches bottom-6
const updateFill = () => {
if (!fill) return;
const doc = document.documentElement;
const max = doc.scrollHeight - doc.clientHeight;
const frac = max > 0 ? Math.max(0, Math.min(1, window.scrollY / max)) : 0;
// Fill height as a percentage of the track length (which spans the card
// minus top/bottom padding).
const cardEl = fill.parentElement;
if (!cardEl) return;
const trackPx = cardEl.clientHeight - trackTop - trackBottom;
fill.style.height = `${(frac * trackPx).toFixed(1)}px`;
};
updateFill();
window.addEventListener("scroll", updateFill, { passive: true });
window.addEventListener("resize", updateFill);
}
setupSectionSpy();
document.addEventListener("astro:page-load", setupSectionSpy);
+2 -1
View File
@@ -55,7 +55,8 @@ const techStack = [
{
icon: "git",
label: "Tools",
value: "Git, Gitea, CI/CD pipelines",
value:
"PostgreSQL, VSCode, Unity, XCode, Android Studio, Git, Gitea, CI/CD pipelines",
hint: "Add the rest of your toolchain.",
},
{
+16 -11
View File
@@ -43,23 +43,28 @@ const rendered = await Promise.all(
{rendered.map(({ entry, Content }, i) => (
<li class="relative pl-12">
<Reveal delay={i * 80}>
{/* Marker */}
{/* Marker dot on the timeline line (kept small; the icon now lives inline
with the company name inside the card so it never clips the card). */}
<span
class="absolute left-0 top-0 inline-flex h-9 w-9 items-center justify-center rounded-full border-4"
style="background: var(--color-surface-raised); border-color: var(--color-surface);"
class="absolute left-[1.15rem] top-7 h-3 w-3 -translate-x-1/2 rounded-full"
style="background: var(--color-accent); box-shadow: 0 0 0 4px var(--color-surface);"
aria-hidden="true"
>
<span
class="inline-flex h-9 w-9 items-center justify-center rounded-full"
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent);"
>
<Icon name={entry.data.icon} class="h-4 w-4" />
</span>
</span>
<div class="card flex flex-col gap-3">
<div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
<h3 style="color: var(--color-text);">{entry.data.company}</h3>
{/* Header row: icon + company name inline, period on the right. */}
<div class="flex flex-wrap items-center justify-between gap-x-4 gap-y-1">
<h3 class="flex items-center gap-3" style="color: var(--color-text);">
<span
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full"
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent);"
aria-hidden="true"
>
<Icon name={entry.data.icon} class="h-5 w-5" style="color: var(--color-accent);" />
</span>
{entry.data.company}
</h3>
<span class="font-mono text-xs uppercase tracking-[0.18em]" style="color: var(--color-text-subtle);">
{entry.data.period}
</span>
+19
View File
@@ -0,0 +1,19 @@
---
# Experience entry for Implico Inc.
# This seeds the Experience timeline. The narrative bullets below are the spec's
# template bullets; Greg should replace the bracketed placeholders with real
# outcomes, metrics, and dates. See docs/CONTENT.md for how to add more roles.
company: "Comcast"
title: "Co-op - Junior Developer"
period: "Sep 2014 - Sep 2015"
focus: "Worked on inventory management systems and Natural Language Processing tools."
icon: "briefcase"
order: 20
---
Key contributions:
- Developed and maintained NLP based data gathering system and associated databases using Java, Groovy, Grails, Elasticsearch, and MySQL
- Led and managed team of senior interns and a full time developer using Rally and GitHub
- Developed and maintained features for internal logistics web platform using PHP and MySQL
- Worked in weeklong sprints using an Agile workflow
+18
View File
@@ -0,0 +1,18 @@
---
# Experience entry for Implico Inc.
# This seeds the Experience timeline. The narrative bullets below are the spec's
# template bullets; Greg should replace the bracketed placeholders with real
# outcomes, metrics, and dates. See docs/CONTENT.md for how to add more roles.
company: "Drexel University College of Computing and Informatics"
title: "Assistant System Administrator"
period: "Sep 2016 - Mar 2017"
focus: "Assisted students and teachers with technical issues and supported the college's technical infrastructure."
icon: "briefcase"
order: 15
---
Key contributions:
- Maintained and improved college wide linux server network using Ansible, Bash, Python, and MySQL
- Used ceph storage system to backup and protect college wide data
- Handled technical issues for faculty and staff
+18
View File
@@ -0,0 +1,18 @@
---
# Placeholder project card.
# This exists so the Projects grid has structure to show until Greg adds real
# entries. To replace it: copy this file, edit the frontmatter + body, and bump
# `placeholder: false`. Or just delete this file and add your own. See
# docs/CONTENT.md. The schema lives in src/content.config.ts.
title: "Carrier Connect"
role: "Lead Developer and Architect"
tech: ["JavaScript", "Node.js", "SAP BTP", "SAP CAP", "PostgreSQL"]
summary: >
A complex suite of CAP applications and associated mobile application endpoints hosted on SAP BTP to enable 3rd party carrier integration into proprietary logistics software. Includes order and trip delegation, BOL and Delivery record tracking, image processing, location inventory polling, geofencing, exception management, CSV upload and download, and RestfulAPIs.
order: 105
placeholder: false
---
Drop in a real project writeup here. Markdown is supported, including headings,
lists, code blocks, and links. Keep the frontmatter summary short (2-3
sentences) and use this body for the longer story if you want case-study depth.
+33
View File
@@ -0,0 +1,33 @@
---
# Placeholder project card.
# This exists so the Projects grid has structure to show until Greg adds real
# entries. To replace it: copy this file, edit the frontmatter + body, and bump
# `placeholder: false`. Or just delete this file and add your own. See
# docs/CONTENT.md. The schema lives in src/content.config.ts.
title: "Cosmos"
role: "Lead Developer / Architect / Admin"
tech:
[
"OpenClaw",
"Node.js",
"Docker",
"Python",
"Nginx Proxy Manager",
"LLM",
"Discord",
"Next.js",
"NextCloud",
"Kavita",
"Gitea",
"Home Assistant",
"PiHole",
]
summary: >
Converted a Lenovo Tiny workstation computer into a homelab server hosting multiple containerized tools from PiHole to Home Assistant. Supports a suite of 17 AI Agents managed utilizing OpenClaw and a shared knowledge repository to handle personal assistant, research, and coding tasks.
order: 101
placeholder: false
---
Drop in a real project writeup here. Markdown is supported, including headings,
lists, code blocks, and links. Keep the frontmatter summary short (2-3
sentences) and use this body for the longer story if you want case-study depth.
+18
View File
@@ -0,0 +1,18 @@
---
# Placeholder project card.
# This exists so the Projects grid has structure to show until Greg adds real
# entries. To replace it: copy this file, edit the frontmatter + body, and bump
# `placeholder: false`. Or just delete this file and add your own. See
# docs/CONTENT.md. The schema lives in src/content.config.ts.
title: "Driver App"
role: "Lead Developer / Architect"
tech: ["React", "React Native", "SAP BTP"]
summary: >
A react native mobile application designed for use with the Carrier Connect suite. Accepts order and trip delegation from the back end suite, handles geofencing, exception management, delivery and order progress tracking, image and signature capture and upload. Designed to fit into existing driver workflows.
order: 110
placeholder: false
---
Drop in a real project writeup here. Markdown is supported, including headings,
lists, code blocks, and links. Keep the frontmatter summary short (2-3
sentences) and use this body for the longer story if you want case-study depth.
+6 -8
View File
@@ -4,15 +4,13 @@
# entries. To replace it: copy this file, edit the frontmatter + body, and bump
# `placeholder: false`. Or just delete this file and add your own. See
# docs/CONTENT.md. The schema lives in src/content.config.ts.
title: "PUT PROJECTS HERE"
role: "Lead Developer"
tech: ["TypeScript", "Node.js", "SAP BTP"]
title: "OmniTracker"
role: "Lead Developer / Architect"
tech: ["TypeScript", "Node.js", "NFC tags", "Tasker", "PostgreSQL"]
summary: >
This is a placeholder so the project grid has the right layout. Replace it
with a real 2-3 sentence description of what the project does and my
contribution, then add links below.
order: 100
placeholder: true
A responsive web application designed to track beverage, supplement, and medication intake for personal needs with minimal friction of data entry. Tracks and estimate caffeine levels, medication timing, and supplement schedules. Calculates calories, caffeine, sugar, sodium, and other nutrional information utilizing public endpoints and configurable item cataloges. Utilizes NFC tags to log events throughout personal environment along with endpoints and AI MCP configuration for personal assistant AI integration.
order: 120
placeholder: false
---
Drop in a real project writeup here. Markdown is supported, including headings,
+33 -24
View File
@@ -12,20 +12,20 @@
*
* Page transitions use Astro's <ClientRouter /> (View Transitions API).
*/
import '@/styles/global.css';
import "@/styles/global.css";
// Self-hosted fonts (woff2, variable). Avoids external requests for perf + privacy.
import '@fontsource-variable/inter';
import '@fontsource-variable/plus-jakarta-sans';
import '@fontsource-variable/jetbrains-mono';
import "@fontsource-variable/inter";
import "@fontsource-variable/plus-jakarta-sans";
import "@fontsource-variable/jetbrains-mono";
import { ClientRouter } from 'astro:transitions';
import NavBar from '@components/nav/NavBar.astro';
import Footer from '@components/layout/Footer.astro';
import { themeInitScript } from '@/scripts/theme-init';
import { siteIdentity } from '@lib/site';
import { ClientRouter } from "astro:transitions";
import NavBar from "@components/nav/NavBar.astro";
import Footer from "@components/layout/Footer.astro";
import { themeInitScript } from "@/scripts/theme-init";
import { siteIdentity } from "@lib/site";
type Surface = 'neutral' | 'professional' | 'interest';
type Surface = "neutral" | "professional" | "interest";
interface Props {
title?: string;
@@ -38,23 +38,29 @@ interface Props {
const {
title,
description = 'Greg Kilmer is a software developer, maker, and tabletop gamemaster based in Philadelphia. This is where his professional and personal worlds meet.',
surface = 'neutral',
description = "Greg Kilmer is a software developer, maker, and tabletop gamemaster based in Philadelphia. This is where his professional and personal worlds meet.",
surface = "neutral",
ogImage,
} = Astro.props;
// True on the Home page. Used to switch the NavBar's view-transition-name so
// the Home hero can morph into / out of the nav bar (expand/collapse effect).
const isHome = Astro.url.pathname === '/';
const isHome = Astro.url.pathname === "/";
const fullTitle = title
? `${title} | ${siteIdentity.name}`
: `${siteIdentity.name} — Developer. Maker. Storyteller.`;
const canonical = new URL(Astro.url.pathname, Astro.site ?? 'https://patiun.com');
const canonical = new URL(
Astro.url.pathname,
Astro.site ?? "https://patiun.com",
);
const ogImageUrl = ogImage
? new URL(ogImage, Astro.site ?? 'https://patiun.com').toString()
: new URL('/images/hero-portrait.jpg', Astro.site ?? 'https://patiun.com').toString();
? new URL(ogImage, Astro.site ?? "https://patiun.com").toString()
: new URL(
"/images/hero-portrait.jpg",
Astro.site ?? "https://patiun.com",
).toString();
---
<!doctype html>
@@ -84,7 +90,7 @@ const ogImageUrl = ogImage
{ogImageUrl && <meta property="og:image" content={ogImageUrl} />}
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<!-- Theme boot: blocking inline script prevents flash of the wrong theme.
Dark mode is currently forced (light mode deferred), so this just pins
@@ -96,7 +102,7 @@ const ogImageUrl = ogImage
Loaded as a module so it self-initializes on load + on View Transitions
(it listens for astro:page-load internally). -->
<script>
import '@/scripts/reveal-init';
import "@/scripts/reveal-init";
</script>
<!-- View Transitions flash-killer. The incoming page is snapshotted with its
@@ -105,18 +111,21 @@ const ogImageUrl = ogImage
<html> around the swap so global.css forces reveals visible mid-flight,
then lets the observer take over for scroll reveals on the new page. -->
<script is:inline>
document.addEventListener('astro:before-swap', () => {
document.documentElement.classList.add('vt-transitioning');
document.addEventListener("astro:before-swap", () => {
document.documentElement.classList.add("vt-transitioning");
});
document.addEventListener('astro:after-swap', () => {
document.documentElement.classList.remove('vt-transitioning');
document.addEventListener("astro:after-swap", () => {
document.documentElement.classList.remove("vt-transitioning");
});
</script>
<ClientRouter />
</head>
<body data-surface={surface} data-page={isHome ? 'home' : 'other'}>
<a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:rounded-lg focus:bg-(--color-amber) focus:px-4 focus:py-2 focus:text-(--color-ink)">
<body data-surface={surface} data-page={isHome ? "home" : "other"}>
<a
href="#main"
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:rounded-lg focus:bg-(--color-amber) focus:px-4 focus:py-2 focus:text-(--color-ink)"
>
Skip to content
</a>
+2 -2
View File
@@ -38,9 +38,9 @@ const quickFacts = [
description="The human story behind the resume: Greg Kilmer on curiosity, tabletop RPGs, resin dice, and a Philadelphia apartment with too many board games."
>
<!-- Hero — full animated Milky Way field, matching the Interests hero. -->
<section class="relative min-h-[60vh] overflow-hidden pt-28 sm:pt-32">
<section class="relative overflow-hidden pb-8 pt-28 sm:pt-32">
<AnimatedBackground client:idle className="vt-sky" />
<div class="container-prose relative z-10 flex flex-col gap-10 py-16">
<div class="container-prose relative z-10 flex flex-col gap-8 py-8">
<Reveal>
<SectionHeader
tone="hero"
+12 -3
View File
@@ -21,17 +21,20 @@ const professional = [
{
icon: "email-outline",
label: "Email",
hint: '<a href="mailto:Kilmer.GM@Gmail.com">Kilmer.GM@Gmail.com</a>',
href: "mailto:Kilmer.GM@Gmail.com",
hint: "Kilmer.GM@Gmail.com",
},
{
icon: "linkedin",
label: "LinkedIn",
hint: '<a href="https://www.linkedin.com/in/gregory-kilmer-36646bb5/" target="_blank" rel="noopener noreferrer">LinkedIn</a>',
href: "https://www.linkedin.com/in/gregory-kilmer-36646bb5/",
hint: "Gregory Kilmer",
},
{
icon: "github",
label: "GitHub",
hint: '<a href="https://github.com/patiun" target="_blank" rel="noopener noreferrer">GitHub</a>',
href: "https://github.com/patiun",
hint: "patiun",
},
];
@@ -103,8 +106,14 @@ const social = [
<span
class="text-xs"
style="color: var(--color-text-subtle);"
>
<a
href={item.href}
target="_blank"
rel="noopener noreferrer"
>
{item.hint}
</a>
</span>
</span>
</li>
+2 -2
View File
@@ -63,7 +63,7 @@ const cards = [
<Reveal as="div" class="max-w-2xl grow">
<span
class="mb-5 inline-flex items-center gap-2 rounded-full border px-3 py-1 font-mono text-xs uppercase tracking-[0.18em]"
style="border-color: var(--color-border); color: var(--color-text-muted); background: color-mix(in srgb, var(--color-surface-raised) 60%, transparent);"
style="border-color: var(--color-border); color: var(--accent-contrast); background: color-mix(in srgb, var(--color-surface-raised) 75%, transparent);"
>
<span
class="h-2 w-2 rounded-full animate-pulse-glow"
@@ -75,7 +75,7 @@ const cards = [
<p
class="mt-3 font-sans text-xl font-medium sm:text-2xl"
style="color: var(--color-accent);"
style="color: var(--accent);"
>
Software Developer &middot; Maker &middot; Tabletop Gamemaster
</p>
+2 -2
View File
@@ -74,13 +74,13 @@ const interests = [
description="The fun wing: tabletop RPGs, gaming, reading, resin dice and 3D printing, and Pokémon. Where Greg's passions live as stories and collections."
>
<!-- Hero -->
<section class="relative min-h-[60vh] overflow-hidden pt-28 sm:pt-32">
<section class="relative overflow-hidden pb-8 pt-28 sm:pt-32">
{
/* Full animated Milky Way field + stars, matching the Home hero treatment. */
}
<AnimatedBackground client:idle className="vt-sky" />
<div
class="container-prose relative z-10 flex flex-col items-start gap-6 py-16"
class="container-prose relative z-10 flex flex-col items-start gap-6 py-8"
>
<Reveal>
<SectionHeader
+23 -12
View File
@@ -45,13 +45,15 @@ const projects = (await getCollection("projects")).sort(
a star hero is element-to-element equivalent — no separate gradient
flash. Shares view-transition-name "sky" via vt-sky. */
}
<CircuitField client:idle className="vt-sky opacity-80" />
<CircuitField client:idle className="vt-sky opacity-100" />
{/* Slowly rotating astrolabe ornament behind the header. */}
<Astrolabe
class="pointer-events-none absolute z-0 -right-48 -top-24 h-[34rem] w-[34rem] animate-spin-slow"
style="color: var(--color-accent); opacity:.13;"
/>
<div class="container-prose relative z-10 flex flex-col gap-10">
<div
class="container-prose relative z-10 flex flex-col gap-10 lg:ml-56 lg:max-w-[calc(72rem+14rem)]"
>
<Reveal>
<SectionHeader
tone="hero"
@@ -64,15 +66,22 @@ const projects = (await getCollection("projects")).sort(
</div>
</section>
{/* In-page section nav — sits BELOW the hero (out of the morphing region),
opaque against the dark surface so it stays readable while scrolling. */}
<div class="container-prose relative z-20 pt-4">
<Reveal>
{
/* Fixed left section rail. It is position:fixed so it renders once here
and stays pinned to the viewport's left edge while the page scrolls.
The main content column below is given lg:pl-* to clear the 14rem rail. */
}
<SectionNav />
</Reveal>
</div>
<div class="container-prose flex flex-col gap-20 py-16 sm:gap-24 sm:py-20">
{
/* Content column. On lg+ the fixed left rail (w-56 = 14rem) takes the left
edge, so this column is offset by that width via margin (NOT padding on a
centered max-width box — that caused asymmetric overflow / broken grids).
It keeps container-prose's max-width but lets it sit beside the rail. */
}
<div
class="container-prose flex flex-col gap-20 py-16 sm:gap-24 sm:py-20 lg:ml-56 lg:max-w-[calc(72rem+14rem)]"
>
{/* ===== 1. Summary ===== */}
<section id="summary" class="scroll-mt-28 flex flex-col gap-6">
<Reveal>
@@ -159,10 +168,12 @@ const projects = (await getCollection("projects")).sort(
kicker="04"
icon="view-grid"
title="Projects"
subtitle="What I've built. [TODO: Replace the placeholder with 3-6 of my projects]"
subtitle="What I've built."
/>
</Reveal>
<div class="grid gap-5 sm:grid-cols-2">
<div class="grid
gap-5
sm:grid-cols-2">
{
projects.map((project, i) => (
<Reveal delay={i * 80}>
@@ -202,7 +213,7 @@ const projects = (await getCollection("projects")).sort(
kicker="06"
icon="email"
title="Let's talk"
subtitle="Open to opportunities in cloud development, SAP BTP, and technical leadership."
subtitle="Open to opportunities in AI/ML, mobile app, and cloud development, VR and Game Development, and technical leadership."
/>
</Reveal>
<Reveal delay={80}>
+3 -3
View File
@@ -327,13 +327,13 @@
}
::view-transition-group(hero-sky) {
animation-duration: 0.4s;
animation-duration: 0.26s;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
::view-transition-old(hero-sky),
::view-transition-new(hero-sky) {
/* A gentle height/scale blend reads as expand/collapse rather than a swap. */
animation-duration: 0.4s;
animation-duration: 0.26s;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
overflow: clip;
}
@@ -363,7 +363,7 @@
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.28s;
animation-duration: 0.18s;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}