Content Updates with projects and experience
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 482 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 912 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -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"}
|
||||||
@@ -134,12 +134,12 @@ export default function AnimatedBackground({
|
|||||||
const cy = 0;
|
const cy = 0;
|
||||||
const maxR = Math.sqrt(cx * cx + height * height) + 40;
|
const maxR = Math.sqrt(cx * cx + height * height) + 40;
|
||||||
|
|
||||||
// Dense starfield: ~3x the previous density for a richly star-packed sky.
|
// Ultra-dense starfield: ~5x the previous density for a richly packed sky.
|
||||||
// One star per ~1100 px^2 of the disk, capped for perf (~15% anchors).
|
// One star per ~220 px^2 of the disk, capped for perf (~17% anchors).
|
||||||
const diskArea = Math.PI * maxR * maxR;
|
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) => {
|
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.
|
// Uniform-ish distribution across the disk via sqrt on radius.
|
||||||
const rr = maxR * Math.sqrt(Math.random());
|
const rr = maxR * Math.sqrt(Math.random());
|
||||||
const a = Math.random() * Math.PI * 2;
|
const a = Math.random() * Math.PI * 2;
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ const {
|
|||||||
} = Astro.props;
|
} = 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).
|
<!-- Full animated Milky Way field + stars (matches the Home/Interests hero).
|
||||||
Carries the per-page morph name (one .vt-sky per page snapshot). -->
|
Carries the per-page morph name (one .vt-sky per page snapshot). -->
|
||||||
<AnimatedBackground client:idle className="vt-sky" />
|
<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>
|
<Reveal>
|
||||||
<a
|
<a
|
||||||
href="/interests"
|
href="/interests"
|
||||||
|
|||||||
@@ -2,20 +2,15 @@
|
|||||||
/**
|
/**
|
||||||
* CelestialFrame.astro
|
* CelestialFrame.astro
|
||||||
* ---------------------------------------------------------------------------
|
* ---------------------------------------------------------------------------
|
||||||
* An ornamental frame for imagery: rounded image container with thin gold
|
* An ornamental frame for imagery: rounded image container with a gold border
|
||||||
* hairline + small star-burst flourishes in the corners and a faint sacred-
|
* + small star-burst flourishes in the corners and a faint sacred-geometry tint
|
||||||
* geometry tint behind. Wraps an <img> (or any media) passed via the default
|
* behind. Wraps an <img> (or any media) passed via the default slot.
|
||||||
* slot.
|
|
||||||
*
|
*
|
||||||
* The frame is structural (not just decorative), so it owns the rounded
|
* CORNER FIT: the gold border lives on the outer <figure>. The media container
|
||||||
* corners + border + overflow. The corner sparkles are absolutely positioned
|
* is INSET by exactly the border width (via --frame-bw) and its corner radius
|
||||||
* SVGs that inherit the accent color.
|
* 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
|
||||||
* Usage:
|
* the border width changes.
|
||||||
* <CelestialFrame>
|
|
||||||
* <img src="/images/hero.jpg" alt="..." class="h-full w-full object-cover" />
|
|
||||||
* </CelestialFrame>
|
|
||||||
* <CelestialFrame rounded="none"> ... </CelestialFrame>
|
|
||||||
*/
|
*/
|
||||||
import StarBurst from "./StarBurst.astro";
|
import StarBurst from "./StarBurst.astro";
|
||||||
|
|
||||||
@@ -32,30 +27,33 @@ const {
|
|||||||
emphasis = "subtle",
|
emphasis = "subtle",
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
const radius = {
|
// Outer radius (px) per option. The inner media radius is outer - border width
|
||||||
none: "rounded-none",
|
// so the image corner nests perfectly inside the border's corner.
|
||||||
lg: "rounded-lg",
|
const outerPx = { none: 0, lg: 8, xl: 12, "2xl": 16 }[rounded];
|
||||||
xl: "rounded-xl",
|
const bw = emphasis === "strong" ? 4 : 3; // border width in px (matches border-2 / 3px)
|
||||||
"2xl": "rounded-2xl",
|
const innerPx = Math.max(0, outerPx - bw);
|
||||||
}[rounded];
|
|
||||||
|
|
||||||
const borderStyle = emphasis === "strong" ? "border-2" : "border";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<figure
|
<figure
|
||||||
class:list={["relative", radius, borderStyle, className]}
|
class:list={["relative", className]}
|
||||||
style="border-color: color-mix(in srgb, var(--color-accent) 75%, transparent); border-width: 3px;"
|
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
|
<div
|
||||||
class:list={["absolute inset-0", radius]}
|
class="absolute inset-0"
|
||||||
style="background: color-mix(in srgb, var(--color-violet, var(--color-accent)) 14%, var(--color-surface-sunken));"
|
style={`border-radius: ${outerPx}px; background: color-mix(in srgb, var(--color-violet, var(--color-accent)) 14%, var(--color-surface-sunken));`}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* The media itself, clipped to the frame. */}
|
{/* The media itself, in normal flow (defines the figure's height via the slot's
|
||||||
<div class:list={["relative overflow-hidden", radius]}>
|
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 />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -79,3 +77,5 @@ const borderStyle = emphasis === "strong" ? "border-2" : "border";
|
|||||||
style="color: var(--color-accent); opacity:.7;"
|
style="color: var(--color-accent); opacity:.7;"
|
||||||
/>
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ export default function CircuitField({
|
|||||||
canvas.height = Math.floor(height * dpr);
|
canvas.height = Math.floor(height * dpr);
|
||||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
|
||||||
// Build an orthogonal circuit on a grid: place nodes at grid intersections
|
// Build a clean, ALIGNED orthogonal circuit on a grid: pads sit exactly at
|
||||||
// (with jitter + random omissions), then connect neighbors with right-angle
|
// grid intersections (no jitter) and traces connect them in straight
|
||||||
// traces. This reads as a PCB / schematic, not a star map.
|
// right-angle lines. Reads as an orderly schematic / PCB layout.
|
||||||
const step = 64;
|
const step = 64;
|
||||||
const cols = Math.ceil(width / step) + 1;
|
const cols = Math.ceil(width / step) + 1;
|
||||||
const rows = Math.ceil(height / step) + 1;
|
const rows = Math.ceil(height / step) + 1;
|
||||||
@@ -83,9 +83,8 @@ export default function CircuitField({
|
|||||||
// ~62% of intersections get a pad.
|
// ~62% of intersections get a pad.
|
||||||
present[r][c] = Math.random() < 0.62;
|
present[r][c] = Math.random() < 0.62;
|
||||||
if (present[r][c]) {
|
if (present[r][c]) {
|
||||||
const jx = (Math.random() - 0.5) * step * 0.25;
|
// Snapped to the grid — no jitter, so traces land flush on pads.
|
||||||
const jy = (Math.random() - 0.5) * step * 0.25;
|
nodes.push({ x: c * step, y: r * step });
|
||||||
nodes.push({ x: c * step + jx, y: r * step + jy });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,21 +235,15 @@ export default function CircuitField({
|
|||||||
className={`pointer-events-none absolute inset-0 -z-10 overflow-hidden ${className}`}
|
className={`pointer-events-none absolute inset-0 -z-10 overflow-hidden ${className}`}
|
||||||
aria-hidden="true"
|
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
|
<div
|
||||||
className="absolute inset-0"
|
className="absolute inset-0"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
'linear-gradient(160deg, #0a0e2a 0%, #14123a 38%, #1a1648 64%, #0e0c28 100%)',
|
'linear-gradient(180deg, #04061a 0%, #070b24 32%, #0a1230 58%, #0e1a3e 82%, #12224a 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%',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Circuit traces + nodes + signal pulses. */}
|
{/* Circuit traces + nodes + signal pulses. */}
|
||||||
|
|||||||
@@ -92,12 +92,11 @@ export default function ConstellationField({
|
|||||||
const cx = width;
|
const cx = width;
|
||||||
const cy = 0;
|
const cy = 0;
|
||||||
const maxR = Math.sqrt(cx * cx + height * height) + 40;
|
const maxR = Math.sqrt(cx * cx + height * height) + 40;
|
||||||
// Denser field for richer constellations. One star per ~4500 px^2, scaled
|
// Ultra-dense: ~5x. One star per ~300 px^2, scaled by density; ~17%
|
||||||
// ~3x density for a richly star-packed sky. One star per ~1500 px^2,
|
// anchors for a very dense constellation backbone.
|
||||||
// scaled by density; ~15% anchors for a dense constellation backbone.
|
|
||||||
const count = Math.min(
|
const count = Math.min(
|
||||||
900,
|
3000,
|
||||||
Math.floor(((Math.PI * maxR * maxR) / 1500) * density),
|
Math.floor(((Math.PI * maxR * maxR) / 300) * density),
|
||||||
);
|
);
|
||||||
stars = Array.from({ length: count }, (_, i) => {
|
stars = Array.from({ length: count }, (_, i) => {
|
||||||
const anchor = i % 7 === 0;
|
const anchor = i % 7 === 0;
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
/**
|
/**
|
||||||
* SectionNav.astro
|
* SectionNav.astro
|
||||||
* ---------------------------------------------------------------------------
|
* ---------------------------------------------------------------------------
|
||||||
* Sticky in-page sub-navigation for the Professional page. Jumps between the
|
* Fixed LEFT-SIDE vertical rail for the Professional page. Shows the six
|
||||||
* six anchor sections (Summary, Experience, Skills, Projects, Education,
|
* sections as a stacked icon+label list, highlights the section currently in
|
||||||
* Contact) via smooth scroll. Hidden on small screens where the page reads as a
|
* view, and lets the user click any item to smooth-scroll to it. A vertical
|
||||||
* simple scroll. Active state is computed client-side via IntersectionObserver.
|
* 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
|
* Stylistically it mirrors the page's `.card` language (same raised surface,
|
||||||
* readable while scrolling over content. Sticky so it tracks the viewport once
|
* border, radius) so it reads as a sibling of the content cards rather than a
|
||||||
* the user scrolls past it.
|
* separate chrome element. Icons + labels use bright tokens so they pop on the
|
||||||
|
* dark surface. Hidden below `lg`.
|
||||||
*/
|
*/
|
||||||
import Icon from "@components/Icon.astro";
|
import Icon from "@components/Icon.astro";
|
||||||
const sections = [
|
const sections = [
|
||||||
@@ -22,28 +23,79 @@ const sections = [
|
|||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<nav aria-label="Professional sections" class="hidden lg:block">
|
<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
|
<div
|
||||||
class="sticky top-20 z-30 flex flex-wrap gap-1 rounded-xl border p-1.5 shadow-lg"
|
class="card relative flex flex-col gap-1 p-3"
|
||||||
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);"
|
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) => (
|
{sections.map((s) => (
|
||||||
<a
|
<a
|
||||||
href={`#${s.id}`}
|
href={`#${s.id}`}
|
||||||
data-section-link={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"
|
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.7;"
|
style="color: var(--color-cream); opacity: 0.85;"
|
||||||
>
|
>
|
||||||
<Icon name={s.icon} class="h-4 w-4" />
|
<span
|
||||||
{s.label}
|
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>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</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>
|
<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() {
|
function setupSectionSpy() {
|
||||||
|
const nav = document.querySelector<HTMLElement>("[data-pro-nav]");
|
||||||
|
if (!nav) return;
|
||||||
|
|
||||||
const links = Array.from(
|
const links = Array.from(
|
||||||
document.querySelectorAll<HTMLAnchorElement>("[data-section-link]"),
|
document.querySelectorAll<HTMLAnchorElement>("[data-section-link]"),
|
||||||
);
|
);
|
||||||
@@ -51,20 +103,16 @@ const sections = [
|
|||||||
const sections = links
|
const sections = links
|
||||||
.map((l) => document.getElementById(l.dataset.sectionLink!))
|
.map((l) => document.getElementById(l.dataset.sectionLink!))
|
||||||
.filter((el): el is HTMLElement => Boolean(el));
|
.filter((el): el is HTMLElement => Boolean(el));
|
||||||
|
const fill = nav.querySelector<HTMLElement>("[data-pro-fill]");
|
||||||
|
|
||||||
const setActive = (id: string) => {
|
const setActive = (id: string) => {
|
||||||
for (const l of links) {
|
for (const l of links) {
|
||||||
const active = l.dataset.sectionLink === id;
|
const active = l.dataset.sectionLink === id;
|
||||||
l.style.color = active
|
l.setAttribute("data-active", active ? "true" : "false");
|
||||||
? "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";
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Highlight the most-visible section.
|
||||||
const io = new IntersectionObserver(
|
const io = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
const visible = entries
|
const visible = entries
|
||||||
@@ -75,6 +123,25 @@ const sections = [
|
|||||||
{ rootMargin: "-30% 0px -55% 0px", threshold: [0, 0.25, 0.5] },
|
{ rootMargin: "-30% 0px -55% 0px", threshold: [0, 0.25, 0.5] },
|
||||||
);
|
);
|
||||||
sections.forEach((s) => io.observe(s));
|
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();
|
setupSectionSpy();
|
||||||
document.addEventListener("astro:page-load", setupSectionSpy);
|
document.addEventListener("astro:page-load", setupSectionSpy);
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ const techStack = [
|
|||||||
{
|
{
|
||||||
icon: "git",
|
icon: "git",
|
||||||
label: "Tools",
|
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.",
|
hint: "Add the rest of your toolchain.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,23 +43,28 @@ const rendered = await Promise.all(
|
|||||||
{rendered.map(({ entry, Content }, i) => (
|
{rendered.map(({ entry, Content }, i) => (
|
||||||
<li class="relative pl-12">
|
<li class="relative pl-12">
|
||||||
<Reveal delay={i * 80}>
|
<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
|
<span
|
||||||
class="absolute left-0 top-0 inline-flex h-9 w-9 items-center justify-center rounded-full border-4"
|
class="absolute left-[1.15rem] top-7 h-3 w-3 -translate-x-1/2 rounded-full"
|
||||||
style="background: var(--color-surface-raised); border-color: var(--color-surface);"
|
style="background: var(--color-accent); box-shadow: 0 0 0 4px var(--color-surface);"
|
||||||
aria-hidden="true"
|
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>
|
</span>
|
||||||
|
|
||||||
<div class="card flex flex-col gap-3">
|
<div class="card flex flex-col gap-3">
|
||||||
<div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
|
{/* Header row: icon + company name inline, period on the right. */}
|
||||||
<h3 style="color: var(--color-text);">{entry.data.company}</h3>
|
<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);">
|
<span class="font-mono text-xs uppercase tracking-[0.18em]" style="color: var(--color-text-subtle);">
|
||||||
{entry.data.period}
|
{entry.data.period}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -4,15 +4,13 @@
|
|||||||
# entries. To replace it: copy this file, edit the frontmatter + body, and bump
|
# 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
|
# `placeholder: false`. Or just delete this file and add your own. See
|
||||||
# docs/CONTENT.md. The schema lives in src/content.config.ts.
|
# docs/CONTENT.md. The schema lives in src/content.config.ts.
|
||||||
title: "PUT PROJECTS HERE"
|
title: "OmniTracker"
|
||||||
role: "Lead Developer"
|
role: "Lead Developer / Architect"
|
||||||
tech: ["TypeScript", "Node.js", "SAP BTP"]
|
tech: ["TypeScript", "Node.js", "NFC tags", "Tasker", "PostgreSQL"]
|
||||||
summary: >
|
summary: >
|
||||||
This is a placeholder so the project grid has the right layout. Replace it
|
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.
|
||||||
with a real 2-3 sentence description of what the project does and my
|
order: 120
|
||||||
contribution, then add links below.
|
placeholder: false
|
||||||
order: 100
|
|
||||||
placeholder: true
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Drop in a real project writeup here. Markdown is supported, including headings,
|
Drop in a real project writeup here. Markdown is supported, including headings,
|
||||||
|
|||||||
@@ -12,20 +12,20 @@
|
|||||||
*
|
*
|
||||||
* Page transitions use Astro's <ClientRouter /> (View Transitions API).
|
* 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.
|
// Self-hosted fonts (woff2, variable). Avoids external requests for perf + privacy.
|
||||||
import '@fontsource-variable/inter';
|
import "@fontsource-variable/inter";
|
||||||
import '@fontsource-variable/plus-jakarta-sans';
|
import "@fontsource-variable/plus-jakarta-sans";
|
||||||
import '@fontsource-variable/jetbrains-mono';
|
import "@fontsource-variable/jetbrains-mono";
|
||||||
|
|
||||||
import { ClientRouter } from 'astro:transitions';
|
import { ClientRouter } from "astro:transitions";
|
||||||
import NavBar from '@components/nav/NavBar.astro';
|
import NavBar from "@components/nav/NavBar.astro";
|
||||||
import Footer from '@components/layout/Footer.astro';
|
import Footer from "@components/layout/Footer.astro";
|
||||||
import { themeInitScript } from '@/scripts/theme-init';
|
import { themeInitScript } from "@/scripts/theme-init";
|
||||||
import { siteIdentity } from '@lib/site';
|
import { siteIdentity } from "@lib/site";
|
||||||
|
|
||||||
type Surface = 'neutral' | 'professional' | 'interest';
|
type Surface = "neutral" | "professional" | "interest";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -38,23 +38,29 @@ interface Props {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
description = 'Greg Kilmer is a software developer, maker, and tabletop gamemaster based in Philadelphia. This is where his professional and personal worlds meet.',
|
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',
|
surface = "neutral",
|
||||||
ogImage,
|
ogImage,
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
// True on the Home page. Used to switch the NavBar's view-transition-name so
|
// 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).
|
// 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
|
const fullTitle = title
|
||||||
? `${title} | ${siteIdentity.name}`
|
? `${title} | ${siteIdentity.name}`
|
||||||
: `${siteIdentity.name} — Developer. Maker. Storyteller.`;
|
: `${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
|
const ogImageUrl = ogImage
|
||||||
? new URL(ogImage, 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();
|
: new URL(
|
||||||
|
"/images/hero-portrait.jpg",
|
||||||
|
Astro.site ?? "https://patiun.com",
|
||||||
|
).toString();
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -84,7 +90,7 @@ const ogImageUrl = ogImage
|
|||||||
{ogImageUrl && <meta property="og:image" content={ogImageUrl} />}
|
{ogImageUrl && <meta property="og:image" content={ogImageUrl} />}
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<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.
|
<!-- Theme boot: blocking inline script prevents flash of the wrong theme.
|
||||||
Dark mode is currently forced (light mode deferred), so this just pins
|
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
|
Loaded as a module so it self-initializes on load + on View Transitions
|
||||||
(it listens for astro:page-load internally). -->
|
(it listens for astro:page-load internally). -->
|
||||||
<script>
|
<script>
|
||||||
import '@/scripts/reveal-init';
|
import "@/scripts/reveal-init";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- View Transitions flash-killer. The incoming page is snapshotted with its
|
<!-- 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,
|
<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. -->
|
then lets the observer take over for scroll reveals on the new page. -->
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
document.addEventListener('astro:before-swap', () => {
|
document.addEventListener("astro:before-swap", () => {
|
||||||
document.documentElement.classList.add('vt-transitioning');
|
document.documentElement.classList.add("vt-transitioning");
|
||||||
});
|
});
|
||||||
document.addEventListener('astro:after-swap', () => {
|
document.addEventListener("astro:after-swap", () => {
|
||||||
document.documentElement.classList.remove('vt-transitioning');
|
document.documentElement.classList.remove("vt-transitioning");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ClientRouter />
|
<ClientRouter />
|
||||||
</head>
|
</head>
|
||||||
<body data-surface={surface} data-page={isHome ? 'home' : 'other'}>
|
<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)">
|
<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
|
Skip to content
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
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. -->
|
<!-- 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" />
|
<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>
|
<Reveal>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
tone="hero"
|
tone="hero"
|
||||||
|
|||||||
@@ -21,17 +21,20 @@ const professional = [
|
|||||||
{
|
{
|
||||||
icon: "email-outline",
|
icon: "email-outline",
|
||||||
label: "Email",
|
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",
|
icon: "linkedin",
|
||||||
label: "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",
|
icon: "github",
|
||||||
label: "GitHub",
|
label: "GitHub",
|
||||||
hint: '<a href="https://github.com/patiun" target="_blank" rel="noopener noreferrer">GitHub</a>',
|
href: "https://github.com/patiun",
|
||||||
|
hint: "patiun",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -104,7 +107,13 @@ const social = [
|
|||||||
class="text-xs"
|
class="text-xs"
|
||||||
style="color: var(--color-text-subtle);"
|
style="color: var(--color-text-subtle);"
|
||||||
>
|
>
|
||||||
{item.hint}
|
<a
|
||||||
|
href={item.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{item.hint}
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const cards = [
|
|||||||
<Reveal as="div" class="max-w-2xl grow">
|
<Reveal as="div" class="max-w-2xl grow">
|
||||||
<span
|
<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]"
|
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
|
<span
|
||||||
class="h-2 w-2 rounded-full animate-pulse-glow"
|
class="h-2 w-2 rounded-full animate-pulse-glow"
|
||||||
@@ -75,7 +75,7 @@ const cards = [
|
|||||||
|
|
||||||
<p
|
<p
|
||||||
class="mt-3 font-sans text-xl font-medium sm:text-2xl"
|
class="mt-3 font-sans text-xl font-medium sm:text-2xl"
|
||||||
style="color: var(--color-accent);"
|
style="color: var(--accent);"
|
||||||
>
|
>
|
||||||
Software Developer · Maker · Tabletop Gamemaster
|
Software Developer · Maker · Tabletop Gamemaster
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -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."
|
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 -->
|
<!-- 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. */
|
/* Full animated Milky Way field + stars, matching the Home hero treatment. */
|
||||||
}
|
}
|
||||||
<AnimatedBackground client:idle className="vt-sky" />
|
<AnimatedBackground client:idle className="vt-sky" />
|
||||||
<div
|
<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>
|
<Reveal>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
|
|||||||
@@ -45,13 +45,15 @@ const projects = (await getCollection("projects")).sort(
|
|||||||
a star hero is element-to-element equivalent — no separate gradient
|
a star hero is element-to-element equivalent — no separate gradient
|
||||||
flash. Shares view-transition-name "sky" via vt-sky. */
|
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. */}
|
{/* Slowly rotating astrolabe ornament behind the header. */}
|
||||||
<Astrolabe
|
<Astrolabe
|
||||||
class="pointer-events-none absolute z-0 -right-48 -top-24 h-[34rem] w-[34rem] animate-spin-slow"
|
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;"
|
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>
|
<Reveal>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
tone="hero"
|
tone="hero"
|
||||||
@@ -64,15 +66,22 @@ const projects = (await getCollection("projects")).sort(
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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. */}
|
/* Fixed left section rail. It is position:fixed so it renders once here
|
||||||
<div class="container-prose relative z-20 pt-4">
|
and stays pinned to the viewport's left edge while the page scrolls.
|
||||||
<Reveal>
|
The main content column below is given lg:pl-* to clear the 14rem rail. */
|
||||||
<SectionNav />
|
}
|
||||||
</Reveal>
|
<SectionNav />
|
||||||
</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 ===== */}
|
{/* ===== 1. Summary ===== */}
|
||||||
<section id="summary" class="scroll-mt-28 flex flex-col gap-6">
|
<section id="summary" class="scroll-mt-28 flex flex-col gap-6">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
@@ -159,10 +168,12 @@ const projects = (await getCollection("projects")).sort(
|
|||||||
kicker="04"
|
kicker="04"
|
||||||
icon="view-grid"
|
icon="view-grid"
|
||||||
title="Projects"
|
title="Projects"
|
||||||
subtitle="What I've built. [TODO: Replace the placeholder with 3-6 of my projects]"
|
subtitle="What I've built."
|
||||||
/>
|
/>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<div class="grid gap-5 sm:grid-cols-2">
|
<div class="grid
|
||||||
|
gap-5
|
||||||
|
sm:grid-cols-2">
|
||||||
{
|
{
|
||||||
projects.map((project, i) => (
|
projects.map((project, i) => (
|
||||||
<Reveal delay={i * 80}>
|
<Reveal delay={i * 80}>
|
||||||
@@ -202,7 +213,7 @@ const projects = (await getCollection("projects")).sort(
|
|||||||
kicker="06"
|
kicker="06"
|
||||||
icon="email"
|
icon="email"
|
||||||
title="Let's talk"
|
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>
|
||||||
<Reveal delay={80}>
|
<Reveal delay={80}>
|
||||||
|
|||||||
@@ -327,13 +327,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
::view-transition-group(hero-sky) {
|
::view-transition-group(hero-sky) {
|
||||||
animation-duration: 0.4s;
|
animation-duration: 0.26s;
|
||||||
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
}
|
}
|
||||||
::view-transition-old(hero-sky),
|
::view-transition-old(hero-sky),
|
||||||
::view-transition-new(hero-sky) {
|
::view-transition-new(hero-sky) {
|
||||||
/* A gentle height/scale blend reads as expand/collapse rather than a swap. */
|
/* 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);
|
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
}
|
}
|
||||||
@@ -363,7 +363,7 @@
|
|||||||
|
|
||||||
::view-transition-old(root),
|
::view-transition-old(root),
|
||||||
::view-transition-new(root) {
|
::view-transition-new(root) {
|
||||||
animation-duration: 0.28s;
|
animation-duration: 0.18s;
|
||||||
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||