Skip to content

Commit d525491

Browse files
committed
v3 design refresh: paper palette, less power-user chrome
design bundle update from claude design tool: - new tokens.css: cool off-white paper (#f2f1ec), no beige. deeper burgundy red accent (#b21f1a). subtle SVG noise texture. gutter 40px. - new site.css: content sits on a paper card (.site-shell now has background + border-left/right). section markers are now a centered '· · ·' ornament + small italic '— a random musing —' kicker, not '// 01 · words' mono captions. headings are serif weight 600, not mono. tags are serif italic. .sp-tree replaced by nested ul. new .sp-photo-frame + .sp-photo-meta + .sp-photo-counter classes. - templates: - drop mono '~/musings · 67 posts' breadcrumbs everywhere. - musing/category/musings-index now share the .musings-layout with a tree sidebar (was: index didn't have the tree). class renamed musings-main -> musings-body. - serif italic crumbs with › separators (Musings › Travel › Japan). - written-out dates (May 11, 2026) instead of 2026·05·11. - drop file-path lines from random widgets — title is the title. - photo page switches table → <dl class="sp-photo-meta"> + <b>n</b> of total counter + keyboard hint. - build.mjs: fmtLongDate helper, exposed to templates. shuffled musings/ photos + recent + stats all carry dateLong now. - footer: drop "est <year>" line; version <li> gets class="version".
1 parent 6ce9249 commit d525491

13 files changed

Lines changed: 702 additions & 519 deletions

design/site.css

Lines changed: 237 additions & 202 deletions
Large diffs are not rendered by default.

design/tokens.css

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
11
/* georgemain.com — design tokens
2-
Flat background, narrow column, retro personal-site feel.
3-
:root holds dark defaults; .light overrides them.
2+
Aged-paper palette, IBM Plex pair, dark + light only.
43
*/
54

65
:root {
7-
/* ── palette: dark (the base; .light overrides) ─────────── */
8-
--bg: #16130f;
9-
--bg-2: #1d1915;
10-
--bg-3: #26211b;
11-
--ink: #ece6d8;
12-
--ink-2: #a8a097;
13-
--ink-3: #6d665d;
14-
--rule: #2d2822;
15-
--rule-2: #3d362d;
16-
/* signature accentred, only red. */
17-
--accent: #e84a3f;
6+
/* ── palette: dark (warm walnut) ────────────────────────── */
7+
--bg: #1b1611;
8+
--bg-2: #211c16;
9+
--bg-3: #2a241c;
10+
--ink: #ebe2cd;
11+
--ink-2: #a8a08e;
12+
--ink-3: #756d5d;
13+
--rule: #322c23;
14+
--rule-2: #423a2d;
15+
/* deeper redless candy-bright than before */
16+
--accent: #e75c4f;
1817
--accent-2: #ff8a7a;
1918
--accent-3: #4a1812;
20-
--selection: rgba(232,74,63,.32);
19+
--selection: rgba(231,92,79,.30);
2120

22-
/* no grid texture — flat background. */
23-
--grid-color: transparent;
24-
--grid-size: 32px;
21+
/* very faint paper noise (SVG turbulence). Used for both modes;
22+
looks like grain on dark, like aged paper on light. */
23+
--paper-noise: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.045 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
2524

26-
/* ── type ────────────────────────────────────────────────── */
25+
/* ── type ──────────────────────────────────────────────── */
2726
--font-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
2827
--font-body: "IBM Plex Serif", Georgia, "Iowan Old Style", serif;
2928

@@ -40,7 +39,7 @@
4039
--lh-snug: 1.4;
4140
--lh-body: 1.65;
4241

43-
/* ── spacing ─────────────────────────────────────────────── */
42+
/* ── spacing ───────────────────────────────────────────── */
4443
--sp-1: 4px;
4544
--sp-2: 8px;
4645
--sp-3: 12px;
@@ -51,30 +50,29 @@
5150
--sp-8: 64px;
5251
--sp-9: 96px;
5352

54-
/* ── layout — narrow on purpose ─────────────────────────── */
53+
/* ── layout ────────────────────────────────────────────── */
5554
--reading-width: 680px;
5655
--content-width: 760px;
5756
--max-width: 820px;
58-
--page-gutter: 32px;
57+
--page-gutter: 40px;
5958

60-
/* ── motion ──────────────────────────────────────────────── */
59+
/* ── motion ────────────────────────────────────────────── */
6160
--ease: cubic-bezier(.2,.7,.3,1);
6261
}
6362

64-
/* ── light mode (palette swap only — no layout changes) ──── */
63+
/* ── light mode: cool off-white paper, no beige ────────── */
6564
.site-page.light,
6665
.light .site-page {
67-
--bg: #f5f0e6;
68-
--bg-2: #ede7d7;
69-
--bg-3: #e0d9c6;
70-
--ink: #1c1814;
71-
--ink-2: #564f47;
72-
--ink-3: #908779;
73-
--rule: #d6cdb9;
74-
--rule-2: #b8ad97;
75-
--accent: #b8281a;
76-
--accent-2: #8c1d10;
77-
--accent-3: #f0d0c8;
78-
--selection: rgba(184,40,26,.20);
79-
--grid-color: transparent;
66+
--bg: #f2f1ec;
67+
--bg-2: #fafaf6;
68+
--bg-3: #e4e3dc;
69+
--ink: #15171b;
70+
--ink-2: #4a4d53;
71+
--ink-3: #828690;
72+
--rule: #c9c8c0;
73+
--rule-2: #a3a39a;
74+
--accent: #b21f1a;
75+
--accent-2: #8a1813;
76+
--accent-3: #f0d2cf;
77+
--selection: rgba(178,31,26,.20);
8078
}

scripts/_bulk-upload.mjs

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// One-off: upload all 5 outback/mona galleries with proper slugs + alt text.
2+
import { promises as fs } from 'node:fs';
3+
import { execFileSync } from 'node:child_process';
4+
import path from 'node:path';
5+
import os from 'node:os';
6+
import { fileURLToPath } from 'node:url';
7+
import sharp from 'sharp';
8+
import yaml from 'js-yaml';
9+
import exifr from 'exifr';
10+
11+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
12+
const BUCKET = 'georgemain-com-media';
13+
const BASE = `https://${BUCKET}.s3.amazonaws.com`;
14+
15+
const formatShutter = (t) => t == null ? null : t >= 1 ? t + 's' : '1/' + Math.round(1/t);
16+
17+
function s3up(local, key) {
18+
execFileSync('aws', ['s3', 'cp', local, `s3://${BUCKET}/${key}`,
19+
'--content-type', 'image/jpeg',
20+
'--cache-control', 'public, max-age=31536000, immutable',
21+
'--no-progress'], { stdio: ['ignore', 'ignore', 'inherit'] });
22+
return `${BASE}/${key}`;
23+
}
24+
25+
const GALLERIES = [
26+
{
27+
folder: 'uluru_outback', path: 'australia/outback/uluru',
28+
title: 'Uluru', subtitle: 'Ayers Rock at every hour of the day.',
29+
location: 'Uluru-Kata Tjuta National Park, NT, Australia',
30+
desc: 'Photos of Uluru (Ayers Rock) from midday through sunset.',
31+
keywords: ['Uluru', 'Ayers Rock', 'Outback', 'Northern Territory'],
32+
photos: [
33+
['DSC03149.JPG', 'uluru-wide-daytime', 'Uluru from a distance, midday.'],
34+
['DSC03282.JPG', 'uluru-sunset', 'Uluru glowing red at sunset.'],
35+
['DSC03295.JPG', 'uluru-cliff-detail', "Cliff face of Uluru at sunset, close-up."],
36+
['DSC03308.JPG', 'uluru-profile', 'Side profile of Uluru in evening light.'],
37+
['DSC03313.JPG', 'outback-sky-clouds', 'Dramatic clouds with sun breaking through.'],
38+
['DSC03326.JPG', 'adventure-tours-bus', 'The Adventure Tours bus.'],
39+
['DSC03330.JPG', 'uluru-behind-grass', 'Uluru visible above tall grass and a fence.'],
40+
['DSC03337.JPG', 'red-dirt-tire-tracks', 'Tire tracks in red outback dirt.'],
41+
['DSC03342.JPG', 'uluru-from-viewpoint', 'Uluru framed by grass at a viewpoint.'],
42+
['DSC03346.JPG', 'uluru-dusk', 'Uluru at dusk from the viewing area.'],
43+
['DSC03347.JPG', 'uluru-cliff-face-close', "Uluru's cliff face at sunset, close."],
44+
['DSC03370.JPG', 'uluru-distant-dusk', 'Uluru in the distance at dusk.'],
45+
['DSC03383.JPG', 'outback-sunset', 'Sunset over the outback.'],
46+
],
47+
},
48+
{
49+
folder: 'olgas_outback', path: 'australia/outback/kata-tjuta',
50+
title: 'Kata Tjuta (The Olgas)', subtitle: 'The Valley of the Winds.',
51+
location: 'Uluru-Kata Tjuta National Park, NT, Australia',
52+
desc: 'Photos of Kata Tjuta (The Olgas) — the domed rock formations west of Uluru.',
53+
keywords: ['Kata Tjuta', 'The Olgas', 'Valley of the Winds', 'Outback'],
54+
photos: [
55+
['DSC03146.JPG', 'kata-tjuta-from-lookout', 'The domes of Kata Tjuta from a distant lookout.'],
56+
['DSC03177.JPG', 'valley-of-the-winds-entry','Hikers walking between two domes at the entry.'],
57+
['DSC03183.JPG', 'valley-of-the-winds-trail','Hikers heading up the Valley of the Winds trail.'],
58+
['DSC03184.JPG', 'pitted-rock-wall', 'Pock-marked surface of a Kata Tjuta dome.'],
59+
['DSC03187.JPG', 'gum-tree-against-rock', 'A white-trunked gum tree against the red rock.'],
60+
['DSC03190.JPG', 'conglomerate-texture', 'Close-up of the red conglomerate rock texture.'],
61+
['DSC03194.JPG', 'talus-slope', 'Loose talus rock on a slope.'],
62+
['DSC03195.JPG', 'red-scree', 'Red scree slope below a dome.'],
63+
['DSC03196.JPG', 'split-boulder', 'A large boulder split open.'],
64+
['DSC03197.JPG', 'trail-into-valley', 'Stairs and trail leading into the Valley of the Winds.'],
65+
['DSC03198.JPG', 'hikers-on-trail', 'Group of hikers on the rocky trail.'],
66+
['DSC03201.JPG', 'boulders-with-grass', 'Cluster of boulders with native grass.'],
67+
['DSC03216.JPG', 'gap-between-domes', 'Narrow gap between two Kata Tjuta domes.'],
68+
['DSC03230.JPG', 'valley-floor-view', 'View from the Valley of the Winds toward the plain.'],
69+
['DSC03242.JPG', 'stone-path', 'Stone path winding between the domes.'],
70+
['DSC03246.JPG', 'tour-bus-at-kata-tjuta', 'Tour bus with Kata Tjuta in the background.'],
71+
['DSC03251.JPG', 'kata-tjuta-evening', 'The full Kata Tjuta formation at evening.'],
72+
['DSC03260.JPG', 'twin-domes', 'Two adjacent domes, close view.'],
73+
],
74+
},
75+
{
76+
folder: 'kings_canyon_outback', path: 'australia/outback/kings-canyon',
77+
title: 'Kings Canyon', subtitle: 'Watarrka National Park.',
78+
location: 'Watarrka National Park, NT, Australia',
79+
desc: 'Photos from the Kings Canyon rim walk in Watarrka National Park.',
80+
keywords: ['Kings Canyon', 'Watarrka', 'Outback', 'Northern Territory'],
81+
photos: [
82+
['DSC03426.JPG', 'balanced-boulder', 'A large boulder balanced on the trailhead at dawn.'],
83+
['DSC03454.JPG', 'striated-canyon-wall', 'Detail of the layered canyon walls.'],
84+
['DSC03460.JPG', 'first-dome', 'Misty first beehive dome on the rim walk.'],
85+
['DSC03463.JPG', 'overhang', 'Looking up at a cliff overhang.'],
86+
['DSC03467.JPG', 'misty-plateau', 'Eroded plateau under low mist.'],
87+
['DSC03472.JPG', 'plateau-gum-tree', 'A gum tree growing on the canyon plateau.'],
88+
['DSC03477.JPG', 'eroded-rim', 'Eroded canyon rim in the rain.'],
89+
['DSC03483.JPG', 'mushroom-rock', 'Mushroom-shaped eroded rock formation.'],
90+
['DSC03488.JPG', 'lost-city-domes', "The 'Lost City' of beehive domes."],
91+
['DSC03491.JPG', 'person-on-rim', 'A figure standing on the canyon rim.'],
92+
['DSC03492.JPG', 'cliff-detail', 'Close-up of the canyon cliff.'],
93+
['DSC03496.JPG', 'shelby-on-rim', 'Shelby on the rim of Kings Canyon.'],
94+
['DSC03507.JPG', 'garden-of-eden-hikers', 'Hikers near the Garden of Eden.'],
95+
['DSC03526.JPG', 'garden-of-eden-bridge', 'Wooden bridge in the Garden of Eden.'],
96+
['DSC03538.JPG', 'garden-of-eden-pool', 'Pool in the Garden of Eden.'],
97+
['DSC03556.JPG', 'lost-city-overview', 'Wide view over the Lost City domes.'],
98+
['DSC03563.JPG', 'trail-back-down', 'Trail leading back down off the rim.'],
99+
],
100+
},
101+
{
102+
folder: 'misc_outback', path: 'australia/outback/misc',
103+
title: 'Outback (misc)', subtitle: 'Red dirt, long roads, and roadside stops.',
104+
location: 'Red Centre, Australia',
105+
desc: 'Loose photos from the Australian Outback — landscapes, roads, and stops between the headline parks.',
106+
keywords: ['Outback', 'Red Centre', 'Australia road trip', 'Northern Territory'],
107+
photos: [
108+
['DSC03096.JPG', 'roadside-from-bus', 'Outback roadside seen through a bus window at dawn.'],
109+
['DSC03109.JPG', 'emu-at-roadhouse', 'An emu in a pen at a roadhouse.'],
110+
['DSC03111.JPG', 'kulgera-dawn', 'Roadside sign in early dawn light.'],
111+
['DSC03115.JPG', 'outback-petrol-station', 'An empty outback petrol station at dawn.'],
112+
['DSC03116.JPG', 'northern-territory-sign', 'Welcome to the Northern Territory sign.'],
113+
['DSC03129.JPG', 'bus-fueling', 'Adventure Tours bus at a fuel stop.'],
114+
['DSC03130.JPG', 'erldunda-sign', "Erldunda 'centre of the centre' roadhouse sign."],
115+
['DSC03131.JPG', 'outback-junction', 'Quiet outback road junction.'],
116+
['DSC03133.JPG', 'uluru-turnoff', 'The turnoff sign for Uluru.'],
117+
['DSC03134.JPG', 'erldunda-motel-sign', 'Roadside signs for the Erldunda Motel.'],
118+
['DSC03139.JPG', 'memorial-bell', 'Cast-iron memorial bell at a rest stop.'],
119+
['DSC03140.JPG', 'cockatiel', 'A cockatiel perched in a wire cage.'],
120+
['DSC03142.JPG', 'red-dirt-campground', 'Red dirt campground with native trees.'],
121+
['DSC03143.JPG', 'desert-camp', 'Desert camp buildings from above.'],
122+
['DSC03144.JPG', 'road-from-hilltop', 'Outback road seen from a hilltop.'],
123+
['DSC03154.JPG', 'desert-oaks', 'Desert oaks dotting the landscape.'],
124+
['DSC03156.JPG', 'glamping-tents-overview', 'Glamping tents at a desert camp.'],
125+
['DSC03157.JPG', 'camp-from-above', 'Camp from above with tents and trees.'],
126+
['DSC03160.JPG', 'glamping-tents', 'Two glamping tents in the red dirt.'],
127+
],
128+
},
129+
{
130+
folder: 'mona', path: 'australia/tasmania/mona',
131+
title: 'MONA', subtitle: 'Museum of Old and New Art.',
132+
location: 'Berriedale, Hobart, Tasmania, Australia',
133+
desc: 'Photos from MONA — the Museum of Old and New Art in Hobart, Tasmania.',
134+
keywords: ['MONA', 'Museum of Old and New Art', 'Hobart', 'Tasmania', 'contemporary art'],
135+
photos: [
136+
['DSC03605.JPG', 'cement-truck', "Wim Delvoye's intricately patterned cement truck sculpture."],
137+
['DSC03612.JPG', 'the-butterfly-text', "Illuminated 'The Butterfly' text piece in a dark room."],
138+
['DSC03621.JPG', 'cloaca-vessels', 'Glass vessels of the Cloaca digestion machine.'],
139+
['DSC03623.JPG', 'cloaca-mechanism-detail', 'Inner mechanics of Cloaca — tubes and glass.'],
140+
['DSC03633.JPG', 'god-is-your-enemy', "'God Is Your Enemy' text artwork."],
141+
['DSC03634.JPG', 'noodles-on-pedestal', 'Instant noodles arranged as art on a pedestal.'],
142+
['DSC03636.JPG', 'drinking-piss-text', 'A red text piece by an artist commenting on artists.'],
143+
['DSC03637.JPG', 'so-much-for-optimism', "'So much for my fucking optimism' text piece."],
144+
['DSC03639.JPG', 'fat-car', "Erwin Wurm's bulging red 'Fat Car' sculpture."],
145+
['DSC03648.JPG', 'bit-fall', 'Looking up at the bit.fall water-text installation.'],
146+
['DSC03649.JPG', 'cloaca-glass-dish', 'Glass dish suspended inside the Cloaca mechanism.'],
147+
['DSC03651.JPG', 'cloaca-tubes', 'Backside of Cloaca tubes and pumps.'],
148+
['DSC03656.JPG', 'cloaca-output', 'Output of Cloaca on a glass dish.'],
149+
['DSC03660.JPG', 'white-temple', 'Carved white timber temple installation.'],
150+
['DSC03667.JPG', 'photo-wall', 'Wall of framed photographs.'],
151+
['DSC03671.JPG', 'erosion-texture', 'Textural close-up resembling stalactites.'],
152+
['DSC03677.JPG', 'bonsai-vitrine', 'A bonsai tree in a glass vitrine.'],
153+
['DSC03681.JPG', 'siloam-tunnel', 'Visitors viewing a tapestry through a concrete tunnel.'],
154+
['DSC03684.JPG', 'hanging-films', 'Hanging strips of burnt film.'],
155+
['DSC03687.JPG', 'snake-installation', 'Inside the snake installation.'],
156+
['DSC03694.JPG', 'snake-overview', 'Wide view of the snake installation.'],
157+
['DSC03706.JPG', 'glass-ceiling-detail', 'Detail of a glass-and-light ceiling installation.'],
158+
['DSC03710.JPG', 'screens-corridor', 'Long corridor with rows of small video screens.'],
159+
],
160+
},
161+
];
162+
163+
for (const g of GALLERIES) {
164+
const stagingDir = path.join(ROOT, 'staging', g.folder);
165+
console.log(`\n=== ${g.title} → /media/${g.path} ===`);
166+
167+
const images = [];
168+
let galleryDate = null;
169+
for (let n = 0; n < g.photos.length; n++) {
170+
const [srcName, slug, alt] = g.photos[n];
171+
const srcAbs = path.join(stagingDir, srcName);
172+
try { await fs.access(srcAbs); }
173+
catch { console.log(` [${n+1}/${g.photos.length}] ${srcName} — missing, skip`); continue; }
174+
175+
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
176+
const full = path.join(os.tmpdir(), `bulk-${id}-full.jpg`);
177+
const med = path.join(os.tmpdir(), `bulk-${id}-med.jpg`);
178+
const thumb = path.join(os.tmpdir(), `bulk-${id}-thumb.jpg`);
179+
180+
await sharp(srcAbs).rotate().jpeg({ quality: 92, progressive: true, mozjpeg: true }).toFile(full);
181+
await sharp(srcAbs).rotate().resize({ width: 1400, withoutEnlargement: true }).jpeg({ quality: 82, progressive: true, mozjpeg: true }).toFile(med);
182+
await sharp(srcAbs).rotate().resize({ width: 600, withoutEnlargement: true }).jpeg({ quality: 78, progressive: true, mozjpeg: true }).toFile(thumb);
183+
184+
const bytes = (await fs.stat(full)).size;
185+
const fullMeta = await sharp(full).metadata();
186+
const ex = await exifr.parse(srcAbs, { exif: true, tiff: true });
187+
188+
const key = `${g.path}/${slug}`;
189+
process.stdout.write(` [${n+1}/${g.photos.length}] ${srcName}${slug} (${(bytes/1024/1024).toFixed(1)}MB)…`);
190+
const s3 = s3up(full, `${key}.jpg`);
191+
const s3_med = s3up(med, `${key}__med.jpg`);
192+
const s3_thumb = s3up(thumb, `${key}__thumb.jpg`);
193+
console.log(' ✓');
194+
195+
await fs.unlink(full); await fs.unlink(med); await fs.unlink(thumb);
196+
await fs.unlink(srcAbs);
197+
198+
const exif = {};
199+
if (ex?.Make || ex?.Model) exif.camera = [ex.Make, ex.Model].filter(Boolean).join(' ').trim();
200+
if (ex?.LensModel) exif.lens = ex.LensModel;
201+
if (ex?.ISO) exif.iso = ex.ISO;
202+
if (ex?.FNumber) exif.aperture = 'f/' + ex.FNumber;
203+
if (ex?.ExposureTime) exif.shutter = formatShutter(ex.ExposureTime);
204+
if (ex?.FocalLength) exif.focal_length = Math.round(ex.FocalLength) + 'mm';
205+
if (fullMeta?.width) exif.width = fullMeta.width;
206+
if (fullMeta?.height) exif.height = fullMeta.height;
207+
208+
const captureDate = ex?.DateTimeOriginal ? ex.DateTimeOriginal.toISOString().slice(0, 10) : null;
209+
if (!galleryDate && captureDate) galleryDate = captureDate;
210+
211+
images.push({
212+
file: `${slug}.jpg`,
213+
alt,
214+
...(captureDate ? { date: captureDate } : {}),
215+
s3, s3_med, s3_thumb, bytes,
216+
exif,
217+
});
218+
}
219+
220+
try { await fs.rmdir(stagingDir); } catch {}
221+
222+
const galleryDir = path.join(ROOT, 'content/media', g.path);
223+
await fs.mkdir(galleryDir, { recursive: true });
224+
const meta = {
225+
title: g.title, subtitle: g.subtitle, location: g.location,
226+
...(galleryDate ? { date: galleryDate } : {}),
227+
seo: { description: g.desc, keywords: g.keywords },
228+
images,
229+
};
230+
await fs.writeFile(path.join(galleryDir, 'metadata.yaml'), yaml.dump(meta, { lineWidth: 100, noRefs: true, quotingType: '"' }));
231+
console.log(` → wrote ${galleryDir}/metadata.yaml`);
232+
}
233+
234+
console.log('\nALL DONE.');

0 commit comments

Comments
 (0)