Match the site to the approved designs

Denser star field, the page nebula, a favicon package, the filament banner
without its box, the About portrait, Kashi and tea photos and Scorpius, the
Store's own header, a header logo glow, a resume PDF that fills its page,
one divider on the Library and Shelf, star-chart art for coverless
favorites, Shelf Players, Play time and blog filters with a Show all fold,
make kind pills on craft pages, and full-width closing bands.

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-12 03:33:15 +00:00
parent 3f6167455d
commit 151bed230a
45 changed files with 671 additions and 166 deletions
+6
View File
@@ -399,6 +399,7 @@ export const productLine = def({
channel: z.enum(["here", "Etsy", "Patiun.com", "coming soon"]).meta({ title: "Where to buy" }),
channelUrl: z.url().meta({ title: "Shop link", description: "For Etsy or Patiun.com lines." }).optional(),
summary: line({ title: "Summary" }),
menuName: line({ title: "Menu name", description: "A short name for the Store menu, like Proxy cards." }).optional(),
icon: line({ title: "Icon" }).optional(),
order: z.number().int().meta({ title: "Order" }).optional(),
slots: z
@@ -501,6 +502,11 @@ export const page = def({
schema: note("page", {
page: line({ title: "Page", description: "The page address this text belongs to, like /about." }),
lede: line({ title: "Lede", description: "The one line under the page title." }).optional(),
portrait: image({ title: "Portrait", description: "Greg's photo on the landing and About pages." }).optional(),
photos: z
.array(z.object({ file: image({ title: "Image" }), alt: line({ title: "Alt text" }), caption: line({ title: "Caption" }).optional() }))
.meta({ title: "Photos", description: "A row of captioned photos, like Kashi and the tea on About." })
.optional(),
quickFacts: z
.array(z.object({ icon: line({ title: "Icon" }), text: line({ title: "Text" }) }))
.meta({ title: "Quick facts" })
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F4F2ED" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="Download"><path d="M12 4v11M7 10.5l5 5 5-5M5 19.5h14"/></svg>

After

Width:  |  Height:  |  Size: 239 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 400"><line x1="565.0" y1="34.9" x2="578.5" y2="43.6" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="591.7" y1="55.4" x2="605.3" y2="72.1" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-dasharray="2 5" stroke-linecap="round"/><line x1="582.8" y1="56.8" x2="567.6" y2="96.5" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="559.7" y1="112.7" x2="557.7" y2="116.1" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="552.6" y1="132.8" x2="552.2" y2="140.5" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="553.1" y1="158.4" x2="561.7" y2="216.6" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="567.7" y1="233.2" x2="580.0" y2="253.6" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="588.7" y1="269.4" x2="603.1" y2="298.0" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="601.1" y1="312.7" x2="584.4" y2="330.7" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="570.7" y1="342.2" x2="535.2" y2="365.1" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="519.1" y1="367.2" x2="497.5" y2="360.0" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="490.2" y1="348.3" x2="490.0" y2="349.4" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="492.8" y1="331.6" x2="495.0" y2="319.5" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-dasharray="2 5" stroke-linecap="round"/><line x1="505.2" y1="307.9" x2="495.2" y2="311.0" stroke="#2BC2B7" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><polygon points="557.4,23.0 558.5,28.9 564.4,30.0 558.5,31.1 557.4,37.0 556.3,31.1 550.4,30.0 556.3,28.9" fill="#9A70F5"/><polygon points="586.1,41.4 587.2,47.3 593.1,48.4 587.2,49.6 586.1,55.4 584.9,49.6 579.1,48.4 584.9,47.3" fill="#9A70F5"/><circle cx="611.0" cy="79.1" r="2.4" fill="#F4F2ED"/><circle cx="564.3" cy="104.9" r="2.4" fill="#F4F2ED"/><polygon points="553.1,110.8 555.2,121.7 566.1,123.8 555.2,125.9 553.1,136.8 551.0,125.9 540.1,123.8 551.0,121.7" fill="#C9A24A"/><circle cx="551.8" cy="149.5" r="2.4" fill="#F4F2ED"/><polygon points="563.0,218.5 564.2,224.4 570.0,225.5 564.2,226.7 563.0,232.5 561.9,226.7 556.0,225.5 561.9,224.4" fill="#9A70F5"/><circle cx="584.6" cy="261.3" r="2.4" fill="#F4F2ED"/><circle cx="607.2" cy="306.1" r="2.4" fill="#F4F2ED"/><circle cx="578.3" cy="337.3" r="2.4" fill="#F4F2ED"/><polygon points="527.7,363.0 528.8,368.9 534.7,370.0 528.8,371.1 527.7,377.0 526.6,371.1 520.7,370.0 526.6,368.9" fill="#9A70F5"/><circle cx="489.0" cy="357.2" r="2.4" fill="#F4F2ED"/><circle cx="491.3" cy="340.5" r="2.4" fill="#F4F2ED"/><circle cx="503.8" cy="308.3" r="2.4" fill="#F4F2ED"/><polygon points="496.6,303.6 497.7,309.5 503.6,310.6 497.7,311.7 496.6,317.6 495.5,311.7 489.6,310.6 495.5,309.5" fill="#9A70F5"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

@@ -1,14 +1,21 @@
---
/** Sparse, static specks behind a page; about one in five gold. */
/**
* Static specks behind a page; about one in five gold. Two tiles of different
* sizes repeat across the page, so long pages are as starry as short ones and
* the repeat is hard to spot.
*/
import { dust, seedOf } from "../lib/ornaments.ts";
interface Props {
count?: number;
seed?: string;
}
const { count = 48, seed = Astro.url.pathname } = Astro.props;
const { seed = Astro.url.pathname } = Astro.props;
const base = seedOf(seed);
---
<div class="gk-dust" aria-hidden="true" style={`background:${dust(count, seedOf(seed))}`}></div>
<div class="gk-dust" aria-hidden="true">
<div class="layer near" style={`background-image:${dust(26, base)}`}></div>
<div class="layer far" style={`background-image:${dust(30, base + 7919)}`}></div>
</div>
<style>
.gk-dust {
@@ -16,4 +23,16 @@ const { count = 48, seed = Astro.url.pathname } = Astro.props;
inset: 0;
pointer-events: none;
}
.layer {
position: absolute;
inset: 0;
background-repeat: repeat;
}
.near {
background-size: 520px 520px;
}
.far {
background-size: 790px 790px;
background-position: 213px 157px;
}
</style>
+14 -3
View File
@@ -12,7 +12,7 @@ import ConstellationDust from "./ConstellationDust.astro";
import EdgeSeals from "./EdgeSeals.astro";
import SiteFooter from "./SiteFooter.astro";
import SiteHeader from "./SiteHeader.astro";
import type { NavKey } from "./nav.ts";
import type { NavKey, StoreNav } from "./nav.ts";
import type { PageFamily, SealName } from "./seals.ts";
interface Props {
@@ -22,10 +22,12 @@ interface Props {
/** A craft page's own seal, overriding the family's. */
seal?: SealName;
current?: NavKey;
/** The Store's own header, on Store pages. */
store?: StoreNav;
/** Leave the edge seals off, for a page led by a photo. */
noSeals?: boolean;
}
const { title, description, family = "landing", seal, current, noSeals } = Astro.props;
const { title, description, family = "landing", seal, current, store, noSeals } = Astro.props;
const fullTitle = title === "Greg Kilmer" ? title : `${title} · Greg Kilmer`;
---
<!doctype html>
@@ -35,6 +37,10 @@ const fullTitle = title === "Greg Kilmer" ? title : `${title} · Greg Kilmer`;
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="dark" />
<meta name="theme-color" content="#0E1522" />
<link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<title>{fullTitle}</title>
{description && <meta name="description" content={description} />}
<slot name="head" />
@@ -44,7 +50,7 @@ const fullTitle = title === "Greg Kilmer" ? title : `${title} · Greg Kilmer`;
<div class="gk-page">
<ConstellationDust />
{!noSeals && <EdgeSeals family={family} seal={seal} />}
<SiteHeader current={current}><slot name="hero" /></SiteHeader>
<SiteHeader current={current} store={store} hero={Astro.slots.has("hero")}><slot name="hero" /></SiteHeader>
<main id="main" class="gk-wrap gk-main">
<slot />
</main>
@@ -58,6 +64,11 @@ const fullTitle = title === "Greg Kilmer" ? title : `${title} · Greg Kilmer`;
position: relative;
min-height: 100vh;
overflow-x: clip;
/* The page's nebula: violet from the top right, teal from the bottom left. */
background:
radial-gradient(ellipse at 100% 0%, rgba(106, 47, 224, 0.28), transparent 50%),
radial-gradient(ellipse at 0% 100%, rgba(20, 163, 154, 0.2), transparent 55%),
var(--gk-sky);
}
.gk-main {
position: relative;
+81 -37
View File
@@ -2,65 +2,89 @@
/**
* The site header: nebula and filament banner behind the primary nav, an
* optional page hero (the `hero` slot), and the header divider. The filament
* banner and divider appear here only, never elsewhere on a page.
* banner and divider appear here only, never elsewhere on a page. Store pages
* pass `store` and get the Store's own header: Store.GregKilmer.com, its
* product lines, Order request and a way back to the main site.
*/
import monogram from "../assets/brand/gk-monogram.svg?url";
import Divider from "./Divider.astro";
import Star from "./Star.astro";
import { main, tail, world, type NavKey } from "./nav.ts";
import { main, tail, world, type NavKey, type StoreNav } from "./nav.ts";
import { filamentBanner } from "./seals.ts";
interface Props {
/** Which nav entry the page belongs to. */
current?: NavKey;
store?: StoreNav;
/** Whether the page has a hero; PageShell always fills the slot, so it says. */
hero?: boolean;
}
const { current } = Astro.props;
const { current, store, hero = Astro.slots.has("default") } = Astro.props;
const inWorld = world.some((w) => w.key === current);
const aria = (key: NavKey) => (key === current ? "page" : undefined);
---
<header class="gk-site-header">
<div class="nebula" aria-hidden="true"></div>
<img src={filamentBanner} alt="" aria-hidden="true" class="banner" />
<nav aria-label="Primary" class="nav">
<a href="/" class="brand" aria-current={aria("home")}>
<img src={monogram} alt="" width="72" height="72" class="mono" />
<span class="words">
<span class="name">GREG KILMER</span>
<span class="tag">DEVELOPER<Star size={8} tone="teal" />MAKER<Star size={8} tone="teal" />STORYTELLER</span>
</span>
</a>
<ul class="links">
{main.map((l) => <li><a href={l.href} aria-current={aria(l.key)}>{l.label}</a></li>)}
<li class="sep" aria-hidden="true"><Star size={12} /></li>
<li class="mw">
<a href="/library" class="mw-top" aria-haspopup="true" aria-current={inWorld ? "true" : undefined}>
My world
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 9l6 6 6-6" /></svg>
</a>
<ul class="mw-menu">
{
world.map((w) => (
<li>
<a href={w.href} aria-current={aria(w.key)}>
<span class="mw-name">{w.label}</span>
<span class="mw-blurb">{w.blurb}</span>
</a>
</li>
))
}
</ul>
</li>
{tail.map((l) => <li><a href={l.href} aria-current={aria(l.key)} class="dim">{l.label}</a></li>)}
</ul>
</nav>
{
Astro.slots.has("default") && (
store ? (
<nav aria-label="Store" class="nav">
<a href="/store" class="brand" aria-current={store.current === "/store" ? "page" : undefined}>
<img src={monogram} alt="" width="72" height="72" class="mono" />
<span class="words">
<span class="name store-name">Store.GregKilmer.com</span>
<span class="store-tag">a Patiun LLC workshop</span>
</span>
</a>
<ul class="links">
{store.links.map((l) => <li><a href={l.href} aria-current={l.href === store.current ? "page" : undefined}>{l.label}</a></li>)}
<li class="sep" aria-hidden="true"><Star size={12} /></li>
<li><a href="/" class="dim">Back to GregKilmer.com</a></li>
</ul>
</nav>
) : (
<nav aria-label="Primary" class="nav">
<a href="/" class="brand" aria-current={aria("home")}>
<img src={monogram} alt="" width="72" height="72" class="mono" />
<span class="words">
<span class="name">GREG KILMER</span>
<span class="tag">DEVELOPER<Star size={8} tone="teal" />MAKER<Star size={8} tone="teal" />STORYTELLER</span>
</span>
</a>
<ul class="links">
{main.map((l) => <li><a href={l.href} aria-current={aria(l.key)}>{l.label}</a></li>)}
<li class="sep" aria-hidden="true"><Star size={12} /></li>
<li class="mw">
<a href="/library" class="mw-top" aria-haspopup="true" aria-current={inWorld ? "true" : undefined}>
My world
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 9l6 6 6-6" /></svg>
</a>
<ul class="mw-menu">
{
world.map((w) => (
<li>
<a href={w.href} aria-current={aria(w.key)}>
<span class="mw-name">{w.label}</span>
<span class="mw-blurb">{w.blurb}</span>
</a>
</li>
))
}
</ul>
</li>
{tail.map((l) => <li><a href={l.href} aria-current={aria(l.key)} class="dim">{l.label}</a></li>)}
</ul>
</nav>
)
}
{
hero && (
<div class="gk-wrap hero">
<slot />
</div>
)
}
<div class="gk-wrap divider"><Divider /></div>
{hero && <div class="gk-wrap divider"><Divider /></div>}
</header>
<style>
@@ -114,6 +138,17 @@ const aria = (key: NavKey) => (key === current ? "page" : undefined);
.mono {
width: 72px;
height: 72px;
border-radius: 50%;
transition: filter 0.25s ease;
}
.brand:hover .mono,
.brand:focus-visible .mono {
filter: drop-shadow(0 0 6px rgba(216, 179, 90, 0.7)) drop-shadow(0 0 16px rgba(43, 194, 183, 0.45));
}
@media (prefers-reduced-motion: reduce) {
.mono {
transition: none;
}
}
.words {
display: flex;
@@ -126,6 +161,15 @@ const aria = (key: NavKey) => (key === current ? "page" : undefined);
letter-spacing: 0.14em;
font-size: 17px;
}
.store-name {
font-variant: small-caps;
letter-spacing: 0.06em;
font-size: 19px;
}
.store-tag {
font-size: 12px;
color: var(--gk-soft);
}
.tag {
display: flex;
align-items: center;
+6
View File
@@ -17,3 +17,9 @@ export const tail: { key: NavKey; label: string; href: string }[] = [
{ key: "blog", label: "Blog", href: "/blog" },
{ key: "store", label: "Store", href: "/store" },
];
/** The Store's own header: its pages in order, and which one is open. */
export interface StoreNav {
links: { label: string; href: string }[];
current?: string;
}
+5
View File
@@ -5,6 +5,11 @@
box-sizing: border-box;
}
/* A component's own display never un-hides what a script hid. */
[hidden] {
display: none !important;
}
html {
-webkit-text-size-adjust: 100%;
}