Start the rebuild: note schema, vault kit and data migration

Phase 1 of the functional design plan.

- Move the old site into legacy/, unchanged, for reference. It still
  builds from there until cutover.
- Add packages/schema: the one definition of all 20 note types, with
  field titles and descriptions, note and vault checks, and JSON Schema
  export so later editing tools and the MCP server share it.
- Add vault-kit: Obsidian templates generated from the schema, and the
  Site guide for 24.01_PersonalWebsite.
- Add tools/migrate: turns the legacy JSON, collections, store data and
  page text into 545 Site folder notes and 84 images, and checks the
  result with the schema.

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 18:40:55 +00:00
parent 4ded4b277b
commit a2f723ca2d
324 changed files with 11538 additions and 7556 deletions
+94
View File
@@ -0,0 +1,94 @@
/**
* animations.css
* ---------------------------------------------------------------------------
* Keyframes used by hero backgrounds, decorative motion, and ambient effects.
* All animations are disabled by the global prefers-reduced-motion rule in
* global.css, so they are safe to use freely.
*/
/* Slowly drift radial-gradient stops for the hero mesh background. */
@keyframes gradient-shift {
0% {
background-position:
0% 0%,
100% 0%,
50% 100%;
}
50% {
background-position:
100% 100%,
0% 100%,
50% 0%;
}
100% {
background-position:
0% 0%,
100% 0%,
50% 100%;
}
}
/* Soft vertical float for decorative shapes / icons. */
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
/* Slow rotation for ambient rings. */
@keyframes spin-slow {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Pulsing glow for accent dots / status indicators. */
@keyframes pulse-glow {
0%,
100% {
opacity: 0.5;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.15);
}
}
/* Nav backdrop blur is handled via class toggle, but the entry of the nav uses
* a fade-down so it doesn't pop on first paint. */
@keyframes fade-down {
from {
opacity: 0;
transform: translateY(-12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Utility classes bound to the keyframes above. */
.animate-gradient-shift {
background-size: 200% 200%;
animation: gradient-shift 18s var(--ease-in-out-soft, ease-in-out) infinite;
}
.animate-float {
animation: float 6s var(--ease-in-out-soft, ease-in-out) infinite;
}
.animate-spin-slow {
animation: spin-slow 40s linear infinite;
}
.animate-pulse-glow {
animation: pulse-glow 4s var(--ease-in-out-soft, ease-in-out) infinite;
}
.animate-fade-down {
animation: fade-down 0.5s var(--ease-out-soft, ease-out) both;
}
+421
View File
@@ -0,0 +1,421 @@
/**
* global.css
* ---------------------------------------------------------------------------
* Tailwind v4 entry + global base layer for Greg Kilmer's profile site.
*
* Tailwind v4 uses a CSS-first config. The `@import "tailwindcss"` pulls in the
* engine and the `@theme` block below exposes the project palette as utility
* classes (e.g. `bg-surface`, `text-accent`). Section/light-dark tokens live in
* themes.css and are referenced here via CSS variables.
*
* Import order matters: theme tokens first (so @theme can reference them), then
* Tailwind, then animations, then base.
*/
/* Palette + light/dark + section surface tokens (CSS variables). */
@import "./themes.css";
/* Keyframes (gradient shift, fade-up, blur, float). */
@import "./animations.css";
/* Tailwind engine. In v4 this is a single import. */
@import "tailwindcss";
/* ---------------------------------------------------------------------------
* Theme bridge: expose CSS variables from themes.css to Tailwind utilities.
* Each entry below generates a utility family, e.g. `--color-surface` -> the
* `bg-surface` / `text-surface` / `border-surface` classes.
* The values point at the CSS variables so that changing `data-theme` or the
* section wrapper (`data-surface`) instantly restyles every component.
* ------------------------------------------------------------------------- */
@theme {
/* Brand corner radius: cards and panels are 10px; buttons and tags are pills. */
--radius-xl: 10px;
--radius-2xl: 10px;
--radius-3xl: 10px;
/* Brand colors (fixed, do not change with theme). The Patiun LLC palette,
* shared by Patiun (craft) and Greg Kilmer (web). Source of truth: the
* Patiun brand record / Claude Design tokens. Gold is structure and sparkle
* (stars, rules, small labels), never a large fill. Violet and indigo are
* background light only (nebula glows, gradient ends), never text. */
--color-teal: #14a39a; /* Patiun Teal, primary: buttons, fills, the glyph */
--color-teal-bright: #2bc2b7; /* teal for text/links on dark grounds */
--color-teal-deep: #0f8c84; /* teal on light grounds; hover/active icons */
--color-slate-teal: #2d4a58; /* dark end of the Tide gradient */
--color-gold: #c9a24a; /* Alchemist Gold, flat gold on screen */
--color-gold-bright: #d8b35a; /* gold text/eyebrows on dark grounds */
--color-gold-deep: #8a6516; /* gold text under 24px on Starlight */
--color-violet: #6a2fe0; /* Nebula Violet, background light only */
--color-indigo: #3b1f8f; /* Astral Indigo, gradient midpoints */
--color-midnight: #131c2b; /* Midnight: panels, tiles, theme color */
--color-sky: #0e1522; /* night-sky ground */
--color-starlight: #f4f2ed; /* Starlight: text on dark, paper ground */
/* Semantic tokens (resolve per theme/section via variables). */
--color-surface: var(--surface);
--color-surface-raised: var(--surface-raised);
--color-surface-sunken: var(--surface-sunken);
--color-text: var(--text);
--color-text-muted: var(--text-muted);
--color-text-subtle: var(--text-subtle);
--color-border: var(--border);
--color-accent: var(--accent);
--color-accent-contrast: var(--accent-contrast);
--color-secondary: var(--secondary);
/* Typography. */
/* Typography. Cinzel for names, wordmarks and headings (never paragraphs);
* Montserrat for body, UI, labels and uppercase eyebrows. JetBrains Mono is
* kept only for genuinely code-like blocks. */
--font-display: "Cinzel Variable", Cinzel, "Trajan Pro", Georgia, serif;
--font-sans: "Montserrat Variable", Montserrat, "Helvetica Neue", Arial,
ui-sans-serif, sans-serif;
--font-body: "Montserrat Variable", Montserrat, "Helvetica Neue", Arial,
ui-sans-serif, sans-serif;
--font-eyebrow: "Montserrat Variable", Montserrat, "Helvetica Neue", Arial,
ui-sans-serif, sans-serif;
--font-mono: "JetBrains Mono Variable", ui-monospace, "SFMono-Regular", monospace;
/* Motion tokens (used across reveal + transition classes). */
--ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1);
--ease-in-out-soft: cubic-bezier(0.65, 0, 0.35, 1);
}
/* ---------------------------------------------------------------------------
* Base layer: resets, defaults, focus styles.
* ------------------------------------------------------------------------- */
@layer base {
:root {
color-scheme: light dark;
}
html {
scroll-behavior: smooth;
/* Offset so anchor jumps clear the fixed nav. */
scroll-padding-top: 6rem;
-webkit-text-size-adjust: 100%;
}
/* Honor reduced motion globally. */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
}
body {
background-color: var(--color-surface);
color: var(--color-text);
font-family: var(--font-body);
line-height: 1.65;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100dvh;
}
h1,
h2,
h3,
h4 {
font-family: var(--font-display);
font-weight: 700;
line-height: 1.15;
/* Cinzel is a capital-led display face: open tracking, never negative. */
letter-spacing: 0.04em;
color: var(--color-text);
}
/* Strong typographic rhythm for headings. */
h1 {
font-size: clamp(2.25rem, 5vw + 1rem, 4rem);
}
h2 {
font-size: clamp(1.75rem, 2.5vw + 1rem, 2.5rem);
}
h3 {
font-size: clamp(1.25rem, 1vw + 1rem, 1.5rem);
}
a {
color: var(--color-accent);
text-decoration: none;
transition: color 0.2s var(--ease-out-soft);
}
a:hover {
color: var(--color-gold-bright);
}
/* Visible, on-brand focus ring for keyboard users. */
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
border-radius: 4px;
}
::selection {
background: var(--color-teal);
color: var(--color-midnight);
}
/* Better default for images: never overflow, lazy by default in components. */
img {
max-width: 100%;
height: auto;
}
}
/* ---------------------------------------------------------------------------
* Component-ish utilities used across the site. Keeping them here (rather than
* as repeated Tailwind class strings) makes tone adjustments easy.
* ------------------------------------------------------------------------- */
@layer components {
/* Content width container. */
.container-prose {
width: 100%;
max-width: 72rem;
margin-inline: auto;
padding-inline: 1.25rem;
}
@media (min-width: 768px) {
.container-prose {
padding-inline: 2rem;
}
}
/* The recurring "card" surface. */
.card {
background-color: var(--color-surface-raised);
border: 1px solid var(--color-border);
border-radius: 10px;
padding: 1.5rem;
transition:
transform 0.3s var(--ease-out-soft),
box-shadow 0.3s var(--ease-out-soft),
border-color 0.3s var(--ease-out-soft);
}
.card-interactive:hover {
transform: translateY(-4px);
box-shadow: 0 18px 40px -24px rgba(14, 21, 34, 0.6);
border-color: var(--color-accent);
}
/* Pill / tag. */
.pill {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.7rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 500;
background: color-mix(in srgb, var(--color-accent) 15%, transparent);
color: var(--color-text);
border: 1px solid color-mix(in srgb, var(--color-accent) 35%, transparent);
}
/* Primary (Patiun Teal) and secondary (outlined) CTA buttons. */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 999px;
font-family: var(--font-sans);
font-weight: 600;
font-size: 1rem;
line-height: 1;
cursor: pointer;
transition:
transform 0.2s var(--ease-out-soft),
box-shadow 0.2s var(--ease-out-soft),
background-color 0.2s var(--ease-out-soft),
border-color 0.2s var(--ease-out-soft);
}
.btn:active {
transform: translateY(1px);
}
/* Filled buttons: Patiun Teal with Midnight text, never white text. */
.btn-primary {
background: var(--color-teal);
color: var(--color-midnight);
box-shadow: 0 10px 30px -12px rgba(20, 163, 154, 0.7);
}
.btn-primary:hover {
background: var(--color-teal-bright);
color: var(--color-midnight);
transform: translateY(-2px);
}
.btn-secondary {
background: transparent;
color: var(--text);
border: 1.5px solid var(--color-border);
}
.btn-secondary:hover {
border-color: var(--color-accent);
color: var(--color-text);
transform: translateY(-2px);
}
}
/* Night-sky bands (nav, page heroes). The semantic Tailwind tokens resolve at
* :root, where the Starlight values live, so anything sitting on the dark sky
* re-points accent and gold to their on-dark brand values here. */
.on-sky {
--color-accent: var(--color-teal-bright);
--color-secondary: var(--color-gold-bright);
--icon-gold: var(--color-gold);
}
/* Gold star accent inside the brand icons: deep gold on Starlight (matches the
* light icon set), flat Alchemist Gold on the night sky (.on-sky above). */
:root {
--icon-gold: var(--color-gold-deep);
}
/* Visually hidden but available to screen readers. */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ---------------------------------------------------------------------------
* Reveal-on-scroll utility. The Reveal.astro component toggles `.is-visible`.
*
* The `.no-js` guard below is critical: <html> starts with class="no-js"
* (set in BaseLayout.astro) and reveal-init.ts removes it once it has wired up
* the observer. If JS is disabled OR the script fails to load, that class is
* never removed — so this fallback forces reveals visible rather than leaving
* the page blank. This is the safety net against the original invisibility bug.
* ------------------------------------------------------------------------- */
.no-js .reveal {
opacity: 1 !important;
transform: none !important;
}
/* Elegant entrance: a slow rise + soft focus-clear, with a gentle scale settle.
* Slower and smoother than a plain fade-up so text "settles" into place rather
* than popping. The slight blur-to-sharp adds a dreamy, mystical quality. */
.reveal {
opacity: 0;
transform: translateY(18px) scale(0.985);
filter: blur(6px);
transition:
opacity 0.9s var(--ease-out-soft),
transform 1s var(--ease-out-soft),
filter 0.9s var(--ease-out-soft);
will-change: opacity, transform, filter;
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0px);
}
@media (prefers-reduced-motion: reduce) {
.reveal {
opacity: 1;
transform: none;
filter: none;
transition: none;
}
}
/* ---------------------------------------------------------------------------
* View Transition name utility. Assigns `view-transition-name: sky` to a single
* hero/section field per page so the browser morphs the star/circuit field as
* one element across navigations (constellations <-> circuitry).
*
* IMPORTANT: the View Transitions API requires this name to be UNIQUE within a
* page snapshot. Assigning it to more than one element per page throws
* "Unexpected duplicate view-transition-name: sky" and aborts the transition.
* Only ONE field per page should ever carry `.vt-sky`.
*
* Implemented as a CSS class (not React inline style) so SSR + hydration
* attributes match exactly and React never rewrites the property.
* ------------------------------------------------------------------------- */
.vt-sky {
view-transition-name: sky;
}
/* Hero expand/collapse morph. The Home hero section carries this name on Home;
* the NavBar carries it on every OTHER page. So leaving Home morphs the tall
* hero down into the nav bar (collapse), and arriving Home expands it back up
* from the nav bar. The custom animation below makes the morph a smooth
* vertical scale + position interpolation rather than the default cross-fade,
* so the starry field reads as growing/shrinking rather than fading. */
.vt-hero-sky {
view-transition-name: hero-sky;
}
::view-transition-group(hero-sky) {
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.26s;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
overflow: clip;
}
/* ---------------------------------------------------------------------------
* View Transitions — smooth cross-page navigation.
*
* Astro's <ClientRouter /> uses the native View Transitions API. Without the
* rules below the browser falls back to a blunt default cross-fade that feels
* harsh, and worse: the NEW page is snapshotted while its `.reveal` elements
* are still at opacity:0 (the scroll-reveal observer hasn't fired yet), so all
* content flashes out during the morph and re-appears after the swap. The two
* fixes here:
*
* 1. `::view-transition-group(root)` gets a short, soft duration + ease so
* the swap is quick and gentle instead of slow and jarring.
* 2. `html:has(...)::view-transition-*` + an `astro:before-swap` class forces
* every `.reveal` to `is-visible` DURING the transition — so the snapshot
* of the incoming page shows its content, eliminating the flash. (The
* reveal-init.ts observer still handles the normal scroll case.)
*
* Reduced-motion users get an instant swap (no animation).
* ------------------------------------------------------------------------- */
@view-transition {
navigation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.18s;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
/* During a transition, force all reveal content visible so the incoming page's
* snapshot isn't blank. `.vt-transitioning` is toggled by the script below on
* astro:before-swap and removed on astro:after-swap. */
html.vt-transitioning .reveal {
opacity: 1 !important;
transform: none !important;
filter: none !important;
}
@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root),
::view-transition-group(*) {
animation: none !important;
}
}
+129
View File
@@ -0,0 +1,129 @@
/**
* themes.css
* ---------------------------------------------------------------------------
* Two-axis theming model for the site:
*
* 1. Global light/dark mode -> set on <html data-theme="light|dark">
* 2. Section surface tone -> set on <body data-surface="...">
* data-surface="neutral" Home / About / Contact (Patiun night sky)
* data-surface="professional" Professional wing (Tide: slate-teal night)
* data-surface="interest" Interests wing (Nebula: indigo night)
*
* PALETTE: every value here comes from the Patiun brand (Night sky theme for
* dark, Starlight theme for light). Text uses Starlight tones, links and
* accents use teal, small gold labels use the bright gold, and violet/indigo
* only tint the surfaces (never text).
*
* SELECTOR MODEL: data-theme lives on <html>, data-surface on <body>. We match
* the descendant combination `html[data-theme=...] body[data-surface=...]`.
* `:root` holds the light/neutral FALLBACK only; every other rule beats it via
* specificity (0,2,0) > (0,1,0).
*
* STATUS: DARK MODE IS FORCED (light mode deferred). The dark rules below are
* tuned for contrast first — --text / --text-muted / --text-subtle are kept
* bright against the deep surfaces so content always stands out. Light rules
* are retained for the day light mode is reintroduced.
*
* `theme-init.ts` (inlined in <head>) pins data-theme="dark" before paint.
*/
/* ===========================================================================
* DEFAULT / NEUTRAL SURFACE (Home, About, Contact)
* ======================================================================== */
/* Light + neutral: Starlight paper (fallback; not currently used, dark is
* forced in theme-init, but these are the values the content sections
* actually render with, since the Tailwind tokens resolve at :root). The
* brand's deep teal #0F8C84 is 3.7:1 on Starlight, so --accent uses a
* slightly darker teal that passes AA for small text. */
:root,
html[data-theme='light'] body[data-surface='neutral'] {
--surface: #f4f2ed; /* Starlight */
--surface-raised: #ffffff;
--surface-sunken: #e9e5dc;
--text: #131c2b; /* Midnight */
--text-muted: #4a5f6b;
--text-subtle: #5c6e78;
--border: #d9d4c8;
--accent: #0a726b; /* deep teal, darkened to 5.2:1 so small links pass AA */
--accent-contrast: #f4f2ed;
--secondary: #8a6516; /* deep gold, 4.75:1: small gold text and eyebrows */
}
/* Dark + neutral: the canonical Patiun night sky. High-contrast text. */
html[data-theme='dark'] body[data-surface='neutral'] {
--surface: #0e1522; /* night sky */
--surface-raised: #131c2b; /* Midnight panel */
--surface-sunken: #0a1019; /* deeper void */
--text: #ece9e2; /* Starlight text, 15:1 */
--text-muted: #b9c8cf;
--text-subtle: #9fb3bd; /* brand muted, 7.9:1 on panels */
--border: #26344a;
--accent: #2bc2b7; /* teal on dark, 7.7:1 */
--accent-contrast: #0e1522;
--secondary: #d8b35a; /* gold accents and chips */
}
/* ===========================================================================
* PROFESSIONAL SURFACE: the web side. A slate-teal night (the Tide gradient),
* dark by default.
* ======================================================================== */
body[data-surface='professional'],
html[data-theme='dark'] body[data-surface='professional'] {
--surface: #0c1820;
--surface-raised: #12222c;
--surface-sunken: #081117;
--text: #ece9e2;
--text-muted: #b9c8cf;
--text-subtle: #9fb3bd;
--border: #23394a;
--accent: #2bc2b7;
--accent-contrast: #0c1820;
--secondary: #d8b35a;
}
html[data-theme='light'] body[data-surface='professional'] {
--surface: #2d4a58; /* Slate Teal: the professional wing stays dark */
--surface-raised: #355766;
--surface-sunken: #243c48;
--text: #f4f2ed;
--text-muted: #d9e2e6;
--text-subtle: #bccbd2;
--border: #46687a;
--accent: #7fdcd5;
--accent-contrast: #131c2b;
--secondary: #e2c57e;
}
/* ===========================================================================
* INTEREST SURFACE: the craft side. An indigo night lit by the Nebula
* gradient, dark by default.
* ======================================================================== */
body[data-surface='interest'],
html[data-theme='dark'] body[data-surface='interest'] {
--surface: #110f22; /* indigo night */
--surface-raised: #1a1730;
--surface-sunken: #0b0a18;
--text: #ece9e2;
--text-muted: #c3bfd6;
--text-subtle: #a49fbd;
--border: #2e2a52;
--accent: #2bc2b7;
--accent-contrast: #110f22;
--secondary: #d8b35a;
}
html[data-theme='light'] body[data-surface='interest'] {
--surface: #f4f2ed;
--surface-raised: #ffffff;
--surface-sunken: #e9e5dc;
--text: #131c2b;
--text-muted: #4a5f6b;
--text-subtle: #5c6e78;
--border: #d9d4c8;
--accent: #0f8c84;
--accent-contrast: #131c2b;
--secondary: #8a6516;
}