<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://chameth.com/">
    <title>Chameth.com - posts like debugging-beyond-the-debugger, modern-css-is-fun, simple-backups-restic-hetzner but not finding-an-awkward-bug-with-claude-code, why-you-should-be-using-https</title>
    <subtitle>Personal homepage of Chris Smith</subtitle>
    <link href="https://chameth.com/feeds/posts/like/debugging-beyond-the-debugger,modern-css-is-fun,simple-backups-restic-hetzner/unlike/finding-an-awkward-bug-with-claude-code,why-you-should-be-using-https/" rel="self"/>
    <link href="https://chameth.com/"/>
    <icon>https://chameth.com/favicon.png</icon>
    <updated>2026-04-30T00:00:00Z</updated>
    <id>https://chameth.com/</id>
    <author>
        <name>Chris Smith</name>
    </author>
    <entry>
        <title>Migrating from GitHub to Forgejo</title>
        <link href="https://chameth.com/migrating-from-github-to-forgejo/"/>
        <updated>2026-04-30T00:00:00Z</updated>
        <id>https://chameth.com/migrating-from-github-to-forgejo/</id>
        <content xml:lang="en" type="html">&lt;p&gt;When Microsoft bought GitHub in 2018 my kneejerk reaction — like so many others — was to start looking for alternatives. For a while I self hosted a &lt;a href=&#34;https://about.gitea.com/&#34;&gt;Gitea&lt;/a&gt; instance but I never totally bought into it: some repositories I still pushed to GitHub, some I pushed to Gitea and they got mirrored, and I ended up causing myself problems when I got the two confused. Part of the problem was that the GitHub UI was faster and cleaner than Gitea’s at the time; using Gitea felt like a chore compared to GitHub. I ended up not maintaining it and eventually binning it and just going back to GitHub.&lt;/p&gt;
&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/migrating-from-github-to-forgejo/unicorn.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/migrating-from-github-to-forgejo/unicorn.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/migrating-from-github-to-forgejo/unicorn.png&#34; alt=&#34;A screenshot of the GitHub error page, featuring an angry-looking Unicorn.&#34; loading=&#34;lazy&#34; width=&#34;575&#34; height=&#34;477&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;An all-too familiar unicorn&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Fast forward eight years, and GitHub is about what we all imagined when Microsoft bought it. If you take the most pessimistic way of counting, they have &lt;a href=&#34;https://mrshu.github.io/github-statuses/&#34;&gt;zero nines of reliability&lt;/a&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. If you take the most generous, they have a single nine. That’s around 30 minutes of downtime every day. It feels like it must be more than that, given how many times you see the damned unicorn.&lt;/p&gt;
&lt;p&gt;Uptime aside, they’ve crowbarred LLMs in all over the place; they &lt;a href=&#34;https://github.com/actions/create-release/issues/119&#34;&gt;don’t have the time&lt;/a&gt; to maintain official GitHub actions; the ones they do maintain have &lt;a href=&#34;https://github.com/actions/toolkit/compare/09cb71a033743b7545c8a9181facc06d7d6012ba...7ae5c2f423367fd11aa625ddcc0bbb0a8e5de5fa&#34;&gt;Copilot running roughshod all over them&lt;/a&gt;&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;; actions themselves are slow to run&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;, often have weird transient failures, and there are &lt;em&gt;so many&lt;/em&gt; footguns that I’m pretty sure GitHub actions should be an entire category in the CWE top 10.&lt;/p&gt;
&lt;p&gt;Speaking of security, while I was drafting this post a remote code execution &lt;a href=&#34;https://www.wiz.io/blog/github-rce-vulnerability-cve-2026-3854&#34;&gt;was reported&lt;/a&gt; and the details are… impressive. The actual security issue was a pretty stupid oversight. Those happen. But they end up being able to execute code as a globally shared &lt;code&gt;git&lt;/code&gt; user with access to all the other repositories on the node. What? I can’t quite get my head around it. No sandboxing, no containers, it just runs as a &lt;code&gt;git&lt;/code&gt; user?&lt;/p&gt;
&lt;p&gt;So, yeah, I’m not a fan of GitHub in its current state. A few months back I took the plunge and set up &lt;a href=&#34;https://forgejo.org/&#34;&gt;Forgejo&lt;/a&gt;, a fork of Gitea where they did radical things like add tests and ensure that it’s community operated not twisted for commercial use. Forgejo also potentially &lt;a href=&#34;https://dustri.org/b/carrot-disclosure-forgejo.html&#34;&gt;has some fun security problems&lt;/a&gt;&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:4&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;, but the way I host it mitigates more-or-less all problems. It’s also slightly less shocking when an open source project has lapses in basic security versus a $2 trillion corporation.&lt;/p&gt;
&lt;p&gt;It’s taken a bit of doing, but I’m very happy with the setup I have.&lt;/p&gt;
&lt;h3 id=&#34;the-basic-setup&#34;&gt;The basic setup&lt;/h3&gt;
&lt;p&gt;I run all my server software using Docker, and Forgejo is no exception. They have &lt;a href=&#34;https://forgejo.org/docs/latest/admin/installation/docker/&#34;&gt;decent documentation&lt;/a&gt; on how to get it running, and publish rootless images which is nice (and diminishes my urge to make an &lt;a href=&#34;https://chameth.com/artisanal-docker-images/&#34;&gt;artisanal version myself&lt;/a&gt;). You can set configuration options using environment variables, which is perfect for containers, although the names can end up a bit unwieldy. Like this thing: &lt;code&gt;FORGEJO__repository.signing__SIGNING_NAME=Chris Smith&lt;/code&gt;. It gets the job done, but the environment-to-ini mapping is pretty ugly.&lt;/p&gt;
&lt;p&gt;I didn’t want to expose Forgejo publicly, as I wanted to avoid having to try and secure it&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:5&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;, or dealing with bots scraping it, or users signing up, and so on. I wanted somewhere to store my git repositories, handle CI for me, and mirror them somewhere public for other people to see and interact with. If you’ve read some of my other posts you’ll probably guess where this is heading: &lt;a href=&#34;https://tailscale.com/&#34;&gt;Tailscale&lt;/a&gt;. Alongside the Forgejo container, I run a Tailscale instance with a &lt;code&gt;serve.json&lt;/code&gt; that covers both the web frontend and the SSH listener used for git operations:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;TCP&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;22&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;TCPForward&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;forgejo:2222&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;443&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;HTTPS&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-kc&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;Web&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;${TS_CERT_DOMAIN}:443&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;Handlers&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;                &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;                    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;Proxy&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;http://forgejo:3000&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;                &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;AllowFunnel&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;${TS_CERT_DOMAIN}:443&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-kc&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This allows me to interact normally with Forgejo from any of my devices or servers (which all run Tailscale), while not exposing it to the Internet at all. It also handles the TLS certificates automatically, which saves me setting up something else to do that.&lt;/p&gt;
&lt;p&gt;For CI, I run the Forgejo runner image on the same server, and gave it a &lt;code&gt;dind&lt;/code&gt; container to use to actually run the workloads. &lt;code&gt;dind&lt;/code&gt; is the cutesy name for Docker-in-Docker, basically a Docker daemon running inside a Docker container. That keeps the CI workload separated nicely from the “production” workload running on the server, which is nice from both a security and a monitoring point of view.&lt;/p&gt;
&lt;h3 id=&#34;the-uncanny-valley-of-actions&#34;&gt;The uncanny valley of actions&lt;/h3&gt;
&lt;p&gt;Forgejo actions are basically like GitHub actions: they run a series of steps using one or more container images. You define those steps in the exact same clunky YAML format as with GitHub. The big difference between them is that GitHub defaults to using an absolutely massive base image filled with &lt;a href=&#34;https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md&#34;&gt;out-of-date preinstalled software&lt;/a&gt;, and Forgejo leaves that for the administrator to configure. You could, of course, just use the same image as GitHub, but lugging around a multi-gigabyte image that doesn’t even have recent versions of the software I want isn’t really my style. Instead, I set about using a plain &lt;code&gt;debian&lt;/code&gt; image as the base, and then did a face-palm when I tried to run a “normal” action and remembered they’re all written in JavaScript, so expect node to exist.&lt;/p&gt;
&lt;p&gt;Accommodating JavaScript isn’t really my style, either, so I did the very sensible thing of writing &lt;a href=&#34;https://github.com/csmith/actions&#34;&gt;my own suite of actions in Go&lt;/a&gt;. Being written in Go means they can be compiled statically, published as a container, and manage their own very limited dependencies. For example, the &lt;code&gt;checkout&lt;/code&gt; action is built into an &lt;code&gt;alpine&lt;/code&gt; container with &lt;code&gt;git&lt;/code&gt; added, while the &lt;code&gt;dockerbuild&lt;/code&gt; action uses the &lt;code&gt;buildah&lt;/code&gt; image as a base. Each image has the tools it needs, pinned to a recent version, and nothing more. This is very much not a sensible approach to take for most people, but it made me happy. With six basic actions, I could replace almost all the ad-hoc workflows I had in GitHub&lt;sup id=&#34;fnref:6&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:6&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Other than the base images and my self-inflicted JavaScript machinations, everything works the same as GitHub. But faster. So much faster. Most of my CI workflows finish on Forgejo before GitHub would even have allocated a runner to the job&lt;sup id=&#34;fnref:7&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:7&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;. And I can actually view the logs without them glitching out.&lt;/p&gt;
&lt;h3 id=&#34;step-aside-dependabot-here-comes-renovate&#34;&gt;Step aside Dependabot, here comes Renovate&lt;/h3&gt;
&lt;p&gt;One of the boons and/or banes of hosting code on GitHub is access to Dependabot&lt;sup id=&#34;fnref:8&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:8&#34; role=&#34;doc-noteref&#34;&gt;8&lt;/a&gt;&lt;/sup&gt;. It’s a tool that monitors your dependencies, and automatically submits PRs when new versions are available. It also does security alerts, but they’re comically bad for Go at least&lt;sup id=&#34;fnref:9&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:9&#34; role=&#34;doc-noteref&#34;&gt;9&lt;/a&gt;&lt;/sup&gt;. I do like having dependencies automatically handled, especially with &lt;a href=&#34;https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns&#34;&gt;a cooldown&lt;/a&gt;, which is how I had Dependabot configured before moving.&lt;/p&gt;
&lt;p&gt;To replace it, I’ve configured &lt;a href=&#34;https://github.com/renovatebot/renovate&#34;&gt;Renovate&lt;/a&gt;, an open source alternative. I’d seen it used a bunch before: both by projects on GitHub who prefer it over Dependabot, and by people using GitLab and other platforms where Dependabot isn’t. It mostly does the same things, but Renovate has a few very nice extras.&lt;/p&gt;
&lt;p&gt;Firstly, it supports transcluding config from another repo. So in each of my many, many projects, I just have this stub of a renovate config:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;$schema&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;https://docs.renovatebot.com/renovate-schema.json&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;extends&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;local&amp;gt;meta/renovate&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then the actual config lives in a &lt;code&gt;default.json&lt;/code&gt; in the &lt;code&gt;meta/renovate&lt;/code&gt; repository. I &lt;a href=&#34;https://github.com/csmith/forgejo-renovate&#34;&gt;mirror that repository&lt;/a&gt; to GitHub, if you want to have a look. This is an amazing feature. When I enabled cooldowns in my Dependabot config it was a &lt;em&gt;slog&lt;/em&gt; to go through all my active repositories and make the same change over and over again. With Renovate I can configure that centrally. As far as I know Dependabot can’t do anything like that, even for enterprises.&lt;/p&gt;
&lt;p&gt;Within that central config I use some of the other nice features. It has the ability to auto merge changes. I use this to automatically accept changes from trusted projects, or my own libraries:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchManagers&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;gomod&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchPackagePatterns&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;^github\\.com\\/csmith\\/&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;^chameth\\.com\\/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchUpdateTypes&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;minor&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;patch&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;automerge&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;minimumReleaseAge&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;0 days&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchManagers&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;gomod&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchPackagePatterns&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;^github\\.com\\/csmith\\/&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;^chameth\\.com\\/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchUpdateTypes&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;major&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;minimumReleaseAge&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;0 days&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This takes any minor or patch release of my libraries, and automerges it with no cooldown. For major version changes it overrides the cooldown but doesn’t automerge. To automerge Dependabot PRs you have to write an action, implement that logic yourself, and hope you’ve successfully avoided all the footguns inherent in that.&lt;/p&gt;
&lt;p&gt;Speaking of major version updates, Renovate also has an option to automatically update the imports in Go packages when it’s offering a major update. In the config I have:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;postUpdateOptions&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;gomodTidy&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;gomodUpdateImportPaths&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which means that every time it does an update it’ll run &lt;code&gt;go mod tidy&lt;/code&gt;, and it’ll update the import paths. In Go, &lt;code&gt;v2&lt;/code&gt; of a module has a different import path to &lt;code&gt;v1&lt;/code&gt; (so they can coexist if transient dependencies need different versions), so usually you have to go through and find and replace those paths. Renovate handles that for me now! I still have to address whatever breaking changes are in the new version, but there’s a lot less grunt work.&lt;/p&gt;
&lt;p&gt;It can also run custom commands&lt;sup id=&#34;fnref:10&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:10&#34; role=&#34;doc-noteref&#34;&gt;10&lt;/a&gt;&lt;/sup&gt;, so for example in my &lt;a href=&#34;https://github.com/csmith/legotapas&#34;&gt;legotapas&lt;/a&gt; project I have this extra snippet in the &lt;code&gt;renovate.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;postUpgradeTasks&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;commands&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;sh -c &amp;#39;rm plate_*.go&amp;#39;&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;go run ./cmd/generate&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;executionMode&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;branch&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;fileFilters&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;*.go&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I won’t go into the gory details, but this handles automatically regenerating a bunch of files that change every time a certain dependency is updated. Previously I had a separate action to do that on GitHub; just being able to drop commands in the config file is much nicer.&lt;/p&gt;
&lt;p&gt;To actually run Renovate, I simply have a workflow in Forgejo on a schedule that uses their official Docker image. It also triggers if I update the config, which is nice when I add a new rule, as it’ll immediately update the open PRs to comply. Overall, Renovate has felt like a pretty big quality-of-life upgrade.&lt;/p&gt;
&lt;h3 id=&#34;odd-problems&#34;&gt;Odd problems&lt;/h3&gt;
&lt;p&gt;Not everything has been smooth sailing. Sometimes when Renovate automerged a change, it would just… disappear. The PR was closed as merged, but the commit never landed on master. It wasn’t a huge issue as the next Renovate run would just recreate it, but it would then &lt;em&gt;disable&lt;/em&gt; the automerge and I’d get confused as to why. It turns out there was a major problem with how I had everything set up. Renovate was telling Forgejo to automerge when the required checks pass, but I had no branch protection rules set up in Forgejo, so as soon as the PR was created it was eligible to merge. As the CI was so fast I hadn’t noticed that the automerges were happening &lt;em&gt;before&lt;/em&gt; the CI results were posted.&lt;/p&gt;
&lt;p&gt;The disappearing commits were because the repository was getting into a bad state when Renovate tried to automerge multiple changes instantly in the same repo. Forgejo has a bunch of maintenance commands that helped to fix that problem, and I then wrote a little script to enable branch protection rules so the CI would actually run. The automerging has been smooth since.&lt;/p&gt;
&lt;p&gt;Another issue I had early on was that merging would fail because the GPG keyring was locked. I had Forgejo configured to sign all the automatic commits, so you can verify what commits came from my infrastructure. Forgejo is the only process using the keyring, so I wasn’t sure why it was becoming locked. It turns out that GnuPG uses the hostname as part of the lockfile, and because Forgejo was running in Docker it was getting random hostnames based on the container ID. Explicitly setting a hostname made GnuPG and by extension Forgejo much happier.&lt;/p&gt;
&lt;h3 id=&#34;webhooks-and-updating&#34;&gt;Webhooks and updating&lt;/h3&gt;
&lt;p&gt;Forgejo has this amazing feature: you can add a webhook that’s called for all your repos. GitHub, for some reason, supports this for organisations but not people. Even though the two are treated the same in a lot of places. It doesn’t seem like it would be hard, and it’s such an obvious thing to want, but… no. I wrote a &lt;a href=&#34;https://github.com/csmith/webhooked&#34;&gt;whole tool&lt;/a&gt; to add webhooks for me to get around this. Now I don’t need it, Forgejo does the right thing!&lt;/p&gt;
&lt;p&gt;One particularly interesting use of these webhooks has been making my Docker containers auto-update when a new image is pushed. I previously had a webhook receiver for a few projects that would update them automatically, but GitHub package notifications were extremely laggy. Every other webhook fired within a minute or so of the event happening, but package webhooks would sometimes take 30-60+ minutes to fire. I opened a support case with GitHub and it got escalated to engineering and fixed about 10 weeks later&lt;sup id=&#34;fnref:11&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:11&#34; role=&#34;doc-noteref&#34;&gt;11&lt;/a&gt;&lt;/sup&gt;. It was better for a while, and then got worse again. Trying to do continuous deployment with a 30 minute random lag was frustrating.&lt;/p&gt;
&lt;p&gt;So now I had reliable webhooks, but the update process was a bit gross. I have a hangup about internet-facing services not having access to the Docker socket. It just feels like too big a security risk. So I had a process that received hooks, and then another that figured out if containers needed to be updated and restarted them. Adding a new container to the system meant recompiling the second process, which was a massive faff.&lt;/p&gt;
&lt;p&gt;Fortunately, I was saved from this mess by my friend &lt;a href=&#34;https://greboid.com/&#34;&gt;Greg&lt;/a&gt;. He wrote &lt;a href=&#34;https://github.com/greboid/adze&#34;&gt;adze&lt;/a&gt;, a tool that receives webhooks and uses Docker Compose to update matching containers. He wrote &lt;a href=&#34;https://greboid.com/ramblings/2026/04/Keeping-containers-up-to-date&#34;&gt;a blog post about it&lt;/a&gt;. It’s all one process, but that’s no longer a blocker for me because the webhooks can be delivered privately from my Forgejo instance without exposing adze to the internet. It finds matching containers automatically based on the images they’re using&lt;sup id=&#34;fnref:12&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:12&#34; role=&#34;doc-noteref&#34;&gt;12&lt;/a&gt;&lt;/sup&gt;, and combined with Forgejo not forcing me to add a webhook to every single repository makes my life so much easier. I write some code, push it to Forgejo, and within a minute or so it’s running on my dev instance.&lt;/p&gt;
&lt;p&gt;I have IRC notifications set up for when adze does updates, and I still have some old ones that announce GitHub package webhooks. I keep seeing things like this and chuckling:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;15:44:42 &amp;lt;@ircjag&amp;gt; [ADZE] chameth.com: pending updating git.yak-wall.ts.net/public/chameth.com
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;15:44:45 &amp;lt;@ircjag&amp;gt; [ADZE] chameth.com: success updating git.yak-wall.ts.net/public/chameth.com
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;17:11:54 &amp;lt;@ircjag&amp;gt; [GHCR] Container pushed to csmith/chameth.com:dev.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I pushed a change at 15:43, Forgejo built it and pushed the image (to both its own registry and to GitHub), then at 15:44 adze updated the container to the new version. An hour and a half later GitHub gets around to delivering its own webhook.&lt;/p&gt;
&lt;h3 id=&#34;mirroring-and-prs&#34;&gt;Mirroring and PRs&lt;/h3&gt;
&lt;p&gt;The final piece I had to figure out was how to actually interact with people. Most of my projects are small and don’t get a massive amount of public contributions, but the option should still be there. And ideally not involve anyone having to figure out how to e-mail patches&lt;sup id=&#34;fnref:13&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:13&#34; role=&#34;doc-noteref&#34;&gt;13&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Mirroring outwards is simple: Forgejo supports it natively. Whenever I push to Forgejo, it pushes out to the corresponding repo on GitHub. I’ve also started mirroring some projects to &lt;a href=&#34;https://codeberg.org/&#34;&gt;Codeberg&lt;/a&gt;, to avoid having GitHub as my single “public” presence. But what happens when I get a pull request? If I merge it in the GitHub web UI, Forgejo will push its own branch over the top and get rid of it&lt;sup id=&#34;fnref:14&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:14&#34; role=&#34;doc-noteref&#34;&gt;14&lt;/a&gt;&lt;/sup&gt;. I could pull the changes, merge them, and push the result to Forgejo but then I don’t get the benefit of any CI.&lt;/p&gt;
&lt;p&gt;I ended up writing a small private tool that deals with this for me. I give it a GitHub PR, and it effectively imports it into Forgejo for me. I created a &lt;code&gt;prs&lt;/code&gt; organisation, and it’ll create a fork of the project in that org if it doesn’t exist. Then it pushes the PR contents to a branch, and creates a PR from the &lt;code&gt;prs&lt;/code&gt; repo into the main one. It runs as a user which can write to the PRs org, but only submit pull requests to public repositories outside of it.&lt;/p&gt;
&lt;p&gt;When the PR is imported, I can review it in the Forgejo UI, and then I can approve the CI workflows for that particular PR. This gives me some extra defence-in-depth in case anyone is trying to abuse one of those action footguns. Once the CI passes, I can merge it as normal in the Forgejo UI, it gets mirrored out to GitHub, and GitHub marks the PR as merged automatically. It’s not the most elegant solution, but the actual workflow is pretty smooth, especially for the low volumes of PRs I get.&lt;/p&gt;
&lt;p&gt;With that, I have Forgejo set up to do everything I need from a git host, and I’m no longer hamstrung every time GitHub goes down.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;This way of counting isn’t &lt;a href=&#34;https://evanhahn.com/in-defense-of-githubs-poor-uptime/&#34;&gt;particularly fair&lt;/a&gt;, but it’s somewhat realistic. GitHub break out their uptime for things like git operations, pull requests, actions, webhooks and packages. Realistically if any of those independent things are down then my overall workflow doesn’t, well, work. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;To be clear, I’m not objecting to the use of Copilot here. It’s a tool. But if you use a tool to do something fairly simple and it spews out 35 commits, including at least 5 reverts, then either it’s a terrible tool or you’re using it very wrong. I didn’t hunt down this example, I came across it while trying to debug something and wasted a bunch of time trying to figure out what on earth was going on. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;You could self-host runners, which may help, except they’ve already threatened to &lt;a href=&#34;https://github.blog/changelog/2025-12-16-coming-soon-simpler-pricing-and-a-better-experience-for-github-actions/&#34;&gt;charge you for the privilege of bringing your own runners&lt;/a&gt;. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;If people could stop finding RCEs in git services for a few days so I can stop redrafting this intro, that’d be great. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:4&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:5&#34;&gt;
&lt;p&gt;I’m feeling pretty smug about this decision after reading about the security issues. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:5&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:6&#34;&gt;
&lt;p&gt;There’s one holdout: my &lt;a href=&#34;https://github.com/csmith/dockerfiles/&#34;&gt;dockerfiles repo&lt;/a&gt; which is a very special snowflake with a very complex workflow and I’m still deciding how to handle it. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:6&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:7&#34;&gt;
&lt;p&gt;Which is to be expected, given I’ve got a server dedicated to one person, and they’re trying to serve however-many million users at once. That doesn’t make it any less nice, though! &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:7&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:8&#34;&gt;
&lt;p&gt;I’d link to it but there’s nowhere really good to link to. It’s just been subsumed into the ‘security’ functions of GitHub, along with a bunch of copilot nonsense. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:8&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:9&#34;&gt;
&lt;p&gt;The go project maintain a &lt;a href=&#34;https://pkg.go.dev/vuln/&#34;&gt;vulnerability database&lt;/a&gt; that includes details about which exact symbols are affected, and the &lt;a href=&#34;https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck&#34;&gt;govulncheck&lt;/a&gt; tool uses this to only report security issues in code that’s actually used. Dependabot doesn’t do that: it constantly alerts about code that’s not imported, so won’t ever be shipped. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:9&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:10&#34;&gt;
&lt;p&gt;In the default config it doesn’t allow anything of the sort, of course. You can whitelist individual commands, but because this is a private server I can merrily whitelist &lt;code&gt;.*&lt;/code&gt; and do whatever I like. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:10&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:11&#34;&gt;
&lt;p&gt;The turnaround time isn’t particularly impressive, but the fact that they kept the support case open, and the (human!) support agent updated me several times is. I don’t have a lot of good things to say about GitHub, but I don’t think I’ve ever had a support experience that good from a company of a similar size. Especially as I wasn’t a paying customer at the time. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:11&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:12&#34;&gt;
&lt;p&gt;Why didn’t I do that in the first place? It seems so obvious now… &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:12&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:13&#34;&gt;
&lt;p&gt;I really like the &lt;em&gt;idea&lt;/em&gt; of an e-mail based workflow, but I absolutely hate actually doing it. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:13&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:14&#34;&gt;
&lt;p&gt;This is what happened to me a lot when I was half using Gitea. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:14&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Monthly Meanderings: March 2026</title>
        <link href="https://chameth.com/monthly-meanderings-2026-03/"/>
        <updated>2026-04-01T00:00:00Z</updated>
        <id>https://chameth.com/monthly-meanderings-2026-03/</id>
        <content xml:lang="en" type="html">&lt;p&gt;Since &lt;a href=&#34;https://chameth.com/monthly-meanderings-2026-02/&#34;&gt;last month’s update&lt;/a&gt; I’ve been unpleasantly reminded that I’m middle aged, through the medium of a dodgy knee. There’s nothing quite like not being able to stand up without groaning to underscore that you’re not young any more. The ongoing game of “will this seemingly mundane activity make my knee go funny again?” is a &lt;em&gt;delight&lt;/em&gt; to play, too. I’m great at it.&lt;/p&gt;
&lt;h3 id=&#34;website-updates&#34;&gt;Website updates&lt;/h3&gt;
&lt;p&gt;Two new blog posts this month: &lt;a href=&#34;https://chameth.com/the-longest-way-to-represent-a-date/&#34;&gt;the longest way to represent a date&lt;/a&gt; is a short thought experiment on absurd date formats, and &lt;a href=&#34;https://chameth.com/modern-css-is-fun/&#34;&gt;modern CSS is fun&lt;/a&gt; goes through some neat new CSS features I’ve used recently for this site.&lt;/p&gt;
&lt;p&gt;I did some design tweaks this month: headers now have a blue background behind them&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;, and a lot of components have been redesigned to have a consistent style. You can see that component in action in the new “now playing” widget I’ve added:&lt;/p&gt;
&lt;div class=&#34;now-playing raised-box&#34; data-title=&#34;Now playing&#34;&gt;
    &lt;img src=&#34;https://chameth.com/music/albums/456/cover.jpg&#34; alt=&#34;The Rise and Fall of a Midwest Princess&#34; loading=&#34;lazy&#34;/&gt;
    &lt;div class=&#34;now-playing-info&#34;&gt;
        &lt;span class=&#34;artist-track&#34;&gt;Chappell Roan — After Midnight&lt;/span&gt;
        &lt;span class=&#34;album-name&#34;&gt;The Rise and Fall of a Midwest Princess&lt;/span&gt;
        &lt;span class=&#34;play-status&#34;&gt;Scrobbled 19m ago&lt;/span&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Instead of each component defining its own styles (which were &lt;em&gt;mostly&lt;/em&gt; copied and pasted), there’s now a single class that handles the drop shadow, border, background colour, and the optional title. It also applies a consistent format to elements that are clickable: they use the blue accent colour for their borders, and have a glow effect shown on hover.&lt;/p&gt;
&lt;p&gt;The now playing widget is part of a larger bit of work I did on importing some music stats. I’m pulling in the data from my &lt;a href=&#34;https://www.navidrome.org/&#34;&gt;Navidrome&lt;/a&gt; instance. A new &lt;a href=&#34;https://chameth.com/music/&#34;&gt;music page&lt;/a&gt; shows my most listened albums and artists. Navidrome doesn’t store a complete play history, just the last play and the count, so I can’t do “what I listened to last month” stats until next month.&lt;/p&gt;
&lt;h3 id=&#34;other-projects&#34;&gt;Other projects&lt;/h3&gt;
&lt;p&gt;Other than a minor bug-fix update to &lt;a href=&#34;https://github.com/csmith/contempt&#34;&gt;contempt&lt;/a&gt;, my Dockerfile templating/updating tool, I’ve not done much on my other open source projects. I’ve got a bit more work to do finish migrating all my repositories from GitHub to my private &lt;a href=&#34;https://forgejo.org/&#34;&gt;Forgejo&lt;/a&gt; instance. I’ve also been thinking about mirroring my public repositories to &lt;a href=&#34;https://codeberg.org/&#34;&gt;Codeberg&lt;/a&gt; so there’s a non-GitHub way to access them.&lt;/p&gt;
&lt;h3 id=&#34;entertainment&#34;&gt;Entertainment&lt;/h3&gt;
&lt;p&gt;I apparently only watched a single film in March:&lt;/p&gt;
&lt;div class=&#34;film-review-parent&#34;&gt;
  &lt;section class=&#34;film-review raised-box&#34;&gt;
    &lt;img src=&#34;https://chameth.com/films/253/poster.jpg&#34; alt=&#34;Poster for The Substance&#34; loading=&#34;lazy&#34;/&gt;
    &lt;header&gt;
      &lt;h3 class=&#34;plain-header&#34;&gt;&lt;a href=&#34;https://chameth.com/films/the-substance-2024/&#34;&gt;The Substance&lt;/a&gt;&lt;/h3&gt;
      &lt;div&gt;&lt;/div&gt;
      &lt;div title=&#34;5/10&#34;&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
      &lt;time&gt;2026-03-18&lt;/time&gt;
    &lt;/header&gt;
    &lt;div&gt;&lt;p&gt;I really didn’t get on with this. It’s too long for what it is, and over-the-top in a way that just didn’t work for me.&lt;/p&gt;
&lt;p&gt;Demi Moore and Margaret Qualley were great, but nothing else really was. The cinematography and sound design were too affected, the plot was pretty shallow and obvious, most of the body horror fell into the uncanny CGI valley for me.&lt;/p&gt;
&lt;p&gt;It’s not completely without merit: there were around 40 minutes in the middle where I was really into it, but that’s not really enough given it’s closing in on a 2.5 hour runtime.&lt;/p&gt;
&lt;p&gt;There’s something to be said for the message it’s presenting about women in Hollywood, too, but it was very heavy handed. Dennis Quaid’s character is even called Harvey; it’s not so much a subtle nod as a sledgehammer to the face.&lt;/p&gt;
&lt;/div&gt;
  &lt;/section&gt;
&lt;/div&gt;
&lt;p&gt;Instead of films, I’ve been watching a bunch of TV. After devouring &lt;a href=&#34;https://www.themoviedb.org/tv/250307-the-pitt&#34;&gt;The Pitt&lt;/a&gt; last month, I struggled to find something to fill the hole it left. I settled on catching up on &lt;a href=&#34;https://www.themoviedb.org/tv/44006-chicago-fire&#34;&gt;Chicago Fire&lt;/a&gt; and &lt;a href=&#34;https://www.themoviedb.org/tv/58841-chicago-p-d&#34;&gt;Chicago P.D.&lt;/a&gt;. They’re both mostly about getting things done without personal drama becoming the primary focus. They’re not quite as grounded as The Pitt, but at least they’ve not completely gone off the deep end like &lt;a href=&#34;https://www.themoviedb.org/tv/75219-9-1-1&#34;&gt;9-1-1&lt;/a&gt; where the first responders end up in space somehow?&lt;/p&gt;
&lt;p&gt;I’ve also watched the first two episodes of &lt;a href=&#34;https://www.themoviedb.org/tv/288670-saturday-night-live-uk&#34;&gt;SNL UK&lt;/a&gt;. I didn’t have very high expectations, and it gave me a pleasant surprise. Both episodes so far have some hilarious sketches, and are obviously written by British comics with the typical darker, more sardonic humour that separates us from the USA.&lt;/p&gt;
&lt;p&gt;Last but not least, board games! I spent a weekend at a tabletop/LAN event with friends, so got to physically play some games for a change. I also kept up with some turn-based games on &lt;a href=&#34;https://boardgamearena.com/&#34;&gt;Board Game Arena&lt;/a&gt;, as usual.&lt;/p&gt;
&lt;ul class=&#34;played-boardgames&#34;&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Just One (2018)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/254640/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/254640/image.jpg&#34; alt=&#34;Box art of Just One&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;6 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;7 Wonders Dice (2025)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/446231/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/446231/image.jpg&#34; alt=&#34;Box art of 7 Wonders Dice&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;4 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Can&amp;#39;t Stop (1980)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/41/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/41/image.png&#34; alt=&#34;Box art of Can&amp;#39;t Stop&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;4 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Martian Dice (2011)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/99875/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/99875/image.png&#34; alt=&#34;Box art of Martian Dice&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;4 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Cartographers (2019)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/263918/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/263918/image.png&#34; alt=&#34;Box art of Cartographers&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;3 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Coffee Rush (2023)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/377061/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/377061/image.jpg&#34; alt=&#34;Box art of Coffee Rush&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;3 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Super Mega Lucky Box (2021)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/341530/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/341530/image.png&#34; alt=&#34;Box art of Super Mega Lucky Box&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;3 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Cthulhu Fluxx (2012)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/122159/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/122159/image.jpg&#34; alt=&#34;Box art of Cthulhu Fluxx&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Obsession (2018)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/231733/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/231733/image.png&#34; alt=&#34;Box art of Obsession&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Plant-Based Riot (2022)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/342940/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/342940/image.jpg&#34; alt=&#34;Box art of Plant-Based Riot&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Space Base (2018)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/242302/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/242302/image.jpg&#34; alt=&#34;Box art of Space Base&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Terraforming Mars (2016)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/167791/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/167791/image.jpg&#34; alt=&#34;Box art of Terraforming Mars&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Next Station: Tokyo (2023)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/380165/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/380165/image.jpg&#34; alt=&#34;Box art of Next Station: Tokyo&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;around-the-web&#34;&gt;Around the web&lt;/h3&gt;
&lt;h4 id=&#34;warranty-void-if-regeneratedhttpsnearzerosoftwarepwarranty-void-if-regenerated&#34;&gt;&lt;a href=&#34;https://nearzero.software/p/warranty-void-if-regenerated&#34;&gt;Warranty Void if Regenerated&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Some really cool speculative fiction about a future where all software is written by LLMs, and the social effects that may have. Unfortunately the story itself is written with an LLM, and that becomes painfully obvious about a third of the way in. By that point I was already hooked, though.&lt;/p&gt;
&lt;h4 id=&#34;my-2-step-process-for-ai-free-blogginghttpsmanuelmorealecomthoughtsmy-2-step-process-for-ai-free-blogging&#34;&gt;&lt;a href=&#34;https://manuelmoreale.com/thoughts/my-2-step-process-for-ai-free-blogging&#34;&gt;My 2-step process for AI-free blogging&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Speaking of LLM-generated text, this short piece by Manuel Moreale made me laugh. I’m by no means anti-LLM, but I really hate people publishing the output like it’s their own work. It breaks the social assumption that the writer put more effort in than the reader, and makes me wonder how much is hallucinated.&lt;/p&gt;
&lt;h4 id=&#34;25-years-of-eggshttpswwwjohn-rushcompostseggs-25-years-20260219html&#34;&gt;&lt;a href=&#34;https://www.john-rush.com/posts/eggs-25-years-20260219.html&#34;&gt;25 Years of Eggs&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;An interesting applied use of coding agents. I’m a sucker for personal statistics. I have no explicit desire to keep my receipts, or generate graphs of my egg consumption, but seeing the graphs makes me question that. The journey to get there is a great read, too.&lt;/p&gt;
&lt;h4 id=&#34;i-made-a-one-page-notebookhttpsjoelchronoxyzblogi-made-a-one-page-notebook&#34;&gt;&lt;a href=&#34;https://joelchrono.xyz/blog/i-made-a-one-page-notebook/&#34;&gt;I made a one-page notebook&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;The clever paper folding is interesting, but I really like some of the small touches on the page. There’s a nice little “in reply to” callout providing context at the top of the page, you can click to rotate the image further down, and there’s a details element containing a textual version of it. It has that great ‘small web’ vibe of someone who really cares about what they’re doing.&lt;/p&gt;
&lt;h4 id=&#34;workers-who-love-synergizing-paradigms-might-be-bad-at-their-jobshttpsnewscornelledustories202603workers-who-love-synergizing-paradigms-might-be-bad-their-jobs&#34;&gt;&lt;a href=&#34;https://news.cornell.edu/stories/2026/03/workers-who-love-synergizing-paradigms-might-be-bad-their-jobs&#34;&gt;Workers who love ‘synergizing paradigms’ might be bad at their jobs&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;I haven’t read the full paper, but the summary given by the article is equal parts interesting and amusing. They developed a “Corporate Bullshit Receptivity Scale”, what more is there to say?&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;It’s meant to resemble painter’s tape, but I’m not sure quite how well it works. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Modern CSS is fun</title>
        <link href="https://chameth.com/modern-css-is-fun/"/>
        <updated>2026-03-17T00:00:00Z</updated>
        <id>https://chameth.com/modern-css-is-fun/</id>
        <content xml:lang="en" type="html">&lt;p&gt;I’ve been doing a bunch of CSS tweaking recently, and keep being surprised by how nice modern CSS is to work with. As someone grey-haired enough to remember writing HTML &lt;em&gt;without&lt;/em&gt; CSS, it’s amazing to think how far along web technology has come&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. I wanted to demonstrate some of the handy bits and pieces I’ve used recently.&lt;/p&gt;
&lt;h3 id=&#34;has&#34;&gt;:has&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;:has&lt;/code&gt; selector allows you to effectively query for child elements. While &lt;code&gt;a span&lt;/code&gt; will match a &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; within an &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;, &lt;code&gt;a:has(span)&lt;/code&gt; will match an &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; that contains a &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;. This really shines when combined with more complex selectors, for example:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nt&#34;&gt;input&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;border-radius&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-n&#34;&gt;border-radius&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-k&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;chroma-nd&#34;&gt;:has&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;(+&lt;/span&gt; &lt;span class=&#34;chroma-nc&#34;&gt;.results&lt;/span&gt;&lt;span class=&#34;chroma-nd&#34;&gt;:not&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nd&#34;&gt;:empty&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;border-bottom-left-radius&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;border-bottom-right-radius&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is part of the styling for my film search box. When the search box is presented initially it has fully rounded corners. But when it is followed by a non-empty results element, it removes the rounding on the bottom corners so that the border continues in a straight line down into the results. You can see it in action below; just enter a few characters (like “the”) to get some results:&lt;/p&gt;
&lt;div data-form-search=&#34;&#34;&gt;
    &lt;noscript&gt;JavaScript required for film search&lt;/noscript&gt;
&lt;/div&gt;
&lt;p&gt;You could achieve the same effect by having JavaScript add a class to the &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; element, but I’ll take a CSS solution over a JavaScript solution any day.&lt;/p&gt;
&lt;h3 id=&#34;nested-rules&#34;&gt;Nested rules&lt;/h3&gt;
&lt;p&gt;You probably spotted this in the example above. It’s what finally made me switch from &lt;a href=&#34;https://sass-lang.com/&#34;&gt;SCSS&lt;/a&gt; to plain CSS. If you have a rule for &lt;code&gt;.foo&lt;/code&gt; and a rule for &lt;code&gt;.foo .bar&lt;/code&gt; you can just nest them. Not only does it save repeating yourself an awful lot, it keeps everything organised nicely. For example, the CSS for my film list embeds looks like this:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nt&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;chroma-nc&#34;&gt;.film-list&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;display&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;grid&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;grid-template-areas&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;header images&amp;#34;&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;description images&amp;#34;&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;meta images&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;grid-template-columns&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;fr&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;auto&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;grid-template-rows&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;auto&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;fr&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;auto&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;row-gap&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-ni&#34;&gt;small&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;chroma-ni&#34;&gt;space&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-k&#34;&gt;@media&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-ni&#34;&gt;width&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;800&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;px&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;grid-template-areas&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;header&amp;#34;&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;images&amp;#34;&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;meta&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;grid-template-columns&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;fr&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;grid-template-rows&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;auto&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;auto&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;auto&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;auto&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-k&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;chroma-k&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;margin-top&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-ni&#34;&gt;medium&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;chroma-ni&#34;&gt;space&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;h3&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;font-size&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-n&#34;&gt;font-size-xxlarge&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nc&#34;&gt;.description&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-cm&#34;&gt;/* ... */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Everything is wrapped up in a single bundle, including the &lt;code&gt;@media&lt;/code&gt; rules for changing the layout on smaller screens, the lovely little &lt;code&gt;&amp;amp; + &amp;amp;&lt;/code&gt; rule that adds some extra margin if there are two lists in a row, etc. The &lt;code&gt;&amp;amp;&lt;/code&gt; syntax refers to the parent selector, so &lt;code&gt;&amp;amp; + &amp;amp;&lt;/code&gt; in this case is the same as &lt;code&gt;a.film-list + a.film-list&lt;/code&gt;: it’s very handy! The best part about this is that it’s the exact same syntax as SCSS, so converting is easy&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;. The styled element looks like this:&lt;/p&gt;
&lt;a class=&#34;film-list raised-box plain-link&#34; data-title=&#34;Film list&#34; href=&#34;https://chameth.com/films/lists/ranking/&#34;&gt;
  &lt;h3 class=&#34;plain-header&#34;&gt;Watched films ranked&lt;/h3&gt;
  &lt;div class=&#34;poster-stack&#34;&gt;
    &lt;img src=&#34;https://chameth.com/films/265/poster.jpg&#34; alt=&#34;Poster of Midsommar&#34;/&gt;&lt;img src=&#34;https://chameth.com/films/4/poster.jpg&#34; alt=&#34;Poster of Easy A&#34;/&gt;&lt;img src=&#34;https://chameth.com/films/97/poster.jpg&#34; alt=&#34;Poster of Hackers&#34;/&gt;&lt;img src=&#34;https://chameth.com/films/25/poster.jpg&#34; alt=&#34;Poster of The Matrix&#34;/&gt;&lt;img src=&#34;https://chameth.com/films/202/poster.jpg&#34; alt=&#34;Poster of Sinners&#34;/&gt;
  &lt;/div&gt;
  &lt;div class=&#34;description&#34;&gt;&lt;p&gt;Every film I’ve watched since I started logging, ranked.&lt;/p&gt;
&lt;p&gt;Obviously super subjective, and subject to change often and arbitrarily.&lt;/p&gt;
&lt;/div&gt;
  &lt;p class=&#34;count&#34;&gt;264 films&lt;/p&gt;
&lt;/a&gt;
&lt;h3 id=&#34;media-range-syntax&#34;&gt;Media range syntax&lt;/h3&gt;
&lt;p&gt;Once again, you might have spotted this in the previous snippet. I’m not deliberately teasing things, I promise! Back in the day, you did media queries like so:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;@media&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-na&#34;&gt;min-width&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;1000&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;px&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-ow&#34;&gt;and&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-na&#34;&gt;max-width&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;2000&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;px&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-cm&#34;&gt;/* some rules that work for screens at least 1000px wide and at most 2000px wide */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I always hated this syntax. I always got muddled up as to whether I wanted “min” or “max”, and whether they were inclusive or not. It’s easy to reason through,
but it never came naturally. Fortunately you can now just use ranges:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;@media&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-mi&#34;&gt;1000&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;px&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;width&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;2000&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;px&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-cm&#34;&gt;/* some rules that work for screens at least 1000px wide and at most 2000px wide */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I find this style so much easier to write and understand.&lt;/p&gt;
&lt;h3 id=&#34;anchor-positioning&#34;&gt;Anchor positioning&lt;/h3&gt;
&lt;p&gt;Trying to dynamically position one element next to another used to exclusively fall within the purview of JavaScript. Fortunately anchor positioning
fixes all this. I use this in the film search field I showed above:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nc&#34;&gt;.film-search&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;input&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;anchor-name&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-n&#34;&gt;film-search-box&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nc&#34;&gt;.results&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;position&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;absolute&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;position-anchor&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-n&#34;&gt;film-search-box&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;position-area&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;bottom&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;center&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;position-visibility&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;always&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;anchor-size&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This does lots of fun things. The &lt;code&gt;.results&lt;/code&gt; element is anchored to the &lt;code&gt;input&lt;/code&gt; field (via the &lt;code&gt;--film-search-box&lt;/code&gt; name), and it’s positioned on the bottom,
directly under the anchor. The &lt;code&gt;position-area&lt;/code&gt; rule describes a 3x3 grid, with the anchor at the center, so &lt;code&gt;top left&lt;/code&gt; would put it diagonally adjacent to
the anchor. Next, &lt;code&gt;position-visibility&lt;/code&gt; keeps the &lt;code&gt;.results&lt;/code&gt; element visible when the anchor &lt;em&gt;isn’t&lt;/em&gt;; otherwise when you scroll the anchor off the screen
the results would immediately vanish. Finally, the special &lt;code&gt;anchor-size()&lt;/code&gt; function makes the &lt;code&gt;.results&lt;/code&gt; element take up the same exact width as the anchor.&lt;/p&gt;
&lt;p&gt;All-in-all, this puts the results exactly where I want them, without having to deal with any JavaScript at all. I can see how it’d also be amazingly useful
trying to do tooltips or other forms of ‘floating’ content too. You can even leave the positioning up to the browser, giving it hints about which order to
try, or what property to optimise for (e.g. you can say “anchor to the left or right, whichever has more horizontal space”).&lt;/p&gt;
&lt;h3 id=&#34;attr-function&#34;&gt;attr() function&lt;/h3&gt;
&lt;p&gt;This one blew my mind a little. In some of the elements on the site, I’ve added a little label to the top. It looks something like this:&lt;/p&gt;
&lt;figure class=&#34;rating-distribution raised-box&#34; data-title=&#34;Film rating distribution&#34;&gt;
 &lt;div class=&#34;chart-container&#34;&gt;
  &lt;span class=&#34;left-label&#34;&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/span&gt;
  &lt;svg width=&#34;200&#34; height=&#34;50&#34; viewBox=&#34;0 0 200 50&#34; xmlns=&#34;http://www.w3.org/2000/svg&#34; role=&#34;img&#34; aria-label=&#34;Film rating distribution chart&#34;&gt;
   &lt;rect x=&#34;1&#34; y=&#34;37&#34; width=&#34;18&#34; height=&#34;13&#34; fill=&#34;var(--accent-colour)&#34; class=&#34;rating-bar&#34;&gt;
    &lt;title&gt;0.5 stars: 11 films&lt;/title&gt;
   &lt;/rect&gt;&lt;rect x=&#34;21&#34; y=&#34;29&#34; width=&#34;18&#34; height=&#34;21&#34; fill=&#34;var(--accent-colour)&#34; class=&#34;rating-bar&#34;&gt;
    &lt;title&gt;1.0 stars: 18 films&lt;/title&gt;
   &lt;/rect&gt;&lt;rect x=&#34;41&#34; y=&#34;33&#34; width=&#34;18&#34; height=&#34;17&#34; fill=&#34;var(--accent-colour)&#34; class=&#34;rating-bar&#34;&gt;
    &lt;title&gt;1.5 stars: 14 films&lt;/title&gt;
   &lt;/rect&gt;&lt;rect x=&#34;61&#34; y=&#34;31&#34; width=&#34;18&#34; height=&#34;19&#34; fill=&#34;var(--accent-colour)&#34; class=&#34;rating-bar&#34;&gt;
    &lt;title&gt;2.0 stars: 16 films&lt;/title&gt;
   &lt;/rect&gt;&lt;rect x=&#34;81&#34; y=&#34;12&#34; width=&#34;18&#34; height=&#34;38&#34; fill=&#34;var(--accent-colour)&#34; class=&#34;rating-bar&#34;&gt;
    &lt;title&gt;2.5 stars: 32 films&lt;/title&gt;
   &lt;/rect&gt;&lt;rect x=&#34;101&#34; y=&#34;13&#34; width=&#34;18&#34; height=&#34;37&#34; fill=&#34;var(--accent-colour)&#34; class=&#34;rating-bar&#34;&gt;
    &lt;title&gt;3.0 stars: 31 films&lt;/title&gt;
   &lt;/rect&gt;&lt;rect x=&#34;121&#34; y=&#34;7&#34; width=&#34;18&#34; height=&#34;43&#34; fill=&#34;var(--accent-colour)&#34; class=&#34;rating-bar&#34;&gt;
    &lt;title&gt;3.5 stars: 36 films&lt;/title&gt;
   &lt;/rect&gt;&lt;rect x=&#34;141&#34; y=&#34;0&#34; width=&#34;18&#34; height=&#34;50&#34; fill=&#34;var(--accent-colour)&#34; class=&#34;rating-bar&#34;&gt;
    &lt;title&gt;4.0 stars: 42 films&lt;/title&gt;
   &lt;/rect&gt;&lt;rect x=&#34;161&#34; y=&#34;19&#34; width=&#34;18&#34; height=&#34;31&#34; fill=&#34;var(--accent-colour)&#34; class=&#34;rating-bar&#34;&gt;
    &lt;title&gt;4.5 stars: 26 films&lt;/title&gt;
   &lt;/rect&gt;&lt;rect x=&#34;181&#34; y=&#34;4&#34; width=&#34;18&#34; height=&#34;46&#34; fill=&#34;var(--accent-colour)&#34; class=&#34;rating-bar&#34;&gt;
    &lt;title&gt;5.0 stars: 39 films&lt;/title&gt;
   &lt;/rect&gt;
  &lt;/svg&gt;
  &lt;span class=&#34;right-label&#34;&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;/span&gt;
&lt;/span&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;It’s meant to just be a visual flourish, not a semantic title or anything, so I initially just added a &lt;code&gt;::before&lt;/code&gt; selector to each element, customising the
&lt;code&gt;content&lt;/code&gt; to have the right value. I didn’t like the duplication, though. Now, instead, I do this:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;chroma-nd&#34;&gt;::before&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;content&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;attr&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-n&#34;&gt;data-title&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Each component defines its title in a data attribute: &lt;code&gt;&amp;lt;div class=&amp;#34;raised-box&amp;#34; data-title=&amp;#34;an example&amp;#34;&amp;gt;&lt;/code&gt;, and the &lt;code&gt;attr&lt;/code&gt; function plucks it out and
puts it in the content rule. It’s worth noting that this is only widely supported for the &lt;code&gt;content&lt;/code&gt; rule, you can’t yet use it for colours or dimensions
or other things. It still feels a bit magical, though. Like you’re making a reusable, customisable component with just HTML and CSS.&lt;/p&gt;
&lt;h3 id=&#34;layers&#34;&gt;layers&lt;/h3&gt;
&lt;p&gt;I’d come across layers a few times, but I never found a need for them. It seemed like something you’d only really need with complicated design systems,
or something. Then I had a problem and layers were the perfect solution!&lt;/p&gt;
&lt;p&gt;The headings on this site have a whole bunch of CSS attached to them. Currently they look a bit like blue painter’s tape with handwriting on them.
This style is applied to all headers, and then the various places that &lt;em&gt;don’t&lt;/em&gt; want it had to manually reset everything. I got fed up with that, so
decided to add a &lt;code&gt;plain-header&lt;/code&gt; class which would “disable” the extra styling:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nt&#34;&gt;h2&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nt&#34;&gt;h3&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nt&#34;&gt;h4&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nt&#34;&gt;h5&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nt&#34;&gt;h6&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-cm&#34;&gt;/* common styles for all headers, regardless */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;font-family&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;Chris Hand&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;sans-serif&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;font-size&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-n&#34;&gt;font-size-xxlarge&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;color&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-ni&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;chroma-n&#34;&gt;highlight-colour&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;margin&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-ni&#34;&gt;medium&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;chroma-ni&#34;&gt;space&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;line-height&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-k&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;chroma-nd&#34;&gt;:not&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nc&#34;&gt;.plain-header&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-cm&#34;&gt;/* do fancy stuff if it _doesn&amp;#39;t_ have the .plain-header class */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;transform&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;rotate&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;chroma-mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;chroma-mf&#34;&gt;.15&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;deg&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;padding&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-n&#34;&gt;smedium-space&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;font-weight&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;800&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-na&#34;&gt;filter&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;drop-shadow&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;px&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;px&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;px&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;rgb&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nt&#34;&gt;a&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-na&#34;&gt;background&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;none&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-na&#34;&gt;text-decoration-skip-ink&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-ni&#34;&gt;auto&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This might seem a bit backwards but I want the default to be styled, as those are far more common. I added this change,
sprinkled around some “plain-header” classes, tidied up a lot of duplicate CSS, and things mostly worked. Mostly.
Some things stopped working. In a few places the headers had slight tweaks, and they stopped working entirely.&lt;/p&gt;
&lt;p&gt;It turns out in moving the rules from a plain &lt;code&gt;h2&lt;/code&gt; selector to a &lt;code&gt;h2:not(.plain-header)&lt;/code&gt;, I’d made them more specific.
Previously a selector like &lt;code&gt;h2.special&lt;/code&gt; would have been more specific, so its properties would override those from the
less specific &lt;code&gt;h2&lt;/code&gt; selector. My first reaction was to try and hack around it. Changing all the overrides to
&lt;code&gt;body h2.special&lt;/code&gt; would make them more specific again, but I didn’t want to have to remember to do that forever more.&lt;/p&gt;
&lt;p&gt;Instead, I defined some custom layers, and put the header definitions in one:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;@layer&lt;/span&gt; &lt;span class=&#34;chroma-nt&#34;&gt;reset&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-nt&#34;&gt;links&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-nt&#34;&gt;headings&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;chroma-nt&#34;&gt;layer&lt;/span&gt; &lt;span class=&#34;chroma-nt&#34;&gt;headings&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;h2&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;h3&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-cm&#34;&gt;/* etc */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first &lt;code&gt;@layer&lt;/code&gt; rule defines some layers that will be handled in a specific order: first the “reset” layer, then
the “links” layer, then the “headings” layer, and then anything not in a layer after that. Rules in one layer don’t
have to worry about beating the specificity of rules in another layer, because they’re handled separately.&lt;/p&gt;
&lt;p&gt;Note that I couldn’t just put the headings in a layer and call it a day: their rules have to come after the CSS reset,
and after the normal link styles, as headers have a bit of extra styling for nested links. If the CSS reset weren’t in
a layer, then the &lt;code&gt;* { margin: 0; padding: 0; }&lt;/code&gt; type reset would apply over the rules from the headings layer.&lt;/p&gt;
&lt;p&gt;Using layers is definitely a bit fiddly, and is not necessary for a lot of sites, but it’s a much cleaner alternative
to ugly specificity hacking.&lt;/p&gt;
&lt;h3 id=&#34;things-im-looking-forward-to&#34;&gt;Things I’m looking forward to&lt;/h3&gt;
&lt;p&gt;There are a few things that aren’t (widely) available yet, that particularly interest me:&lt;/p&gt;
&lt;h4 id=&#34;sibling-index-and-sibling-count&#34;&gt;sibling-index() and sibling-count()&lt;/h4&gt;
&lt;p&gt;These new functions are available in WebKit and Blink based browsers, but not Firefox. They give you the index of
the element within its siblings, or the count of siblings. For the film list component above, I currently have
this abomination for the overlapping posters:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;chroma-nd&#34;&gt;:nth-child&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nt&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;z-index&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;left&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;chroma-nd&#34;&gt;:nth-child&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nt&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;z-index&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;left&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;calc&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-mi&#34;&gt;133&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;px&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-n&#34;&gt;overlap&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;chroma-nd&#34;&gt;:nth-child&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nt&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;z-index&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;left&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;calc&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-mi&#34;&gt;133&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;px&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-n&#34;&gt;overlap&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;chroma-nd&#34;&gt;:nth-child&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nt&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;z-index&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;left&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;calc&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-mi&#34;&gt;133&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;px&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-n&#34;&gt;overlap&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;chroma-nd&#34;&gt;:nth-child&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nt&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;z-index&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;left&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;calc&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-mi&#34;&gt;133&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;px&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-n&#34;&gt;overlap&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These could be replaced with something like:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-na&#34;&gt;z-index&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;calc&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;sibling-count&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;sibling-index&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;());&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-na&#34;&gt;left&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;calc&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-mi&#34;&gt;133&lt;/span&gt;&lt;span class=&#34;chroma-kt&#34;&gt;px&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;chroma-nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;chroma-n&#34;&gt;overlap&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nf&#34;&gt;sibling-index&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;chroma-mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As an added bonus, it would scale to any number of posters. &lt;code&gt;index&lt;/code&gt; being 1-based not 0-based is a
bit unfortunate as it makes using it in calculations awkward (see &lt;code&gt;1&lt;/code&gt; offset in both of those rules!),
but it’s leagues better than writing 5 separate rules.&lt;/p&gt;
&lt;h4 id=&#34;random&#34;&gt;random()&lt;/h4&gt;
&lt;p&gt;Currently only available in Safari. There are a few places where I’d like to have slight random variations
of the style. Things like elements that are rotated slightly for aesthetics; they look a bit silly if
they’re all identically positioned. I also randomise the icons and positions of my rating stars to break
up the visual monotony. Here are a few examples:&lt;/p&gt;
&lt;p&gt;&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Currently I do the stars by hardcoding a bunch of classes and having the backend randomly apply one when
it generates the markup for stars. Being able to do it in CSS would be great, though.&lt;/p&gt;
&lt;h4 id=&#34;mixins&#34;&gt;Mixins&lt;/h4&gt;
&lt;p&gt;This one’s so far off it’s not even listed on &lt;a href=&#34;https://caniuse.com/&#34;&gt;caniuse.com&lt;/a&gt; yet. Mixins are another handy feature of
SCSS, that allow you to define reusable blocks of rules, then import them when needed:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;@mixin&lt;/span&gt;&lt;span class=&#34;chroma-nf&#34;&gt; fancy-background&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-na&#34;&gt;background&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-cm&#34;&gt;/*...&lt;/span&gt;&lt;span class=&#34;chroma-c&#34;&gt;*/&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nc&#34;&gt;.some-element&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-k&#34;&gt;@include&lt;/span&gt;&lt;span class=&#34;chroma-nd&#34;&gt; fancy-background&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nn&#34;&gt;#other-element&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-k&#34;&gt;@include&lt;/span&gt;&lt;span class=&#34;chroma-nd&#34;&gt; fancy-background&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This would be particularly useful if you can’t easily control the markup to add classes to everything. For example, my
footnotes are rendered by a markdown plugin, and don’t easily have a way to add extra classes to them. To style it the
same as another element, I currently duplicate a bunch of rules between them both. Mixins would allow me to define those
rules once, and then import them in both places.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;Aside from JavaScript, which seems to just endlessly reinvent new frameworks and ways to make the most sprawling dependency tree possible. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;It also means that if your website’s syntax highlighting library doesn’t seem to understand nested rules, you can just pretend your perfectly valid CSS is actually SCSS and it’ll magically work. Grumble, grumble. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Thinking more about backups</title>
        <link href="https://chameth.com/thinking-more-about-backups/"/>
        <updated>2025-11-01T00:00:00Z</updated>
        <id>https://chameth.com/thinking-more-about-backups/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/thinking-more-about-backups/backblaze.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/thinking-more-about-backups/backblaze.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/thinking-more-about-backups/backblaze.png&#34; alt=&#34;The Backblaze logo: a stylised flame above the word Backblaze&#34; loading=&#34;lazy&#34; width=&#34;500&#34; height=&#34;320&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The Backblaze logo&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Almost a year ago I wrote about &lt;a href=&#34;https://chameth.com/simple-backups-restic-hetzner/&#34;&gt;how I do backups with Restic and Hetzner&lt;/a&gt;.
That system has been ticking along well ever since, but recently I had some… thoughts. These backups are
all well and good if I accidentally delete a file, or a database gets corrupted, or something, but there are
two glaring issues:&lt;/p&gt;
&lt;p&gt;Firstly, I’m backing up my Hetzner server to Hetzner cloud storage. If something happens to Hetzner — or
my Hetzner account — then all my eggs go down with that basket. Obviously Hetzner are a big organisation
and aren’t likely to just vanish overnight, but I’m less confident about my account. Could a false abuse
report get it suspended? What if the UK passes
&lt;a href=&#34;https://www.legislation.gov.uk/ukpga/2023/50/contents&#34;&gt;even more dumb laws&lt;/a&gt; and Hetzner decide it’s easier
just to not do business with people here? This is the same sort of concern I have about Google accounts:
if you have half of your life in Google Drive and Google Mail, what happens if you comment on a YouTube
video, get flagged by an AI moderation process, and your account gets suspended? It’s probably not very
likely, but these are things my brain likes to dwell on.&lt;/p&gt;
&lt;p&gt;Secondly, the credentials to access the backups sit on each machine that is backed up. If someone malicious
gained access to the machine, they’d also have access to delete or tamper with all the backups. It feels
a little silly that the same attack could take down both the originals and the backups. There’s no way to
avoid that with Hetzner’s S3 implementation, as far as I can tell.&lt;/p&gt;
&lt;h3 id=&#34;exploring-options&#34;&gt;Exploring options&lt;/h3&gt;
&lt;p&gt;I toyed with the idea of making local copies of the backups, but the only way to avoid the same problems
would be to keep them offline and do a manual copy every now and then. I didn’t really want to do that,
and was concerned that if I did a monthly offline backup then I stood to lose up to a month of data in
the worst case.&lt;/p&gt;
&lt;p&gt;I then looked around at other S3 providers. &lt;a href=&#34;https://aws.amazon.com/s3/storage-classes/glacier/&#34;&gt;Amazon’s glacier offering&lt;/a&gt;
is tempting due to its very low storage costs, but you pay for that if you ever want to restore anything.
There are also lots of weird pricing edge cases around moving data between storage classes, minimum file
sizes, and so on. A much better option is &lt;a href=&#34;https://www.backblaze.com/cloud-storage&#34;&gt;Backblaze’s B2&lt;/a&gt; product.
Their pricing is much more straight-forward, and they have an interesting feature that’s particularly useful
in this case: &lt;a href=&#34;https://www.backblaze.com/blog/backblaze-b2-lifecycle-rules/&#34;&gt;lifecycle rules&lt;/a&gt;. Coupled with
the ability to create API keys that don’t have access to delete files (just “hide” them), this allows for
what’s effectively an append-only store.&lt;/p&gt;
&lt;p&gt;This works more-or-less out of the box with Restic. &lt;a href=&#34;https://pricey.uk/blog/restic-backups-without-delete/&#34;&gt;Joseph Price has a guide&lt;/a&gt;
that goes into the setup in a bit more depth. Basically, whenever Restic would delete a file (e.g. during
a “forget” or “prune” operation), it instead gets hidden and is only deleted when the B2 lifecycle rules
decide it should be. I’ve kept the existing Hetzner S3 backups for now, and just added an extra step to
the end of my script: a simple &lt;code&gt;restic copy&lt;/code&gt; and a &lt;code&gt;restic forget&lt;/code&gt;. B2 actually works out cheaper than the
Hetzner storage, as they don’t bill you for a minimum of 1TB storage; my current usage is around $3/month.
Not a bad price for some extra peace of mind!&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>Further Adventures in Music Organisation</title>
        <link href="https://chameth.com/further-adventures-in-music-organisation/"/>
        <updated>2025-09-21T00:00:00Z</updated>
        <id>https://chameth.com/further-adventures-in-music-organisation/</id>
        <content xml:lang="en" type="html">&lt;p&gt;I wrote before about how I’d &lt;a href=&#34;https://chameth.com/escaping-spotify-the-hard-way/&#34;&gt;dropped Spotify in favour of locally stored music&lt;/a&gt;,
but things have advanced a bit since. I had a few issues: Tauon would
occasionally manage to lose its database and along with it all my carefully
constructed playlists and song ratings&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;, and the experience on my phone was
not very fun.&lt;/p&gt;
&lt;p&gt;I had to manually sync the music by plugging my phone in to the
computer, and sometimes it just refused to mount the right partition. I don’t
think there’s really a good way to debug an Apple phone not behaving properly
when connected to a Linux desktop. Then I started wanting more than one playlist
synced&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;, and trying to find a way to make that work just broke me.&lt;/p&gt;
&lt;p&gt;I spent a while looking at different ways of hosting the music centrally.
&lt;a href=&#34;https://www.plex.tv/en-gb/plexamp/&#34;&gt;Plexamp&lt;/a&gt; gets lots of good reviews, and
I’ve used Plex a fair bit. I set about spinning up a Plex server, and just
could not get it working. The server ran fine, served the web interface, but
would neither associate with my account nor run standalone. The docs were
contradictory and there was very little useful logging. After a lot of
frustration, I stumbled across mentions that
&lt;a href=&#34;https://lowendbox.com/blog/plex-blocks-hetzner-in-move-against-piracy/&#34;&gt;Plex block running on Hetzner&lt;/a&gt;.
I assume that’s the cause of my issues, although I have no way to know for sure.
I use Hetzner for all my servers and other hosted services, but Plex have
decided I can’t run the self-hosted software that I have a lifetime subscription
for there. What the actual fuck?&lt;/p&gt;
&lt;p&gt;I could have probably worked around the arbitrary restriction, but I didn’t
want to throw more time down the drain. Instead, I set up
&lt;a href=&#34;https://www.navidrome.org/&#34;&gt;Navidrome&lt;/a&gt;, an open source music server. It
supports the Subsonic protocol, which means you can use a whole slew of
different clients with it (or even write your own). It also means there’s a
nice way to get data in and out of it programmatically, which I recall being
a bit of a fight with Plex.&lt;/p&gt;
&lt;!--more--&gt;
&lt;h3 id=&#34;syncing-and-organising&#34;&gt;Syncing and organising&lt;/h3&gt;
&lt;p&gt;All my music lived on my desktop, but now I wanted it on my server. At first
I just used &lt;code&gt;rsync&lt;/code&gt; to copy everything up. I’d maintain a local “master” copy,
and periodically shove the changes to the server&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;. That quickly got old,
so I set up &lt;a href=&#34;https://syncthing.net/&#34;&gt;Syncthing&lt;/a&gt; to keep the two folders in
sync. That worked… for a while.&lt;/p&gt;
&lt;p&gt;Concurrently, I was looking at improving the organisation of my music. It
was mostly organised by some Go programs I’d thrown together to automate the
importing, with no real validation of metadata or anything else. Some albums
got split up into multiple folders because the tracks had different artists
(and didn’t have an album artist set); some artists ended up with multiple
folders with slight spelling or case variations. It was upsetting.&lt;/p&gt;
&lt;p&gt;I’d used &lt;a href=&#34;https://picard.musicbrainz.org/&#34;&gt;MusicBrainz Picard&lt;/a&gt; before to fix
some issues, but I didn’t really like the UI, especially when trying to do
bulk actions. The main alternative is &lt;a href=&#34;https://beets.io/&#34;&gt;beets&lt;/a&gt;, which
describes itself as “the music geek’s media organiser”, and is entirely
command line. I was intrigued.&lt;/p&gt;
&lt;p&gt;I started playing around with beets, and quickly noticed a problem. Every time
I changed a metadata tag in a music file, Syncthing had to upload the entire
file. I was frequently changing tags across the whole library as I got beets
set up how I wanted, and Syncthing was handling it by uploading gigabytes of
files for every minor change. I was already a bit unhappy with having the
content duplicated in two places, so now I was using a command line organiser
I figured I could just get rid of my local copy and move everything to the
server.&lt;/p&gt;
&lt;p&gt;I set up a Docker container for beets, and then wrote some incredibly hacky
shell scripts so I could run &lt;code&gt;beet&lt;/code&gt; locally on my desktop and it would SSH
to the server over Tailscale and exec into the container, passing the arguments
along. Then I did one final sync of the music library, got rid of Syncthing,
double checked I had a backup and deleted all of my local music.&lt;/p&gt;
&lt;h3 id=&#34;bears-beets-battlestar-galactica&#34;&gt;Bears, Beets, Battlestar Galactica&lt;/h3&gt;
&lt;p&gt;Beets is amazing. It has a vast array of plugins that can do almost anything
you could want with a music library, and the command line workflow works really
well for me. It’s very well documented, and all the individual parts are
pleasingly simple and easy to understand.&lt;/p&gt;
&lt;p&gt;Here’s what it looks like when importing some new music:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/further-adventures-in-music-organisation/beets-importing.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/further-adventures-in-music-organisation/beets-importing.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/further-adventures-in-music-organisation/beets-importing.png&#34; alt=&#34;Screenshot of beets output when importing an album. It shows the source folder in blue, the matched metadata information in white with green highlights, the MusicBrainz URL, then a listing of all tracks&#34; loading=&#34;lazy&#34; width=&#34;782&#34; height=&#34;377&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;Beets importing an album&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;If it doesn’t get a perfect match then it shows the closest matches, and
summarises what’s different about them (missing tracks, different names,
etc), and lets you decide what to do. Aside from the metadata matching, it’s
doing a lot of things under the hood that aren’t necessarily apparent. It:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Looks up the genre from Last.fm&lt;/li&gt;
&lt;li&gt;Analyses all the tracks and writes ReplayGain metadata to them&lt;/li&gt;
&lt;li&gt;Fetches album art&lt;/li&gt;
&lt;li&gt;Checks all the files to make sure they’re actually playable&lt;/li&gt;
&lt;li&gt;Scrubs any existing metadata tags&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It also maintains its own database, and you can query against it. The query
language is both simple and quite powerful, like the rest of beets. As an
example:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;$ beet ls artist:&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;Linkin Park&amp;#34;&lt;/span&gt; year:2025 length:2:00..2:30 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;Linkin Park - From Zero - Casualty
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Almost all the other commands also let you use the query syntax, which makes
the whole tool really powerful. Want to delete all country tracks that you
added last month? No problem. Redownload all the album art for a certain
band? Easy. You can even do smart playlists using the filters.&lt;/p&gt;
&lt;h3 id=&#34;actually-playing-music&#34;&gt;Actually playing music&lt;/h3&gt;
&lt;p&gt;With all this organisation, I’ve not actually mentioned one tiny detail: how
I actually play music now. Navidrome has a web UI, which is perfectly usable,
but I don’t really want my web browser involved as it makes balancing sound
levels tricky, and getting media keys working is a pain. Thankfully there
are loads of Subsonic clients. I’ve tried several, but eventually settled on
&lt;a href=&#34;https://github.com/jeffvli/feishin&#34;&gt;Feishin&lt;/a&gt; on the desktop. It’s very pretty:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/further-adventures-in-music-organisation/feishin.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/further-adventures-in-music-organisation/feishin.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/further-adventures-in-music-organisation/feishin.jpg&#34; alt=&#34;A screenshot of Feishin. On the left side of the screen is the album art (Love, Drugs &amp;amp; Misery by Eva Under Fire), with the track, album, artist, year and file format below it. On the right hand side is a tabbed panel, currently showing &amp;#39;Up Next&amp;#39; which shows a queue of music. At the bottom is a standard player interface with play/pause/skip/etc buttons.&#34; loading=&#34;lazy&#34; width=&#34;1723&#34; height=&#34;985&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;Feishin’s ’now playing’ screen&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;What attracted me to it, though, was its built-in support for Navidrome’s smart
playlists. These aren’t even properly exposed in Navidrome’s web UI yet, but
Feishin has a visual editor that lets you create and edit them. I ended up with
an identical system to the playlists I had in Tauon: a “blacklist” playlist
which are tracks I never want to play, a “favourites” playlist, and then an
“everything” playlist which is the entire catalogue minus the blacklist.&lt;/p&gt;
&lt;p&gt;On iOS I’ve settled on &lt;a href=&#34;https://www.reddit.com/r/arpeggiApp/&#34;&gt;Arpeggi&lt;/a&gt;. It’s
not actually on the App Store yet, but available via TestFlight. It’s one of
those really nicely polished apps that are obviously a labour of love, not just
out to do the minimum possible to get your money. It can cache songs offline,
and automatically download entire playlists, and supports all of the standard
Subsonic features like rating, reporting plays back to the server, and so on.&lt;/p&gt;
&lt;p&gt;One unexpected benefit of using a central server is that it handles reporting
plays to &lt;a href=&#34;https://last.fm/&#34;&gt;Last.fm&lt;/a&gt; and &lt;a href=&#34;https://listenbrainz.org/&#34;&gt;ListenBrainz&lt;/a&gt;
instead of the clients. I don’t think I’ve ever bothered to configure my mobile
clients to do that before, and now it just works automagically. I’m relying on
those services more for recommendations and discovery, as that’s something you
lose out on when self-hosting.&lt;/p&gt;
&lt;h3 id=&#34;making-bad-decisions-about-bitrates&#34;&gt;Making bad decisions about bitrates&lt;/h3&gt;
&lt;p&gt;With all the moving, copying, and organising, I started thinking about how
large the collection was. The songs were all in different formats, depending
on when and where I’d picked them up, and I couldn’t tell the difference
between them. I did a test and transcoded an MP3 file down to 128kbps, and
still couldn’t tell the difference. So armed with that sample size of 1, I
transcoded the entire library and saved &lt;em&gt;so much&lt;/em&gt; space.&lt;/p&gt;
&lt;p&gt;I don’t remember which track I did that first test with, but it must have
been a very unlucky pick. I quickly started to notice the distortions caused
by the low bitrate. It was particularly bad in any song with a lot of treble,
and started to really annoy me. I started a painful process of reimporting
things in their original format. Beets came in clutch again, both with the
import process (if you import a duplicate album, it asks if you want to
replace the original, and shows the format and bitrate of them for comparison),
and keeping track of what was left to fix (&lt;code&gt;beet ls bitrate:..128000&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Having started paying more attention to the sound quality, I think I’ve been
nibbled on by the audiophile bug. I’ve been trying to get new music in FLAC
format where possible. I’m pretty sure I &lt;em&gt;can’t&lt;/em&gt; tell the difference between
a decent bitrate MP3 and a FLAC, but maybe if I got a better DAC and some nice
headphones…? Someone please hide my credit card!&lt;/p&gt;
&lt;p&gt;As for the library taking up a lot of space, I now realise it’s a price worth
paying. I might need to swap out the server to one with more storage at some
point in the future, but it’s generally worth upgrading every few years with
rented dedicated servers anyway, as you often get more for the same amount of
money. Next upgrade I’ll just make sure there’s an appropriately-sized disk
instead of focusing entirely on RAM and CPU.&lt;/p&gt;
&lt;h3 id=&#34;writing-more-code&#34;&gt;Writing more code&lt;/h3&gt;
&lt;p&gt;Naturally, this whole process spawned several side projects. When browsing
albums in Navidrome, I was a bit upset that the album art wasn’t all a
consistent size. I thought about scripting something to crop them consistently,
but then I had a better idea, and &lt;a href=&#34;https://github.com/csmith/jewelcase&#34;&gt;jewelcase&lt;/a&gt;
was born. It takes the album art, crops it down to a consistent size, and then
renders it inside a jewel case. It applies some slight effects, like adjusting
the colours, rounding the corners, and tweaking the edges so it looks a bit more
real. I don’t know why&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:4&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;, but every time I see the effect it gives me a little
spark of joy.&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/further-adventures-in-music-organisation/jewelcase.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/further-adventures-in-music-organisation/jewelcase.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/further-adventures-in-music-organisation/jewelcase.jpg&#34; alt=&#34;A comparison screenshot. On the left are four albums, with their unaltered artwork. Some are different aspect ratios. On the right are the same four albums, but the art work is now consistently rendered as though its in a jewel case.&#34; loading=&#34;lazy&#34; width=&#34;1737&#34; height=&#34;702&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;Album art, before and after jewelcase is applied&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The other project is &lt;a href=&#34;https://github.com/csmith/bass&#34;&gt;BASS&lt;/a&gt;, a tool that uses
the Subsonic API to grab information about my music catalogue, and then
generates a “Daily Mix” playlist. It uses a system of weights to select tracks
semi-randomly, biasing towards favourite tracks, those that haven’t been played
much, and a few other criteria. This gives me a nice balance between having my
favourites on repeat, and exploring the full library at random.&lt;/p&gt;
&lt;p&gt;All the weights in BASS are customisable, so I can tweak it to my heart’s
desire, and anyone else can also run it and configure it entirely differently
to me if they want to. I can’t imagine I would’ve been able to do that if I were
using Plex!&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;I was running a version from git as the stable release had fun dependency
issues on Arch, so it’s possible this wouldn’t be an issue in normal use. I also
started backing up the database, but it’s still annoying to have to restore it. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;A “favourites” playlist for me, and a more “family friendly” playlist
for when I’m playing music out loud that has a bit less screaming/swearing/etc. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;One of the reasons for starting this was being annoyed by the periodic
sync to my phone, obviously it makes perfect sense to introduce a new, different
periodic sync. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;Nostalgia? A little bit of OCD? Both? &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:4&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
</feed>
