summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Alexander Hunt <me@liz.coffee>2026-07-24 19:23:50 -0700
committerElizabeth Alexander Hunt <me@liz.coffee>2026-07-24 19:51:41 -0700
commitbfd784b234588565b2b2f3d4c432000836e20f7b (patch)
tree2af532504f0a0fc28a4df99c465241e1a95b7eb6
parent4959e1cd05e7a01caaadacddf66f777c3a771641 (diff)
downloadlizdotcoffee-bfd784b234588565b2b2f3d4c432000836e20f7b.tar.gz
lizdotcoffee-bfd784b234588565b2b2f3d4c432000836e20f7b.zip
Adding some AI slop but it's okay because it looks really cool imo
-rw-r--r--html/assets/snow.js423
-rw-r--r--html/assets/so-much-drama-in-the-phd-monzy.mp3bin0 -> 1333376 bytes
-rw-r--r--html/index.php39
3 files changed, 451 insertions, 11 deletions
diff --git a/html/assets/snow.js b/html/assets/snow.js
new file mode 100644
index 0000000..ba9a46a
--- /dev/null
+++ b/html/assets/snow.js
@@ -0,0 +1,423 @@
+(function () {
+ var canvas = document.getElementById("snow");
+ var ctx = canvas.getContext("2d", { alpha: true });
+
+ // Cap the backing store on hi-dpi screens. Full 3x on a laptop panel
+ // triples the per-frame clear cost for detail nobody sees at 8px.
+ var DPR = Math.min(window.devicePixelRatio || 1, 2);
+
+ var W = 0, H = 0; // CSS pixels — all physics happens in this space
+ var rand = function (min, max) { return Math.random() * (max - min) + min; };
+
+ /* =============================================================
+ KOCH SNOWFLAKE SPRITES
+
+ Each flake is a Koch snowflake, rasterised once into a small
+ offscreen canvas and then blitted every frame — so the fractal
+ costs nothing at runtime no matter how deep we take it.
+
+ The Koch snowflake has 3-fold rotational symmetry (unlike a real
+ 6-armed crystal), so a sprite set has to cover 120° of rotation
+ rather than 60° to spin seamlessly.
+ ============================================================= */
+ var SIZES = [3, 6, 9, 13, 18]; // CSS px, across the circumcircle
+ var ROTATIONS = 12;
+ var ROT_SPAN = (Math.PI * 2) / 3;
+
+ var SHAPES = [
+ { fill: 0.18, line: 0.085, inner: false },
+ { fill: 0.32, line: 0.070, inner: true },
+ { fill: 0.08, line: 0.105, inner: false }
+ ];
+
+ // Unit-circumradius outlines, generated once per depth and reused
+ // for every size and rotation that needs them.
+ var kochCache = {};
+
+ function subdivide(ax, ay, bx, by, depth, out) {
+ if (depth === 0) {
+ out.push(ax, ay);
+ return;
+ }
+ var dx = (bx - ax) / 3, dy = (by - ay) / 3;
+ var p1x = ax + dx, p1y = ay + dy;
+ var p3x = ax + dx * 2, p3y = ay + dy * 2;
+
+ // Apex of the bump: perpendicular to the middle third, pushed out
+ // to equilateral height. Pick the sign that points away from the
+ // centre so bumps grow outward whatever the winding order.
+ var mx = (p1x + p3x) / 2, my = (p1y + p3y) / 2;
+ var ex = p3x - p1x, ey = p3y - p1y;
+ var elen = Math.sqrt(ex * ex + ey * ey);
+ var px = -ey / elen, py = ex / elen;
+ if (mx * px + my * py < 0) { px = -px; py = -py; }
+ var h = elen * Math.sqrt(3) / 2;
+ var p2x = mx + px * h, p2y = my + py * h;
+
+ subdivide(ax, ay, p1x, p1y, depth - 1, out);
+ subdivide(p1x, p1y, p2x, p2y, depth - 1, out);
+ subdivide(p2x, p2y, p3x, p3y, depth - 1, out);
+ subdivide(p3x, p3y, bx, by, depth - 1, out);
+ }
+
+ function kochOutline(depth) {
+ if (kochCache[depth]) return kochCache[depth];
+ var v = [];
+ for (var i = 0; i < 3; i++) {
+ var a = -Math.PI / 2 + i * (Math.PI * 2 / 3);
+ v.push(Math.cos(a), Math.sin(a));
+ }
+ var pts = [];
+ for (var s = 0; s < 3; s++) {
+ var n = (s + 1) % 3;
+ subdivide(v[s * 2], v[s * 2 + 1], v[n * 2], v[n * 2 + 1], depth, pts);
+ }
+ kochCache[depth] = pts;
+ return pts;
+ }
+
+ function tracePath(c, pts, R) {
+ c.beginPath();
+ c.moveTo(pts[0] * R, pts[1] * R);
+ for (var i = 2; i < pts.length; i += 2) {
+ c.lineTo(pts[i] * R, pts[i + 1] * R);
+ }
+ c.closePath();
+ }
+
+ // Iteration depth has to match the pixel budget: past roughly one
+ // segment per pixel the detail just turns to grey mush.
+ function depthForSize(sizeCss) {
+ if (sizeCss < 7) return 1;
+ if (sizeCss < 12) return 2;
+ return 3;
+ }
+
+ function drawKoch(c, R, p, sizeCss) {
+ var pts = kochOutline(depthForSize(sizeCss));
+
+ tracePath(c, pts, R);
+ c.fillStyle = "rgba(255,255,255," + p.fill + ")";
+ c.fill();
+
+ c.strokeStyle = "#ffffff";
+ c.lineWidth = Math.max(0.75, R * p.line);
+ c.lineJoin = "round";
+ c.stroke();
+
+ if (p.inner) {
+ // a second, counter-rotated flake inside — reads as facets
+ c.save();
+ c.rotate(Math.PI / 3);
+ tracePath(c, kochOutline(1), R * 0.42);
+ c.lineWidth = Math.max(0.6, R * 0.05);
+ c.stroke();
+ c.restore();
+ }
+ }
+
+ function makeSprite(sizeCss, shape, rotation) {
+ var pad = 2;
+ var dim = Math.ceil((sizeCss + pad * 2) * DPR);
+ var s = document.createElement("canvas");
+ s.width = dim;
+ s.height = dim;
+ var c = s.getContext("2d");
+
+ c.translate(dim / 2, dim / 2);
+ c.rotate(rotation);
+ var R = (sizeCss / 2) * DPR;
+
+ if (sizeCss < 4.5) {
+ // Below ~4px even one Koch iteration is mush. A soft disc reads
+ // better, and these are the distant flakes anyway.
+ var g = c.createRadialGradient(0, 0, 0, 0, 0, R);
+ g.addColorStop(0, "rgba(255,255,255,1)");
+ g.addColorStop(0.55, "rgba(255,255,255,0.9)");
+ g.addColorStop(1, "rgba(255,255,255,0)");
+ c.fillStyle = g;
+ c.beginPath();
+ c.arc(0, 0, R, 0, Math.PI * 2);
+ c.fill();
+ } else {
+ drawKoch(c, R, shape, sizeCss);
+ }
+ return s;
+ }
+
+ // sprites[shape][sizeTier] -> array of ROTATIONS canvases
+ var sprites = [];
+ for (var si = 0; si < SHAPES.length; si++) {
+ var byTier = [];
+ for (var ti = 0; ti < SIZES.length; ti++) {
+ var frames = [];
+ for (var ri = 0; ri < ROTATIONS; ri++) {
+ frames.push(makeSprite(SIZES[ti], SHAPES[si], (ri / ROTATIONS) * ROT_SPAN));
+ }
+ byTier.push(frames);
+ }
+ sprites.push(byTier);
+ }
+
+ /* =============================================================
+ VELOCITY FIELD
+
+ A coarse grid over the viewport. The cursor deposits momentum
+ into the cells it passes through; each frame that momentum
+ spreads to neighbours and decays, so a gust stays local to
+ where the cursor actually was.
+ ============================================================= */
+ var CELL = 44;
+ var cols = 0, rows = 0, gu, gv, tu, tv;
+
+ function buildGrid() {
+ cols = Math.ceil(W / CELL) + 2;
+ rows = Math.ceil(H / CELL) + 2;
+ var n = cols * rows;
+ gu = new Float32Array(n);
+ gv = new Float32Array(n);
+ tu = new Float32Array(n);
+ tv = new Float32Array(n);
+ }
+
+ var DEPOSIT_RADIUS = 3; // cells
+ var DEPOSIT_STRENGTH = 34; // px/s of field velocity per px of cursor travel
+ var CELL_MAX = 1400; // clamp so a fast flick can't explode the field
+ var DIFFUSION = 0.08; // neighbour blend per frame at 60fps
+ var DECAY_PER_SEC = 0.05; // fraction of a gust still present a second later
+
+ function deposit(px, py, dx, dy) {
+ var cx = px / CELL, cy = py / CELL;
+ var i0 = Math.max(0, Math.floor(cx) - DEPOSIT_RADIUS);
+ var i1 = Math.min(cols - 1, Math.floor(cx) + DEPOSIT_RADIUS);
+ var j0 = Math.max(0, Math.floor(cy) - DEPOSIT_RADIUS);
+ var j1 = Math.min(rows - 1, Math.floor(cy) + DEPOSIT_RADIUS);
+
+ for (var j = j0; j <= j1; j++) {
+ for (var i = i0; i <= i1; i++) {
+ var ddx = i - cx, ddy = j - cy;
+ var falloff = Math.exp(-(ddx * ddx + ddy * ddy) / (DEPOSIT_RADIUS * 0.75));
+ if (falloff < 0.01) continue;
+ var k = j * cols + i;
+ var u = gu[k] + dx * falloff * DEPOSIT_STRENGTH;
+ var v = gv[k] + dy * falloff * DEPOSIT_STRENGTH;
+ gu[k] = u > CELL_MAX ? CELL_MAX : (u < -CELL_MAX ? -CELL_MAX : u);
+ gv[k] = v > CELL_MAX ? CELL_MAX : (v < -CELL_MAX ? -CELL_MAX : v);
+ }
+ }
+ }
+
+ function relaxField(dt) {
+ var decay = Math.pow(DECAY_PER_SEC, dt);
+ var diffusion = Math.min(0.5, DIFFUSION * dt * 60);
+ for (var j = 0; j < rows; j++) {
+ for (var i = 0; i < cols; i++) {
+ var k = j * cols + i;
+ var su = 0, sv = 0, n = 0;
+ if (i > 0) { su += gu[k - 1]; sv += gv[k - 1]; n++; }
+ if (i < cols - 1) { su += gu[k + 1]; sv += gv[k + 1]; n++; }
+ if (j > 0) { su += gu[k - cols]; sv += gv[k - cols]; n++; }
+ if (j < rows - 1) { su += gu[k + cols]; sv += gv[k + cols]; n++; }
+ tu[k] = (gu[k] + (su / n - gu[k]) * diffusion) * decay;
+ tv[k] = (gv[k] + (sv / n - gv[k]) * diffusion) * decay;
+ }
+ }
+ var a = gu; gu = tu; tu = a;
+ var b = gv; gv = tv; tv = b;
+ }
+
+ var sampleU = 0, sampleV = 0;
+ function sampleField(px, py) {
+ var cx = px / CELL, cy = py / CELL;
+ var i = Math.floor(cx), j = Math.floor(cy);
+ if (i < 0) i = 0; else if (i > cols - 2) i = cols - 2;
+ if (j < 0) j = 0; else if (j > rows - 2) j = rows - 2;
+ var fx = cx - i, fy = cy - j;
+ if (fx < 0) fx = 0; else if (fx > 1) fx = 1;
+ if (fy < 0) fy = 0; else if (fy > 1) fy = 1;
+
+ var k = j * cols + i;
+ var w00 = (1 - fx) * (1 - fy), w10 = fx * (1 - fy);
+ var w01 = (1 - fx) * fy, w11 = fx * fy;
+
+ sampleU = gu[k] * w00 + gu[k + 1] * w10 + gu[k + cols] * w01 + gu[k + cols + 1] * w11;
+ sampleV = gv[k] * w00 + gv[k + 1] * w10 + gv[k + cols] * w01 + gv[k + cols + 1] * w11;
+ }
+
+ /* =============================================================
+ AMBIENT AIR
+
+ Turbulence comes from the curl of a stream function built out of
+ two octaves of sine. Taking the curl makes the field
+ divergence-free — air can swirl but never pile up or vanish —
+ which is most of the difference between "windy" and "jelly".
+ The coarse octave drifts slowly, the fine one churns fast.
+ ============================================================= */
+ var O1 = { kx: 0.0042, ky: 0.0058, amp: 12500, sx: 0.25, sy: -0.19 };
+ var O2 = { kx: 0.0125, ky: 0.0155, amp: 2400, sx: -0.62, sy: 0.78 };
+ var U1 = O1.amp * O1.ky, V1 = O1.amp * O1.kx;
+ var U2 = O2.amp * O2.ky, V2 = O2.amp * O2.kx;
+
+ // per-frame time offsets, hoisted out of the flake loop
+ var p1a = 0, p1b = 0, p2a = 0, p2b = 0;
+ var gustX = 0, gustY = 0;
+
+ function updateAir(t) {
+ p1a = t * O1.sx; p1b = t * O1.sy;
+ p2a = t * O2.sx; p2b = t * O2.sy;
+
+ // Gusts: three detuned sines summed, then squared with the sign
+ // kept. Squaring is what makes it gust rather than sway — long
+ // lulls near zero, then a sharp shove.
+ var env = Math.sin(t * 0.17) + 0.55 * Math.sin(t * 0.29 + 2.1)
+ + 0.30 * Math.sin(t * 0.53 + 4.4);
+ var gust = env > 0 ? env * env : -env * env;
+ gustX = gust * 46 + Math.sin(t * 0.041) * 18;
+ gustY = gust * Math.sin(t * 0.37 + 1.3) * 8;
+ }
+
+ var airU = 0, airV = 0;
+ function sampleAir(x, y) {
+ var a1 = x * O1.kx + p1a, b1 = y * O1.ky + p1b;
+ var a2 = x * O2.kx + p2a, b2 = y * O2.ky + p2b;
+ airU = U1 * Math.sin(a1) * Math.cos(b1) + U2 * Math.sin(a2) * Math.cos(b2) + gustX;
+ airV = -V1 * Math.cos(a1) * Math.sin(b1) - V2 * Math.cos(a2) * Math.sin(b2) + gustY;
+ }
+
+ /* ============================ flakes ============================ */
+ var flakes = [];
+
+ function buildFlakes() {
+ // scale the count to the viewport so a small window isn't overdrawn
+ var count = Math.round(Math.min(170, Math.max(55, (W * H) / 11000)));
+ flakes.length = 0;
+ for (var i = 0; i < count; i++) {
+ var depth = Math.random();
+ var tier = Math.min(SIZES.length - 1, Math.floor(depth * SIZES.length));
+ flakes.push({
+ frames: sprites[(Math.random() * SHAPES.length) | 0][tier],
+ half: 0,
+ x: rand(0, W),
+ // seed across the whole viewport, already at terminal speed, so
+ // the first frame looks like snow that has been falling a while
+ // rather than a batch dumped in from above
+ y: rand(-40, H),
+ vx: 0,
+ vy: 12 + depth * 30,
+ fall: 12 + depth * 30, // base fall speed, px/s
+ // fast, shallow flutter — the individual tumble of one crystal,
+ // distinct from the air it is riding in
+ sway: rand(0, Math.PI * 2),
+ swaySpeed: rand(2.2, 4.4),
+ swayAmp: 3 + (1 - depth) * 7,
+ spin: rand(-0.3, 0.3), // sprite cycles/sec (1 cycle = 120°)
+ angle: Math.random(),
+ alpha: 0.3 + depth * 0.55,
+ // Light flakes catch more of the wind AND change speed faster;
+ // heavy ones plough through. That split is what sells it as air
+ // rather than everything sloshing in unison.
+ response: 0.5 + (1 - depth) * 0.85,
+ drag: 3.0 + (1 - depth) * 6.5 // velocity response rate, per second
+ });
+ var f = flakes[i];
+ f.half = f.frames[0].width / 2;
+ }
+ }
+
+ function resize() {
+ W = window.innerWidth;
+ H = window.innerHeight;
+ canvas.width = Math.round(W * DPR);
+ canvas.height = Math.round(H * DPR);
+ buildGrid();
+ if (!flakes.length) buildFlakes();
+ }
+
+ /* ============================= input ============================ */
+ var lastX = null, lastY = null;
+
+ window.addEventListener("pointermove", function (e) {
+ var x = e.clientX, y = e.clientY;
+ if (lastX !== null) {
+ var dx = x - lastX, dy = y - lastY;
+ var dist = Math.sqrt(dx * dx + dy * dy);
+ // walk the segment so a fast flick paints one continuous gust
+ // instead of two disconnected puffs
+ var steps = Math.min(10, Math.max(1, Math.round(dist / (CELL * 0.5))));
+ for (var s = 1; s <= steps; s++) {
+ var t = s / steps;
+ deposit(lastX + dx * t, lastY + dy * t, dx / steps, dy / steps);
+ }
+ }
+ lastX = x;
+ lastY = y;
+ }, { passive: true });
+
+ window.addEventListener("pointerleave", function () { lastX = lastY = null; });
+ window.addEventListener("resize", resize);
+
+ /* ============================== loop ============================ */
+ var lastT = null, raf = 0;
+
+ function step(now) {
+ if (lastT === null) lastT = now;
+ var dt = Math.min((now - lastT) / 1000, 0.05);
+ lastT = now;
+
+ relaxField(dt);
+ updateAir(now / 1000);
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ for (var i = 0; i < flakes.length; i++) {
+ var f = flakes[i];
+ sampleField(f.x, f.y); // cursor gusts
+ sampleAir(f.x, f.y); // ambient turbulence
+
+ f.sway += f.swaySpeed * dt;
+ var targetVX = (airU + sampleU) * f.response + Math.sin(f.sway) * f.swayAmp;
+ var targetVY = (airV + sampleV) * f.response + f.fall;
+
+ // exponential approach: correct at any timestep, and the rate is
+ // per-flake so light and heavy crystals visibly disagree
+ var ease = 1 - Math.exp(-f.drag * dt);
+ f.vx += (targetVX - f.vx) * ease;
+ f.vy += (targetVY - f.vy) * ease;
+
+ f.x += f.vx * dt;
+ f.y += f.vy * dt;
+
+ // tumble harder the faster the air is dragging past
+ f.angle += (f.spin + f.vx * 0.006) * dt;
+
+ if (f.y > H + 20) { f.y = -20; f.x = rand(0, W); f.vy = f.fall; }
+ else if (f.y < -80) { f.y = H + 20; }
+ if (f.x < -30) f.x = W + 30;
+ else if (f.x > W + 30) f.x = -30;
+
+ var ri = ((f.angle * ROTATIONS) | 0) % ROTATIONS;
+ if (ri < 0) ri += ROTATIONS;
+
+ ctx.globalAlpha = f.alpha;
+ // integer destination + unscaled source = fastest blit path
+ ctx.drawImage(f.frames[ri], (f.x * DPR - f.half) | 0, (f.y * DPR - f.half) | 0);
+ }
+
+ ctx.globalAlpha = 1;
+ raf = requestAnimationFrame(step);
+ }
+
+ // don't burn battery in a background tab
+ document.addEventListener("visibilitychange", function () {
+ if (document.hidden) {
+ cancelAnimationFrame(raf);
+ raf = 0;
+ } else if (!raf) {
+ lastT = null;
+ raf = requestAnimationFrame(step);
+ }
+ });
+
+ resize();
+ raf = requestAnimationFrame(step);
+})();
diff --git a/html/assets/so-much-drama-in-the-phd-monzy.mp3 b/html/assets/so-much-drama-in-the-phd-monzy.mp3
new file mode 100644
index 0000000..5fecfbb
--- /dev/null
+++ b/html/assets/so-much-drama-in-the-phd-monzy.mp3
Binary files differ
diff --git a/html/index.php b/html/index.php
index f2afded..5fff408 100644
--- a/html/index.php
+++ b/html/index.php
@@ -67,6 +67,9 @@ $penguin = implode(' ', array_map($gen, range(1, $n)));
--fg: white;
--a: #c4d0c8;
--h: #7d978c;
+ /* held back so a bright flake drifting behind white text
+ never fights it for contrast */
+ --snow: 0.5;
}
* {
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" width="32" height="24"><path d="M1 3h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v2H9v1h1v2h1v2h-1v1H8v-1H7v-2H6v-2H5v1H4v1H3v1H1" fill="%23ff69b4" stroke="%23b19cd9" stroke-width="0.5"/><path d="M2 5h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1H8v2h1v2h1v2H8v-2H7v-2H6v-1H5v1H4v1H3v1H2" fill="white" stroke="%23ff69b4" stroke-width="0.5"/></svg>')
@@ -100,9 +103,23 @@ $penguin = implode(' ', array_map($gen, range(1, $n)));
.pro:target {
scroll-margin-top: 40vh;
}
+ #snow {
+ display: block;
+ position: fixed;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ /* behind the text but still above the propagated body background */
+ z-index: -1;
+ /* never eat a click; window listeners still get the cursor */
+ pointer-events: none;
+ opacity: var(--snow);
+ }
</style>
</head>
<body>
+<canvas id="snow" aria-hidden="true"></canvas>
+<main>
<h1><?= pronoun('Elizabeth', 'Alexander') ?> Hunt (<?= pronoun('she', 'he') ?>) (ca 2003)</h1>
<div>
[me at <a href="https://en.wikipedia.org/wiki/Logarithmic_integral_function" target="_blank">li(z)</a> dot coffee (<a class="tt" href="mailto:me@liz.coffee">mailto</a>)] ·
@@ -121,14 +138,20 @@ $penguin = implode(' ', array_map($gen, range(1, $n)));
<div><p><?= pronoun('She', 'He') ?> is a programmer at a Big Ol' Tech™ company in Seattle (unfortunately, probably the one you're thinking of). And will continue to refer to <?= pronoun('her', 'him') ?>self in the third person for the remainder of this World Wide Web document, which you find yourself laying your eyes on. And in so doing, probably don't yet understand the context of the next words you are about to read. Except, that they are there. They take up space. And reading through them consumes YOUR oh-so-precious time - of which sup ∈ ℝ<sup>+</sup>, need <?= pronoun('she', 'he') ?> remind you.</p><div>
<div><p id="genpenguin"><?= htmlspecialchars($penguin) ?></p></div>
- <p><?= pronoun('She', 'He') ?> enjoys recreational programming and searching the space of math and CS outside of work; some of <?= pronoun('her', 'his') ?> current interests are in PLs, type theory, game programming (&lt;3 pico8), and homelab stuff. <?= pronoun('She', 'He') ?> was an advocate of LISPy stuff while president of the <a href="https://linux.usu.edu">USU Free Software and Linux Club</a>, <?= pronoun('she', 'he') ?> still can't shake off Emacs and probably should augment <?= pronoun('her', 'his') ?> <a href="assets/emacs.png">hand accordingly</a>.</p>
+ <p><?= pronoun('She', 'He') ?> enjoys recreational programming and searching the space of math and CS outside of work; some of <?= pronoun('her', 'his') ?> current interests are in PLs and type theory (trying to figure out what exactly, but reads about continuations, gradual typing, effects, STLC, etc), game programming (&lt;3 pico8) but can never <a href="https://code.liz.coffee/dyl8.git">actually</a> <a href="https://code.liz.coffee/the-abstraction-engine-ts.git/">finish</a> <a href="https://code.liz.coffee/jumpstorm.git">one</a>, homelab stuff. <?= pronoun('She', 'He') ?> was an advocate of LISPy stuff while president of the <a href="https://linux.usu.edu">USU Free Software and Linux Club</a>, <?= pronoun('she', 'he') ?> still can't shake off Emacs and probably should augment <?= pronoun('her', 'his') ?> <a href="assets/emacs.png">hand accordingly</a>.</p>
<p>When not doing computer stuff, <?= pronoun('she', 'he') ?> runs, bikes, watches anime, rides trains, reads, or bakes bread. <?= pronoun('She', 'He') ?> may be heard practicing violin or speedcubing. Penguins are absolutely fascinating to <?= pronoun('her', 'him') ?>, and <?= pronoun('she', 'he') ?> collects books on said species as well as plenty of stuffed animals, and visits them when possible. Maybe <?= pronoun('she', 'he') ?> will one day <span style="display: none">(soon?) </span>reincarnate to be one.</p>
- <p><?= pronoun('She', 'He') ?> is a vegetarian (except for when it comes to billionaires) arch user (you know because <?= pronoun('she', 'he') ?> just told you), bicycle, poly, and a bit fucked up.</p>
+ <p>Other notable things about <?= pronoun('her', 'him') ?> are that <?= pronoun('she', 'he') ?> is vegetarian (except for when it comes to billionaires), bicycle, poly, terrible at <?= pronoun('her', 'his') ?> job, and a bit fucked up.</p>
<p>Gus! Bread!</p>
<div><img src="assets/gus.jpeg" width="300"> <img src="assets/bread.jpeg" width="300"></div>
+ <hr>
+ <div>
+ <p>I jam to this so hard so I'm putting it on my website:</p>
+ <audio controls src="assets/so-much-drama-in-the-phd-monzy.mp3"></audio>
+ <p><i><a href="https://www.youtube.com/embed/JimUMZTiAjo" target="_blank">So Much Drama In The PhD - Monzy</a></i></p>
+ </div>
<div style="display: flex; align-items: center; justify-content: center;"><img width="100" src="assets/penguin.gif"><a href="https://liz.coffee"><img src="https://adelie.liz.coffee/img/blinkies/liz--coffee.gif" width="150" height="20"></a></div>
<div>
<?php if ($commit !== ''): ?>
@@ -136,14 +159,8 @@ $penguin = implode(' ', array_map($gen, range(1, $n)));
<?php endif; ?>
built with <a href="https://code.liz.coffee/ci.git/tree/">lizCI</a>
</div>
- <hr>
-
- <br>
- <div>
- <p>I jam to this so hard so I'm putting it on my website:</p>
- <iframe width="280" height="auto" src="https://www.youtube.com/embed/JimUMZTiAjo?si=WTerhBRHan-5IcuG" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
- </div>
-
- <script src="assets/oneko.js"></script>
+</main>
+<script src="assets/oneko.js"></script>
+<script src="assets/snow.js"></script>
</body>
</html>