<?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/*.feed</id>
	<author>
		<name>Chris Morgan</name>
		<uri>https://chrismorgan.info</uri>
		<email>me@chrismorgan.info</email>
	</author>
	<updated>2026-05-20T11:05:00+05:30</updated>
	<link href="/*.feed" rel="self" type="application/atom+xml"/>
	<link href="/"/>
	<title type="html">Chris Morgan’s everything feed</title>
	<link href="/*.feed" rel="first" type="application/atom+xml"/>
	<link href="/2019b-feed.xml" rel="next" type="application/atom+xml"/>
	<link href="/2019b-feed.xml" rel="last" type="application/atom+xml"/>
	<entry>
		<title type="html">A few ways of specifying &lt;br class=w&gt;&lt;b&gt;per-theme colours&lt;&#x2f;b&gt;&lt;br class=w&gt; in only CSS</title>
		<published>2026-05-16T16:35:00+05:30</published>
		<updated>2026-05-20T11:05:00+05:30</updated>
		<link href="css-themed-colours" type="text/html"/>
		<id>https://chrismorgan.info/css-themed-colours</id>
		<content type="html"><![CDATA[
<p><i>You’re reading from my feeds; you will accordingly probably miss out on a few styles and niceties.
<br>In this article’s case: ✔/✓/±/?¿/✗/✘ list markers, and pretty colours on the summary table.
<br><a href="/css-themed-colours">View it on my website</a> for the Authentic Intended Experience. Or just take it as it is.</i>
<hr>

<aside>
	<p style=margin-bottom:0><strong>Table of contents:</strong>
	<br><a href=#assumed-markup>The assumed basic HTML and CSS</a>
	<ol style=margin-block:0;padding-left:2em>
		<li><a href=#old-school>Write it all out the hard way</a> (doesn’t scale very well)
		<li><a href=#palette-variables>Lots of colour palette variables</a> (what normal people do)
		<li><a href=#space-toggle-hack>The space toggle hack</a> (poor man’s <code>if()</code>)
		<li><a href=#color-mix><code>color-mix()</code> with one variable per theme</a> (I settled on this for this site, but I’m weird)
		<li><a href=#light-dark><code>light-dark()</code></a> (consider using this these days, instead of or combined with palette variables)
		<li><a href=#if><code>if()</code></a> (maybe some time soon)
		<li><a href=#keyframes>Paused <code><i>@keyframes</i></code> animation</a> (probably a bad idea)
	</ol>
	<p style=margin-top:0><a href=#summary>Summary</a> (a comparison table)
	<br><a href=#parametric-colours>Bonus 1. Parametric colours</a>
	<br><a href=#function-and-mixin>Bonus 2. <code><i>@function</i></code> and <code><i>@mixin</i></code></a>
</aside>

<p>I was thinking about this as part of putting this website together.
<br class=w>Actually it was because I forgot about <code>light-dark()</code>;
<br class=w>if I’d remembered that earlier I probably wouldn’t have ended up with all this!

<p style=margin-bottom:0><strong>My requirements:</strong> (which may not match your requirements)
<ul style=margin-top:0>
	<li>Must support auto (based on <code>prefers-color-scheme</code>),<br class=w> light, and dark, chosen by radio buttons.
	<li>Must work without any JavaScript (persistence is out of scope).
</ul>

<section id=assumed-markup>
	<h2>The assumed basic HTML and CSS</h2>

	<p>Up to you exactly how to include and structure it, but all the examples that follow assume something like this:

	<pre><code><b>&lt;fieldset&gt;
	&lt;legend></b>Theme<b>&lt;/legend>
	&lt;label>&lt;input type</b>=<span class=s>radio</span> <b>name</b>=<span class=s>theme</span> <b>id</b>=<span class=s>theme-auto</span> <b>checked></b> Follow system<b>&lt;/label>
	&lt;label>&lt;input type</b>=<span class=s>radio</span> <b>name</b>=<span class=s>theme</span> <b>id</b>=<span class=s>theme-light</span><b>></b> Light<b>&lt;/label>
	&lt;label>&lt;input type</b>=<span class=s>radio</span> <b>name</b>=<span class=s>theme</span> <b>id</b>=<span class=s>theme-dark</span><b>></b> Dark<b>&lt;/label>
&lt;/fieldset&gt;</b></code></pre>

	<p>I’m going to assume the availability of a few CSS features:

	<ol>
	<li><p><code><i>@media (prefers-color-scheme: dark)</i></code> for automatic selection.
		<br class=w>It shipped in 2019–2020, which is generally enough.

	<li><p style=margin-bottom:0><code><i>:has()</i></code> for manual selection without needing additional JavaScript.
		<br class=w>It’s newer, <a href=https://caniuse.com/css-has>supported back to <time>2023-12</time></a>;
		<br class=w><em>I</em> consider that enough to rely on in general,
		<br class=w>but if you’re not happy with it, you have two options:

		<style>
		@counter-style x-parenthesised-lower-alpha {
			system: extends lower-alpha;
			suffix: ") ";
		}
		</style>
		<ol type=a style=margin-top:0;list-style-type:x-parenthesised-lower-alpha>
			<li>Only support one behaviour (probably auto) sans-JS.
				<br class=w>This is a very reasonable choice.
				<br class=w>You might add a <code>light</code> or <code>dark</code> class to the root element.
			<li>Shift the radio buttons to be direct children of the body,
				hiding them visually,
				<br class=w>and then target <code><i>#theme-foo:checked ~ * …</i></code>
				<br class=w>instead of <code><i>:root:has(#theme-foo:checked) …</i></code>.
				<br class=w>Messy and with some consequences and inconveniences, but generally possible.
		</ol>
	<li><p>I will also use nested selectors in some places in this article (<time>2023-12</time>);
		<br class=w>but they’re easily flattened if you wish.
	</ol>
</section>

<p>Now to the seven techniques.

<style>
	/* Numbered headings: if they wrap, begin the next line after the number. */
	/* … also used for footnote sorts of things. */
	.x-numbered {
		display: flex;
		& > :first-child {
			white-space-collapse: preserve;
		}
		& > :last-child {
			flex: 1;
		}
	}

	.\✔ { list-style-type: '✔ '; &::marker { color: color-mix(in oklab, #0c0, #0f0 var(--d3)) } }
	.\✓ { list-style-type: '✓ '; &::marker { color: color-mix(in oklab, #090, #0a0 var(--d3)) } }
	.\± { list-style-type: '± '; &::marker { color: color-mix(in oklab, #c84, #d95 var(--d3)); font-weight: bold } }
	.\?\¿ { list-style-type: '?¿ '; &::marker { color: color-mix(in oklab, #c84, #d95 var(--d3)); font-weight: bold } }
	.\✗ { list-style-type: '✗ '; &::marker { color: color-mix(in oklab, #900, #a00 var(--d3)) } }
	.\✘ { list-style-type: '✘ '; &::marker { color: color-mix(in oklab, #d00, #f00 var(--d3)) } }
</style>

<section id=old-school>
	<h2 class=x-numbered><span>1. </span><span>Write it all out the hard way</span></h2>

<p>Old-school and verbose.

<ul>
	<li class=✔>The most compatible.
	<li class=✔>It’s easy to see how to add more themes than just light and dark.
	<li class=✘>Syntax is verbose.
	<li class=✘>Dark theme value has to be repeated.
</ul>

<pre><code><i>some-element</i> {
	<b>color:</b> darkred;

	<i>:root:has(#theme-dark:checked) &amp;</i> {
		<b>color:</b> pink;
	}

	<i>@media (prefers-color-scheme: dark)</i> {
		<i>:root:has(#theme-auto:checked) &amp;</i> {
			<b>color:</b> pink;
		}
	}
}</code></pre>
<p>It’s often structured differently, with the nesting effectively inverted as in the next example,
<br class=w>but that’s the gist of it.
</section>

<section id=palette-variables>
	<h2 class=x-numbered><span>2. </span><span>Lots of colour palette variables</span></h2>

<p>Probably the most popular approach, traditionally.
<p>Though I was surprised to realise the implementations only landed in 2014–2017.
<br class=w>Sure feels longer ago than that. But it still feels okay to call it “traditional”,
<br class=w>because people often used Sass variables to similar effect before.
<br class=w>(You didn’t often have multiple themes in those days.)

<ul>
	<li class=✔>Works since <time>2017-04</time>.
	<li class=✔>It’s easy to see how to add more themes than just light and dark.
	<li class=✔>Use-site syntax is ideal.
	<li class=±>Separating colour definitions from their use location may help or hinder,
		<br class=w>and may or may not fit nicely into your project.
		<br class=w>It <em>can</em> encourage some bad and some good patterns, and may yield a little more overhead.
		<br class=w>(I’m being vague deliberately, and will not elaborate here.)
	<li class=✘>Dark theme value has to be repeated.
</ul>

<p>The declaration:

<pre><code><i>:root</i> {
	<b>--color-somepurpose:</b> darkred;
<span class=unimportant>	<b>--color-another:</b> …;</span>
<span class=very-unimportant>	⋮</span>
}

<i>:root:has(#theme-dark:checked)</i> {
	<b>--color-somepurpose:</b> pink;
<span class=unimportant>	<b>--color-another:</b> …;</span>
<span class=very-unimportant>	⋮</span>
}

<i>@media (prefers-color-scheme: dark)</i> {
	<i>:root:has(#theme-auto:checked)</i> {
		<b>--color-somepurpose:</b> pink;
<span class=unimportant>		<b>--color-another:</b> …;</span>
<span class=very-unimportant>		⋮</span>
	}
}</code></pre>

<p>Adding more themes is trivial.

<p>And use site:

<pre><code><i>some-element</i> {
	<b>color:</b> var(--color-somepurpose);
}</code></pre>

<p>You can also obviously use variables for most of the remaining approaches.
</section>

<section id=space-toggle-hack>
	<h2 class=x-numbered><span>3. </span><span>The space toggle hack</span></h2>
	<ul>
		<li class=✔>Works since <time>2017-04</time>.
		<li class=✓>It’s easy to see how to add more themes than just light and dark.<!-- Only ✓ instead of ✔ because you *have* to spell out every theme every time, you can’t really have any sort of inheritance where you only change a small number of things from a parent theme. -->
		<li class=±>Use-site syntax is not <em>bad</em>, but it is strange.
		<li class=✗>Every use must enumerate every theme (no inheritance or else branch).
		<li class=✗>The feature is somewhat confusing, and will feel back to front.
		<li class=✗>Tooling and formatters may be incompatible with the trick
			<br class=w>(though <em>most</em> tools will be canny to it by now).
	</ul>

	<p>This is poor man’s <code>if()</code> (which actual function we’ll <a href=#if>get to<svg role="presentation" width=".6875em" height=".6875em" viewBox="0 0 11 11"><path fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" d="m4 7 3 3 3-3M4 2q2-1 3 3v5"/></svg></a>).
	<br class=w>You get a single-branch conditional spelled <code>var(<var>--condition</var>, <var>value</var>)</code>.
	<br class=w>Honestly it’s not really <em>worse</em> than <code>if()</code> for cases like this, just different.

	<p>It works because of a frankly ridiculous implementation detail in Custom Properties.
	<br class=w>I’ll defer to <a href=https://lea.verou.me/blog/2020/10/the-var-space-hack-to-toggle-multiple-values-with-one-custom-property/>Lea Verou for further explanation</a>.

<p>At the start of the stylesheet:

<pre><code><i>:root</i> {
	<b>--light:</b> initial;
	<b>--dark:</b> ;
}

<i>:root:has(#theme-dark:checked)</i> {
	<b>--light:</b> ;
	<b>--dark:</b> initial;
}

<i>@media (prefers-color-scheme: dark)</i> {
	<i>:root:has(#theme-auto:checked)</i> {
		<b>--light:</b> ;
		<b>--dark:</b> initial;
	}
}</code></pre>

<p>And use site:

<pre><code><i>some-element</i> {
	<b>color:</b> var(--light, darkred) var(--dark, pink);
}</code></pre>

<p>And <i lang=fr>voilà</i>: if light, <code>darkred</code>; if dark, pink.

<p>It’s not a <em>popular</em> technique, and I forgot about it when compiling this list.
<br class=w>But <a href=https://lobste.rs/c/rl6apj>someone reminded me after publication</a>, so I added it:
<br class=w>it’s worth considering, especially if you want longer compatibility,
<br class=w>because it and variants really do work rather well.
</section>

<section id=color-mix>
	<h2 class=x-numbered><span>4. </span><span><code>color-mix()</code> with one variable per theme</span></h2>

<p>I don’t recall encountering this technique before
<br class=w>(actually I’ve been surprised at how little attention <code>color-mix()</code> has been paid),
<br class=w>but it works.

<p>Basically, define a colour as a mixture of all the colours across all themes,
<br class=w>but use a variable to set one of the themes to 100% and the rest to 0%.
<br class=w>(In practice, you’re normally just using two themes: light and dark.)

<ul>
	<li class=✓>Works since <time>2023-05</time>.
	<li class=✔>It’s easy to see how to add more themes than just light and dark.
	<li class=✓>Syntax is reasonably compact.
	<li class=±>Syntax is likely to be unfamiliar, but is fairly straightforward.
		<br class=w>Pity about the interpolation method part that’s currently necessary.
	<li class=?¿>Opens up interesting ideas for actually <em>mixing</em> colours/themes.
		<br class=w>What does <code><b>--dark:</b> <span class=n>50%</span>;</code> mean?
		<br class=w>You can play around with the consequences <button popovertarget=_prefs><span aria-hidden=true style=font-family:sans-serif;opacity:.4>🎨 A<i style=font-family:serif>a</i></span> on this site</button>!
	<li class=✗>If you do try mixing colours and themes, controlling interpolation is a <em>pain</em>;
		<br class=w>you have to figure out how to express your desired function <em>mathematically</em>.
</ul>

<p>This is the technique I settled on for my own site,
<br class=w>because once I realised the potential,
<br class=w>I wanted to play around with mixing themes.
<br class=w><a href=interpolating-themes>I’ve started writing more about it.</a>
<br class=w>It’s more interesting/involved than you might imagine.

<p>At the start of the stylesheet:

<pre><code><i>:root</i> {
	<b>--dark:</b> <span class=n>0%</span>;
}

<i>:root:has(#theme-dark:checked)</i> {
	<b>--dark:</b> <span class=n>100%</span>;
}

<i>@media (prefers-color-scheme: dark)</i> {
	<i>:root:has(#theme-auto:checked)</i> {
		<b>--dark:</b> <span class=n>100%</span>;
	}
}</code></pre>

<p>Then with each colour we can use <code>color-mix()</code> like so:

<pre><code><i>some-element</i> {
	<b>color:</b> color-mix(in oklab, darkred, pink var(--dark));
}</code></pre>

<p>And it will be <code>darkred</code> in light mode and <code>pink</code> in dark mode.

<p>Mostly you only care about changing <em>colours</em>,
<br class=w>but if you wanted to change other things based on the theme you could,
<br class=w>by making the variable a number instead of a percentage, and using <code>calc()</code>.
<br class=w>(Without having thought through the implications—<br class=w>I wish percentages and numbers were unified, with <code class=n>100%</code> equivalent to <code class=n>1</code>.)

<section>
	<h3>Syntax compatibility hazards</h3>
	<p>Beware of following what <a href=https://drafts.csswg.org/css-color-5/#color-mix>the spec</a> allows and what <a href=https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/color-mix>MDN talks of</a>:
	<ol>
		<li><p>The interpolation method is optional, defaulting to <code>oklab</code>,
			but this only happened in <a href=https://github.com/w3c/csswg-drafts/issues/10484#issuecomment-3201053810>late 2025</a>.
			<br class=w>All major browsers have now shipped it,
			<br class=w>but by my definition it’s not going to be safe to rely on for another year and a half or so.
			<br class=w>So for quite some time yet, you’ll still need to write <code>in oklab,</code>.
			<br class=w><a href=https://github.com/mdn/browser-compat-data/issues/28693>MDN browser-compat-data lacks it.</a>
			<br class=w><a href=https://github.com/parcel-bundler/lightningcss/issues/1224>Lightning CSS lacks it.</a>
		<li><p>The spec permits <em>one or more</em> colour specification,
			<br class=w>which is handy for mixing more than two themes,
			<br class=w>but most implementations still only support exactly two.
			<br class=w><a href=https://caniuse.com/mdn-css_types_color_color-mix_variadic_color_arguments>This one <em>is</em> tracked in MDN browser-compat-data.</a>
			<br class=w>Lightning CSS lacks it.
			<br class=w>At the time of writing, only Firefox is shipping this (150, 2026-04-21).
	</ol>
</section>

<section>
	<h3>More than two themes</h3>
<p>You can extend this to more than two themes by setting more variables and mixing more colours.
<br class=w>Defining a couple of new themes named Grass and Ocean:

<pre><code><i>:root</i> {
	<b>--grass:</b> <span class=n>0%</span>;
	<b>--ocean:</b> <span class=n>0%</span>;
}

<i>:root:has(#theme-grass:checked)</i> {
	<b>--grass:</b> <span class=n>100%</span>;
}

<i>:root:has(#theme-ocean:checked)</i> {
	<b>--ocean:</b> <span class=n>100%</span>;
}</code></pre>

And using them:

<pre><code><i>some-element</i> {
	<b>color:</b> <ins>color-mix(in oklab, color-mix(in oklab, </ins>color-mix(in oklab,
		darkred,
		pink var(--dark)<ins>),
		green var(--grass)),
		blue var(--ocean)</ins>
	);
}</code></pre>

<p>Hopefully in 2028 (or once Lightning CSS or similar supports it),
<br class=w>you’ll be able to shorten this to the following
<br class=w>(which will also work better with values between 0% and 100%):

<pre><code>	<b>color:</b> color-mix(<del>in oklab,</del>
		darkred,
		pink var(--dark)<ins>,
		green var(--grass),
		blue var(--ocean)</ins>
	);</code></pre>
</section>

</section>

<section id=light-dark>
	<h2 class=x-numbered><span>5. </span><span><code>light-dark()</code></span></h2>

<ul>
	<li class=±>Works since <time>2024-05</time> (Safari 17.5).
	<br class=w>This just <em>barely</em> satisfies my preferred compatibility requirement,<br class=w>“two years of Safari and a year and a half of the rest”.
	<li class=✗>Can’t use it for more or other themes than just light and dark.
	<li class=✔>Syntax is compact and clear.
</ul>

<p>To begin with, you <em>must</em> set <code><b>color-scheme</b></code> properly.
<br class=w>You <em>should</em> do this with all of the techniques,
<br class=w>for a few reasons such as scroll bar and form element colouring,
<br class=w>but with <code>light-dark()</code> it’s necessary for it to work <em>at all</em>.

<pre><code><i>:root</i> {
	<b>color-scheme:</b> light dark;
}

<i>:root:has(#theme-light:checked)</i> {
	<b>color-scheme:</b> light;
}

<i>:root:has(#theme-dark:checked)</i> {
	<b>color-scheme:</b> dark;
}</code></pre>

<p>Then:

<pre><code><i>some-element</i> {
	<b>color:</b> light-dark(darkred, pink);
}</code></pre>

</section>

<section id=if>
	<h2 class=x-numbered><span>6. </span><span><code>if()</code></span></h2>

<ul>
	<li class=✘>Only implemented by Chromium family (<time>2025-05</time>); not yet supported on Firefox or Safari.
	<li class=✔>It’s easy to see how to add more themes than just light and dark.
	<li class=✓>Syntax largely matches programmer expectations,
	<li class=±>… but is fairly verbose.
</ul>

<p>See also the <a href=#space-toggle-hack>space toggle hack<svg role="presentation" width=".6875em" height=".6875em" viewBox="0 0 11 11"><path fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" d="m4 4l3-3 3 3M4 9q2 1 3-3v-5"/></svg></a>, which is slightly less general than this,
<br class=w>but frankly similarly expressive for colour purposes, and less verbose.

<p>At the start of the stylesheet:

<pre><code><i>:root</i> {
	<b>--theme:</b> light;
}

<i>:root:has(#theme-dark:checked)</i> {
	<b>--theme:</b> dark;
}

<i>@media (prefers-color-scheme: dark)</i> {
	<i>:root:has(#theme-auto:checked)</i> {
		<b>--theme:</b> dark;
	}
}</code></pre>

<p>Then with each colour use:

<pre><code><i>some-element</i> {
	<b>color:</b> if(
		style(--theme: light): darkred;
		style(--theme: dark): pink;
	);
}</code></pre>

<p>This is differently flexible than <code>color-mix()</code>:
<br class=w><code>color-mix()</code> is limited to colours only, and allows continuous interpolation;
<br class=w><code>if()</code> works on all types, but is limited to discrete values.

<p>Adding more themes is trivial based on what has already been shown.
</section>

<section id=keyframes>
	<h2 class=x-numbered><span>7. </span><span>Paused <code><i>@keyframes</i></code> animation</span></h2>

<p>This is a tricky technique I’ve seen applied occasionally,
<br class=w>but I don’t remember ever seeing it used for <em>colours</em>.

<p>Instead of probably defining colour palette <em>variables</em>,
<br class=w>define colour palette <em>keyframes rules</em> (one per property you use it with!),
<br class=w>where “from” means light mode and “to” means dark mode.
<br class=w>Then at use sites, add a paused animation,
<br class=w>and in dark mode apply a negative delay to put the animation at the “to” (dark) value.

<ul>
	<li class=✘>People will think you crazy. There’s an above-average chance they may be right.<!-- Don’t blame me when <em>you</em> get committed, instead of your code. -->
	<li class=✔>Works since <time>2020-01</time> (when Edge adopted Chromium; other than that it’d be <time>2013-07</time>).
	<li class=?¿>As with <code>color-mix()</code>, the technique <em>begs</em> you to allow mixing colours/themes!
	<li class=✓>If you really want to blend themes, you won’t get better than this for controlling interpolation.
		<br class=w>(It’s almost like it was designed for it!)
		<br class=w>(Though… keyframes are not <em>always</em> better than mathematical functions.)
	<li class=✗>Unless you have no other animations, you’ll be stuck with a lot of repetition,
		<br class=w>and some really painful composition of <code><b>animation</b></code> declarations.
	<li class=✗>I bet you’ll run into specificity-style troubles somewhere along the way,
		<br class=w>because animations are even more powerful than <code><i><nobr>!important</nobr></i></code>.
</ul>

<pre><code><i>@keyframes</i> color-somepurpose {
	<i>from</i> { <b>color:</b> darkred; }
	<i>to</i> { <b>color:</b> pink; }
}

<i>some-element</i> {
	<b>animation:</b> <span class=n>1s</span> paused color-somepurpose forwards;

	<i>:root:has(#theme-dark:checked) &amp;</i> {
		<b>animation-delay:</b> <span class=n>-1s</span>;
	}

	<i>@media (prefers-color-scheme: dark)</i> {
		<i>:root:has(#theme-auto:checked) &amp;</i> {
			<b>animation-delay:</b> <span class=n>-1s</span>;
		}
	}
}</code></pre>

<p>I’m not going to explain this one further;
<br class=w>if you don’t understand it, it’s probably for the best—<br class=w>or else take it as a challenge.
<!-- One of my fears in publishing this is that I will inspire someone. -->

</section>

<style>
	.x-table {
		overflow-x: auto;
		table {
			border-collapse: collapse;
			font-variant-numeric: tabular-nums;
		}
		th, td { padding: .25em .5em; border: 1px solid var(--bg); white-space: nowrap; }
		/* Fun detail: *this* blending is in srgb rather than oklab like normal, because we need its nonlinearity-based higher contrast in dark mode. */
		th { background: color-mix(in srgb, var(--bg), var(--color) 16%); }
		/* Alas, this doesn’t work because of .x-table: thead th { position: sticky; top: 0; }/**/
		tbody th { background: color-mix(in srgb, var(--bg), var(--color) 8%) }
		tr > :first-child { position: sticky; left: 0; }
		td { /*border-bottom: 1px solid color-mix(in srgb, var(--bg), var(--color) 8%);*/ text-align: center; }
		td:not([class]) { border-bottom: 1px solid color-mix(in srgb, var(--bg), var(--color) 16%); }
		td:last-child { border-right: 1px solid color-mix(in srgb, var(--bg), var(--color) 16%); }
		tr > :last-child { text-align: left; }

		@media (width < 48rem) {
			margin-inline: clamp(-3rem,-5vw,-1rem);
			padding-inline: clamp(1rem,5vw,3rem);
			tr > :first-child { position: sticky; left: clamp(-3rem,-5vw,-1rem); }
		}
	}

	.x-ideal    { background-color: color-mix(in oklab, oklch(0.8  0.25 140), oklch(0.4  0.1  140) var(--d1)); color: var(--bold-color) }
	.x-good     { background-color: color-mix(in oklab, oklch(0.8  0.20 130), oklch(0.3  0.08 130) var(--d1)); color: var(--bold-color) }
	.x-okay     { background-color: color-mix(in oklab, oklch(0.8  0.18 100), oklch(0.3  0.07 100) var(--d1)); color: var(--bold-color) }
	.x-poor     { background-color: color-mix(in oklab, oklch(0.8  0.16  60), oklch(0.3  0.08  60) var(--d1)); color: var(--bold-color) }
	.x-bad      { background-color: color-mix(in oklab, oklch(0.8  0.14  30), oklch(0.3  0.1   30) var(--d1)); color: var(--bold-color) }
	.x-horrible { background-color: color-mix(in oklab, oklch(0.73 0.2   25), oklch(0.35 0.16  25) var(--d1)); color: var(--bold-color) }
</style>

<p id=summary>Finally as summary, here’s a comparison table.
<div class=x-table>
	<table>
		<thead>
			<tr>
				<th>Technique
				<th>Supported
				<th>Selector<br>repetition
				<th>Pretty/<br>maintainable?
				<th>Interpolation
				<th>Themes
				<th>What using it suggests
		<tbody>
            <tr><th>Manual<td class=x-ideal>Forever<td class=x-bad>Everywhere<td class=x-bad>Unwieldy<td class=x-poor>No<td class=x-good>Any<td>You only <em>have</em> one theme.<!-- Incidentally, at that stage preprocessor variables are better. CSS Custom Properties only becomes useful with things like @media (prefers-color-scheme: dark). -->
            <tr><th>Variables <span class=unimportant>†</span><td class=x-good>2017-04<td class=x-bad>At definition<br>site only<td class=x-good>Barring the definition<br>repetition, great to use<td class=x-poor>No<td class=x-good>Any<td>You’re pretty normal,<br>but not <em>fashionable</em>.
            <tr><th>Space toggle<br>hack<td class=x-good>2017-04<td class=x-good>No<td class=x-good>Slightly odd,<br>but very effective<td class=x-poor>No<td class=x-good>Any<td>You’re not afraid of<br>esoteric solutions.
            <tr><th><code>color-mix()</code><td class=x-okay>2023-05<td class=x-good>No<td class=x-okay>Unfamiliar, temporary<br>slight verbosity<td class=x-good>Yes, but fiddly<br>if nonlinear<td class=x-good>Any<td>You’re probably having<br>too much fun.<!-- I am, I assure you. -->
            <tr><th><code>light-dark()</code><td class=x-poor>2024-05<td class=x-good>No<td class=x-ideal>Paragon<td class=x-poor>No<td class=x-bad>Only light/dark<td>You’re fashionable.<!-- You’re doing the latest thing, not entirely caring if it hurts others distant from you… yeah, sounds about right. -->
            <tr><th><code>if()</code><td class=x-bad>Only<br>Chromium<td class=x-good>No<td class=x-okay>Clear but verbose<td class=x-poor>No<td class=x-good>Any<td>You don’t care about the<br>health of the web <em>at all</em>.
            <tr><th><code><i>@keyframes</i></code><td class=x-good>2020-01<td class=x-horrible>It’s so much<br>worse than<br>just that<td class=x-horrible>This cell is too small<br>to adequately describe<br>the nightmare<td class=x-ideal>Near-ultimate<br>flexibility<td class=x-poor>In theory any<br>(but please<br>don’t try)<td>Disturbed genius?<br>That’s about my limit for<br>benevolent interpretation.
			<!-- What are the odds I’ll decide to use @keyframes at least once like this? Definitely nonzero. -->
	</table>
</div>
<p class=x-numbered><span class=unimportant>† </span><span>This is variables applied to manual, but you can definitely use it with <br class=w><code>color-mix()</code>, <code>light-dark()</code> and <code>if()</code>, and will probably do so.
<br class=w><code><i>@keyframes</i></code> kinda already includes its own alternative, and it’s probably worse than it sounds.</span>

<aside id=parametric-colours>
	<h2 class=x-numbered><b>Bonus 1. </b><span>Parametric colours</span></h2>

	<p>I don’t put this one in the main list, because it’s not a general solution.
	<br class=w>In my opinion, it’s only useful in isolated circumstances.
	<br class=w>It’s fairly unsound to build colour palettes using it,
	<br class=w>because <em>colour just doesn’t work that way</em>.
	<br class=w>Especially yellow. And relative lightness. Both are headaches.

	<p>(I actually forgot about this until someone reminded me, after publishing.
	<br class=w>In fact, I forgot about it until someone reminded me, after publishing.
	<br class=w>I kinda wrote the technique off back in probably 2021–2022,
	<br class=w>when it was in specs but not yet implemented.)

	<ul>
		<li class=✗>Only works since <time>2024-07</time> (with minor caveats until around <time>2024-11</time>).
		<li class=✘>Very limiting in what you can do.
		<li class=✗>Attempts to apply tidy mathematics to perception, and doesn’t do well.
		<li class=✓>If you can apply it, it can make certain broad changes easy.
	</ul>

	<p>I’m not going to give a full example, but here are a couple of very simple demonstrations,
	<br class=w>and I leave further application to you:
	<pre><code>oklch(from <var>&lt;color></var> l c calc(h + 20) / alpha)
oklch(<var>lightness</var> <var>chroma</var> <var>hue</var>)</code></pre>

</aside>

<aside id=function-and-mixin>
	<h2 class=x-numbered><b>Bonus 2. </b><span><code><i>@function</i></code> and <code><i>@mixin</i></code></span></h2>
	<p>Where syntax is less pleasant, in the future <a href=https://drafts.csswg.org/css-mixins/><code><i>@function</i></code> and <code><i>@mixin</i></code></a> may improve it.
	<br class=w>Even the paused animation approach can be made <em>somewhat</em> less horrible.
	<br class=w>I won’t provide any samples, because you can’t use it yet;
	<br class=w>this stuff is immature, more experimental than <code>if()</code>.
	<br class=w>(Despite this, Chromium has, in its usual trouble-making fashion,
	<br class=w><a href=https://caniuse.com/mdn-css_at-rules_function>already shipped</a> <a href="https://wpt.fyi/results/css/css-mixins?label=experimental&label=master&aligned">most of it</a>. This will probably cause problems.)
</aside>]]></content>
		<category term="css" label="CSS"/>
		<category term="meta=also" label="Meta also"/>
	</entry>
	<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>

<!--
<section id=changed>
	<h2>Changed</h2>
	<p>If you happen to change what you do because of this, I’d <a href=mailto:me@chrismorgan.info>like to hear</a>.
	<ul>
		<li><a href=https://susam.net/no-query-strings.html>Susam Pal was inspired to stop adding <span class=path>?via=…</span> in Wander</a>
	</ul>
</section>
-->

<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="url" label="URL"/>
		<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>
