summary refs log tree commit diff
path: root/www
diff options
context:
space:
mode:
Diffstat (limited to 'www')
-rw-r--r--www/causal.agency/.gitignore3
-rw-r--r--www/causal.agency/Makefile11
-rw-r--r--www/causal.agency/alpha.html92
-rw-r--r--www/causal.agency/dais.html11
-rw-r--r--www/causal.agency/index.730
-rw-r--r--www/causal.agency/lands.html176
-rw-r--r--www/causal.agency/style.css5
-rw-r--r--www/git.causal.agency/.gitignore1
-rw-r--r--www/git.causal.agency/Makefile11
-rw-r--r--www/git.causal.agency/filter.c10
-rw-r--r--www/git.causal.agency/index.781
-rw-r--r--www/photo.causal.agency/.gitignore7
-rw-r--r--www/photo.causal.agency/c35/body1
-rw-r--r--www/photo.causal.agency/c35/lens1
-rw-r--r--www/photo.causal.agency/fx-3/body1
-rw-r--r--www/photo.causal.agency/fx-3/lens1
-rw-r--r--www/photo.causal.agency/gear.html68
-rw-r--r--www/photo.causal.agency/generate.sh287
-rw-r--r--www/photo.causal.agency/mastodon.sh54
-rw-r--r--www/photo.causal.agency/rsync.sh5
-rw-r--r--www/photo.causal.agency/trips.html373
-rw-r--r--www/temp.causal.agency/up.c2
-rw-r--r--www/text.causal.agency/037-care.762
-rw-r--r--www/text.causal.agency/039-apologies.781
-rw-r--r--www/text.causal.agency/040-sound-memory.7165
-rw-r--r--www/text.causal.agency/041-albums-2022.7185
-rw-r--r--www/text.causal.agency/042-comfort-music.762
-rw-r--r--www/text.causal.agency/043-little-blessings.778
-rw-r--r--www/text.causal.agency/044-film-review.7208
-rw-r--r--www/text.causal.agency/045-time-2025.7131
-rw-r--r--www/text.causal.agency/Makefile13
-rw-r--r--www/text.causal.agency/feed.sh1
-rw-r--r--www/they.causal.agency/Makefile4
-rw-r--r--www/they.causal.agency/post-update.sh41
34 files changed, 2215 insertions, 47 deletions
diff --git a/www/causal.agency/.gitignore b/www/causal.agency/.gitignore
index 7935a3c1..b00b1c3c 100644
--- a/www/causal.agency/.gitignore
+++ b/www/causal.agency/.gitignore
@@ -1,3 +1,4 @@
-*.html
+index.html
+leveler.html
 scheme.css
 scheme.png
diff --git a/www/causal.agency/Makefile b/www/causal.agency/Makefile
index 75849db0..d3af7265 100644
--- a/www/causal.agency/Makefile
+++ b/www/causal.agency/Makefile
@@ -1,11 +1,14 @@
 WEBROOT = /var/www/causal.agency
 
-FILES = index.html style.css scheme.css scheme.png
+GEN = index.html scheme.css scheme.png
+FILES = ${GEN} style.css alpha.html dais.html lands.html
 
 all: ${FILES}
 
-index.html: index.7
-	mandoc -T html -O style=style.css index.7 > index.html
+.SUFFIXES: .7 .html
+
+.7.html:
+	mandoc -T html -O style=style.css $< > $@
 
 scheme.css:
 	scheme -st > scheme.css
@@ -17,4 +20,4 @@ install: ${FILES}
 	install -C -m 644 ${FILES} ${WEBROOT}
 
 clean:
-	rm -f index.html scheme.css scheme.png
+	rm -f ${GEN}
diff --git a/www/causal.agency/alpha.html b/www/causal.agency/alpha.html
new file mode 100644
index 00000000..0d83f530
--- /dev/null
+++ b/www/causal.agency/alpha.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>all 26 letters of the alphabet RANKED</title>
+<style>
+body, button { font-size: 200%; text-align: center; }
+button { margin: 1em; padding: 1ch; }
+button#shuffle { font-size: 100%; }
+</style>
+
+which letter do you like more?
+<p>
+<button id="a">A</button>
+<button id="b">B</button>
+<p>
+<details>
+<summary>current ranking</summary>
+<p>
+<span id="ranking">ABCDEFGHIJKLMNOPQRSTUVWXYZ</span>
+<p>
+<button id="shuffle">reshuffle</button>
+</details>
+
+<script>
+let buttonA = document.getElementById("a");
+let buttonB = document.getElementById("b");
+let ranking = document.getElementById("ranking");
+
+let alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
+let rand = (bound) => Math.floor(Math.random() * bound);
+function shuffle() {
+	for (let i = alpha.length - 1; i > 0; --i) {
+		let j = rand(i + 1);
+		let x = alpha[i];
+		alpha[i] = alpha[j];
+		alpha[j] = x;
+	}
+}
+if (localStorage.getItem("alpha")) {
+	alpha = localStorage.getItem("alpha").split("");
+} else {
+	shuffle();
+}
+
+let index = 0;
+let even = true;
+function choose(o) {
+	if (o == "b") {
+		let x = alpha[index];
+		alpha[index] = alpha[index + 1];
+		alpha[index + 1] = x;
+	}
+	index += 2;
+	if (index > alpha.length - 2) {
+		even = !even;
+		index = (even ? 0 : 1);
+	}
+	update();
+}
+
+document.onkeydown = function(event) {
+	if (event.key.toUpperCase() == alpha[index]) {
+		choose("a");
+	} else if (event.key.toUpperCase() == alpha[index + 1]) {
+		choose("b");
+	}
+}
+
+function update() {
+	localStorage.setItem("alpha", alpha.join(""));
+	ranking.innerText = alpha.join("");
+	let a = buttonA;
+	let b = buttonB;
+	if (rand(2)) {
+		a = buttonB;
+		b = buttonA;
+	}
+	let lc = (c) => c;
+	if (rand(2)) lc = (c) => c.toLowerCase();
+	a.innerText = lc(alpha[index]);
+	b.innerText = lc(alpha[index + 1]);
+	a.onclick = () => choose("a");
+	b.onclick = () => choose("b");
+}
+update();
+
+document.getElementById("shuffle").onclick = function() {
+	if (confirm("Are you SURE you want to throw away all your hard work?")) {
+		shuffle();
+		update();
+	}
+}
+</script>
diff --git a/www/causal.agency/dais.html b/www/causal.agency/dais.html
new file mode 100644
index 00000000..109654d3
--- /dev/null
+++ b/www/causal.agency/dais.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<title>Dais</title>
+<meta charset="utf-8">
+<style>
+body { line-height: 1.5em; max-width: 78ch; margin: auto; padding: 0.5em 1ch; }
+</style>
+
+<h1>First occurrences of the word “dais”</h1>
+<ol>
+<li>A. K. Larkwood, The Unspoken Name: p. 19
+</ol>
diff --git a/www/causal.agency/index.7 b/www/causal.agency/index.7
index c270f477..bc212a5e 100644
--- a/www/causal.agency/index.7
+++ b/www/causal.agency/index.7
@@ -1,10 +1,10 @@
-.Dd November  3, 2021
+.Dd August 27, 2025
 .Dt CAUSAL.AGENCY 7
 .Os "Causal Agency"
 .
 .Sh NAME
 .Nm june
-.Nd computer enthusiast (her)
+.Nd enthusiast (she/they)
 .
 .Sh SYNOPSIS
 .Nm mail
@@ -15,24 +15,21 @@ in
 on tilde.chat
 .
 .Sh DESCRIPTION
-I make mostly IRC software in C.
-I like
-.Ox
-but also the GPL.
-I just want to read books
-and try to learn to be kinder.
-When I can I'd like to talk to strangers
-and experience more magic.
+primarily a photographer these days.
+I used to write IRC software in C.
+I still use it every day.
 .
 .Pp
-.Lk https://git.causal.agency code
+.Lk https://photo.causal.agency photos
 \(em
 .Lk https://text.causal.agency words
 \(em
+.Lk https://git.causal.agency code
+\(em
 .Lk /list/ mailist
 .
 .Pp
-These are some things I've done:
+these are some computer things I've done:
 .Bl -tag -width Ds
 .It Lk https://git.causal.agency/pounce/about pounce
 a multi-client-first IRC bouncer
@@ -42,7 +39,7 @@ a cosy IRC client
 a full-text search IRC logger
 .It Lk https://git.causal.agency/scooper/about scooper
 a web interface for litterbox
-.It Lk https://git.causal.agency/catsit/about catsit
+.It Lk https://git.causal.agency/kitd/about kitd
 a process supervisor
 .It Lk https://git.causal.agency/imbox/about "imbox & git-fetch-email"
 a tool to pull patches out of IMAP
@@ -64,4 +61,11 @@ an earthy terminal colour scheme
 .El
 .
 .Sh SEE ALSO
+.Bl -bullet
+.It
 .Lk /bin/ bin
+.It
+.Lk lands.html "Magic lands quiz"
+.It
+.Lk alpha.html "alphabet ranking game"
+.El
diff --git a/www/causal.agency/lands.html b/www/causal.agency/lands.html
new file mode 100644
index 00000000..7aaadd80
--- /dev/null
+++ b/www/causal.agency/lands.html
@@ -0,0 +1,176 @@
+<!DOCTYPE html>
+<title>Lands Quiz</title>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<style>
+html { font: 14pt sans-serif; line-height: 1.5em; }
+body { padding: 1em 1ch; max-width: 78ch; margin: auto; }
+h1 { text-align: center; }
+h2 { margin-top: 0; }
+button { font-size: 100%; padding: 0.5em 1ch; }
+img { max-width: 100%; }
+div.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 2ch; }
+</style>
+
+<h1 id="loading">Loading...</h1>
+<h1 id="error" hidden>Failed to load cards :(</h1>
+
+<div id="game" hidden>
+<h1>Magic Lands Quiz</h1>
+<p>Try to guess the colours of mana each land produces!</p>
+<div class="cols">
+	<div>
+		<img id="back" src="https://backs.scryfall.io/normal/0/a/0aeebaf5-8c7d-4636-9e82-8c27447861f7.jpg">
+		<a id="link" target="_blank">
+			<img id="image1" hidden>
+			<img id="image2" hidden>
+		</a>
+	</div>
+	<div>
+		<h2 id="name"></h2>
+		<input type="checkbox" id="w"> <label for="w">White</label><br>
+		<input type="checkbox" id="u"> <label for="u">Blue</label><br>
+		<input type="checkbox" id="b"> <label for="b">Black</label><br>
+		<input type="checkbox" id="r"> <label for="r">Red</label><br>
+		<input type="checkbox" id="g"> <label for="g">Green</label><br>
+		<p><button id="submit">Submit</button></p>
+		<h3>Score: <span id="score">0</span>/<span id="total">0</span></h3>
+	</div>
+</div>
+</div>
+
+<script>
+function shuffle(arr) {
+	let rand = (bound) => Math.floor(Math.random() * bound);
+	for (let i = arr.length-1; i > 0; --i) {
+		let j = rand(i+1);
+		let x = arr[i];
+		arr[i] = arr[j];
+		arr[j] = x;
+	}
+}
+
+const CardBack =
+"https://backs.scryfall.io/normal/0/a/0aeebaf5-8c7d-4636-9e82-8c27447861f7.jpg";
+
+function hideCard() {
+	document.getElementById("back").hidden = false;
+	document.getElementById("image1").hidden = true;
+	document.getElementById("image2").hidden = true;
+}
+
+function showCard(card) {
+	document.getElementById("back").hidden = true;
+	document.getElementById("link").href = card.scryfall_uri;
+	let image1 = document.getElementById("image1");
+	let image2 = document.getElementById("image2");
+	if (card.card_faces) {
+		image1.src = card.card_faces[0].image_uris.normal;
+		image2.src = card.card_faces[1].image_uris.normal;
+		image1.hidden = false;
+		image2.hidden = false;
+	} else {
+		image1.src = card.image_uris.normal;
+		image1.hidden = false;
+	}
+}
+
+function resetChecks() {
+	for (let c of "wubrg") {
+		let input = document.getElementById(c);
+		input.checked = false;
+		input.disabled = false;
+		input.labels[0].style.fontWeight = "normal";
+	}
+}
+
+function checkChecks(card) {
+	let score = 0;
+	let total = 0;
+	let checked = 0;
+	for (let c of "wubrg") {
+		let input = document.getElementById(c);
+		let produced = card.produced_mana.includes(c.toUpperCase());
+		if (produced) {
+			total++;
+			input.labels[0].style.fontWeight = "bold";
+			if (input.checked) score++;
+		}
+		if (input.checked) checked++;
+		input.disabled = true;
+	}
+	if (checked > total) score -= (checked - total);
+	if (score < 0) score = 0;
+	return { score: score, total: total };
+}
+
+document.onkeydown = function(event) {
+	for (let c of "wubrg") {
+		if (event.key == c) {
+			let input = document.getElementById(c);
+			if (!input.disabled) input.checked ^= true;
+		}
+	}
+	if (event.key == "Enter") {
+		document.getElementById("submit").click();
+	}
+}
+
+let score = 0;
+let total = 0;
+let cards = [];
+let card = null;
+
+function nextCard() {
+	hideCard();
+	resetChecks();
+	card = cards.shift();
+	document.getElementById("name").innerText = card.name;
+}
+
+document.getElementById("submit").onclick = function() {
+	if (card) {
+		let { score: cardScore, total: cardTotal } = checkChecks(card);
+		total += cardTotal;
+		score += cardScore;
+		document.getElementById("score").innerText = score;
+		document.getElementById("total").innerText = total;
+		showCard(card);
+		card = null;
+		if (cards.length) {
+			this.innerText = "Next card";
+		} else {
+			this.disabled = true;
+			this.innerText = "No more cards";
+		}
+	} else {
+		nextCard();
+		this.innerText = "Submit";
+	}
+}
+
+function loadCards(resp) {
+	let loading = document.getElementById("loading");
+	let error = document.getElementById("error");
+	let game = document.getElementById("game");
+	if (resp.status != 200) {
+		loading.hidden = true;
+		error.hidden = false;
+	}
+	resp.json().then((json) => {
+		cards.push(...json.data);
+		if (json.has_more) {
+			setTimeout(() => fetch(json.next_page).then(loadCards), 50);
+		} else {
+			loading.hidden = true;
+			game.hidden = false;
+			shuffle(cards);
+			nextCard();
+		}
+	});
+}
+
+const Search =
+"https://api.scryfall.com/cards/search?q=t:land+id>=2+produces>=2+produces!=wubrg";
+fetch(Search).then(loadCards);
+
+</script>
diff --git a/www/causal.agency/style.css b/www/causal.agency/style.css
index 368d8da1..265c62c2 100644
--- a/www/causal.agency/style.css
+++ b/www/causal.agency/style.css
@@ -11,6 +11,11 @@ dl.Bl-diag > dt { font-weight: bold; }
 code.Nm, code.Fl, code.Cm, code.Ic, code.In, code.Fd, code.Fn,
 code.Cd { font-weight: bold; font-family: inherit; }
 
+div.head, div.foot { display: flex; justify-content: space-between; }
+.head-ltitle, .foot-date { flex: 1; }
+.head-vol { flex: 0 1 auto; text-align: center; }
+.head-rtitle, .foot-os { flex: 1; text-align: right; }
+
 html { font-family: monospace; line-height: 1.25em; }
 body { max-width: 80ch; margin: 1em auto; padding: 0 1ch; }
 table { border-collapse: collapse; }
diff --git a/www/git.causal.agency/.gitignore b/www/git.causal.agency/.gitignore
index 25e26cc8..eaed8039 100644
--- a/www/git.causal.agency/.gitignore
+++ b/www/git.causal.agency/.gitignore
@@ -1,3 +1,4 @@
+*.html
 about-filter
 compress
 ctags
diff --git a/www/git.causal.agency/Makefile b/www/git.causal.agency/Makefile
index f05d4a4a..86b9f3eb 100644
--- a/www/git.causal.agency/Makefile
+++ b/www/git.causal.agency/Makefile
@@ -2,6 +2,7 @@ PREFIX = /var/www
 CONFDIR = ${PREFIX}/conf
 DATADIR = ${PREFIX}/cgit
 BINDIR = ${PREFIX}/bin
+WEBROOT = ${PREIFX}/git.causal.agency
 
 CFLAGS += -Wall -Wextra
 LDFLAGS = -static -pie
@@ -17,7 +18,9 @@ BINS += mtags
 BINS += owner-filter
 BINS += source-filter
 
-all: ${BINS}
+HTMLS = index.html
+
+all: ${BINS} ${HTMLS}
 
 compress ctags mandoc:
 	${MAKE} -C /usr/src/usr.bin/$@ LDFLAGS='${LDFLAGS}'
@@ -35,12 +38,16 @@ hilex htagml mtags:
 about-filter email-filter owner-filter source-filter: filter
 	ln -f filter $@
 
+index.html: index.7
+	mandoc -Thtml -Ostyle=https://causal.agency/style.css index.7 >index.html
+
 install: cgitrc custom.css ${BINS}
 	install -m 644 cgitrc ${CONFDIR}
 	install -m 644 custom.css ${DATADIR}
 	install -d -o www -g daemon ${PREFIX}/cache/cgit
 	install -d -m 1700 -o www -g daemon ${PREFIX}/tmp
 	install -s ${BINS} ${BINDIR}
+	install -m 644 ${HTMLS} ${WEBROOT}
 
 clean:
-	rm -f compress filter ${BINS}
+	rm -f compress filter ${BINS} ${HTMLS}
diff --git a/www/git.causal.agency/filter.c b/www/git.causal.agency/filter.c
index 9ed9ee17..7c7e9320 100644
--- a/www/git.causal.agency/filter.c
+++ b/www/git.causal.agency/filter.c
@@ -32,12 +32,8 @@ static int email(void) {
 	size_t cap = 0;
 	char *buf = NULL;
 	if (getline(&buf, &cap, stdin) < 0) err(1, "getline");
-	long x = 1;
-	for (char *ch = buf; *ch && *ch != ' '; ++ch) {
-		x *= *ch;
-	}
-	if (buf[0] == 'C' && x == 1251729952200L) {
-		printf("C.%s", buf + strcspn(buf, " "));
+	if (buf[0] == 'C' && !strncmp(&buf[strcspn(buf, " ")], " McEnroe", 8)) {
+		printf("June%s", &buf[strcspn(buf, " ")]);
 	} else {
 		printf("%s", buf);
 	}
@@ -143,6 +139,7 @@ static int source(int argc, char *argv[]) {
 }
 
 int main(int argc, char *argv[]) {
+#ifdef __OpenBSD__
 	int error;
 	switch (getprogname()[0]) {
 		break; case 'a': error = pledge("stdio exec", NULL);
@@ -150,6 +147,7 @@ int main(int argc, char *argv[]) {
 		break; default:  error = pledge("stdio", NULL);
 	}
 	if (error) err(1, "pledge");
+#endif
 	switch (getprogname()[0]) {
 		case 'a': return about(argc, argv);
 		case 'e': return email();
diff --git a/www/git.causal.agency/index.7 b/www/git.causal.agency/index.7
new file mode 100644
index 00000000..58a40dfe
--- /dev/null
+++ b/www/git.causal.agency/index.7
@@ -0,0 +1,81 @@
+.Dd January 12, 2024
+.Dt GIT.CAUSAL.AGENCY 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm causal agency
+.Nd \(dqI think some people from the Gentoo project are behind this.\(dq
+.
+.Sh DESCRIPTION
+basically cgit (awful software)
+getting hammered by web crawlers
+keeps making my machine crash.
+this static page will be here
+until I can find a better solution.
+clone urls and tarball urls are still functional.
+.
+.Bl -tag
+.It src \(em dontfiles
+.Dl git clone https://git.causal.agency/src
+.It ascii.town
+.Bl -tag
+.It torus \(em collaborative ASCII art
+.Dl git clone https://git.causal.agency/torus
+.It play \(em some games for SSH
+.Dl git clone https://git.causal.agency/play
+.El
+.It email
+.Bl -tag
+.It imbox \(em IMAP to mbox
+.Dl git clone https://git.causal.agency/imbox
+.It bubger \(em IMAP archive generator
+.Dl git clone https://git.causal.agency/bubger
+.It notemap \(em notemap
+.Dl git clone https://git.causal.agency/notemap
+.El
+.It forks
+.Bl -tag
+.It shulker \(em Discord to vanilla Minecraft bridge
+.Dl git clone https://git.causal.agency/shulker
+.It cgit-pink \(em web frontend for git
+.Dl git clone https://git.causal.agency/cgit-pink
+.It dash \(em patched shell with cmake build
+.Dl git clone https://git.causal.agency/dash
+.El
+.It games
+.Bl -tag
+.It wep \(em Windows Entertainment Pack recreations
+.Dl git clone https://git.causal.agency/wep
+.It cards \(em CARDS.DLL loader for SDL
+.Dl git clone https://git.causal.agency/cards
+.El
+.It irc
+.Bl -tag
+.It scooper \(em web interface for litterbox
+.Dl git clone https://git.causal.agency/scooper
+.It litterbox \(em IRC logger
+.Dl git clone https://git.causal.agency/litterbox
+.It pounce \(em IRC bouncer
+.Dl git clone https://git.causal.agency/pounce
+.It catgirl \(em IRC client
+.Dl git clone https://git.causal.agency/catgirl
+.El
+.It ports
+.Bl -tag
+.It jorts \(em my own ports tree for macOS
+.Dl git clone https://git.causal.agency/jorts
+.It exman \(em manuals for other systems
+.Dl git clone https://git.causal.agency/exman
+.It libretls \(em libtls for OpenSSL
+.Dl git clone https://git.causal.agency/libretls
+.It ports \(em Fx and Ox ports for this software
+.Dl git clone https://git.causal.agency/ports
+.El
+.It system
+.Bl -tag
+.It kitd \(em process supervisor for OpenBSD
+.Dl git clone https://git.causal.agency/kitd
+.It catsit \(em (deprecated) process supervisor
+.Dl git clone https://git.causal.agency/catsit
+.El
+.El
diff --git a/www/photo.causal.agency/.gitignore b/www/photo.causal.agency/.gitignore
new file mode 100644
index 00000000..22c11f6e
--- /dev/null
+++ b/www/photo.causal.agency/.gitignore
@@ -0,0 +1,7 @@
+[0-9]*/
+*.jpg
+*.JPG
+app.json
+posted.txt
+static/
+token.json
diff --git a/www/photo.causal.agency/c35/body b/www/photo.causal.agency/c35/body
new file mode 100644
index 00000000..3676b877
--- /dev/null
+++ b/www/photo.causal.agency/c35/body
@@ -0,0 +1 @@
+Konica C35 Automatic
diff --git a/www/photo.causal.agency/c35/lens b/www/photo.causal.agency/c35/lens
new file mode 100644
index 00000000..3fef9a43
--- /dev/null
+++ b/www/photo.causal.agency/c35/lens
@@ -0,0 +1 @@
+Konica Hexanon 38mm f/2.8
diff --git a/www/photo.causal.agency/fx-3/body b/www/photo.causal.agency/fx-3/body
new file mode 100644
index 00000000..0962ee7d
--- /dev/null
+++ b/www/photo.causal.agency/fx-3/body
@@ -0,0 +1 @@
+Yashica FX-3
diff --git a/www/photo.causal.agency/fx-3/lens b/www/photo.causal.agency/fx-3/lens
new file mode 100644
index 00000000..eaab4375
--- /dev/null
+++ b/www/photo.causal.agency/fx-3/lens
@@ -0,0 +1 @@
+Carl Zeiss Planar T* 50mm f/1.7
diff --git a/www/photo.causal.agency/gear.html b/www/photo.causal.agency/gear.html
new file mode 100644
index 00000000..04cd3781
--- /dev/null
+++ b/www/photo.causal.agency/gear.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Photography Equipment</title>
+<style>
+html { color: #bbb; background-color: black; font-family: monospace; }
+body { max-width: 76ch; margin: auto; }
+</style>
+
+<h1>Photography Equipment</h1>
+<p>
+this is a (probably incomplete) list of equipment I use to Do Photography.
+
+<h2>Cameras</h2>
+<ul>
+<li>Yashica FX-3 (every day)
+<li>Konica C35 Automatic (Hexanon 38mm f/2.8)
+<li>Minolta XE-5
+<li>Praktica MTL3
+<li>Zenit-122
+<li>Yashica FX-2
+<li>Yashica-D (Yashikor 80mm f/3.5)
+</ul>
+
+<h2>Lenses</h2>
+<ul>
+<li>Carl Zeis Planar T* 50mm f/1.7 (FX-3 default)
+<li>Yashica DSB 50mm f/1.9 (FX-2 kit lens)
+<li>Helios-44M-5 58mm f/2 (Zenit-122 kit lens)
+<li>Pentacon 50mm f/1.8 (MTL3 kit lens)
+<li>Minolta MD Rokkor-X 50mm f/1.7 (XE-5 kit lens)
+<li>Osawa MC 70-210mm f/4-5 (C/Y)
+<li>Takumar SMC 35mm f/3.5
+<li>Super-Takumar 135mm f/3.5
+<li>Yashica ML 28-85mm f/3.5-4.5 (wonky focus at 28mm)
+<li>Yashica ML 42-75mm f/3.5-4.5
+<li>Yashica MC 35-70mm f/3.5-4.5 (bad aperture)
+<li>Yashica ML 50mm f/2 (sticky aperture)
+<li>Yashica DSB 28mm f/2.8
+<li>Yashica DSB 135mm f/2.8
+<li>Yashica ML Macro 55mm f/2.8
+<li>Yashica ML 28mm f/2.8
+<li>Yashica ML 75-150mm f/4
+</ul>
+
+<h2>Flash</h2>
+<ul>
+<li>Reflx Lab Simple Flash
+<li>Starblitz 318M
+</ul>
+
+<h2>Tripod</h2>
+<ul>
+<li>Sirui Traveler 5C
+</ul>
+
+<h2>Scanning</h2>
+<ul>
+<li>Filmomat SmartConvert
+<li>Fujifilm X-T5
+<li>Yashica ML Macro 55mm f/2.8 (at f/11)
+<li>Yashica 13mm extension tube (also have 20mm and 27mm)
+<li>Urth C/Y-X adapter
+<li>Skier CS-700 copy stand
+<li>Valoi 135 & 120 film holders
+<li>CineStill CS-Lite
+<li>the box the CS-Lite came in
+</ul>
diff --git a/www/photo.causal.agency/generate.sh b/www/photo.causal.agency/generate.sh
new file mode 100644
index 00000000..2fbdcb68
--- /dev/null
+++ b/www/photo.causal.agency/generate.sh
@@ -0,0 +1,287 @@
+#!/bin/sh
+set -eu
+
+mkdir -p static/preview static/thumbnail
+
+resize() {
+	local photo=$1 size=$2 output=$3
+	if ! test -f $output; then
+		# FIXME: convert complains about not understanding XML
+		echo $output >&2
+		convert $photo -auto-orient -thumbnail $size $output 2>/dev/null ||:
+	fi
+}
+
+preview() {
+	local photo=$1
+	local preview=preview/${photo##*/}
+	resize $photo 1500000@ static/$preview
+	echo $preview
+}
+
+thumbnail() {
+	local photo=$1
+	local thumbnail=thumbnail/${photo##*/}
+	resize $photo 60000@ static/$thumbnail
+	echo $thumbnail
+}
+
+encode() {
+	sed '
+		s/&/\&amp;/g
+		s/</\&lt;/g
+		s/"/\&quot;/g
+	' "$@"
+}
+
+page_title() {
+	case $1 in
+		(leader) echo 'Film Leader';;
+		(20*) date -j -f '%F' $1 '+%B %e, %Y';;
+		(0*) echo Roll $(dc -e "${1}p");;
+	esac
+}
+
+page_head() {
+	local page=$1
+	local title=$(page_title $page)
+	local date body lens film chem note
+
+	if test -f $page/date; then
+		date=$(sed 's/\([0-9]\)-\([0-9]\)/\1–\2/g' $page/date | encode)
+	fi
+	if test -f $page/body; then
+		body=$(encode $page/body)
+	fi
+	if test -f $page/lens; then
+		lens=$(
+			sed '
+				s,f/,ƒ/,g
+				s/\([0-9]\)-\([0-9]\)/\1–\2/g
+			' $page/lens |
+			encode
+		)
+	else
+		lens=$(
+			identify -format '%[EXIF:LensModel]' \
+				$page/$(ls -1 $page | head -n 1) 2>/dev/null |
+			sed '
+				s/\([A-Z]\)\([0-9]\)/\1 \2/
+				s,f/,ƒ/,
+				s/\([0-9]\)-\([0-9]\)/\1–\2/g
+			' |
+			encode
+		)
+	fi
+	if test -f $page/film; then
+		film=$(encode $page/film)
+	fi
+	if test -f $page/chem; then
+		chem=$(encode $page/chem)
+	fi
+	if test -f $page/note; then
+		note=$(encode $page/note)
+	fi
+
+	cat <<-EOF
+	<!DOCTYPE html>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<link rel="alternate" type="application/atom+xml" href="../feed.atom">
+	<title>${title}</title>
+	<style>
+	html { color: #bbb; background-color: black; font-family: monospace; }
+	p { text-align: center; }
+	figure { margin: 1em; padding-top: 0.5em; text-align: center; }
+	img { max-width: calc(100vw - 2.5em); max-height: calc(100vh - 2.5em); }
+	details { max-width: 78ch; margin: 0.5em auto; }
+	</style>
+	<h1>${title}</h1>
+	<p>${date:+📆 }${date:-} 📷 ${body:-}${body:+ 🔘 }${lens:-}${film:+ 🎞️ }${film:-}${chem:+ 🧪 }${chem:-}</p>
+	${note:+<p>}${note:-}${note:+</p>}
+	EOF
+}
+
+photo_info() {
+	local photo=$1
+	ExposureTime=
+	FNumber=
+	FocalLength=
+	PhotographicSensitivity=
+	eval $(
+		identify -format '%[EXIF:*]' $photo 2>/dev/null |
+		grep -E 'ExposureTime|FNumber|FocalLength|PhotographicSensitivity' |
+		sed 's/^exif://'
+	)
+}
+
+photo_id() {
+	local photo=$1
+	photo=${photo##*/}
+	photo=${photo%%.*}
+	echo $photo
+}
+
+page_photo() {
+	local photo=$1 preview=$2 description=$3
+	photo_info $photo
+	cat <<-EOF
+	<figure id="$(photo_id $photo)">
+		<a href="${photo##*/}">
+	EOF
+	if test -f $description; then
+		cat <<-EOF
+			<img src="../${preview}" alt="$(encode $description)">
+		EOF
+	else
+		cat <<-EOF
+			<img src="../${preview}">
+		EOF
+	fi
+	cat <<-EOF
+		</a>
+		<figcaption>
+	EOF
+	if test -n "${ExposureTime}"; then
+		cat <<-EOF
+			${ExposureTime} ·
+			ƒ/$(bc -S 1 -e ${FNumber}) ·
+			$(bc -e ${FocalLength}) mm ·
+			${PhotographicSensitivity} ISO
+		EOF
+	fi
+	if test -f $description; then
+		cat <<-EOF
+			<details>
+				<summary>description</summary>
+				$(encode $description)
+			</details>
+		EOF
+	fi
+	cat <<-EOF
+		</figcaption>
+	</figure>
+	EOF
+}
+
+index_head() {
+	cat <<-EOF
+	<!DOCTYPE html>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<link rel="alternate" type="application/atom+xml" href="feed.atom">
+	<title>Photos</title>
+	<style>
+	html { color: #bbb; background-color: black; font-family: sans-serif; }
+	a { text-decoration: none; color: inherit; }
+	</style>
+	EOF
+}
+
+index_page() {
+	local date=$1 root=${2:-}
+	cat <<-EOF
+	<h1><a href="${root}${root:+/}${date}/">$(page_title $date)</a></h1>
+	EOF
+}
+
+index_photo() {
+	local date=$1 photo=$2 thumbnail=$3 root=${4:-}
+	cat <<-EOF
+	<a href="${root}${root:+/}${date}/#$(photo_id $photo)">
+		<img src="${root}${root:+/}${thumbnail}">
+	</a>
+	EOF
+}
+
+Root=https://photo.causal.agency
+
+atom_head() {
+	local updated=$(date -u '+%FT%TZ')
+	cat <<-EOF
+	<?xml version="1.0" encoding="utf-8"?>
+	<feed xmlns="http://www.w3.org/2005/Atom">
+	<title>Photos</title>
+	<author><name>june</name><email>june@causal.agency</email></author>
+	<link href="${Root}"/>
+	<link rel="self" href="${Root}/feed.atom"/>
+	<id>${Root}/</id>
+	<updated>${updated}</updated>
+	EOF
+}
+
+atom_entry_head() {
+	local date=$1
+	local updated=$(
+		date -ju -f '%s' $(stat -f '%m' static/${date}/index.html) '+%FT%TZ'
+	)
+	cat <<-EOF
+	<entry>
+	<title>$(page_title $date)</title>
+	<link href="${Root}/${date}/"/>
+	<id>${Root}/${date}/</id>
+	<updated>${updated}</updated>
+	<content type="html">
+	EOF
+}
+
+atom_entry_tail() {
+	cat <<-EOF
+	</content>
+	</entry>
+	EOF
+}
+
+atom_tail() {
+	cat <<-EOF
+	</feed>
+	EOF
+}
+
+set --
+for entry in 20* 0*; do
+	mkdir -p static/${entry}
+	page=static/${entry}/index.html
+	if ! test -f $page; then
+		echo $page >&2
+		page_head $entry >$page
+		for photo in ${entry}/*.[Jj][Pp][Gg]; do
+			preview=$(preview $photo)
+			if ! test -f static/${photo}; then
+				ln $photo static/${photo}
+			fi
+			page_photo $photo $preview ${photo%.[Jj][Pp][Gg]}.txt >>$page
+		done
+	fi
+	set -- $entry "$@"
+done
+
+mkdir -p static/leader
+page=static/leader/index.html
+if [ leader -nt $page ]; then
+	echo $page >&2
+	page_head leader >$page
+	for photo in leader/*.[Jj][Pp][Gg]; do
+		preview=$(preview $photo)
+		if ! test -f static/${photo}; then
+			ln $photo static/${photo}
+		fi
+		page_photo $photo $preview xxx >>$page
+	done
+fi
+
+echo static/index.html >&2
+index_head >static/index.html
+echo static/feed.atom >&2
+atom_head >static/feed.atom
+for date; do
+	index_page $date >>static/index.html
+	atom_entry_head $date >>static/feed.atom
+	for photo in ${date}/*.[Jj][Pp][Gg]; do
+		thumbnail=$(thumbnail $photo)
+		index_photo $date $photo $thumbnail >>static/index.html
+		index_photo $date $photo $thumbnail $Root | encode >>static/feed.atom
+	done
+	atom_entry_tail >>static/feed.atom
+done
+atom_tail >>static/feed.atom
diff --git a/www/photo.causal.agency/mastodon.sh b/www/photo.causal.agency/mastodon.sh
new file mode 100644
index 00000000..1eaa1114
--- /dev/null
+++ b/www/photo.causal.agency/mastodon.sh
@@ -0,0 +1,54 @@
+#!/bin/sh
+set -eu
+
+Instance=https://tilde.zone
+Root=${1:-static}
+
+if ! test -f app.json; then
+	echo 'No app.json!' >&2
+	exit 1
+fi
+chmod 600 app.json
+
+if ! test -f token.json; then
+	client_id=$(jq -r .client_id app.json)
+	client_secret=$(jq -r .client_secret app.json)
+	echo "Please open ${Instance}/oauth/authorize?client_id=${client_id}&scope=write&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code"
+	printf 'Enter code: '
+	read -r code
+	curl -Ss -X POST \
+		-F 'grant_type=authorization_code' \
+		-F "client_id=${client_id}" \
+		-F "client_secret=${client_secret}" \
+		-F 'redirect_uri=urn:ietf:wg:oauth:2.0:oob' \
+		-F "code=${code}" \
+		${Instance}/oauth/token >token.json
+fi
+chmod 600 token.json
+
+access_token=$(jq -r .access_token token.json)
+
+if ! test -f posted.txt; then
+	touch posted.txt
+fi
+
+photo=$(
+	find ${Root} -type f -path '*/0*/*.jpg' |
+	sort | comm -13 posted.txt - | head -n 1
+)
+preview=${Root}/preview/${photo##*/}
+
+media_id=$(
+	curl -Ss -X POST \
+		-H "Authorization: Bearer ${access_token}" \
+		-F "file=@${preview}" \
+		${Instance}/api/v2/media |
+	jq -r .id
+)
+
+curl -Ss -X POST \
+	-H "Authorization: Bearer ${access_token}" \
+	-F "media_ids[]=${media_id}" \
+	${Instance}/api/v1/statuses >/dev/null
+
+echo ${photo} >>posted.txt
diff --git a/www/photo.causal.agency/rsync.sh b/www/photo.causal.agency/rsync.sh
new file mode 100644
index 00000000..957911d2
--- /dev/null
+++ b/www/photo.causal.agency/rsync.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+set -eu
+
+sh generate.sh
+rsync -av static/ scout:/var/www/photo.causal.agency
diff --git a/www/photo.causal.agency/trips.html b/www/photo.causal.agency/trips.html
new file mode 100644
index 00000000..e81be6ef
--- /dev/null
+++ b/www/photo.causal.agency/trips.html
@@ -0,0 +1,373 @@
+<!DOCTYPE html>
+<title>Photo Trips</title>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+<style>
+body {
+	font-family: sans-serif;
+	line-height: 1.5em;
+	max-width: 52ch;
+}
+input, button, select { font-size: 100%; }
+form {
+	display: grid;
+	grid-template-columns: auto 1fr;
+	gap: 0.5em 1ch;
+}
+input[type="number"] { width: 5ch; }
+#trip-lens { width: 100%; }
+#lens-length { width: 7ch; }
+#lens-aperture { width: 8ch; }
+</style>
+
+<section id="rolls">
+<h1>Rolls</h1>
+<ul>
+</ul>
+
+<form>
+<label for="roll-body">Camera:</label>
+<select id="roll-body" class="body" required>
+</select>
+<label for="roll-film">Film:</label>
+<input id="roll-film" list="films" required>
+<span>Exposures:</span>
+<span>
+<input id="roll-36" type="radio" name="roll-exposures" value="36" checked>
+<label for="roll-36">36</label>
+<input id="roll-27" type="radio" name="roll-exposures" value="27">
+<label for="roll-27">27</label>
+<input id="roll-24" type="radio" name="roll-exposures" value="24">
+<label for="roll-24">24</label>
+<input id="roll-12" type="radio" name="roll-exposures" value="12">
+<label for="roll-12">12</label>
+</span>
+<button type="button" onclick="loadRoll()">Load</button>
+</form>
+
+<datalist id="films">
+</datalist>
+</section>
+
+<section id="trips">
+<h1>Trips</h1>
+
+<form>
+<label for="trip-date">Date:</label>
+<input id="trip-date" type="date" required>
+<label for="trip-body">Camera:</label>
+<select id="trip-body" class="body" onchange="setTripBody()" required>
+</select>
+<label for="trip-lens">Lens:</label>
+<select id="trip-lens" required>
+</select>
+<label for="trip-film">Film:</label>
+<input id="trip-film" readonly required>
+<label for="trip-first">Exposures:</label>
+<span>
+<input id="trip-first" type="number" required min="0" max="36">
+–
+<input id="trip-last" type="number" required min="0" max="36">
+</span>
+<label for="trip-note">Note:</label>
+<input id="trip-note">
+<button type="button" onclick="addTrip()">Record</button>
+</form>
+
+<ul>
+</ul>
+</section>
+
+<section id="bodies">
+<h1>Cameras</h1>
+<ul>
+</ul>
+
+<form>
+	<label for="body-name">Name:</label>
+	<input id="body-name" required>
+	<label for="body-mount">Mount:</label>
+	<input id="body-mount" list="mounts" required>
+	<button type="button" onclick="addBody()">Add</button>
+</form>
+
+<datalist id="mounts">
+	<option>Contax/Yashica</option>
+	<option>M42</option>
+</datalist>
+</section>
+
+<section id="lenses">
+<h1>Lenses</h1>
+<ul>
+</ul>
+
+<form>
+	<label for="lens-name">Name:</label>
+	<input id="lens-name" required>
+	<label for="lens-length">Focal length:</label>
+	<span><input id="lens-length" required pattern="[0-9-]+">mm</span>
+	<label for="lens-aperture">Aperture:</label>
+	<span>ƒ/<input id="lens-aperture" required pattern="[0-9.-]+"></span>
+	<label for="lens-mount">Mount:</label>
+	<input id="lens-mount" list="mounts" required>
+	<button type="button" onclick="addLens()">Add</button>
+</form>
+</section>
+
+<script>
+let bodies = JSON.parse(localStorage.getItem("bodies")) || [];
+let lenses = JSON.parse(localStorage.getItem("lenses")) || [];
+let rolls = JSON.parse(localStorage.getItem("rolls")) || {};
+let trips = JSON.parse(localStorage.getItem("trips")) || [];
+let nextId = +localStorage.getItem("nextId") || 1;
+
+document.getElementById("trip-date").valueAsDate = new Date();
+
+function removeButton(onclick) {
+	let remove = document.createElement("a");
+	remove.appendChild(document.createTextNode("⛔"));
+	remove.onclick = onclick;
+	return remove;
+}
+
+function setBodies() {
+	localStorage.setItem("bodies", JSON.stringify(bodies));
+	let ul = document.querySelector("#bodies > ul");
+	let selects = document.querySelectorAll("select.body");
+	ul.innerHTML = "";
+	selects.forEach(select => select.innerHTML = "");
+	for (let [index, body] of bodies.entries()) {
+		let li = document.createElement("li");
+		li.appendChild(document.createTextNode(`
+			${body.name} (${body.mount})
+		`));
+		li.appendChild(removeButton(removeBody.bind(null, index)));
+		ul.appendChild(li);
+		for (let select of selects) {
+			let option = document.createElement("option");
+			option.appendChild(document.createTextNode(body.name));
+			select.appendChild(option);
+		}
+	}
+	if (trips.length) {
+		selects.forEach(select => select.value = trips[trips.length-1].body);
+	}
+}
+setBodies();
+
+function endashify(str) {
+	return str.replaceAll("-", "–");
+}
+function lensString(lens) {
+	return `
+		${lens.name}
+		${endashify(lens.focalLength)}mm
+		ƒ/${endashify(lens.aperture)}
+	`.replace(/\s+/g, " ").trim();
+}
+
+function setLenses() {
+	localStorage.setItem("lenses", JSON.stringify(lenses));
+	let ul = document.querySelector("#lenses > ul");
+	ul.innerHTML = "";
+	for (let [index, lens] of lenses.entries()) {
+		let li = document.createElement("li");
+		li.appendChild(document.createTextNode(`
+			${lensString(lens)} (${lens.mount})
+		`));
+		li.appendChild(removeButton(removeLens.bind(null, index)));
+		ul.appendChild(li);
+	}
+}
+setLenses();
+
+function setRolls() {
+	localStorage.setItem("rolls", JSON.stringify(rolls));
+	let ul = document.querySelector("#rolls > ul");
+	ul.innerHTML = "";
+	for (body in rolls) {
+		let roll = rolls[body];
+		let li = document.createElement("li");
+		li.appendChild(document.createTextNode(`
+			${body}: ${roll.film} (${roll.used}/${roll.exposures})
+		`));
+		if (roll.used == roll.exposures) {
+			li.style.textDecoration = "line-through";
+		}
+		ul.appendChild(li);
+	}
+}
+setRolls();
+
+function setTrips() {
+	localStorage.setItem("trips", JSON.stringify(trips));
+	let ul = document.querySelector("#trips > ul");
+	ul.innerHTML = "";
+	let tripsByRoll = Object.groupBy(trips, trip => trip.rollId);
+	for (let rollId = nextId - 1; rollId > 0; --rollId) {
+		let rollTrips = tripsByRoll[rollId];
+		if (!rollTrips) continue;
+		let rollLi = document.createElement("li");
+		let rollB = document.createElement("b");
+		rollB.appendChild(document.createTextNode(rollTrips[0].film));
+		rollLi.appendChild(rollB);
+		rollLi.appendChild(document.createTextNode(` (${rollTrips[0].body})`));
+		let body = bodies.find(body => body.name == rollTrips[0].body);
+		let rollUl = document.createElement("ul");
+		for (let [index, trip] of rollTrips.entries()) {
+			let li = document.createElement("li");
+			let b = document.createElement("b");
+			b.appendChild(document.createTextNode(trip.date));
+			li.appendChild(b);
+			li.appendChild(document.createTextNode(
+				`: ${trip.firstExposure}–${trip.lastExposure}`
+			));
+			if (
+				(!body || body.mount != body.name) &&
+				(!index || trip.lens != rollTrips[index-1].lens)
+			) {
+				li.appendChild(document.createElement("br"));
+				li.appendChild(document.createTextNode(trip.lens));
+			}
+			if (trip.note) {
+				li.appendChild(document.createElement("br"));
+				li.appendChild(document.createTextNode(`“${trip.note}”`));
+			}
+			rollUl.appendChild(li);
+		}
+		rollLi.appendChild(rollUl);
+		ul.appendChild(rollLi);
+	}
+}
+setTrips();
+
+function setTripBody() {
+	let bodyName = document.getElementById("trip-body").value;
+	let body = bodies.find(body => body.name == bodyName);
+	let select = document.getElementById("trip-lens");
+	select.innerHTML = "";
+	for (lens of lenses.filter(lens => lens.mount == body.mount)) {
+		let option = document.createElement("option");
+		option.appendChild(document.createTextNode(lensString(lens)));
+		select.appendChild(option);
+	}
+	let lastTrip = trips.findLast(trip => trip.body == bodyName);
+	if (lastTrip) {
+		select.value = lastTrip.lens;
+	}
+	let roll = rolls[body.name];
+	if (roll) {
+		document.getElementById("trip-film").value = roll.film;
+		let next = (roll.used > 0 ? roll.used + 1 : roll.used);
+		document.getElementById("trip-first").value = next;
+		document.getElementById("trip-last").value = next;
+	} else {
+		document.getElementById("trip-film").value = "";
+		document.getElementById("trip-first").value = "";
+		document.getElementById("trip-last").value = "";
+	}
+}
+setTripBody();
+
+function clearForm(form) {
+	let inputs = form.querySelectorAll("input");
+	for (input of inputs) {
+		if (input.type == "radio") continue;
+		input.value = null;
+	}
+}
+
+function addBody() {
+	let form = document.querySelector("#bodies > form");
+	if (!form.checkValidity()) return;
+	let name = document.getElementById("body-name").value;
+	let mount = document.getElementById("body-mount").value;
+	bodies.push({ name, mount });
+	setBodies();
+	clearForm(form);
+}
+
+function removeBody(index) {
+	let body = bodies[index];
+	if (!confirm(`Are you sure you want to remove ${body.name}?`)) {
+		return;
+	}
+	bodies.splice(index, 1);
+	delete rolls[body.name];
+	setBodies();
+	setRolls();
+}
+
+function addLens() {
+	let form = document.querySelector("#lenses > form");
+	if (!form.checkValidity()) return;
+	let name = document.getElementById("lens-name").value;
+	let focalLength = document.getElementById("lens-length").value;
+	let aperture = document.getElementById("lens-aperture").value;
+	let mount = document.getElementById("lens-mount").value;
+	lenses.push({ name, focalLength, aperture, mount });
+	setLenses();
+	clearForm(form);
+}
+
+function removeLens(index) {
+	let lens = lenses[index];
+	if (!confirm(`Are you sure you want to remove ${lensString(lens)}?`)) {
+		return;
+	}
+	lenses.splice(index, 1);
+	setLenses();
+	setTripBody();
+}
+
+function loadRoll() {
+	let form = document.querySelector("#rolls > form");
+	if (!form.checkValidity()) return;
+	let body = document.getElementById("roll-body").value;
+	let film = document.getElementById("roll-film").value;
+	let exposures = +new FormData(form).get("roll-exposures");
+	rolls[body] = { id: nextId++, film, exposures, used: 0 };
+	localStorage.setItem("nextId", nextId);
+	setRolls();
+	clearForm(form);
+	setTripBody();
+}
+
+function addTrip() {
+	let form = document.querySelector("#trips > form");
+	if (!form.checkValidity()) return;
+	let date = document.getElementById("trip-date").value;
+	let body = document.getElementById("trip-body").value;
+	let lens = document.getElementById("trip-lens").value;
+	let film = document.getElementById("trip-film").value;
+	let firstExposure = +document.getElementById("trip-first").value;
+	let lastExposure = +document.getElementById("trip-last").value;
+	let note = document.getElementById("trip-note").value;
+	let trip = {
+		date, body, lens, film, rollId: rolls[body].id,
+		firstExposure, lastExposure, note
+	};
+	trips.push(trip);
+	rolls[body].used = lastExposure;
+	setTrips();
+	setRolls();
+	document.getElementById("trip-date").valueAsDate = new Date();
+	document.getElementById("trip-note").value = "";
+	setTripBody();
+}
+
+function setFilms() {
+	let datalist = document.getElementById("films");
+	datalist.innerHTML = "";
+	let films = new Set(trips.reverse().map(trip => trip.film));
+	for (let film of films.values().take(20)) {
+		let option = document.createElement("option");
+		option.innerText = film;
+		datalist.appendChild(option);
+	}
+}
+setFilms();
+
+</script>
diff --git a/www/temp.causal.agency/up.c b/www/temp.causal.agency/up.c
index 4b83b564..561a8901 100644
--- a/www/temp.causal.agency/up.c
+++ b/www/temp.causal.agency/up.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2020  June McEnroe <june@causal.agency>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
diff --git a/www/text.causal.agency/037-care.7 b/www/text.causal.agency/037-care.7
index 3fffd778..052a4727 100644
--- a/www/text.causal.agency/037-care.7
+++ b/www/text.causal.agency/037-care.7
@@ -27,6 +27,10 @@ Prices obviously change,
 by which I mean they inevitably go up,
 but I'm gonna give the amounts I paid
 in 2021\(en2022.
+Also if you want more details
+about any of this
+please email me.
+I will be happy to tell you all about it.
 .
 .Ss Medication
 I get HRT through
@@ -65,21 +69,27 @@ for my current prescription
 on the public plan.
 .
 .Ss Hair removal
-I've gotten laser hair removal treatments
-at Dermamode with Audrey.
-They're very nice there.
-The initial consultation was over the phone.
-I paid $1350 for 6 treatments,
-in two installments
-at the first and second appointments,
-plus the ~$200 tax on that
-before the first appointment,
-I think.
-It hasn't gone well for me so far,
-but I'm hoping it will
-now that my testosterone levels
-have dropped.
-I will report back.
+I tried laser hair removal,
+for longer than I should have.
+It was a waste of time and money.
+Do not believe any arguments about
+its convenience over electrolysis.
+.
+.Pp
+I've started getting electrolysis done
+with Dimi.
+Again,
+feel free to email me for contact info.
+He is very good and can do long sessions.
+I really don't find it very painful,
+which I think is partly my own pain tolerance
+and partly good equipment and skill.
+I've also found that taking acetaminophen beforehand
+and dressing warmly to keep my body relaxed help.
+I've paid $85 for hour-long sessions
+and $160 for two-hour sessions.
+I'm still early in treatment,
+but I'm really happy with the results so far!
 .
 .Ss Sex & name change
 The form for this is
@@ -101,7 +111,8 @@ so I made a donation to P10.
 .
 .Pp
 I paid $144 to file mine
-but it's now $148.
+but it's now FREE
+the first time you do it.
 Also $17 to mail it.
 Surprisingly,
 I got an acknowledgment letter
@@ -121,10 +132,12 @@ a week later.
 It takes another 30 days
 to get the certificate of change,
 after which you can
-order a new birth certificate.
-In all it took about 3 months
+order a new birth certificate
+and RAMQ will (slowly) send you a form
+to get a new card.
+In all it took about 4 months
 from when I mailed the application
-to receiving the certificate of change.
+to having ID with my name on it.
 .
 .Ss Therapy
 I'm not seeking therapy
@@ -134,6 +147,17 @@ that's aware of it.
 I'll update this
 if I find one.
 .
+.Ss Piercings
+Ok I know this isn't trans-specific
+but at least for me getting piercings
+was gender-affirming.
+Cuz I got nipple piercings lol.
+Anyway,
+I went to Mauve.
+They're super nice,
+really know what they're doing,
+and their website has lots of info.
+.
 .Sh AUTHORS
 .An june Aq Mt june@causal.agency
 .
diff --git a/www/text.causal.agency/039-apologies.7 b/www/text.causal.agency/039-apologies.7
new file mode 100644
index 00000000..1b15076a
--- /dev/null
+++ b/www/text.causal.agency/039-apologies.7
@@ -0,0 +1,81 @@
+.Dd September 19, 2022
+.Dt APOLOGIES 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Apologies
+.Nd making them
+.
+.Sh DESCRIPTION
+Apologies are very important to me.
+Unfortunately
+I've only recently realized
+how valuable they are.
+I've tried to think about
+what makes a good apology,
+since it's not something
+I was ever taught.
+This is the advice
+I came up with for myself,
+on how to apologize.
+.
+.Bl -enum
+.It
+Make the apology.
+This is the most important part.
+If you feel guilty
+for something you've done,
+or think you might have hurt someone,
+apologize.
+Even if they don't need an apology,
+saying sorry won't hurt.
+And start with that.
+Literally say
+.Dq I'm sorry .
+Sometimes people forget that.
+.Pp
+On the other side,
+if you've been hurt by someone,
+and you trust them,
+let them know.
+Give them a chance to apologize.
+People don't always realize
+they've made a mistake.
+.
+.It
+Explain what you did wrong.
+I think it's important
+for the other person
+to know you understand
+how you've messed up.
+Really think about this!
+It's what will help you learn.
+If you know you've hurt someone
+but you're not sure why,
+you can try asking them.
+Take their answer seriously.
+.
+.It
+Don't make excuses.
+Do not talk about yourself.
+Don't even mention
+how you were feeling stressed that day,
+or whatever.
+It's not relevant.
+We all make mistakes,
+we all have bad days.
+.
+.It
+Commit to doing better.
+Try to learn from your mistakes.
+Say it won't happen again.
+Literally say
+.Dq I won't do that again .
+And then try your hardest to make that true.
+An apology is a commitment,
+not something you're done with
+once you've said it.
+.El
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/040-sound-memory.7 b/www/text.causal.agency/040-sound-memory.7
new file mode 100644
index 00000000..c995de08
--- /dev/null
+++ b/www/text.causal.agency/040-sound-memory.7
@@ -0,0 +1,165 @@
+.Dd November 14, 2022
+.Dt SOUND-MEMORY 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm Sound Memory
+.Nd associations
+.
+.Sh DESCRIPTION
+.Ss Talking Heads \(em "Remain In Light"
+The first time I gave this album a serious listen
+was when I was going for several-hour walks
+at 4 in the morning in,
+I think,
+fall 2020.
+I would stay up all night,
+go out walking at 4am
+for a couple hours,
+come home,
+eat
+.Dq breakfast
+and go to sleep.
+I listened to this album
+walking on completely empty
+big city streets
+in the dark.
+.
+.Ss Buffy Sainte-Marie \(em Up Where We Belong
+I started listening to this album
+after hearing it many mornings
+walking into the cafe on my block
+back in 2019.
+I could tell Vincent was working
+if I heard this when I opened the door.
+.
+.Ss Molasses \(em Trilogie: Toil & Peaceful Life
+I listened to this when I had 8am classes
+in CEGEP.
+In particular my first semester philosophy course,
+which was in the forum.
+I usually got there even earlier
+because of how the bus schedules worked out.
+There was another girl in my class,
+who I always sat next to,
+who also got there early,
+but we never spoke outside of class.
+.
+.Ss Arcade Fire \(em Funeral
+This album just feels like walking outside
+in fresh snow in early winter,
+you know?
+.
+.Ss Molasses \(em Trouble at Jinx Hotel
+I listened to this when I was looking for an apartment.
+I specifically remember listening to it
+walking down Clark toward my new place
+to pick up my keys.
+.
+.Ss Arcade Fire \(em Neon Bible
+The song
+.Dq "No Cars Go"
+is strongly associated for me
+with my earliest gender feelings.
+It's how I date when I first
+started to feel like something was wrong.
+The Suburbs was released in 2010,
+so I was probably listening to Neon Bible
+in 2011.
+Ten years between that
+and coming out.
+.
+.Ss "Do Make Say Think" \(em "You You're a History In Rust"
+I remember hearing
+.Dq "A Tender History In Rust"
+for the first time
+at the office of my first job.
+Me and my coworkers stayed late,
+probably on a Friday night,
+drinking free tech startup booze.
+.
+.Ss mewithoutYou \(em It's All Crazy! It's All False! It's All a Dream! It's Alright
+I exclusively listened to this album
+on a high school trip to Europe.
+Every morning when we got on the bus,
+I heard
+.Dq Every Thought a Thought of You
+and every night before bed
+I listened to
+.Dq The King Beetle on a Coconut Estate .
+.
+.Ss Arcade Fire \(em The Suburbs
+I listened to this album a tonne
+when I was playing
+Minecraft and Urban Terror
+with my online friends
+while I was in high school.
+In particular I remember
+a backyard shed World of Padman map
+and the apartments Minecraft world.
+.
+.Ss Arcade Fire \(em Reflektor
+I associate
+.Dq Afterlife
+with the walk between Laurier metro
+and my first job,
+in the winter.
+Must've just been how the timing worked out
+with my commute at the time.
+.
+.Ss Swans \(em To Be Kind
+I listened to this on one of my playthroughs
+of Half-Life 2.
+In particular I associate
+.Dq Bring the Sun / Toussaint L'Ouverture
+with the Water Hazard chapter.
+.
+.Ss Wrekmeister Harmonies \(em Light Falls
+For a while I put this on whenever I
+left my apartment to go somewhere
+and it was already dark,
+so probably winter.
+.
+.Ss St. Vincent \(em MASSEDUCTION
+This,
+along with the next one,
+I think were all I listened to
+on a family vacation
+to Quebec City and New Brunswick
+some years ago.
+.
+.Ss SOPHIE \(em Oil of Every Pearl's Un-Insides
+Many hours on the road
+on that family vacation.
+Two albums on repeat.
+.
+.Ss Julia Holter \(em Aviary
+This is another album
+I listened to when I was taking
+walks at 4am.
+I wasn't in a good place.
+Yet.
+.
+.Ss Beep Test \(em Laugh Track
+A tape from the first act
+at one of my favourite shows
+I've ever been to,
+at La Sotterenea
+in Suoni 2019.
+I wish I had been out already.
+.
+.Ss The Armed \(em Only Love
+The third of the albums I listened to
+on those dark walks.
+I listened to it loud,
+this album's mixing needs it.
+.
+.Ss Eliza Kavtion \(em The Rez That Summer
+A favourite local artist.
+I remember vividly the first time
+I heard her play,
+opening for Wrekmeister Harmonies
+at La Vitrola in 2018.
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/041-albums-2022.7 b/www/text.causal.agency/041-albums-2022.7
new file mode 100644
index 00000000..48bd3c3d
--- /dev/null
+++ b/www/text.causal.agency/041-albums-2022.7
@@ -0,0 +1,185 @@
+.Dd December 21, 2022
+.Dt ALBUMS-2022 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm albums 2022
+.Nd review
+.
+.Sh DESCRIPTION
+it's the year-end review
+of albums I listened to.
+same as last year,
+I added any albums I got into
+this year to a playlist.
+I've actually done that
+every year since 2018.
+maybe I'll review
+those old playlists some time.
+.
+.Ss ZHAOZE \(em SUMMER INSECTS TALK ABOUT ICE (2021)
+it's a five-and-a-half-minute album!
+you can loop it however long you want.
+it's really lovely.
+.Pp
+favourite track:
+ON HORSEBACK, TO FARAWAY
+.
+.Ss KATE BUSH \(em HOUNDS OF LOVE (1985)
+first of all I do not watch that one show.
+I've known that track for a while actually.
+I mean I probably first heard the CHROMATICS cover.
+but anyway,
+I think someone mentioned this album
+on IRC at just the right time
+and I put it on.
+the second half really shines tbh.
+love a concept album.
+.Pp
+favourite tracks:
+RUNNING UP THAT HILL,
+HOUNDS OF LOVE,
+AND DREAM OF SHEEP,
+WATCHING YOU WITHOUT ME,
+THE MORNING FOG.
+.
+.Ss GODSPEED YOU! BLACK EMPEROR \(em ALL LIGHTS FUCKED ON THE HAIRY AMP DROOLING (1994)
+didn't expect to hear this probably ever.
+still wild that it finally got uploaded.
+and to be honest I'm a little mad
+that it's actually good.
+like yeah it's not a godspeed album
+but it holds up as a tape on its own.
+it's the kind of shit I listen to.
+also can't believe some people
+still thought it was fake.
+like have you not heard
+any other efrim menuck projects?
+.Pp
+favourite tracks:
+$13.13,
+DIMINISHING SHINE,
+DADMOMDADDY,
+333 FRAMES PER SECOND,
+ALL ANGELS GONE.
+.
+.Ss BLACK DRESSES \(em FORGET YOUR OWN FACE (2022)
+woops I think I only listened to this like twice.
+will need to revisit it later for sure.
+I'll like it.
+.
+.Ss BACKXWASH \(em I LIE HERE BURIED WITH MY RINGS AND MY DRESSES (2021)
+only got into this album
+after hearing it live this summer.
+was the first show I went to in years
+and it was really fucking good.
+gotta listen to this shit loud.
+sampling godspeed for a beat fucks.
+honestly back to back bangers.
+.Pp
+favourite tracks:
+I LIE HERE BURIED WITH MY RINGS AND MY DRESSES,
+TERROR PACKETS,
+SONG OF SINNERS,
+BURN TO ASHES.
+.
+.Ss PHILIP GLASS ENSEMBLE \(em EINSTEIN ON THE BEACH (1979)
+actually just the knee plays
+because I can't be bothered
+listening to all of it.
+and I'm embarrassed by how much
+I enjoy this avant-garde bullshit.
+like ok just sing repeating numbers at me
+and my brain is happy.
+what is this?
+my kink?
+anyway I also have kind of an obsession
+with the
+.Dq story of love
+in knee 5.
+I fucking hate it.
+but it's delivered so well.
+and that violin though!
+.Pp
+favourite tracks:
+KNEE 1,
+KNEE 5.
+.
+.Ss KANYE WEST \(em YEEZUS (2013)
+ok look I listened to this
+before recent events.
+what the fuck.
+it's a really good album though?
+pretty sure I listened to it
+because bound 2 kept getting in my head,
+because of that minecraft parody parody
+wayne did ages ago.
+.Pp
+favourite tracks:
+BLACK SKINHEAD,
+HOLD MY LIQUOR,
+BLOOD ON THE LEAVES,
+BOUND 2.
+.
+.Ss FLYING RACCOON SUIT \(em AFTERGLOW (2021)
+I've listened to the whole album
+a few times
+but I'm mostly just here
+for the title track.
+this also happened to be
+dropped in IRC at just the right time.
+good ska-punk-type shit.
+and I like lisps ok.
+.Pp
+favourite track:
+AFTERGLOW.
+.
+.Ss RAMSHACKLE GLORY \(em LIVE THE DREAM (2011)
+one of those albums
+I don't know why I took so long
+to get to.
+I've been listening to johnny hobo
+since I was like in high school.
+ramshackle is a little more hopeful
+and I love that.
+your heart is a muscle the size of your fist.
+keep on loving.
+keep on fighting.
+.Pp
+favourite tracks:
+WE ARE ALL COMPOST IN TRAINING,
+NEVER COMING HOME,
+YOUR HEART IS A MUSCLE THE SIZE OF YOUR FIST.
+.
+.Ss LES RALLIZES D\('ENUD\('ES \(em THE OZ TAPES (2022)
+a pleasant surprise in someone's playlist.
+lately I've been listening to this
+in the metro to or from electrolysis.
+it's good listening for that.
+bold to have two versions
+of the same 24-minute song
+on the same release.
+.Pp
+favourite tracks:
+A SHADOW ON OUR JOY,
+THE LAST ONE_1970 (ver.2).
+.
+.Ss LINGUA IGNOTA \(em SINNER GET READY (2021)
+another I'm only getting into
+after hearing it live.
+just last sunday actually.
+was a good show.
+people will go wild
+to hear a cover live for real.
+.Pp
+favourite tracks:
+I WHO BEND THE TALL GRASSES,
+PENNSYLVANIA FURNACE,
+PERPETUAL FLAME OF CENTRALIA.
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
+.Pp
+I started writing this
+before I saw LINGUA IGNOTA.
+good thing I waited.
diff --git a/www/text.causal.agency/042-comfort-music.7 b/www/text.causal.agency/042-comfort-music.7
new file mode 100644
index 00000000..445e04c3
--- /dev/null
+++ b/www/text.causal.agency/042-comfort-music.7
@@ -0,0 +1,62 @@
+.Dd February 23, 2024
+.Dt COMFORT-MUSIC 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm comfort music
+.Nd feel better
+.
+.Sh DESCRIPTION
+it's been a while.
+and I'm on almost no sleep
+and haven't eaten a real meal
+since noon.
+which is a state I've written
+at least a couple posts in before,
+so what better time
+to return to what has apparently
+become this blog's format:
+lists of some music I like.
+.
+.Pp
+this is a list of music that comforts me.
+.
+.Bl -bullet
+.It
+knee play 5, from einstein on the beach.
+I like the organ and the counting and the cadence of the story.
+.It
+low \(em words.
+and I'm tired.
+.It
+godspeed you! black emperor \(em storm.
+this is like my original comfort music.
+been listening to it since I was teenage.
+the grooves are worn deep in my mind.
+.It
+set fire to flames \(em love song for 15 ontario (w/ singing police car).
+I like how it ends.
+.It
+va, from the beginner's guide.
+I think that's the whole point.
+though maybe it's too sad
+to be truly comforting.
+.It
+undertale, from undertale.
+what can I say?
+.It
+wrekmeister harmonies \(em covered in blood from invisible wounds.
+I find quite a bit of the album comforting really.
+I'm picking this one because I like the cadence
+of the lyrics.
+.It
+lingua ignota \(em pennsylvania furnace and perpetual flame of centralia.
+these are really my go to in recent times.
+I like waiting for the next line.
+.El
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
+.Pp
+I don't think I've said anything
+very interesting here.
diff --git a/www/text.causal.agency/043-little-blessings.7 b/www/text.causal.agency/043-little-blessings.7
new file mode 100644
index 00000000..957c6289
--- /dev/null
+++ b/www/text.causal.agency/043-little-blessings.7
@@ -0,0 +1,78 @@
+.Dd March 24, 2024
+.Dt LITTLE-BLESSINGS 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm little blessings
+.Nd life's
+.
+.Sh DESCRIPTION
+today I went out to go around.
+run some errands and do some shopping.
+along the way I was given
+several of life's little blessings.
+.
+.Pp
+while walking on ste-cath
+between berri and complexe desjardins,
+there was a somewhat disheveled man
+walking in the same direction and singing.
+he had a beautiful voice.
+he was singing a sad song in french,
+and he sung it well and enunciated every word.
+.
+.Pp
+in the mcdonald's at complexe desjardins,
+while waiting for my order,
+there were what appeared to be
+a teenager and her younger brother,
+who must have been
+looking at the display of
+current happy meal toys.
+the teenager was playing smash or pass,
+to the amusement of the younger one.
+they got ice cream
+and ate it across the room from me downstairs.
+.
+.Pp
+later,
+taking the 24 home from atwater
+carrying my new vacuum cleaner,
+the bus got lost.
+I think the driver missed the stop
+and tried to compensate
+by turning north onto peel
+and stopping there.
+but then he had to keep going up peel.
+he turned right onto docteur-penfield,
+which just brings you further up the mountain.
+when it met des pins,
+he turned left and pulled over,
+asking for guidance over the radio.
+we got moving again,
+back towards peel.
+that's how I ended up
+on a 24
+.Dq sherbrooke
+east,
+facing west on des pins.
+it was actually quite scenic.
+and amusing.
+I was in no rush.
+.
+.Pp
+after getting back onto sherbrooke,
+the bus had to take another detour,
+this one planned.
+so my ride on the 24,
+which normally only drives on sherbrooke,
+ended up going on peel,
+docteur-penfield,
+des pins,
+de bleury,
+ren\('e-l\('evesque
+and saint-laurent.
+it was a very exciting bus trip.
+.
+.Sh AUTHORS
+.An june Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/044-film-review.7 b/www/text.causal.agency/044-film-review.7
new file mode 100644
index 00000000..8e8feca8
--- /dev/null
+++ b/www/text.causal.agency/044-film-review.7
@@ -0,0 +1,208 @@
+.Dd October 12, 2024
+.Dt FILM-REVIEW 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm film review
+.Nd stock, that is
+.
+.Sh DESCRIPTION
+this summer I got really into analog photography.
+I've tried out a bunch of different film stocks,
+thanks to the local photo lab,
+and I've
+.Em developed
+(pun intended)
+some preferences.
+here they are.
+.
+.Sh BLACK & WHITE
+.Bl -enum
+.It
+Ilford FP4+ (ISO 125, United Kingdom)
+.Pp
+definitely my favourite b&w film.
+I love the fine grain and contrast
+with good shadow detail.
+really just exactly what I want
+out of a b&w film I think.
+ISO 125 is quite generous for what it is,
+but it's still best suited for sunny days.
+.Pp
+sample:
+.Lk https://photo.causal.agency/2024-09-29/
+.
+.It
+Fomapan Creative (ISO 200, Czech Republic)
+.Pp
+I've only shot one roll of this so far,
+but I really like the balance it strikes
+between fine grain and high speed.
+it just seems like a good go-to film
+for what I like to do with b&w photography,
+given the extra flexibility over FP4.
+.Pp
+sample:
+.Lk https://photo.causal.agency/2024-09-14/
+.
+.It
+Ferrania P30 (ISO 80, Italy)
+.Pp
+another that I've only shot one roll of,
+but I really like the results.
+obviously it swings in the other direction
+in terms of film sensitivity,
+but more importantly
+it has a distinctive look.
+that's harder in b&w than it is in colour!
+.Pp
+sample:
+.Lk https://photo.causal.agency/2024-10-05/
+.
+.It
+Ilford Delta 100 (United Kingdom)
+.Pp
+as far as I'm concerned this is just more expensive FP4.
+it certainly looks good
+but I'd rather save the couple extra dollars.
+.Pp
+sample:
+.Lk https://photo.causal.agency/2024-09-22/
+.
+.It
+Ilford HP5+ (ISO 400, United Kingdom)
+.Pp
+it's like, ok.
+more grainy than I'd like,
+but that's to be expected of high speed.
+my real problem with it
+is the lack of contrast.
+maybe I should only be shooting it pushed,
+but I don't want to pay the extra fee
+to have my local photo lab do that.
+.Pp
+sample:
+.Lk https://photo.causal.agency/2024-09-07/
+.
+.It
+Fomapan Action (ISO 400, Czech Republic)
+.Pp
+I almost wonder if something went wrong
+either in shooting or processing
+the one roll of this I shot.
+everything came out very low contrast.
+.Pp
+sample:
+I didn't end up uploading any.
+.El
+.
+.Sh COLOUR
+.Bl -enum
+.It
+Shanghai Color (ISO 400, China)
+.Pp
+I love the desaturated colours
+and the grain on this.
+I guess I like fine grain in b&w
+and coarse grain in colour.
+I think this is well suited
+to the subjects I like to photograph,
+like old brick buildings,
+but it also does nature quite nicely.
+I think this will be a good one to capture fall with.
+.Pp
+ok so this is almost certainly repackaged
+Wolfen Color NC500
+(made in germany).
+but the thing is,
+shanghai does a better job packaging it.
+they use real metal cassettes
+and add film edge markings.
+and their box design is way nicer.
+and on top of THAT,
+my local photo lab
+sells it for cheaper than NC500.
+.Pp
+sample:
+.Lk https://photo.causal.agency/2024-09-22/
+.
+.It
+Harman Phoenix (ISO 200, United Kingdom)
+.Pp
+phoenix is a fun film!
+the lack of yellow filter
+and anti-halation layer
+can produce some neat effects.
+in the right conditions
+it also sometimes looks exceedingly normal.
+but it also sometimes just...
+doesn't work well.
+underexposed areas can get really bad.
+apparently it can be better to shoot it at ISO 100.
+I should give that a try,
+or just be more diligent with
+how I'm metering.
+.Pp
+sample:
+.Lk https://photo.causal.agency/2024-08-10/
+.
+.It
+CineStill 800T (USA?)
+.Pp
+I can't really say much about this yet.
+I don't have much experience with indoor photography.
+the lack of anti-halation layer
+does tend to make lights look sinister as hell, though.
+I'll probably shoot
+one of the cheaper repackagings
+of ISO 800 cinema film
+in the future.
+.Pp
+sample:
+.Lk https://photo.causal.agency/2024-10-06/
+.
+.It
+Film Washi
+.Dq X
+(ISO 100, France)
+.Pp
+this is mostly pretty normal film
+without a yellow filter.
+not much to say about it.
+I'd be more interested to try washi's
+other repackaged b&w technical films,
+but I think I missed them being in stock here.
+.Pp
+sample:
+.Lk https://photo.causal.agency/2024-08-23/
+.
+.It
+Fujifilm 400 (Japan?)
+.Pp
+I shot my two first ever rolls on this.
+they were surprisingly good!
+the scans did the film dirty though.
+that was before I found the good photo lab.
+.
+.It
+Kodak Gold (ISO 200, USA)
+.Pp
+ok so this is a cheap film, right?
+but it's too damn good.
+fine grain, accurate colour.
+it looks like digital to me,
+and that's not what I want.
+even fuji has a little more character to it than this.
+puts me off kodak.
+.Pp
+sample:
+.Lk https://photo.causal.agency/2024-07-01/
+.El
+.
+.Sh AUTHORS
+.An Juniper Aq Mt june@causal.agency
+.
+.Pp
+if you have suggestions
+for film stocks I should try,
+send me an email.
diff --git a/www/text.causal.agency/045-time-2025.7 b/www/text.causal.agency/045-time-2025.7
new file mode 100644
index 00000000..80fa428b
--- /dev/null
+++ b/www/text.causal.agency/045-time-2025.7
@@ -0,0 +1,131 @@
+.Dd August 18, 2025
+.Dt TIME-2025 7
+.Os "Causal Agency"
+.
+.Sh NAME
+.Nm time
+.Nd 2025
+.
+.Sh DESCRIPTION
+time has passed.
+this blog still exists.
+I don't feel bad about not writing.
+I haven't had anything I want to say in this form.
+.
+.Pp
+I go back and read my own posts here fairly often.
+most recently I looked up how I calculated
+.Dq unique lines of code
+all the way back in 2018.
+I read my own post on apologies a lot.
+I'm glad I wrote that down.
+.
+.Pp
+other people, too,
+still refer to my old posts.
+.Dq operating systems
+has been repeatedly referenced
+by a friend for years.
+I still occasionally get emails in reply to
+.Dq inability .
+I try to wish those people well.
+recently I got asked about
+.Dq names .
+it was interesting trying to explain
+an idea I was playing with four years ago.
+.
+.Pp
+some time in the last year
+I had started writing
+a semi-ficticious history of my life.
+I never finished it
+and I don't think I'm interested in the idea anymore.
+.
+.Pp
+I used to write posts about
+books I'd read
+or albums I'd listened to
+in the year.
+since 2022 I haven't really listened to new music.
+I put a lot of songs I really like
+in a big playlist called
+.Dq more tunes
+and I put that on shuffle
+whenever I want music.
+I don't know why
+music doesn't play the same role
+in my life anymore.
+.
+.Pp
+I've read books since 2021,
+though not at a very high rate.
+I still love Becky Chambers
+and Alix E. Harrow.
+I still need to finish
+the Andrea Stewart trilogy I started.
+I read an old collection
+of short erotic fiction
+by trans authors.
+that was really good.
+I just finished a novel draft by a friend.
+.
+.Pp
+I've shot a lot more film
+since my last post.
+I was wrong about a lot.
+I don't feel like writing more about it.
+.
+.Pp
+in october of 2022
+I started a relationship
+that lasted two years.
+we moved in together in 2023.
+by early 2024 things were going badly.
+in february I posted
+.Dq comfort music .
+I think someone emailed me
+because they didn't think I was doing well.
+I wasn't.
+from summer 2024
+to summer 2025
+was the worst year I've had.
+in october someone I barely knew at the time
+messaged me to ask if I was ok.
+I think I was sitting in a tim hortons
+after getting a blood test.
+I felt bad all the time
+and I didn't know what to do.
+.
+.Pp
+we broke up 2 weeks after 2 years together.
+everything got worse.
+it wasn't a clean breakup.
+I was still clinging onto
+the familiar pieces of the relationship
+that had used to make me happy.
+they didn't anymore.
+it was torture.
+I lived in agony for months.
+I think I lost my mind a little,
+trying to handle things I couldn't.
+.
+.Pp
+in march I went no-contact.
+I started going to therapy.
+I went on a weekend trip to ottawa by myself.
+I looked at art in the national gallery.
+I started trying to become myself again.
+.
+.Pp
+in june I invited people out
+for my 30th birthday.
+I was terrified,
+convinced up until the last second
+that no one was going to come.
+but they did.
+and since then I've been doing better.
+I think I've picked up where I left off,
+at some point in the last few years.
+.
+.Sh AUTHORS
+.Nm june Aq Mt june@causal.agency
diff --git a/www/text.causal.agency/Makefile b/www/text.causal.agency/Makefile
index c5555274..c9e86ab2 100644
--- a/www/text.causal.agency/Makefile
+++ b/www/text.causal.agency/Makefile
@@ -38,15 +38,26 @@ TXTS += 035-addendum-2021.txt
 TXTS += 036-compassion.txt
 TXTS += 037-care.txt
 TXTS += 038-agency.txt
+TXTS += 039-apologies.txt
+TXTS += 040-sound-memory.txt
+TXTS += 041-albums-2022.txt
+TXTS += 042-comfort-music.txt
+TXTS += 043-little-blessings.txt
+TXTS += 044-film-review.txt
+TXTS += 045-time-2025.txt
 
 all: colb ${TXTS}
 
-.SUFFIXES: .7 .txt
+.SUFFIXES: .7 .fmt .txt
 
 .7.txt:
 	mandoc -T utf8 $< | ./colb > $@
 	touch -m -r $< $@
 
+.fmt.txt:
+	fmt $< | sed '1,/^$$/d' > $@
+	touch -m -r $< $@
+
 feed.atom: feed.sh colb ${TXTS}
 	sh feed.sh > feed.atom
 
diff --git a/www/text.causal.agency/feed.sh b/www/text.causal.agency/feed.sh
index 668046ef..71bbf662 100644
--- a/www/text.causal.agency/feed.sh
+++ b/www/text.causal.agency/feed.sh
@@ -27,6 +27,7 @@ set -- *.txt
 shift $(( $# - 20 ))
 for txt; do
 	entry="${txt%.txt}.7"
+	test -f "$entry" || entry="${txt%.txt}.fmt"
 	date=$(grep '^[.]Dd' "$entry" | cut -c 5-)
 	title=$(grep -m 1 '^[.]Nm' "$entry" | cut -c 5- | encode)
 	summary=$(grep '^[.]Nd' "$entry" | cut -c 5- | encode)
diff --git a/www/they.causal.agency/Makefile b/www/they.causal.agency/Makefile
new file mode 100644
index 00000000..81f47ea9
--- /dev/null
+++ b/www/they.causal.agency/Makefile
@@ -0,0 +1,4 @@
+ROOT = /var/www/man
+
+install: post-update.sh
+	install post-update.sh ${ROOT}/post-update
diff --git a/www/they.causal.agency/post-update.sh b/www/they.causal.agency/post-update.sh
new file mode 100644
index 00000000..db2d5936
--- /dev/null
+++ b/www/they.causal.agency/post-update.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+set -eu
+
+do_tree() {
+	tree=$1
+	manpath=$2
+	echo "Copying manuals for ${tree}..."
+	git ls-tree $tree | while read -r mode type hash name; do
+		if [ $type != blob ]; then
+			continue
+		fi
+		case "$name" in
+			(README.7)
+				continue
+				;;
+			(*.[1-9])
+				section=${name##*.}
+				mkdir -p /var/www/man/${manpath}/man${section}
+				git cat-file $type $hash \
+					>/var/www/man/${manpath}/man${section}/${name}
+				;;
+		esac
+	done
+	if test -d /var/www/man/${manpath}; then
+		makewhatis /var/www/man/${manpath}
+		if ! fgrep -q ${manpath} /var/www/man/manpath.conf; then
+			echo $manpath >>/var/www/man/manpath.conf
+			sort -o /var/www/man/manpath.conf /var/www/man/manpath.conf
+		fi
+	fi
+}
+
+do_tree HEAD HEAD
+
+repo=${PWD##*/}
+for tag in $(git tag); do
+	manpath=${repo%.git}-${tag}
+	if ! test -d /var/www/man/${manpath}; then
+		do_tree $tag $manpath
+	fi
+done