<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-AU" xml:base="https://chrismorgan.info/">
	<id>https://chrismorgan.info/meta.feed</id>
	<author>
		<name>Chris Morgan</name>
		<uri>https://chrismorgan.info</uri>
		<email>me@chrismorgan.info</email>
	</author>
	<updated>2026-05-08T20:00:01+05:30</updated>
	<link href="/meta.feed" rel="self" type="application/atom+xml"/>
	<link href="/meta"/>
	<title type="html">Chris Morgan’s pages tagged Meta</title>
	<subtitle type="html">stuff about this site</subtitle>
	<entry>
		<title type="html">I’ve banned query strings</title>
		<published>2026-05-08T20:00:01+05:30</published>
		<updated>2026-05-08T20:00:01+05:30</updated>
		<link href="no-query-strings" type="text/html"/>
		<id>https://chrismorgan.info/no-query-strings</id>
		<content type="html"><![CDATA[<p>I don’t like people adding tracking stuff to URLs.
<br class=w>Still less do I like people adding tracking stuff to <em>my</em> URLs.

<p><span class=path>https://chrismorgan.info<wbr>/no-query-strings<wbr><mark>?ref=example.com</mark></span>? Did I ask?
<br class=w>If I wanted to know I’d look at the <code>Referer</code> header;
and if it isn’t there, it’s probably for a good reason.
<br class=w>You abuse your users by adding that to the link.

<p><span class=path>https://chrismorgan.info<wbr>/no-query-strings<wbr><mark>?utm_source=example&amp;utm_<i class=unimportant>&c.</i></mark></span>?
<br class=w>Hey! That one’s even worse, <a href=https://en.wikipedia.org/wiki/UTM_parameters>UTM parameters</a> are for <em>me</em> to use, not <em>you</em>.
<br class=w>Leave my URLs alone.

<p>So I’ve decided to try a blanket ban for this site:
<strong>no unauthorised query strings</strong>.

<p>At present I don’t use any query strings.
<br class=w>If I ever start using any query strings, I’ll allow only known parameters.
<br class=w>(In past times I used <span class=path>?t=…</span> and <span class=path>?h=…</span> cache-busting URLs for stylesheet URLs;
<br class=w>and I decided I’m okay breaking such requests; there shouldn’t be any legitimate ones.)

<p>Want to see what happens if you add a query string<a href=?>?</a>
Go ahead, try it.
<!-- One fun idea for an *empty* query: serve the original file, but with all text . and ! changed to ? -->

<p>It’s my website: I can do what I want with it.
<p>And you can do what you want with yours!

<p><em>This is currently implemented <a href=Caddyfile#?>in my Caddyfile</a>.</em>

<aside>
	<h2>Aside: this page’s URL</h2>
	<p>It was <em>very</em> tempting to publish this at <span class=path>https://chrismorgan.info/?</span>
	(path «<span class=path></span>», query <span class=path></span><!-- Despite what many tools may assume, empty string ≠ null -->).

	<p>This would have broken a lot of common but incorrect assumptions, and made some tools cry:
	<br class=w><code>curl</code>, for example, seems to illegitimately strip a trailing question mark
	<br class=w>(could be only for the command line, didn’t test library usage).

	<p>In the end, I decided to respect the notion of path and have mercy on people.
	<br class=w>Myself most of all;
	I’m already pushing Caddy in enough directions it’s not happy with.

	<p>My next plan was to publish it at <span class=path>/%3F</span>
	(path «<span class=path>?</span>», query null),
	<br class=w>but I guess no one has ever tried doing such stupid things with Caddy before;
	<br class=w><a href=https://github.com/caddyserver/caddy/issues/7678 title="Caddy issue #7678: try_files mangles paths containing characters like ? and % (whereas file_server without the rewrite works properly)">if <code><b>try_files</b></code> rewriting is involved it can’t cope</a>.

	<p>So <span class=path>/no-query-strings</span> will do.
	<br class=w>I’ll probably use <span class=path>/?</span> or <span class=path>/%3F</span> later for something <em>else</em> about query strings.
</aside>]]></content>
		<category term="web" label="Web"/>
		<category term="opinions" label="Opinions"/>
		<category term="meta=only" label="Meta only"/>
	</entry>
	<entry>
		<title type="html">&lt;strong&gt;Making my new website:&lt;&#x2f;strong&gt;&lt;br class=w&gt; under construction;&lt;br class=w&gt; poke, prod and play</title>
		<published>2026-05-08T20:00:00+05:30</published>
		<updated>2026-05-08T20:00:00+05:30</updated>
		<link href="2026-website" type="text/html"/>
		<id>https://chrismorgan.info/2026-website</id>
		<content type="html"><![CDATA[<section id=history>
	<h2>History</h2>
	<p>In 2013, <a href=/blog/first-post/>I started my website, using Hyde</a>.
	<p>In 2016–2018, I reimplemented the site on Lektor and redesigned it twice (including migrating all content, which was actually fairly involved in at least one of the designs), but decided I didn’t like the direction and never shipped it (although it was genuinely finished).
	<p>In 2019, <a href=/blog/2019-website/>I migrated to Zola</a>, making various changes but keeping all content.
	<p>I kept on writing various things over time—I have a bunch of half-finished draft articles and a very long file of stubs in various stages of completion—but found the bar of polish I’d set to be too high, and didn’t end up publishing much.
	<p>I became fed up with Zola, the constraints of other people’s static site generators, and the traditional structure of my site altogether.
	<p>In 2026, I’ve decided to <a href=pre-2026-index.html>archive all the old site and content</a>, because I have a significantly different vision of what I want now.
	<p>Existing URLs of all public resources (HTML, Atom, SVG) and most subresources will continue to work, but at least for now they won’t be integrated with the new site, and may never be updated.
	<p>I’m abandoning all <span class=path>/blog/…</span> structure (<span class=path>/blog/</span>, <span class=path>/blog/<var>slug</var>/</span>, <span class=path>/blog/tags/<var>slug</var>/{,feed.xml}</span>).
	<p><span class=path>/feed.xml</span> will continue to work, redirecting to its new name <a href=/*.feed>/*.feed</a>. I have archived all the existing feed items to a second page (it had grown to 425 kB and was possibly my most-requested file), and I will probably paginate my feeds more in the future. (I don’t know how many feed readers support pagination, but the content normal people want to fetch will always be on page one, so it doesn’t matter anyway—it’s more a foolish perfectionism that has me keeping <em>all</em> content theoretically accessible through feeds.)
	<p>Theming is <a href=#prefs>implemented differently now</a>, and I decided not to migrate settings transparently (it’d slightly help a very few people, at a small cost to everyone). I patched the old site to read the new settings, but I removed its theme switcher out of laziness.
</section>

<section id=new>
	<h2>New direction</h2>
	<section id=structure>
		<h3>Structure</h3>
		<p><strong>Old site:</strong> other than a few undated pages of special purpose (e.g. <span class=path>/about/</span>, <span class=path>/hire-me/</span>),
		all content was in <span class=path>/blog/<var>slug</var>/</span>.
		I used a single taxonomy, <span class=path>/blog/tags/<var>slug</var>/</span>.
		<p>This exposed a philosophical aspect, too:
		almost all was blog post, private until complete, then published, with minimal updates (only for corrections or changes like updating rustc output—<a href=/blog/rust-fizzbuzz/>rust-fizzbuzz</a> was the most-updated by far over the years, as rustc changes several times required more extensive updates, because it broke a point that I was making, due to introducing non-lexical lifetimes, or automatically hoisting temporary values, or no longer mentioning lifetimes in some error messages—several changes that improved the language and the beginner experience, but at a significant cost to mental model pedagogy, quite possibly making it harder to become expert).
		Overall, it tended toward append-only.
		<p><strong>New site:</strong> I want to play with a different approach: no more tags/categories/taxonomies, I’m aiming for a directed graph with edges powered by link relations (<code><b>&lt;link rel></b></code> and <code><b>&lt;a rel></b></code>). This is more flexible than mere tags: you get subtags (up), series (prev/next/first/last), and tags are themselves pages in a more real sense than with tags (even though I already made my tag pages more custom than most ever do, fighting against Zola to do so).
		<p>Also, it’ll be a bit more reminiscent of a wiki. Often editing existing content rather than appending a new blog posts. A lot more small pages and interlinking. Also publishing incomplete stuff, so marked, and stuff in different stages of polish.
		<p>(A lot of this architecture has been implemented, but the full implications are not yet apparent mostly due to a lack of content.)
	</section>
	<section id=urls>
		<h3>URL styles</h3>
		<p><strong>Old URLs</strong> all ended with a trailing slash, and were mostly <span class=path>/blog/<var>slug</var>/</span>.
		<p><strong>New URLs</strong> are, at least for now, entirely flat, just <span class=path>/<var>slug</var></span>. I may introduce other patterns in the future.
		<p>Thus <span class=path>/</span> itself was the only conflicting URL, so I moved it to <a class=path href=pre-2026-index.html>/pre-2026-index.html</a>.
		<p>Hierarchical URLs work well when your structure is tree-based. Mine is not going to be, any more. Many pages will definitely have a primary parent, but at least for now I don’t want that to influence URLs.
		<p>I considered timestamped URLs (such as <span class=path>/2026/04/07/2026-website</span>), but decided against them. A lot of content will not be dated, and the dividing line between dated and undated content will not always be clear and may change.
	</section>
	<section id=prefs>
		<h3>How preferences are stored, and the implications</h3>
		<p>Settings previously used Local Storage. Now I’m using cookies for a couple of few reasons:
		<ol style=margin-top:0>
			<li><p>Dev mode (which I’ll get back to below) <em>has</em> to be a cookie,
				so the server can send back a different response from the same URL.
				(Alternatives: serve content from multiple URLs,
				or depend on a service worker that proxies requests,
				adding an extra header when in dev mode.)
			<li><p>I want it to work as completely as possible <em>without JavaScript</em>.
				It’s easy enough to lean on <code><i>:checked</i></code> to change styles with only CSS,
				but you can’t <em>persist</em> them without either JS or a form submission that returns cookies.
		</ol>
		<p>Earlier on I was also thinking about the <em>most reliable</em> ways to avoid a Flash of Unstyled Content
			(FOUC; most notably, a brief flash of white background when in dark mode),
			and that the approach I was formerly using required the prefs elements and scripting to be early in the document,
			whereas I’d like to shift them after the content,
			but I ended up realising that, without persistence and thus with JS permitted for applying prefs from local storage,
			there were alternatives to the way I was doing it.
			<p>In the end, it’s nicest for the client if the server sets the load-time preferences in <code><b>&lt;html class=</b><span class="s unimportant">…</span> <nobr><b>style=</b><span class="s unimportant">…</span><b>></b></nobr></code>.
		<p>The <a href=Caddyfile#supporting-theming>Caddy configuration</a> and <a href=Caddyfile#templating-theming>templating</a> to support this isn’t <em>too</em> bad. My loosely-golfed JavaScript, however, is: partly deliberately so, even though the main reason I shrank it so aggressively was because it had to be early in the document at that time (I only implemented the server actually filling in the prefs on root attributes in the day before publishing).
		<p>Then the prefs elements and styles and all can come at the end, which is especially best for browsers that don’t support <code><b>&lt;dialog></b></code> or <code><b>popover</b></code> (old browsers, and all current text-mode browsers that I know of).
	</section>

	<p>I have Ideas.
</section>

<section id=under-construction>
	<h2>🚧 Under construction 🏗️</h2>
	<p>Do you remember those <a href=http://textfiles.com/underconstruction/>“under construction” banners and GIFs of the ’90s</a> <span class=unimportant>(loads 941 images totalling 9 MB)</span>? They tend to depict a work site and the notion of caution or danger.
	<p>A <em>building</em> construction zone is a dangerous place: there will be things to trip over, fall into, and have fall on you.
	<p>Children may still want to play there, because things are <em>interesting</em>.
	<p>A <em>website</em> construction zone is a place where you can safely poke and prod.
	<p>This site is very much under construction. Come take a look! <code><a href=view-source:https://chrismorgan.info title="Your user agent probably stops you from clicking this link.
Maybe even from copying it.
C’est la vie.">view-source:</a></code> everything! Have fun!
	<p>Oh? You say it all looks minified?
	Turn on <strong>dev mode</strong> in the <button popovertarget=_prefs aria-label="Appearance" title="Change site appearance"><span aria-hidden=true style=font-family:sans-serif;opacity:.4>🎨 A<i style=font-family:serif>a</i></span> prefs panel</button> (or set the cookie <code>dev=1</code> by some other means) and the server will give it to you unminified.
</section>

<section id=plans>
	<h2>Glimpses of my plans</h2>
	<p>What’s implemented so far doesn’t actually reflect my <em>ultimate</em> plans very well—it’s still <em>comparatively</em> traditional.
	<p>I also haven’t integrated my own lightweight markup language into it yet, although it will be central in the long run. But although I’ve been doing all my writing in it for five years, I only got serious about <em>implementing</em> it last year and haven’t finished.
	<p>You’ll see my grand plan as it unfolds, but I’ll mention a few things:
	<ul>
		<li>Full pure-CSS 3D environment. Content will be integrated into this.
		<li>Interface reminiscent of a point-and-click adventure (where screen size permits).
		<li>3D, page-turning books that work better than any demo you’ve ever seen (including things like your browser’s find-in-page <em>actually working</em>, taking you to the correct page).
		<li>Handwritten/-drawn content (don’t worry, as accessible as I can make it, and you’ll generally be able to opt for regular text if you don’t like my handwriting, though it might lose <i>panache</i> or a little more).
		<li>A pipe organ (which will, alas, require some JavaScript). Turns out you can synthesise a surprisingly realistic pipe organ in surprisingly little code, under a hundred lines with the Web Audio API. Like physical pipe organs, the actual sound is the easy part, it’s the orchestration of keys, stops, ranks and such that’s… not even <em>hard</em>, just fiddly.
		<li>Multiplayer experiences, including me idling in the virtual space a lot of the time, contactable by visitors.
			(I have plans that will build on this concept and do very useful things with it. Realism suggests I won’t get that far until at least 2028.)
	</ul>
</section>

<section id=archaeology>
	<h2>Something I made a decade ago</h2>
	<p>It was a small part of an idea I had for an about page, partly to do with getting a job.
	I never actually shared it anywhere.
	Other than changing mouseover/mouseout to mouseenter/mouseleave (<em>almost</em> enough to tick off “Fix all the weird glitches with it”)
	and a few small tweaks to reconcile it with my site styles
	(including namespacing classes to this page, for future compatibility),
	I have not modified it at all.
	I would implement it better now, especially around making it render better when zoomed in.
	Click on it! Hover the clock! Sorry mobile users, it won’t work particularly well for you.
	<noscript><ins>(And as for you, not running JavaScript… no interactivity or correct clock time for you.)</ins></noscript>
	<figure onclick="toggleZoom(this.querySelector('.x-room-container'))" class=x-demo>
		<div class=x-room-container>
			<div class=x-room>
				<div class=x-floor>
					<svg class=x-person stroke=#000 stroke-width=3 fill=none viewBox="0 -3 50 103" width=2em>
						<path d="m2e-5,40 50,0M25,65 25,25m-15,75 15,-35 15,35"/>
						<circle cx=25 cy=13 r=12.5 fill=white />
					</svg>
					<svg class=x-person stroke=#000 stroke-width=3 fill=none viewBox="0 -3 50 103" width=2em>
						<path d="m2e-5,40 50,0M25,65 25,25m-15,75 15,-35 15,35"/>
						<circle cx=25 cy=13 r=12.5 fill=white />
					</svg>
				</div>
				<div class="x-wall x-left-wall"><div class=x-painting><span class=x-tree>🎄</span><span class=x-snowman>⛇</span><span class=x-sun>☻</span></div></div>
				<div class="x-wall x-right-wall">

					<div class=x-whiteboard>
						<div class=x-writing>
							This whiteboard intentionally left not blank.<br>
							<ul>
								<li class=x-done>Construct a basic 3D scene with CSS
								<li class=x-done title="OK, so it uses the tiniest of JavaScript snippets to set the time initially.">Construct a pure-CSS clock <sup>†</sup>
								<li class=x-done>Write down a list of tasks
								<li>Fix all the weird glitches with it
								<!-- A couple that I know of:
								- Glitches in hover where it thinks the mouse has moved out when it hasn’t, mostly in Firefox. Putting the mousemove on a dummy 2D hover target over it would probably fix that one, though at the cost of hover events not getting through to what’s underneath.
								- With the 3D transformations active, invisible content overflowing the page width, so that you get a horizontal scrollbar, even though nothing is *actually* going over there. An overflow: hidden; on the appropriate element would handle it, but it would break my posh background overlay technique.
								Tradeoffs, tradeoffs. And deplorable laziness.
								[These remarks were from initial implementation time. I mostly fixed the first one by switching to mouseenter/mouseleave, and the appropriate solution for the second one is a little different now.]
								-->
								<li>Get hired for this demonstration of the impractical
							</ul>
						</div>
						<div class=x-marker-holder>
							<div class=x-marker></div>
							<div class=x-marker></div>
						</div>
					</div>
					<div class=x-clock>
						<div class=x-frame-face></div>
						<div class=x-ticks><div class=x-tick><div class=x-tick><div class=x-tick><div class=x-tick><div class=x-tick><div class=x-tick><div class=x-tick><div class=x-tick><div class=x-tick><div class=x-tick><div class=x-tick><div class=x-tick></div></div></div></div></div></div></div></div></div></div></div></div></div>
						<div class="x-hand x-hours"></div>
						<div class="x-hand x-minutes"></div>
						<div class="x-hand x-seconds"></div>
					</div>
				</div>
			</div>
		</div>

		<style>
			/* The whole x-demo element is new in 2026. */
			.x-demo {
				margin-inline: 0;
				display: flex;
				justify-content: center;
			}

			.x-room-container {
				display: inline-block;
				padding: 2.1em;
				/* At 768px, ~14px; at 1536px, ~28px; cap at 30px. */
				font-size: 1.8vw;
				transition: transform .2s;
				position: relative;
				overflow: hidden;
				/* This is the line I added in 2026. I like my general dynamic line-height incantation, use something like it on about every site. */
				& *, & ::before, & ::after { line-height: initial; }
			}

			/* max-font-size: 30px; */
			@media screen and (min-width: 1667px) {
				.x-room-container {
					font-size: 30px;
				}
			}

			/* Below 768px, the column layout stops, so we could fill the whole viewport width if we wanted to. (We don’t on the larger side of this screen size.) */
			@media screen and (max-width: 767px) {
				.x-room-container {
					font-size: 30px;
				}
			}

			/* … but once it gets small enough, we do. */
			@media screen and (max-width: 540px) {
				.x-room-container {
					font-size: 5.5vw;
				}
			}

			.x-room-container/*:not(.x-zoom)*/ {
				/* padding: 2.1em; should be enough, but to prevent a bogus horizontal scrollbar for the page in some circumstances I want to be able to apply overflow: hidden; which then with the camera position changing needs more vertical padding */
				padding: 3.2em 2.1em;
				margin-top: -.9em;
				margin-bottom: -.9em;
			}

			.x-room-container::before {
				content: "";
				position: absolute;
				top: 0;
				right: 0;
				bottom: 0;
				left: 0;
				transform-style: preserve-3d;
				transform: translateZ(-100em);
				transition: .3s box-shadow;
				border-radius: 50%;
				box-shadow: 0 0 0 0 rgba(0,0,0,0),
							inset 0 0 0 10em rgba(0,0,0,0);
			}

			.x-room-container.x-zoom::before {
				box-shadow: 0 0 0 12em rgba(0,0,0,.5),
							inset 0 0 0 10em rgba(0,0,0,.5);
			}

			.x-room-container, .x-room-container * {
				transform-style: preserve-3d;
			}

			.x-room {
				position: relative;
				transform: rotateX(60deg) rotateZ(45deg);
				transform-origin: 50% 50% 4em;
			}

			.x-room::before {
			}

			.x-room * {
				box-sizing: border-box;
			}

			.x-floor, .x-wall {
				width: 10em;
				height: 10em;
				padding: .5em;
			}

			.x-wall {
				position: absolute;
				/* floor height */
				margin-top: -10em;
			}

			.x-room > * {
				display: flex;
				align-items: center;
				justify-content: center;
			}

			.x-floor {
				font-weight: 900;
				color: white;
				background-color: #393939;
				background-image: linear-gradient(45deg, #333 25%, transparent 25%, transparent 75%, #333 75%, #333),
									linear-gradient(45deg, #333 25%, transparent 25%, transparent 75%, #333 75%, #333);
				background-size: 2em 2em;
				background-position: 0 0, 1em 1em;
				align-items: flex-start;
				justify-content: space-around;
			}
			.x-left-wall {
				transform: rotateZ(-90deg) translateX(-2em) rotateX(-90deg);
				transform-origin: 0 8em;
				background: #e0e0d8;
				height: 8em;
			}
			.x-right-wall {
				transform-origin: 0 8em;
				transform: translateY(-8em) rotateX(-90deg);
				background: #d0d0c8;
				height: 8em;
				flex-direction: column-reverse;
				justify-content: space-between;
			}

			.x-painting {
				width: 6em;
				height: 3.5em;
				background: linear-gradient(skyblue 75%, green 75%);
				border: .5em solid brown;
				box-shadow: -.1em .1em .1em rgba(0,0,0,.5);
				display: flex;
				align-items: flex-start;
				padding: .3em;
				line-height: 1;
				font-weight: 900;
			}
			.x-sun {
				color: yellow;
				text-shadow: 0 0 1em yellow,
							0 0 1em yellow,
							0 0 .1em #f90,
							0 0 .1em #f90,
							0 0 .1em #f60;
			}

			.x-tree {
				padding-top: .2em;
				margin: auto;
				color: #030;
				text-shadow: 0 -.05em white;
			}
			.x-snowman {
				color: white;
				margin: auto;
				padding-top: 1.5em;
				font-size: .5em;
			}

			.x-whiteboard {
				width: 7em;
				height: 3em;
				padding: .25em;
				border: .1em solid #aaa;
				background: #fff;
				box-shadow: .1em -.1em rgba(0,0,0,.25);
				margin-top: .5em;
				margin-bottom: auto;
				position: relative;
			}

			.x-whiteboard .x-writing {
				display: block;
				font-size: .25em;
			}

			.x-whiteboard .x-marker-holder {
				position: absolute;
				bottom: -.1em;
				left: 10%;
				right: 10%;
				transform: rotateX(-90deg);
				transform-origin: 100% 100%;
				border-bottom: .5em solid #666;
				border-left: .5em solid transparent;
				border-right: .5em solid transparent;
			}

			.x-whiteboard .x-marker {
				position: absolute;
				bottom: -.3em;
				width: .8em;
				height: .1em;
				background: #333;
				border-left: .25em solid red;
				transform: rotate(-10deg);
				left: .5em;
			}

			.x-whiteboard .x-marker:last-child {
				left: 2em;
				border-left-color: blue;
				transform: rotate(190deg);
			}

			.x-wall .x-clock {
				align-self: flex-end;
				font-size: .18em;
				transition: .5s transform;
			}

			.x-wall .x-clock:hover {
				transform: scale(2);
			}

			.x-floor .x-person {
				transform: translateY(-75%) rotateX(-90deg) rotateY(-5deg);
				transform-origin: 50% 100%;
			}

			.x-floor .x-person:last-child {
				transform: translateY(-75%) rotateX(-90deg) rotateY(30deg);
			}

			.x-writing {
				color: red;
			}

			.x-writing ul, .x-writing li {
				margin: 0;
				padding: 0;
				list-style: none;
			}

			.x-writing ul {
				color: blue;
			}

			.x-writing li::before {
				color: red;
				content: "☐ ";
			}

			.x-writing .x-done::before {
				content: "☑ ";
			}

			.x-clock {
				position: relative;
				width: 10em;
				height: 10em;
				border-radius: 50%;
			}

			.x-clock .x-frame-face {
				position: relative;
				width: 100%;
				height: 100%;
				border-radius: 50%;
				background: linear-gradient(to bottom, #666,#333);
			}

			.x-clock .x-frame-face::after {
				content: "";
				top: 1em;
				left: 1em;
				bottom: 1em;
				right: 1em;
				border-radius: 50%;
				position: absolute;
				background: linear-gradient(#fff, #ccc);
			}

			@keyframes x-clock-hand { to { transform: rotate(360deg); } }

			.x-clock .x-hand {
				position: absolute;
				bottom: 50%;
				left: 50%;
				height: var(--hand-length);
				transform-origin: calc(var(--hand-width) / 2) calc(var(--hand-length) - var(--hand-width) / 2);
				/*animation: x-clock-hand calc(1s * var(--seconds)) steps(var(--seconds), end) 0s infinite;*/
				width: var(--hand-width);
				margin: calc(var(--hand-width) / -2);
				border-radius: calc(var(--hand-width) / 2);
				animation-delay: inherit;
				animation-name: x-clock-hand;
				animation-iteration-count: infinite;
			}

			.x-clock .x-hours {
				--hand-length: 2.5em;
				--hand-width: .5em;
				background: #121314;
				animation-duration: 43200s;
				animation-timing-function: steps(43200, end);
			}

			.x-clock .x-minutes {
				--hand-length: 3.5em;
				--hand-width: .4em;
				background: #343536;
				animation-duration: 3600s;
				animation-timing-function: steps(3600, end);
			}

			.x-clock .x-seconds {
				--hand-length: 3.5em;
				--hand-width: .3em;
				background: #c00;
				animation-duration: 60s;
				animation-timing-function: steps(60, end);
			}

			.x-clock .x-seconds::after {
				--radius: 1em;
				border-radius: 50%;
				position: absolute;
				content: "";
				background: inherit;
				width: var(--radius);
				height: var(--radius);
				left: calc(var(--radius) / -2 + var(--hand-width) / 2);
				bottom: calc(var(--radius) / -2 + var(--hand-width) / 2);
				margin: 0;
			}

			.x-clock .x-ticks {
				position: absolute;
				top: 1.5em;
				left: 4.9em;
				opacity: .5;
			}

			.x-clock .x-tick {
				border: 0;
				width: .2em;
				height: .5em;
				position: absolute;
				background: black;
				transform-origin: .1em 3.5em;
				transform: rotate(30deg);
			}
		</style>

		<script>
			function toggleZoom(r) {
				if (r.zoom) {
					r.classList.remove("x-zoom");
					removeEventListener("scroll", r.zoom);
					removeEventListener("resize", r.zoom);
					r.style.transform = "";
					delete r.zoom;
				} else {
					r.classList.add("x-zoom");
					r.zoom = function(e) {
						r.style.overflow = "initial";
						if (e) r.style.transition = "all 0s";
						function recursiveOffset(e, property) {
							return e ? e[property] + recursiveOffset(e.offsetParent, property) : 0;
						}
						r.style.transform = "translate(" +
							// 2026 update: putting this inside a `position: relative` main means I need recursive offset calculations
							((innerWidth - r.offsetWidth) / 2 - recursiveOffset(r, "offsetLeft") + scrollX) + "px," +
							((innerHeight - r.offsetHeight) / 2 - recursiveOffset(r, "offsetTop") + scrollY) + "px) scale(" +
							1.0 * Math.min(innerWidth / r.offsetWidth, innerHeight / r.offsetHeight) + ")";
						r.offsetWidth;
						if (e) r.style.transition = "";
					};
					addEventListener("scroll", r.zoom);
					addEventListener("resize", r.zoom);
					r.zoom();
				}
			}

			var r = document.querySelector(".x-room-container");
			var c = r.querySelector(".x-room").style;
			r.addEventListener("mouseenter", function() {
				// .05s: move to the new position fast but smoothly.
				c.transition = 'transform .05s';
			});

			r.addEventListener("mousemove", function(e) {
				// a is for apple, b is for bounds, c is for camera, d is for dreadful,
				// e is for elongated comment, f is for folly, g is for—good riddance!
				// ... [...] ... [distantly] h is for heeeeelllp!
				var b = r.getBoundingClientRect(),
					x = 45 + (.5 - (e.clientY - b.top) / b.height) * 75, // initial value: 60
					z = 45 + (.5 - (e.clientX - b.left) / b.width) * 75; // initial value: 45
				c.transform = "rotateX(" + x + "deg) rotateZ(" + z + "deg)";
			});

			r.addEventListener("mouseleave", function() {
				// 1s: reset position slowly
				c.transition = 'transform 1s';
				c.transform = '';
			});

			r.addEventListener('transitionend', function(e) {
				// This is the last transition to finish, so apply the overflow then.
				if (e.target == r && e.pseudoElement == "::before" && e.propertyName == "box-shadow" && !r.classList.contains("x-zoom")) {
					r.style.overflow = "";
				}
			});

			addEventListener("DOMContentLoaded", function() {
				// Not doing anything about leap-seconds. I’m a coward.
				var n = new Date(), t = -(n.getHours() * 3600 + n.getMinutes() * 60 + n.getSeconds() + n.getMilliseconds() / 1000) + "s";
				Array.prototype.forEach.call(document.querySelectorAll(".x-clock"), function(hand) {
					hand.style.animationDelay = t;
				});
				// BTW, this clock has another weakness: if the browser is suspended (e.g. machine in standby) it gets out of sync. We could trigger this position update every so often, but that would be missing the point of it.
			});

		</script>
	</figure>
</section>]]></content>
		<category term="meta=only" label="Meta only"/>
	</entry>
</feed>
