Shellsharks Blogroll - BlogFlock2026-07-05T23:58:36.999ZBlogFlockAdepts of 0xCC, destructured, Aaron Parecki, Trail of Bits Blog, fLaMEd, gynvael.coldwind//vx.log (pl), Westenberg, James' Coffee Blog, joelchrono, Evan Boehs, Kev Quirk, cool-as-heck, Posts feed, Sophie Koonin, cmdr-nova@internet:~$, <span>Songs</span> on the Security of Networks, Johnny.Decimal, Werd I/O, Robb Knight, Molly White, Hey, it's Jason!, Terence Eden’s BlogTime-based background colour transitions with Temporal and CSS color-mix - localghosthttps://localghost.dev/blog/time-based-background-colour-transitions-with-temporal-and-css-color-mix/2026-07-05T00:00:00.000Z<p>I've given my website a bit of a refresh! There's a slightly updated layout if you're on desktop, plus I ditched the <code>etc</code> page and I've revamped my <a href="https://localghost.dev/links">links page</a> to be powered by <a href="https://raindrop.io">raindrop.io</a>. The <a href="https://localghost.dev/blog/time-based-background-colour-transitions-with-temporal-and-css-color-mix/">minimalist theme</a> is still minimalist, but a bit more fancy. The <a href="https://localghost.dev/blog/time-based-background-colour-transitions-with-temporal-and-css-color-mix/">vaporwave theme</a> has a newly jazzed-up nav bar with some adorable little icons. But the biggest change is to the <a href="https://localghost.dev/blog/time-based-background-colour-transitions-with-temporal-and-css-color-mix/">city theme</a>, which was previously a starry-sky dark mode theme.</p>
<p>If you're reading this between the hours of 9pm - 5am, you might be wondering what all the fuss is about - it looks pretty much the same as it did before. That's because the theme changes depending on the time of day!</p>
<div class="image-grid">
<picture><source type="image/webp" srcset="https://localghost.dev/img/BZAiQcUC0U-280.webp 280w"><img loading="lazy" decoding="async" src="https://localghost.dev/img/BZAiQcUC0U-280.jpeg" alt="A screenshot of the sunrise version of this layout, with pixel art skyscrapers at the bottom. The background is a blue to pink to light orange gradient" width="280" height="300"></picture><picture><source type="image/webp" srcset="https://localghost.dev/img/MeJRe8T7yk-280.webp 280w"><img loading="lazy" decoding="async" src="https://localghost.dev/img/MeJRe8T7yk-280.jpeg" alt="A screenshot of the daytime version of this layout, with pixel art skyscrapers at the bottom. The background is a purple to pink gradient" width="280" height="300"></picture><picture><source type="image/webp" srcset="https://localghost.dev/img/NFDu_9WIlE-280.webp 280w"><img loading="lazy" decoding="async" src="https://localghost.dev/img/NFDu_9WIlE-280.jpeg" alt="A screenshot of the sunset version of this layout, with pixel art skyscrapers at the bottom. The background is a purple to pink to orange gradient" width="280" height="300"></picture><picture><source type="image/webp" srcset="https://localghost.dev/img/6CcP_uABhY-280.webp 280w"><img loading="lazy" decoding="async" src="https://localghost.dev/img/6CcP_uABhY-280.jpeg" alt="A screenshot of the nighttime version of this layout, with pixel art skyscrapers at the bottom. There are pixel art stars in the header and the theme is now dark mode. The background is a dark blue to light blue to purple gradient" width="280" height="300"></picture>
</div>
<p>You can select the time of day using the picker in the top right, after the theme switcher. I'm persisting the choice in session storage so you don't get attacked by sudden light mode when changing pages, but if you visit again in the future it'll reset back to "now".</p>
<p>I was going to just turn the layout into a pastel lo-fi-aesthetic thing, but then I realised that a) I needed <em>some</em> kind of dark mode and b) I'd miss the stars! So I thought... why not both? And why stop at just night and day? (Hat tip to <a href="https://alistairshepherd.uk/">Alistair Shepherd</a> who did something similar with his beautiful Firewatch-inspired website.)</p>
<p>Then I remembered that the Temporal API was available experimentally in Chrome and Firefox, and I'd been looking for an excuse to try it out.</p>
<h2 id="introducing-temporal" tabindex="-1">Introducing Temporal</h2>
<p>For the uninitiated, Temporal is a solution to the objectively terrible Date API in JavaScript. Date was based on Java's Date library, which was also objectively terrible and has long been deprecated.</p>
<p>It's always really confusing that <code>Date</code> instances show either local or UTC time depending on which function you use to display them, and date operations are so fiddly that most of us turn to third party libraries like <code>date-fns</code> or <code>luxon</code>.</p>
<p>Temporal massively simplifies the API, introducing some new concepts:</p>
<ul>
<li><code>PlainDateTime</code>: a date and time with no timezone (TZ)</li>
<li><code>PlainDate</code>: a date with no time information and no TZ</li>
<li><code>PlainTime</code>: a time with no date information and no TZ</li>
<li><code>ZonedDateTime</code>: a date and time in a specified TZ</li>
</ul>
<p><code>PlainTime</code> came in useful for this project, as we don't really care what the day is - only what time it is, so we know what colours to show.</p>
<h3 id="getting-the-users-local-time" tabindex="-1">Getting the user's local time</h3>
<p>The first thing to do was figure out the time according to the user's browser.<br>
The <code>Temporal.Now</code> <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now">namespace</a> has various methods for interacting with the current time, including <code>plainTimeISO()</code> which by default gives us a <code>PlainTime</code> in local time. (You can also pass in a time zone to get a zoned time.)</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> timeNow <span class="token operator">=</span> Temporal<span class="token punctuation">.</span>Now<span class="token punctuation">.</span><span class="token function">plainTimeISO</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Now we need to know when to show the different colours.</p>
<h2 id="defining-the-stages" tabindex="-1">Defining the stages</h2>
<p>The day is split into four stages: sunrise, daytime, sunset and night. Daytime and night are long - 11.5 hours each - whereas sunrise and sunset each last 90 minutes.</p>
<p>The background of the page has a two-colour gradient:</p>
<pre class="language-css"><code class="language-css"> <span class="token property">--background</span><span class="token punctuation">:</span> fixed <span class="token function">linear-gradient</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--bg-gradient-top<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--bg-gradient-mid<span class="token punctuation">)</span> 80%<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<p>The footer has an additional colour that's created with a linear gradient from transparent <code>oklch(0 0 0 / 0)</code> to the chosen third colour.</p>
<pre class="language-css"><code class="language-css"> <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">linear-gradient</span><span class="token punctuation">(</span><span class="token function">oklch</span><span class="token punctuation">(</span>0 0 0 / 0<span class="token punctuation">)</span> 40%<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--bg-gradient-bottom<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>This means the last colour sticks to the bottom of the page rather than stretching across the viewport height (it's hard to control even when you specify a percentage in the gradient). It also gives more of a glow that really looks like the sun rising/setting or the glow of the city, which I love.</p>
<p>I defined an object for the stages and colours:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> stages <span class="token operator">=</span> <span class="token punctuation">{</span>
<span class="token literal-property property">sunrise</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">start</span><span class="token operator">:</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"06:30:00"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token literal-property property">next</span><span class="token operator">:</span> <span class="token string">"day"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color1</span><span class="token operator">:</span> <span class="token string">"oklch(0.618 0.3157 265.76)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color2</span><span class="token operator">:</span> <span class="token string">"oklch(0.8867 0.1222 328.24)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color3</span><span class="token operator">:</span> <span class="token string">"oklch(0.9529 0.1222 106.94)"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token literal-property property">day</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">start</span><span class="token operator">:</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"08:00:00"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token literal-property property">next</span><span class="token operator">:</span> <span class="token string">"sunset"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color1</span><span class="token operator">:</span> <span class="token string">"oklch(58% 0.15433 300)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color2</span><span class="token operator">:</span> <span class="token string">"oklch(85% 0.22133 302)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color3</span><span class="token operator">:</span> <span class="token string">"oklch(98 0.22133 302)"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token literal-property property">sunset</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">start</span><span class="token operator">:</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"19:30:00"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token literal-property property">next</span><span class="token operator">:</span> <span class="token string">"night"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color1</span><span class="token operator">:</span> <span class="token string">"oklch(0.6933 0.1899 297.53)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color2</span><span class="token operator">:</span> <span class="token string">"oklch(75.504% 0.24612 357.26)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color3</span><span class="token operator">:</span> <span class="token string">"oklch(88.591% 0.1422 62.595)"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token literal-property property">night</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">start</span><span class="token operator">:</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"21:00:00"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token literal-property property">next</span><span class="token operator">:</span> <span class="token string">"sunrise"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color1</span><span class="token operator">:</span> <span class="token string">"oklch(25.27% 0.0919 276.73)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color2</span><span class="token operator">:</span> <span class="token string">"oklch(47.35% 0.284 283.78)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color3</span><span class="token operator">:</span> <span class="token string">"oklch(62.831% 0.23521 310.291)"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>CSS custom properties are easy to set via JS - you can use <code>root.style.setProperty</code>:</p>
<pre class="language-js"><code class="language-js"> root<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span>
<span class="token string">"--bg-gradient-top"</span><span class="token punctuation">,</span>
<span class="token string">"oklch(25.27% 0.0919 276.73)"</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Unlike <code>Date</code>, we don't have to do any gymnastics to compare Temporal instances: there's literally a <code>compare</code> function on each type of instance. Just like with other JS comparison functions, it returns <code>1</code> if the first instance is greater than the second, <code>0</code> if the two instances are the same, and <code>-1</code> if the first instance is less than the second.</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">const</span> compare <span class="token operator">=</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span>compare <span class="token comment">// extracted for brevity</span>
<span class="token keyword">switch</span> <span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunrise<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span> <span class="token operator">||</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"night"</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunrise<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span> <span class="token operator">&&</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>day<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"sunrise"</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>day<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span> <span class="token operator">&&</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunset<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"day"</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunset<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span> <span class="token operator">&&</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"sunset"</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">default</span><span class="token operator">:</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Once we've got the stage name, we can look up the colours and set the custom property values.</p>
<pre class="language-js"><code class="language-js">
root<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span>
<span class="token string">"--bg-gradient-top"</span><span class="token punctuation">,</span>
stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color1<span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
root<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span>
<span class="token string">"--bg-gradient-mid"</span><span class="token punctuation">,</span>
stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color2<span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
root<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span>
<span class="token string">"--bg-gradient-bottom"</span><span class="token punctuation">,</span>
stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color3<span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>I'm also setting a data attribute on the root so we can do some additional stage-based customisations, such as showing the stars when it's night.</p>
<pre class="language-js"><code class="language-js">root<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">"data-time"</span><span class="token punctuation">,</span> currentStageName<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>And that will give us our different gradient colours at different times of day!</p>
<p>And <em>then</em> I remembered that <code>color-mix</code> exists. Why restrict ourselves to just 4 times of day and 4 sets of colours, when we could make them... transition into each other?????</p>
<h2 id="blending-transitions-with-color-mix" tabindex="-1">Blending transitions with color-mix</h2>
<p><code>color-mix</code> is an extremely cool CSS function that lets you, well, mix two colours together. You tell it what colour space you're working with, and the colours, and the browser magically outputs the mix between the two.</p>
<pre class="language-css"><code class="language-css"><span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">color-mix</span><span class="token punctuation">(</span>in oklch<span class="token punctuation">,</span> color1<span class="token punctuation">,</span> color2<span class="token punctuation">)</span></code></pre>
<p>Much like with gradients, you can also specify a percentage value for the colours, which indicates the proportions of the colours:</p>
<pre class="language-css"><code class="language-css"><span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">color-mix</span><span class="token punctuation">(</span>in oklch<span class="token punctuation">,</span> color1 20%<span class="token punctuation">,</span> color2<span class="token punctuation">)</span></code></pre>
<p>So I could gradually feed in a bit of the next stage's colour until the next stage took over completely.</p>
<p>To get a percentage value for the next stage colour to feed in, I had to figure out how far through the current stage we are.</p>
<p>First, I'm calculating the time until the next stage - super simple with the <code>until</code> function on <code>Temporal</code> instances:</p>
<pre class="language-js"><code class="language-js">time1<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>time2<span class="token punctuation">)</span></code></pre>
<p>This gives us a <code>Temporal.Duration</code> which represents a period between two time points. So, for example, if it's 7:45pm now and we're calculating <code>timeUntilNextStage</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> timeUntilNextStage <span class="token operator">=</span> timeNow<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>timeUntilNextStage<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token comment">// PT1H15M</span>
</code></pre>
<p><code>Duration</code>s are stringified (and specified) using the <a href="https://en.wikipedia.org/wiki/ISO_8601#Durations">ISO 8601 duration format</a>, so "PT1H15M" means "period, time separator, 1 hour, 15 minutes".Time information appears after the <code>T</code>; if the duration had any date information in it, it'd appear before the <code>T</code>.</p>
<p>We set <code>timeUntilNextStage</code> in the switch statement where we're deciding what stage we're in, for example:</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunrise<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span> <span class="token operator">&&</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>day<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"sunrise"</span><span class="token punctuation">;</span>
timeUntilNextStage <span class="token operator">=</span> timeNow<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Once we've got the duration representing time until the next stage, we need to know the duration between the start of the current stage and the start of the next stage - let's call it the "transition duration". For sunset-to-night and sunrise-to-day, the transition duration is always 90 minutes; for night-sunrise and day-sunset, it'd be 11.5 hours. I didn't want the colour mixing to happen all throughout the day, only around sunrise/sunset like in real life, so I just decided to hardcode the transition duration for day and night to be 90 minutes so it matches the other two.</p>
<p>So for that, I can instantiate a <code>Duration</code> using the same ISO 8601 syntax:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> entireTransitionDuration <span class="token operator">=</span> Temporal<span class="token punctuation">.</span>Duration<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"PT1H30M"</span><span class="token punctuation">)</span></code></pre>
<p>Now I need to calculate the difference between the total duration and the time until next stage - basically, how far into the transition period we are, and therefore how much of a percentage we should mix in of the next colour.</p>
<p>Handily, Temporal gives us a <code>subtract</code> function as well:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> diff <span class="token operator">=</span> entireTransitionDuration<span class="token punctuation">.</span><span class="token function">subtract</span><span class="token punctuation">(</span>timeUntilNextStage<span class="token punctuation">)</span></code></pre>
<p>Then to figure out the transition progress as a percentage, we can divide <code>diff</code> by <code>entireTransitionDuration</code>. We'll do that with the time values in seconds so we can divide them, using the instance's <code>total</code> function:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> entireTransitionDurationInSeconds <span class="token operator">=</span> entireTransitionDuration<span class="token punctuation">.</span><span class="token function">total</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">unit</span><span class="token operator">:</span> <span class="token string">"seconds"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> diffInSeconds <span class="token operator">=</span> diff<span class="token punctuation">.</span><span class="token function">total</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">unit</span><span class="token operator">:</span> <span class="token string">"seconds"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> transitionProgressPercent <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">round</span><span class="token punctuation">(</span><span class="token punctuation">(</span>diffInSeconds <span class="token operator">/</span> entireTransitionDurationInSeconds<span class="token punctuation">)</span><span class="token operator">*</span><span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFixed</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// gives us a string representation with 0 d.p.</span></code></pre>
<h3 id="the-midnight-problem" tabindex="-1">The midnight problem</h3>
<p>It's a little more complicated for the "night" stage, because that crosses midnight into the next day. Remember that our <code>PlainTime</code> only has time information, not date information - so if it's 10pm and you're asking it how long until sunrise at 6:30am, it'll give you a negative number!</p>
<pre class="language-js"><code class="language-js">
<span class="token keyword">const</span> now <span class="token operator">=</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"22:00"</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> sunrise <span class="token operator">=</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"06:30"</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> d <span class="token operator">=</span> now<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>sunrise<span class="token punctuation">)</span> <span class="token comment">// Temporal.Duration -PT15H30M</span></code></pre>
<p>This causes problems at the point where I calculate the diff, as it'll come out as a large number and completely throw off the calculations. I got around this by getting the absolute value of the duration with <code>.abs()</code>, so <code>timeUntilNextStage</code> will always be positive, even if it's before midnight: e.g. what was<code>-PT15H30M</code> will now be <code>PT15H30M</code>. Calculating the diff by subtracting that from a <code>transitionDuration</code> of 90 mins will always yield a negative number.</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunrise<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span> <span class="token operator">||</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"night"</span><span class="token punctuation">;</span>
timeUntilNextStage <span class="token operator">=</span> timeNow<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>stages<span class="token punctuation">.</span>sunrise<span class="token punctuation">.</span>start<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">abs</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Then, we only calculate a transition percentage if <code>diff</code> is greater than 0:</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">let</span> transitionProgressPercent <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>diffInSeconds <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
transitionProgressPercent <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">round</span><span class="token punctuation">(</span><span class="token punctuation">(</span>diffInSeconds <span class="token operator">/</span> entireTransitionDurationInSeconds<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>This works for the daytime stage too: if it's more than 90 mins before sunset, it'll come out with a negative diff - so that will just display the daytime colours and no transition.</p>
<h3 id="lets-mix" tabindex="-1">Let's mix!</h3>
<p>Now we can use that percentage value (which will always be a whole number) in the <code>color-mix</code> function to dictate how much of the next colour we should interpolate.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">color-mix(in oklch, $</span><span class="token punctuation">{</span>color1<span class="token punctuation">}</span> <span class="token selector">$</span><span class="token punctuation">{</span>transitionProgressPercent<span class="token punctuation">}</span><span class="token selector">%, $</span><span class="token punctuation">{</span>color2<span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>I updated my <code>stages</code> object to include the next stage name as well:</p>
<pre class="language-js"><code class="language-js"> <span class="token literal-property property">night</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">start</span><span class="token operator">:</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"21:00:00"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token literal-property property">next</span><span class="token operator">:</span> <span class="token string">"sunrise"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color1</span><span class="token operator">:</span> <span class="token string">"oklch(25.27% 0.0919 276.73)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color2</span><span class="token operator">:</span> <span class="token string">"oklch(47.35% 0.284 283.78)"</span><span class="token punctuation">,</span>
<span class="token literal-property property">color3</span><span class="token operator">:</span> <span class="token string">"oklch(62.831% 0.23521 310.291)"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token comment">// etc</span></code></pre>
<p>So we can get both colours dynamically when we set the variables with <code>color-mix</code>:</p>
<pre class="language-js"><code class="language-js"> root<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span>
<span class="token string">"--bg-gradient-top"</span><span class="token punctuation">,</span>
<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">color-mix(in oklch, </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>stages<span class="token punctuation">[</span>nextStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color1<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>transitionProgressPercent<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">%, </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color1<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">)</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>And that's how we transition the colours!</p>
<h2 id="transitioning-the-transitions" tabindex="-1">Transitioning the transitions</h2>
<p>As a bonus touch, I wanted the colour change to transition smoothly when you switch between stages manually using the picker on the top right. By declaring my <code>bg-gradient-xx</code> variables using <code>@property</code>, I can tell the browsers that yes, they are definitely colours - and therefore they can be animated.</p>
<p>Without this explicit custom property declaration, I could set the value of <code>--bg-gradient-top</code> to a number, or a position, or anything I wanted. By saying it's definitely a colour, the browser knows how to transition it into other values of the same type.</p>
<p>I initially did this with <code>@property</code> declarations in the CSS:</p>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@property</span> --bg-gradient-top</span> <span class="token punctuation">{</span>
<span class="token property">syntax</span><span class="token punctuation">:</span> <span class="token string">"<color>"</span><span class="token punctuation">;</span>
<span class="token property">inherits</span><span class="token punctuation">:</span> true<span class="token punctuation">;</span>
<span class="token property">initial-value</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>...<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token atrule"><span class="token rule">@property</span> --bg-gradient-mid</span> <span class="token punctuation">{</span>
<span class="token property">syntax</span><span class="token punctuation">:</span> <span class="token string">"<color>"</span><span class="token punctuation">;</span>
<span class="token property">inherits</span><span class="token punctuation">:</span> true<span class="token punctuation">;</span>
<span class="token property">initial-value</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>...<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token atrule"><span class="token rule">@property</span> --bg-gradient-bottom</span> <span class="token punctuation">{</span>
<span class="token property">syntax</span><span class="token punctuation">:</span> <span class="token string">"<color>"</span><span class="token punctuation">;</span>
<span class="token property">inherits</span><span class="token punctuation">:</span> true<span class="token punctuation">;</span>
<span class="token property">initial-value</span><span class="token punctuation">:</span> <span class="token function">oklch</span><span class="token punctuation">(</span>...<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
<p>Unfortunately, setting these in the CSS meant that you got a flash of whichever initial values I'd set before the JS kicked in and set the appropriate colours for time of day. If this page were server-driven, or always started from the same colour for everyone, it would've been fine. But the starting colour depends on your time zone and is only calculated when the initial JS runs.</p>
<p>I got around this by setting the properties via JS instead:</p>
<pre class="language-js"><code class="language-js"> window<span class="token punctuation">.</span><span class="token constant">CSS</span><span class="token punctuation">.</span><span class="token function">registerProperty</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"--bg-gradient-top"</span><span class="token punctuation">,</span>
<span class="token literal-property property">syntax</span><span class="token operator">:</span> <span class="token string">"<color>"</span><span class="token punctuation">,</span>
<span class="token literal-property property">inherits</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token literal-property property">initialValue</span><span class="token operator">:</span> stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color1<span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
window<span class="token punctuation">.</span><span class="token constant">CSS</span><span class="token punctuation">.</span><span class="token function">registerProperty</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"--bg-gradient-mid"</span><span class="token punctuation">,</span>
<span class="token literal-property property">syntax</span><span class="token operator">:</span> <span class="token string">"<color>"</span><span class="token punctuation">,</span>
<span class="token literal-property property">inherits</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token literal-property property">initialValue</span><span class="token operator">:</span> stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color2<span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
window<span class="token punctuation">.</span><span class="token constant">CSS</span><span class="token punctuation">.</span><span class="token function">registerProperty</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"--bg-gradient-bottom"</span><span class="token punctuation">,</span>
<span class="token literal-property property">syntax</span><span class="token operator">:</span> <span class="token string">"<color>"</span><span class="token punctuation">,</span>
<span class="token literal-property property">inherits</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token literal-property property">initialValue</span><span class="token operator">:</span> stages<span class="token punctuation">[</span>currentStageName<span class="token punctuation">]</span><span class="token punctuation">.</span>color3<span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>I had to wrap these in a <code>try/catch</code> as it will throw if the property's already been defined. It wasn't super trivial to figure out if this property had already been set, as the CSS does define some values for these with the regular <code>--bg-gradient-xx: ...</code> syntax.</p>
<p>On the <code>body</code> and <code>footer</code> I set <code>transition-property</code> and <code>transition-duration</code> to tell it which properties I want to animate:</p>
<pre class="language-css"><code class="language-css"> <span class="token selector">body</span> <span class="token punctuation">{</span>
<span class="token property">--background</span><span class="token punctuation">:</span> fixed <span class="token function">linear-gradient</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--bg-gradient-top<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--bg-gradient-mid<span class="token punctuation">)</span> 80%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">transition-property</span><span class="token punctuation">:</span> --bg-gradient-top<span class="token punctuation">,</span> --bg-gradient-mid<span class="token punctuation">;</span>
<span class="token property">transition-duration</span><span class="token punctuation">:</span> 0.5s<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">footer</span> <span class="token punctuation">{</span>
<span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">linear-gradient</span><span class="token punctuation">(</span><span class="token function">oklch</span><span class="token punctuation">(</span>0 0 0 / 0<span class="token punctuation">)</span> 40%<span class="token punctuation">,</span> <span class="token function">var</span><span class="token punctuation">(</span>--bg-gradient-bottom<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">transition</span><span class="token punctuation">:</span> --bg-gradient-bottom 0.5s<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>And like motherflipping magic, the colours transition seamlessly into each other when the values change! I love CSS. The animation is such an unnecessary touch, but this is my website so unnecessary is the name of the game.</p>
<h2 id="polyfilling-temporal-for-safari" tabindex="-1">Polyfilling Temporal for Safari</h2>
<p>Alas, Safari is behind the times. We love progressive enhancement, and of course I could have just removed any of the transition logic for people whose browsers don't support Temporal, but that's no fun. They deserve sunsets too!</p>
<p>Writing a shim for Temporal was also no fun, but I did it because I love you.</p>
<p>There are various Temporal polyfills around and about, but I didn't want to end up importing a whole lot of extra JS when I only needed one or two functions. I'm not using any kind of bundler on this site - I use Eleventy to generate the pages, but scripts are just imported vanilla - so I couldn't import something with NPM and expect it to tree-shake any bits I wasn't using. It was a lot more lightweight to just write my own.</p>
<p>To check for Temporal support, it's a matter of just checking if <code>window.Temporal?.PlainTime</code> is undefined:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> supportsTemporal <span class="token operator">=</span> <span class="token keyword">typeof</span> window<span class="token punctuation">.</span>Temporal<span class="token operator">?.</span>PlainTime <span class="token operator">!==</span> <span class="token string">"undefined"</span><span class="token punctuation">;</span></code></pre>
<p>I'm checking for <code>PlainTime</code> specifically as some browsers may have very high level Temporal implementations, but we can't do much without <code>PlainTime</code>.</p>
<p>To get the user's time in a non-Temporal world, we can just call the good old-fashioned <code>new Date()</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">getUserTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>supportsTemporal<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> Temporal<span class="token punctuation">.</span>Now<span class="token punctuation">.</span><span class="token function">plainTimeISO</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>To compare dates, we do it by comparing epoch timestamps. These represent the number of milliseconds since the Unix epoch, 01 Jan 1970.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">jsDateCompare</span><span class="token punctuation">(</span><span class="token parameter">date1<span class="token punctuation">,</span> date2</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> date1Ms <span class="token operator">=</span> date1<span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> date2Ms <span class="token operator">=</span> date2<span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>date1Ms <span class="token operator">===</span> date2Ms<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> date1Ms <span class="token operator"><</span> date2Ms <span class="token operator">?</span> <span class="token operator">-</span><span class="token number">1</span> <span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Then, we can just assign whichever version of the function we need:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> compare <span class="token operator">=</span> supportsTemporal <span class="token operator">?</span> Temporal<span class="token punctuation">.</span>PlainTime<span class="token punctuation">.</span>compare <span class="token operator">:</span> jsDateCompare<span class="token punctuation">;</span></code></pre>
<p>To polyfill <code>until</code>, I've got a <code>durationBetween</code> function which will call <code>until</code> if Temporal's supported, otherwise it'll subtract two epoch timestamps, and divide the result by 1000 to get the duration as seconds:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">durationBetween</span><span class="token punctuation">(</span><span class="token parameter">time1<span class="token punctuation">,</span> time2</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>supportsTemporal<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token punctuation">(</span>time2<span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span> time1<span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">/</span> <span class="token number">1000</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> time1<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>time2<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Then I call it like this:</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">case</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>sunset<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator">>=</span> <span class="token number">0</span> <span class="token operator">&&</span> <span class="token function">compare</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span><span class="token operator">:</span> <span class="token punctuation">{</span>
currentStageName <span class="token operator">=</span> <span class="token string">"sunset"</span><span class="token punctuation">;</span>
timeUntilNextStage <span class="token operator">=</span> <span class="token function">durationBetween</span><span class="token punctuation">(</span>timeNow<span class="token punctuation">,</span> stages<span class="token punctuation">.</span>night<span class="token punctuation">.</span>start<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">break</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>I wrote a whole suite of unit tests (for a PERSONAL project! I know!) to make sure behaviour was exactly the same, and it seems to be working nicely. I'm hoping I can remove the polyfills in time, but given that the web is beautifully backwards-compatible, it's not the end of the world if it stays around longer than it needs to.</p>
<h2 id="fixing-a-weird-background-glitch-in-safari" tabindex="-1">Fixing a weird background glitch in Safari</h2>
<p><picture><source type="image/webp" srcset="https://localghost.dev/img/cmqGHiVsJL-280.webp 280w, https://localghost.dev/img/cmqGHiVsJL-640.webp 640w, https://localghost.dev/img/cmqGHiVsJL-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/cmqGHiVsJL-280.jpeg" alt="A screenshot of the bottom half of my website, with a big white space in the background where the background should be. The background gradient cuts off a third of the way down the screen." width="960" height="616" srcset="https://localghost.dev/img/cmqGHiVsJL-280.jpeg 280w, https://localghost.dev/img/cmqGHiVsJL-640.jpeg 640w, https://localghost.dev/img/cmqGHiVsJL-960.jpeg 960w" sizes="auto"></picture><br>
SAFARI WHY.</p>
<p>I started experiencing a very odd glitch in Safari for MacOS where the background gradient would only show up in the initial viewport - when you scrolled, it went white or black depending on whether it was light or dark mode. I narrowed it down to the <code>background-attachment: fixed</code> property of the body.</p>
<p><video controls="" loading="lazy"><source src="https://localghost.dev/img/background-glitch-safari-demo.mp4" type="video/mp4"></video></p>
<p>After a lot of disabling random CSS and diffing against the <code>main</code> branch, which doesn't have that problem, I found out that it really doesn't play nicely with <code>container-type: inline-size</code>. In the course of redesigning the site, I'd added a new container context to the <code><body></code> element and in the process broken the gradient rendering for Safari, as something goes wrong when it tries to render the gradient background with a <code>fixed</code> attachment. Chrome, Firefox and iOS Safari were totally fine.</p>
<p>When I half-jokingly said I'd have to find out what the modern equivalent of <code><!--[IF IE]></code> was, David Bushell <a href="https://social.lol/@db/116867834799255143">pointed me to</a> Eric Meyer's <a href="https://meyerweb.com/eric/thoughts/2026/05/28/accessible-i-think-split-cell-table-headers/#:~:text=@supports%20(font:%20-apple-system-body)">post about accessible table headers</a>, which in turn led me to <a href="https://browserstrangeness.github.io/css_hacks.html#safari">Browser Strangeness</a>. That did indeed have a <code>@supports</code> query that targeted Safari for MacOS:</p>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@supports</span> <span class="token punctuation">(</span><span class="token keyword">not</span> <span class="token punctuation">(</span><span class="token property">-webkit-text-size-adjust</span><span class="token punctuation">:</span>none<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">and</span> <span class="token punctuation">(</span><span class="token property">font</span><span class="token punctuation">:</span> -apple-system-body<span class="token punctuation">)</span></span> <span class="token punctuation">{</span> <span class="token selector">.selector</span> <span class="token punctuation">{</span> <span class="token property">property</span><span class="token punctuation">:</span>value<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre>
<p>I stuck a <code>position-attachment: initial</code> in there, and lo and behold, the problem went away. It means the background isn't quite how I wanted it to look in Safari, but I'll survive.</p>
<h2 id="this-was-surprisingly-complex" tabindex="-1">This was surprisingly complex</h2>
<p>The individual moving parts of this project - getting the time and choosing colours, mixing the colours by percentages, animating the transitions - were not that complicated in isolation. Sure, they required me to learn things and look things up, but it was a fun thing to build (until the point where Safari came into the picture).</p>
<p>The challenge came wiring it all together in a way that didn't cause flashes of unstyled content (the dreaded FOUC) or flashes of the wrong stage before we calculate the current time. This is a static site, so it's all client-side JS. Ideally I'd compute user's the current time on the server and serve the content with the correct colour values in the HTML, but my web host only supports static sites.</p>
<p>To get around that I had to add another separate <code>init.js</code> script which runs instantly - it's got a bit of a copy and paste job going on with some of the functions, but it does a very rudimentary check of the user's current time and sets the stage accordingly with no transitions, just so there's <em>some</em> styling on initial load. My JS is all modules, so is <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#defer">deferred by default</a>. I experimented with making <em>all</em> the JS render-blocking with <code>blocking="render"</code>, but that felt a bit gross and also didn't fix the FOUC in Firefox.</p>
<p>But that's fine, y'know? It still loads in well under a second, and still looks good if you have JS disabled. It's my personal site and it doesn't need to be perfect.</p>IndieWebCamp Nürnberg 2026 - James' Coffee Bloghttps://jamesg.blog/2026/07/05/indiewebcamp-nurnberg-20262026-07-05T00:00:00.000Z
<p><figure><picture><img alt="All of the participants of IndieWebCamp Nürnberg, lined up in three rows, one row behind the other. I am in the middle of the back row, wearing my green and grey microformats t-shirt. " loading="lazy" src="https://editor.jamesg.blog/content/images/2026/07/1000px-iwc-nbg-2026-10.jpg" style=" max-width: 130%;"/></picture><div class="alt"><label><input aria-label="Toggle image alt text on screen" type="checkbox"/>ALT</label><div class="content">All of the participants of IndieWebCamp Nürnberg, lined up in three rows, one row behind the other. I am in the middle of the back row, wearing my green and grey microformats t-shirt. </div></div></figure></p><p>Last weekend I attended <a href="https://indieweb.org/2026/Nuremberg">IndieWebCamp in Nürnberg, Germany</a>, hosted at the wonderful <a href="https://tollwerk.de/">Tollwerk</a> offices. The two-day event was part of the larger Nürnberg Digital Festival happening across the city. The IndieWebCamp was held primarily in English. Over 23 people attended, many from Germany and some from further afield (including myself!).</p><p>On the first day, we organised discussion sessions in the BarCamp style, in which new participants are encouraged to propose sessions first, followed by regular participants, and then we all agree on where and when during the day to hold each session. On the second day, we made web pages and shared them!</p><p>Indeed, being a two-day event, <a href="https://indieweb.org/IndieWebCamp" rel="noreferrer">IndieWebCamps</a> have room to balance both discussing topics that interest participants as well as creating things on our websites. We also welcome remote participants, and so, in addition to the ~23 attendees we had, there were several people joining sessions from Zoom (and even helping to take notes, which was and is greatly appreciated!).</p><p>I joined the event both as a participant and as something of a co-organiser, helping to ensure Zoom rooms were set up, helping to open Create Day, archiving notes on the wiki, and more. <a href="https://tantek.com/">Tantek</a> and <a href="https://jkphl.is/">Joschi</a> both led organising, with the <a href="https://tollwerk.de/">Tollwerk</a> team also helping in so many ways.</p><p>Throughout the day, <a href="https://www.maxhaesslein.de/" rel="noreferrer">Max</a> took pictures using a camera connected to a thermal printer. All the pictures were monochrome, which created a terrific effect. I have included some of these pictures throughout the post. Thank you Max for taking such great pictures!</p><h2 id="day-one:-discussions">Day One: Discussions</h2><p>I am always surprised and delighted at the variety of discussion topics that come up at IndieWebCamp events. I think the range of discussion topics is in large part because new participants are encouraged to share ideas first. This ensures that we don’t keep talking about the same topics between events.</p><p>This time, we discussed everything from how to welcome more new people to the indie web, calendar subscriptions on the web, beautiful web design, and whether or not the way we will consume websites will permanently change. <a href="https://indieweb.org/2026/Nuremberg/Schedule">Notes for all of these sessions are archived on the IndieWeb wiki</a> in case you would like to take a look at what we discussed.</p><p>Whenever I join a discussion, I like to try and take notes. This started at IndieWebCamp Brighton in 2024 when I realised how much I love being the note-taker. Every discussion needs at least one note-taker, and I’m happy to help (although the more people who take notes, the more we are able to capture, and the more likely the notes are to reflect all that was discussed). I think I helped with note-taking in every session I joined, which was fun.</p><p>Two sessions that stood out to me were “<a href="https://indieweb.org/2026/Nuremberg/blogchat" rel="noreferrer">Archiving live sessions / co-creating</a>” and “<a href="https://indieweb.org/2026/Nuremberg/thenewweb" rel="noreferrer">Will the way we consume websites permanently change?</a>”. The first session, led by <a href="https://thedriftinghealer.com" rel="noreferrer">Una</a>, was about how to create discussion on a website after a live session (i.e. a group event). It was fascinating to think about how the role artefacts (i.e. recording of sessions, summaries of sessions) play in building community. Our discussion led to digital gardens and knowledge graphs, which gave us plenty of points of inspiration.</p><p>The discussion about whether the way we consume websites will permanently change was a fascinating one. Facilitated by <a href="https://noracudazzo.com/">Nora</a>, we discussed the transformations currently happening on the web as a result of AI, the long-term viability of browsers, and more. This session encouraged the optimist in me. As Casey Newton said in the movie Tomorrowland “I get things are bad, but what are we doing to fix it?”</p><p>Starting with an assessment of where we are – a solid foundation from which to start our discussion – we spoke about the 700,000+ people who run sites on Neocities, the long-lived role of information design, the role of the web in government services, media diets, and more. We were then left with many questions, among them:</p><ul><li>How to convince artists to join the web?</li><li>"How do I talk to my friends on there [the web]?”</li><li>Maybe lurkers have a healthier relationship with consumption?</li></ul><p>The notes from the session include a note that “James doesn't have the words to convince someone without a pre-existing interest to start a website.” This reflected my realisation that while I have so many words to say on the web, I don’t think I have successfully encouraged someone to create a website in direct conversation who hasn’t already said something that would indicate an interest in the web.</p><p>Now that I write this, maybe this website has encouraged others to start websites, in which case maybe I do have the words! But I think the note speaks to a discomfort I have: I wish more people had websites, but communicating their value is tricky. Maybe the best thing I can do to advocate for the web is keep doing what I’m doing – writing, talking about websites passionately, making cool things on the web – and be there for anyone and everyone who sees potential in this medium as I do.</p><h2 id="day-two:-creating">Day Two: Creating</h2><p>The second day of an IndieWebCamp event is <a href="https://indieweb.org/Create_Day">Create Day</a>, in which participants are invited to work on their personal websites for the day. Toward the end of the day, everyone – both in-person and remote attendees – are invited to demo what they built on their website if they would like to share what they made.</p><p>At the beginning of Create Day, I stood up to share what the day is all about. Looking back, it amazes me the ease with which I was able to stand up in front of a room and introduce what Create Day is all about. I am bewildered by how anxious I feel at times in social situations and yet how I can stand up in front of a room and talk to over a dozen people with few notes. My confidence has certainly come a long way over the last few years; there are still many places for me to grow, too.</p><figure><picture><img alt="Me standing up in front of the room presenting what Create Day is all about." loading="lazy" src="https://editor.jamesg.blog/content/images/2026/07/IMG_4686-Large.jpeg" style=" max-width: 130%;"/></picture><div class="alt"><label><input aria-label="Toggle image alt text on screen" type="checkbox"/>ALT</label><div class="content">Me standing up in front of the room presenting what Create Day is all about.</div></div></figure><p>The <a href="https://indieweb.org/what_to_make_at_IndieWebCamp">“what to make at IndieWebCamp” page on the wiki</a>, authored by Tantek, was a good starting point for me to present what to do on Create Day. As someone who has attended an IndieWeb event before and tried to take on a big project, the points “start easy” and “prioritise joy” were particularly resonant. These points were mentioned by several participants as being their guide to the day as time passed, which was a delight to hear.</p><p>I had a few ideas in mind of what I wanted to do on Create Day. I made a new <a href="https://jamesg.blog/bookshelf">bookshelf</a> page as my first project. The last version of the page had not been updated in several years. I decided that rather than try to list all of the books I have on my shelves, I wanted the new page to encapsulate my current interests and include a few select recommendations across different genres. I am happy with the page I made.</p><figure><picture><img alt="Me, with long hair, smiling, sitting at a desk with my right hand on my laptop and looking directly into the camera." loading="lazy" src="https://editor.jamesg.blog/content/images/2026/07/IMG_4676-Large.jpeg" style=" max-width: 130%;"/></picture><div class="alt"><label><input aria-label="Toggle image alt text on screen" type="checkbox"/>ALT</label><div class="content">Me, with long hair, smiling, sitting at a desk with my right hand on my laptop and looking directly into the camera.</div></div></figure><p>I also made an <a href="https://jamesg.blog/indiewebcamp">IndieWebCamp category</a> on my blog and added posts explicitly about or written at IndieWebCamp events. This blog post will go in that category.</p><p>Having started with easier projects, I had finished in the first hour or so. By coincidence, something happened that would give me enough to work on for the rest of the day, and beyond. Tantek mentioned he had updated his “daylists” page, which lists the names of Spotify daylists that have been generated for him based on his music tastes. I didn’t see the changes in Artemis, my web reader, which led me to ask why this was the case.</p><p>This led to my working on two changes. First, <a href="https://jamesg.blog/2026/07/04/processing-feed-items-without-urls">Artemis can now process feed items that don’t have URLs</a>, which was at the root of the problem. Second, because Tantek updates his daylists page every few months and backdates the posts that are added to when the daylist appeared in his Spotify, I wouldn’t see many of the items because Artemis shows posts ordered by creation date. This led me to work on a (still-work-in-progress) feature that would display new items in a feed with a tag like “post title [2 days prev.]” to indicate that a post has been newly discovered but was published with a date equal to N days ago. This turned out to be a complex feature and so I am still working on the logic, but I made good progress during the event.</p><p>After several hours of making – and a wonderful salad lunch prepared by the Tollwerk team – we all came together for demos. It was delightful to see what everyone had made: new designs for a portfolio website, an interactive web page that lets you create a resonance signature based on your humming, improvements to photo grids on websites, and more. In total, we had ~17 demos, including demos presented by remote participants.</p><h2 id="heat-safety">Heat safety</h2><p>A few days before the event, myself, another event organiser, and the lead organiser for the IndieWebCamp Nürnberg event had a discussion about the extreme heat that was forecast for the event. This led to our creating a section on the event wiki page called “Heat Safety” which summarised the weather forecast (temperatures expected to range from 21C to 38C), the accommodations made by the venue, and general advice we had (bring a hat, suncream, sunglasses, etc.).</p><p>While the office where we had the event did not have air conditioning, the staff reported the temperature was relatively cool prior to the event. When the event happened, even with ~23 people in the office – which was incredibly spacious – the room remained at a manageable temperature. Thank you to Tollwerk for helping to ensure this was the case!</p><p>While this particular event was manageable in terms of temperature, I did note that we may want to re-think where we organise events in summer. The heatwave that happened could not have been predicted when the event was first scheduled months ago, but with an increased prevalence of heatwaves likely in summer, heat should definitely be something that event organisers, both in the IndieWeb community and in general, consider when scheduling events.</p><p>Admittedly, I was concerned about travelling somewhere that was 10C warmer than Scotland which, at the time, was experiencing the heatwave. I was already warm at home; going somewhere hotter felt daunting. I coped well in the heat in Nürnberg, avoiding the sunlight as much as I could at peak times, and taking other precautions to stay safe, but I did want to record how, even for someone who loves going to events, heat was a real concern.</p><p>On this note, a few years ago, I attended a Beyond Tellerrand event in Berlin in September. It was so warm the organiser moved the event to be over a month later the next year, to help avoid the same heat scenario for future events. This was the first thing that came to mind when I was voicing my concerns about organising events in summer; the IndieWeb community is far from alone in needing to consider accommodations for heat safety.</p><h2 id="reflections">Reflections</h2><p>IndieWebCamp events are among the highlights of my year. It is a delight to see people who make websites in person and to chat about all things that are on our minds. Because there were enough session ideas to have two event tracks, we all had to make choices about which sessions we attended. Thankfully, because notes are taken and sessions are recorded, we can all review the sessions we couldn’t attend afterward. I am planning to look through some of the notes in the coming days to both see what was discussed and see if any new ideas come to mind for things I can do on my website.</p><p>I am delighted by all the progress I made on both my website and Artemis, too. In particular, the logic <a href="https://jamesg.blog/2026/07/04/processing-feed-items-without-urls" rel="noreferrer">I wrote to process web feed entries without URLs</a> helped make Artemis significantly more robust, and allowed me to discover that a project I worked on a while ago that generated feeds whose entries did not have URLs was actually still working; it was just that Artemis couldn’t handle the feed because unique URLs were not generated.</p><p>The IndieWeb community lists all upcoming IndieWebCamp events on the <a href="https://events.indieweb.org/">IndieWeb Events</a> web page. We also have both in-person and online Homebrew Website Club events, which last ~90 minutes (depending on the meeting) where participants can discuss all things related to personal websites. I help host the Edinburgh in-person and Europe online meet-ups. If you’re interested in joining, you are more than welcome. Check the <a href="https://events.indieweb.org/">IndieWeb Events list</a> for a list of upcoming events, and don’t hesitate to <a href="https://jamesg.blog/email">email me</a> if you have questions.</p>
<!--kg-card-begin: html-->
<p><a class="u-syndication" href="https://news.indieweb.org/en">Also posted on IndieNews</a>.</p>
<!--kg-card-end: html--><script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a16500543f2a5e57',t:'MTc4MzI0MDE1OQ=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script>
<a class="tag" href="https://events.indieweb.org/">IndieWeb Events</a>
<a class="tag" href="https://events.indieweb.org/">IndieWeb Events list</a>
<a class="tag" href="https://indieweb.org/2026/Nuremberg">IndieWebCamp in Nürnberg, Germany</a>
<a class="tag" href="https://indieweb.org/2026/Nuremberg/Schedule">Notes for all of these sessions are archived on the IndieWeb wiki</a>
<a class="tag" href="https://indieweb.org/2026/Nuremberg/blogchat">Archiving live sessions / co-creating</a>
<a class="tag" href="https://indieweb.org/2026/Nuremberg/thenewweb">Will the way we consume websites permanently change?</a>
<a class="tag" href="https://indieweb.org/Create_Day">Create Day</a>
<a class="tag" href="https://indieweb.org/IndieWebCamp">IndieWebCamps</a>
<a class="tag" href="https://indieweb.org/what_to_make_at_IndieWebCamp">“what to make at IndieWebCamp” page on the wiki</a>
<a class="tag" href="https://jamesg.blog/2026/07/04/processing-feed-items-without-urls">Artemis can now process feed items that don’t have URLs</a>
<a class="tag" href="https://jamesg.blog/2026/07/04/processing-feed-items-without-urls">I wrote to process web feed entries without URLs</a>
<a class="tag" href="https://jamesg.blog/bookshelf">bookshelf</a>
<a class="tag" href="https://jamesg.blog/email">email me</a>
<a class="tag" href="https://jamesg.blog/indiewebcamp">IndieWebCamp category</a>
<a class="tag" href="https://jkphl.is/">Joschi</a>
<a class="tag" href="https://news.indieweb.org/en">Also posted on IndieNews</a>
<a class="tag" href="https://noracudazzo.com/">Nora</a>
<a class="tag" href="https://tantek.com/">Tantek</a>
<a class="tag" href="https://thedriftinghealer.com">Una</a>
<a class="tag" href="https://tollwerk.de/">Tollwerk</a>
<a class="tag" href="https://www.maxhaesslein.de/">Max</a>
Do you need a Nintendo 3DS? - Joel's Log Fileshttps://joelchrono.xyz/blog/nintendo-3ds2026-07-04T18:39:52.000Z<p>Probably not. I bought one anyway. Figured I’d write some of my memories surrounding it growing up, as well as how I set-up my own.</p>
<h2 id="some-memories">Some memories</h2>
<p>The Nintendo 3DS was one of those consoles that I mostly admired from a distance. There are a few moments in time, sparsed throughout my youth where I interacted with one, an uncommon endeavor compared to my love for the PSP or even the older Game Boy Advance.</p>
<p>When it came out I was too poor to even consider it, it was super expensive when it released, and it was still super expensive when the price lowered. I saw very few of them during the first few years, I was simply aware of its existence and a year and a half later I owned a PSP, not thinking much of what I was missing out on. I had an itch for <em>Ocarina of Time 3D</em> but <a href="/blog/first-contact-with-emulation/">I also knew of emulation already</a>, so there was no need for it.</p>
<p>Somewhere in 2013, I stayed for a few days with a friend in the US. It was my first time on a whole different country without my parents! I should write about this period at some point, but for now, the important bit is that my friend had a Nintendo 3DS, and… <em>I don’t remember playing anything on it</em>. I think he had <em>Kid Icarus Uprising</em> and maybe some other title, but I didn’t do much. He had a different console I played instead, the Wii U, I’ll write about it some other day.</p>
<p>I dit try to convince him to buy <em>Ocarina of Time</em>, and telling him it was a remake of the Nintendo 64 version. He was completely sure it was a fully new game. In any case, he never bought it, at least during my stay.</p>
<p>Eventually I saw some other kid playing taht game during some big church event, not sure if it was before or after that time, I just recall the ReDeads in the middle of the ruined streets of Hyrule, with Link frozen with a scream and eaten alive, or something.</p>
<p>Some other time, it was my turn to have a friend stay with us for a week or so, and during that whole ordeal he brought his Nintendo 3DS, it was the original <em>Aqua Blue</em> model again—all of them so far have been the <em>Aqua Blue</em> model—and he was playing <em>Super Mario 3D Land</em>, a fantastic title for the system. This was the most amount of time I got to spend playing with one myself. The game was great and I completed a bunch of random levels.</p>
<p>My cousins which I interacted with during Christmas and the like also had a Nintendo 2DS each, both playing whatever Pokemon was popular at the time, like <em>Sun</em> & <em>Moon</em>, but I’m really ignorant of this. They continue that tradition with the Switch today, they are absolute Pokemon fans.</p>
<p>Weirdly enough, I just recalled some of my best friends that I saw every Sunday at church more than a decade ago owning one too, for some reason I didn’t remember this at all. I have more memories playing on the Xbox 360 with them, but I <em>think</em> I grabbed it a few times when I visited their house, though I can’t recall what games I played with it—<em>this paragraph was written after everything around it, placed here for continuitiy’s sake</em>.</p>
<p>Last but not least, when I moved to the city I live in today, I made a new friend who owned a couple of 3DS models! From the original to the New 3DS XL. I also stayed at his house for a couple days one time.</p>
<p>I had my PSP and I already was a huge fan of Monster Hunter. My friend owned <em>Monster Hunter 4 Ultimate</em>, but he barely played it at all. I gave it a spin for a few hours and it was awesome—though I kept grumbling about how this game could have run on the PSP with just a couple of compromises and raised my fist at Capcom for abandoning the PSVita—which I never even owned—but whatever.</p>
<p>Other than that, I’ve been aware of 3DS emulation for a while, I even went ahead and setup multiplayer with Citra only a couple years ago! This time to play some <em>Monster Hunter 3 Ultimate</em> using the <a href="https://home.mgn.pub/">Madness Network</a> servers. I talked about playing <a href="/blog/the-monster-hunter-online-experience/">Monster Hunter online already</a>, but somehow never mentioned trying 3DS entries. Only played about a day and my setup and save have been lost to some distro hop.</p>
<p>All in all, other than <em>Super Mario 3D Land</em> and <em>Mario Kart 7</em> and <em>Face Raiders</em>, the 3DS library remained unexplored for me. That was until a few years ago, when this blog was already a thing!</p>
<p>A friend allowed me to borrow his Nintendo 3DS for a couple weeks, I only played two games on it, <em>Fire Emblem Awakening</em>, which I tried for about four hours, and <a href="/blog/metroid-samus-returns/">Metroid Samus Returns</a>, which I finished soon after. That game was so awesome, it is what finally pushed me to write reviews on this site, and that was my last interaction with a 3DS in a while.</p>
<p><img src="/assets/img/blogs/2026-07-04-3ds-2.jpg" alt="The device, closed!" /></p>
<h2 id="recently">Recently</h2>
<p>For many months already, since the <a href="https://www.ayntec.com/products/ayn-thor">AYN Thor</a> and the <a href="https://anbernic.com/products/rgds">Anbernic DS</a> came out in the market, with comparisons and praise and people complaining about the prices or the performance of one or the other, and after being exposed to countless YouTube videos, and lots of discussion over on the TWG Discord server, the itch to acquire a device for this was bound to happen.</p>
<p>The hardware of the Thor is excellent for a huge percentage of the 3DS library, and for the price, it would allow me much more than the that, as it would let me have PS2 and GameCube, Switch and even light PC gaming via <a href="https://gamenative.app/">GameNative</a> or <a href="https://gamehub.xiaoji.com/">GameHub</a>; and even though the RGDS is weak for 3DS, at least I would have save states, fast forward and other features emulating the Nintendo DS with it, on an ideal form factor.</p>
<p>However, as small as the impact of the 3DS was on my life, I couldn’t help myself. It was something I had wanted for a while. With prices not going down anytime soon, and the Switch 2 tempting me every once in a while, I figured this would be a nice middleground.</p>
<p>I looked around for a bit, and eventually landed on a red New Nintendo 3DS XL for about 333 USD with shipping. There were cheaper alternatives out there, such as a regular 3DS XL or a 2DS XL, but this is the top of the line and you can tell the quality is up there once held.</p>
<p><em>As I write this today, I saw a posting selling the same model and color I got at $290! which is better though still expensive, but the week I had to return it is over and I’ve already played with it for a dozen hours, so whatever!</em></p>
<h2 id="my-set-up">My set-up</h2>
<p>My model already came pre-modded, I took some time to make sure the modding tools were up to date. The 3DS modding scene is as crazy as the PSP, and emulation for many systems is available. However, I own plenty of devices that are better at emulation at this point. What I really care about here is the existing library for the Nintendo 3DS, and the original Nintendo DS, as was seen on my current setup.</p>
<p>The first thing I did when I purchased it was check the alternative shop every modded 3DS is bound to have, and grabbed myself a few titles. I do not encourage piracy much nowadays, when buying titles officially is possible, but this is not the case here. Nintendo doesn’t sell carts and they have no digital shops, so whatever. here’s a list of things on my 3DS that will probably be expanded soon.</p>
<h3 id="nintendo-3ds-titles">Nintendo 3DS titles</h3>
<ul>
<li>Ace Attorney Trilogy</li>
<li>Donkey Kong Country Returns 3D</li>
<li>Fire Emblem Awakening</li>
<li>Fire Emblem Echoes</li>
<li>Mario & Luigi Bowser’s Inside Story + Bowser’s Jr’s Journey</li>
<li>Metroid Samus Returns</li>
<li>Pushmo</li>
<li>Shovel Knight: Treasure Trove</li>
<li>The Legend of Zelda: A Link Between Worlds</li>
<li>The Legend of Zelda: Ocarina of Time 3D</li>
</ul>
<h3 id="nintendo-ds-titles">Nintendo DS titles</h3>
<ul>
<li>Dragon Quest IV</li>
<li>Dragon Quest V</li>
<li>Ghost Trick</li>
<li>Pokémon Black</li>
</ul>
<h3 id="homebrew">Homebrew</h3>
<ul>
<li>3DSident</li>
<li>Anemone 3DS Theme Manager</li>
<li>Cave Story</li>
<li>FBI</li>
<li>FTPD</li>
<li>Luma3DS</li>
<li>NDS Forwarder Generator</li>
<li>The Hombrew Launcher</li>
<li>Univeral Updater</li>
<li>hShop</li>
</ul>
<p><img src="/assets/img/blogs/2026-07-04-3ds.jpg" alt="The device, opened!" /></p>
<p>My experience so far has been awesome. Since it came modded I haven’t really had to thinker that much with it, instead, as of the time of writing, I have played about 20 hours of <em>Fire Emblem Awakening</em>—I just beat chapter 13 of the game—and honestly, the hardware is pretty nice! Due to the nature of the game I often quit and reopen it—I’m sorry I can’t handle permadeath—and the loading times are rather quick! I guess it’s up to the title and how well optimized it is, but still impressive. Buttons and battery life on my device have been really nice for me, but I lack a point of reference and I recharge it whenever needed.</p>
<p>I made a couple folders for the built-in software, all the homebrew and a couple GBA games I added but will probably delete. Either way I added it for now. I have installed a few themes using <em>Anemone</em>, right now I’m using a <a href="https://themeplaza.art/item/84660">Parasite Eve theme</a>, but I may try and do my own, and share it in the nearby future! I didn’t like any of the ones for <em>Chrono Trigger</em>, though some of the <em>Final Fantasy</em> ones were tempting. In any case. I’m really happy with how everything looks and feels!</p>
<p>This is day 89 of <a href="https://100daystooffload.com">#100DaysToOffload</a></p>
<p>
<a href="mailto:me@joelchrono.xyz?subject=Do you need a Nintendo 3DS?">Reply to this post via email</a> |
<a href="https://fosstodon.org/@joel/116865637634965192">Reply on Fediverse</a>
</p>Happy 250th birthday, America - Werd I/O6a49135282c96000019ca8902026-07-04T14:11:51.000Z<img src="https://images.unsplash.com/photo-1432837600650-e430234b632a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDExfHw0dGglMjBvZiUyMGp1bHl8ZW58MHx8fHwxNzgzMTc0Mjk3fDA&ixlib=rb-4.1.0&q=80&w=2000" alt="Happy 250th birthday, America"><p>Celebrating the 250th anniversary of a declaration that <a href="https://blackpast.org/african-american-history/declaration-independence-and-debate-over-slavery/?ref=werd.io" rel="noreferrer">had a passage decrying slavery removed</a> because southern states complained by eating <a href="https://www.pcrm.org/news/news-releases/new-us-poll-almost-9-out-10-adults-dont-know-risks-eating-hot-dogs?ref=werd.io" rel="noreferrer">carcinogenic mass produced meat tubes</a> while <a href="https://www.nbcnews.com/politics/donald-trump/trump-speak-mount-rushmore-one-monument-cant-remake-rcna352734?ref=werd.io" rel="noreferrer">the President tells half of us we’re evil Marxists</a> while we swelter through <a href="https://www.nytimes.com/2026/07/03/climate/heat-wave-us-canada-climate-change.html?ref=werd.io" rel="noreferrer">a man made heat wave</a> is incredibly American. Happy 4th!</p><p>All snark aside, genuinely, I do love America. The Declaration of Independence - 250 years ago! - was genuinely important. Most places you go, the people are kind, community-minded, and optimistic. One day, eventually, they will have an equitable government that redresses wrongs and provides real support.</p><p>My entire political worldview is: what if you could maintain the optimism and energy of America but add an inclusive culture of support that gives everyone education, healthcare, a real safety net and a springboard for their lives, in a way that builds communities rather than extracts from them?It's not our current reality, but I'm certain we can get there.</p><p>One day, we'll look back, and the corporatism, the militarism, the racism, and the subjugation will be a thing of the past. In their place will be thriving communities. And, finally, all of this will be great.</p>This blog is written in en-GB - Kev Quirkhttps://kevquirk.com/this-blog-is-written-in-en-gb2026-07-04T13:04:00.000Z<div class="link card"><h2>This blog is written in en-GB</h2><p class="post-author">by Terence Eden</p><p>Terrence talks about some of the wonderful idiosyncrasies of the British language and that, no, he won't be making his writing more global.</p><p><a class="button" target="_blank" href="https://shkspr.mobi/blog/2026/07/this-blog-is-written-in-en-gb/">Read post ➡</a></p></div>
<hr>
<p>I really enjoyed this post and like that Terrance <em>could</em> have said <em>"year sure, I'll try and be more inclusive for you non-Brits"</em>, but he didn't. Instead he said:</p>
<blockquote>
<p>Here's the thing. No. [...] There's a reason for that. It is more than the language I speak; it is the culture I live in, the way that I think, and the accent I use.</p>
</blockquote>
<p>Love this, and I appreciate Terrance holding firm on our wonderful British culture - just like everyone should do on their blog. That's part of the fun - to learn about the idiosyncrasies of difference languages and cultures.</p>
<p>It still surprises me that someone had the gall to leave a comment effectively saying <em>"can you change the way you write to be more inclusive, because <strong>I</strong> don't understand some of the references, and <strong>I</strong> can't be bothered to learn."</em></p>
<p>Some people...</p> <div class="email-hidden">
<hr />
<p>Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️</p>
<p>You can <a href="mailto:19gy@qrk.one?subject=This%20blog%20is%20written%20in%20en-GB">reply to this post by email</a>, or <a href="https://kevquirk.com/this-blog-is-written-in-en-gb#comments">leave a comment</a>.</p>
</div>Combined 1D and 2D Barcodes - Terence Eden’s Bloghttps://shkspr.mobi/blog/?p=709742026-07-04T11:34:57.000Z<p>This was a little idea gnawing at the back of my brain. The humble barcode has been in use <a href="https://en.wikipedia.org/wiki/Universal_Product_Code">since the 1970s</a>. In the next few years it will likely be replaced with a <a href="https://www.gs1uk.org/knowledge-hub/qr-codes-powered-by-gs1/will-qr-codes-replace-barcodes-by-2027">2D QR Code</a>.</p>
<p>I couldn't find anyone who'd made a QR code with an embedded UPC - so I decided to make one.</p>
<img src="https://shkspr.mobi/blog/wp-content/uploads/2026/05/Combined-QR-1D-Gap.webp" alt="A QR code with a 1D barcode embedded in it." width="400" height="400" class="aligncenter size-full wp-image-70975">
<p>If you move your phone close to the code (so it can't see the squares in the corners) it should read the number in the 1D barcode. Zoom out and it'll read the URl in the QR code.</p>
<p>The QR code has a high level of error correction - which allows graphics to be placed within it, <a href="https://shkspr.mobi/blog/2010/11/hiding-space-invaders-in-qr-codes/">as I discussed in 2010</a>.</p>
<p>The UPC has some whitespace padding around its edges - which makes it easier for some scanners to find, although not all scanners seem to accept it.</p>
<p>Is this in any way useful or desirable? I doubt it! I guess most point-of-sale barcode scanners are somewhat regularly updated - so they should all have the ability to scan newer codes. The embedded code destroys some of the error correction, thus making the QR code more fragile. <a href="https://mastodon.social/@PhilA/116504516987179728">It isn't a good idea</a>.</p>
<p>Still, nice to fiddle about with something, eh?</p>
<p>You may also enjoy:</p>
<ul>
<li><a href="https://shkspr.mobi/blog/2025/03/a-recursive-qr-code/">A Recursive QR Code</a>.</li>
<li><a href="https://shkspr.mobi/blog/2025/02/why-are-qr-codes-with-capital-letters-smaller-than-qr-codes-with-lower-case-letters/">Why are QR Codes with capital letters smaller than QR codes with lower-case letters?</a></li>
<li>Or see <a href="https://shkspr.mobi/blog/tag/qr+QR-codes/?order=asc">all my posts about QR Codes since 2007</a>!</li>
</ul>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=70974&HTTP_REFERER=Atom" alt width="1" height="1" loading="eager">Small wishes, II - James' Coffee Bloghttps://jamesg.blog/2026/07/04/small-wishes-ii2026-07-04T00:00:00.000Z
<blockquote><a href="https://jamesg.blog/2026/06/21/small-wishes" rel="noreferrer">June 21st, 2026</a> — After my coffee, a thought came to mind: I hope that one day I can see the countryside from a double-decker bus. It’s a small wish. It may never happen but I hold onto the thought any way.</blockquote><p>I stood at the bus terminal today thinking that it was only a few weeks ago I had wished to go on a double decker bus. Today, it happened: a most delightful surprise.</p><p>I arrived at the bus early enough that I had my pick of seats. I sat on the top deck in the front seat, where I had almost panoramic views of the countryside. Everything looked different. I was thrilled.</p><p>Toward the end of the journey, I saw two children jumping up and down with excitement. Their granny was with them. Getting on the bus, the family came to the top deck and sat in the three remaining chairs at the front, the granny sitting next to me. The granny mentioned how they were only going to the next stop, a few hundred meters away. The driver had stopped the bus until they were all in their seats, and knew exactly where to stop.</p><p>Looking forward with awe from the seats with the best view on the bus, the children were elated.</p><p>Toward the end of the journey, said in the kind of excited voice that a child can speak with such ease, I heard:</p><blockquote>“Granny, your house is like a double decker!”</blockquote><p>I smiled at this observation, and the tone in which it was delivered.</p><p>The family got off and continued their journey, the children delighted from their view of the world from a double decker. I stayed on for a few more stops, watching the world go by and thinking about how the small things really bring joy to the day.</p><script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a1608abb79f2d8c0',t:'MTc4MzE5MzM5OQ=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script>
<a class="tag" href="https://jamesg.blog/2026/06/21/small-wishes">June 21st, 2026</a>
Deduplicating posts after URL changes in Artemis - James' Coffee Bloghttps://jamesg.blog/2026/07/04/artemis-url-deduplication2026-07-04T00:00:00.000Z
<style media="(prefers-color-scheme: dark)">pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #49483e }
.highlight { background: #272822; color: #F8F8F2 }
.highlight .c { color: #959077 } /* Comment */
.highlight .err { color: #ED007E; background-color: #1E0010 } /* Error */
.highlight .esc { color: #F8F8F2 } /* Escape */
.highlight .g { color: #F8F8F2 } /* Generic */
.highlight .k { color: #66D9EF } /* Keyword */
.highlight .l { color: #AE81FF } /* Literal */
.highlight .n { color: #F8F8F2 } /* Name */
.highlight .o { color: #FF4689 } /* Operator */
.highlight .x { color: #F8F8F2 } /* Other */
.highlight .p { color: #F8F8F2 } /* Punctuation */
.highlight .ch { color: #959077 } /* Comment.Hashbang */
.highlight .cm { color: #959077 } /* Comment.Multiline */
.highlight .cp { color: #959077 } /* Comment.Preproc */
.highlight .cpf { color: #959077 } /* Comment.PreprocFile */
.highlight .c1 { color: #959077 } /* Comment.Single */
.highlight .cs { color: #959077 } /* Comment.Special */
.highlight .gd { color: #FF4689 } /* Generic.Deleted */
.highlight .ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */
.highlight .ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #F8F8F2 } /* Generic.Error */
.highlight .gh { color: #F8F8F2 } /* Generic.Heading */
.highlight .gi { color: #A6E22E } /* Generic.Inserted */
.highlight .go { color: #66D9EF } /* Generic.Output */
.highlight .gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */
.highlight .gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #959077 } /* Generic.Subheading */
.highlight .gt { color: #F8F8F2 } /* Generic.Traceback */
.highlight .kc { color: #66D9EF } /* Keyword.Constant */
.highlight .kd { color: #66D9EF } /* Keyword.Declaration */
.highlight .kn { color: #FF4689 } /* Keyword.Namespace */
.highlight .kp { color: #66D9EF } /* Keyword.Pseudo */
.highlight .kr { color: #66D9EF } /* Keyword.Reserved */
.highlight .kt { color: #66D9EF } /* Keyword.Type */
.highlight .ld { color: #E6DB74 } /* Literal.Date */
.highlight .m { color: #AE81FF } /* Literal.Number */
.highlight .s { color: #E6DB74 } /* Literal.String */
.highlight .na { color: #A6E22E } /* Name.Attribute */
.highlight .nb { color: #F8F8F2 } /* Name.Builtin */
.highlight .nc { color: #A6E22E } /* Name.Class */
.highlight .no { color: #66D9EF } /* Name.Constant */
.highlight .nd { color: #A6E22E } /* Name.Decorator */
.highlight .ni { color: #F8F8F2 } /* Name.Entity */
.highlight .ne { color: #A6E22E } /* Name.Exception */
.highlight .nf { color: #A6E22E } /* Name.Function */
.highlight .nl { color: #F8F8F2 } /* Name.Label */
.highlight .nn { color: #F8F8F2 } /* Name.Namespace */
.highlight .nx { color: #A6E22E } /* Name.Other */
.highlight .py { color: #F8F8F2 } /* Name.Property */
.highlight .nt { color: #FF4689 } /* Name.Tag */
.highlight .nv { color: #F8F8F2 } /* Name.Variable */
.highlight .ow { color: #FF4689 } /* Operator.Word */
.highlight .pm { color: #F8F8F2 } /* Punctuation.Marker */
.highlight .w { color: #F8F8F2 } /* Text.Whitespace */
.highlight .mb { color: #AE81FF } /* Literal.Number.Bin */
.highlight .mf { color: #AE81FF } /* Literal.Number.Float */
.highlight .mh { color: #AE81FF } /* Literal.Number.Hex */
.highlight .mi { color: #AE81FF } /* Literal.Number.Integer */
.highlight .mo { color: #AE81FF } /* Literal.Number.Oct */
.highlight .sa { color: #E6DB74 } /* Literal.String.Affix */
.highlight .sb { color: #E6DB74 } /* Literal.String.Backtick */
.highlight .sc { color: #E6DB74 } /* Literal.String.Char */
.highlight .dl { color: #E6DB74 } /* Literal.String.Delimiter */
.highlight .sd { color: #E6DB74 } /* Literal.String.Doc */
.highlight .s2 { color: #E6DB74 } /* Literal.String.Double */
.highlight .se { color: #AE81FF } /* Literal.String.Escape */
.highlight .sh { color: #E6DB74 } /* Literal.String.Heredoc */
.highlight .si { color: #E6DB74 } /* Literal.String.Interpol */
.highlight .sx { color: #E6DB74 } /* Literal.String.Other */
.highlight .sr { color: #E6DB74 } /* Literal.String.Regex */
.highlight .s1 { color: #E6DB74 } /* Literal.String.Single */
.highlight .ss { color: #E6DB74 } /* Literal.String.Symbol */
.highlight .bp { color: #F8F8F2 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #A6E22E } /* Name.Function.Magic */
.highlight .vc { color: #F8F8F2 } /* Name.Variable.Class */
.highlight .vg { color: #F8F8F2 } /* Name.Variable.Global */
.highlight .vi { color: #F8F8F2 } /* Name.Variable.Instance */
.highlight .vm { color: #F8F8F2 } /* Name.Variable.Magic */
.highlight .il { color: #AE81FF } /* Literal.Number.Integer.Long */ .highlight .nn, .highlight .n{color: light-dark(black, var(--dark-foreground-color)) }</style><style media="(prefers-color-scheme: light)">pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight { background: #f8f8f8; }
.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */
.highlight .err { border: 1px solid #F00 } /* Error */
.highlight .k { color: #008000; font-weight: bold } /* Keyword */
.highlight .o { color: #666 } /* Operator */
.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #9C6500 } /* Comment.Preproc */
.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
.highlight .gd { color: #A00000 } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #E40000 } /* Generic.Error */
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.highlight .gi { color: #008400 } /* Generic.Inserted */
.highlight .go { color: #717171 } /* Generic.Output */
.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.highlight .gt { color: #04D } /* Generic.Traceback */
.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008000 } /* Keyword.Pseudo */
.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #B00040 } /* Keyword.Type */
.highlight .m { color: #666 } /* Literal.Number */
.highlight .s { color: #BA2121 } /* Literal.String */
.highlight .na { color: #687822 } /* Name.Attribute */
.highlight .nb { color: #008000 } /* Name.Builtin */
.highlight .nc { color: #00F; font-weight: bold } /* Name.Class */
.highlight .no { color: #800 } /* Name.Constant */
.highlight .nd { color: #A2F } /* Name.Decorator */
.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */
.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #00F } /* Name.Function */
.highlight .nl { color: #767600 } /* Name.Label */
.highlight .nn { color: #00F; font-weight: bold } /* Name.Namespace */
.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #19177C } /* Name.Variable */
.highlight .ow { color: #A2F; font-weight: bold } /* Operator.Word */
.highlight .w { color: #BBB } /* Text.Whitespace */
.highlight .mb { color: #666 } /* Literal.Number.Bin */
.highlight .mf { color: #666 } /* Literal.Number.Float */
.highlight .mh { color: #666 } /* Literal.Number.Hex */
.highlight .mi { color: #666 } /* Literal.Number.Integer */
.highlight .mo { color: #666 } /* Literal.Number.Oct */
.highlight .sa { color: #BA2121 } /* Literal.String.Affix */
.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */
.highlight .sc { color: #BA2121 } /* Literal.String.Char */
.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */
.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
.highlight .s2 { color: #BA2121 } /* Literal.String.Double */
.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */
.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
.highlight .sx { color: #008000 } /* Literal.String.Other */
.highlight .sr { color: #A45A77 } /* Literal.String.Regex */
.highlight .s1 { color: #BA2121 } /* Literal.String.Single */
.highlight .ss { color: #19177C } /* Literal.String.Symbol */
.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #00F } /* Name.Function.Magic */
.highlight .vc { color: #19177C } /* Name.Variable.Class */
.highlight .vg { color: #19177C } /* Name.Variable.Global */
.highlight .vi { color: #19177C } /* Name.Variable.Instance */
.highlight .vm { color: #19177C } /* Name.Variable.Magic */
.highlight .il { color: #666 } /* Literal.Number.Integer.Long */</style>
<style>
@font-face {
font-family: 'MonaspaceArgon';
src: url('/assets/fonts/MonaspaceArgon-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
pre, code {
font-family: 'MonaspaceArgon', ui-monospace, monospace;
}
</style>
<p>Occasionally, sites that publish web feeds change the URL of a post that has just been published. This can be for several reasons. For example, I sometimes change post permalinks immediately after publishing because I notice a typo, or because the CMS I use calculated a default permalink that I don’t want to use and I forgot to change it.</p><p>Until now, <a href="https://artemis.jamesg.blog" rel="noreferrer">Artemis</a>, the calm web reader I maintain, has done nothing if a post has changed its permalink. Importantly, Artemis considers the post URL as a unique identifier. This means that if a post changes its URL, a new record for the post will be added to the database. Today, however, I have added logic to deduplicate posts where the title is the same but the URL has changed.</p><p>The way it works is as follows. When a user’s feed is computed, Artemis checks how many posts published by the same author have the same title per day. If two or more posts have the same title in a day, Artemis will show the most recent version of the post. All other instances of the post will be hidden. This logic is based on the assumption that the most recent URL should be correct.</p><p>With this logic, posts with these titles and permalinks would be merged:</p><ul><li><code>https://jamesg.blog/2026/01/01/testing</code> with the title "Test", published at 00:01.</li><li><code>https://jamesg.blog/2026/01/01/test</code> with the title "Test", published at 00:04.</li></ul><p>The final entry would be:</p><ul><li><code>https://jamesg.blog/2026/01/01/test</code> with the title "Test".</li></ul><p>This is because the permalink that ends in <code>/test</code> was published most recently.</p><p>There is a notable edge case for this implementation: if a site posts two blog posts in the same day with the same title, only the most recent permalink will be displayed. While writing this post, I took a detour to see if I should also compare the hashes of the post content to determine whether the posts are the same. Looking at the instance that inspired me to work on this change, the content on the page whose URL was later changed didn’t have the same hash after the URL was changed: the post content had also changed.</p><p>In addition, this implementation depends on the post title staying the same even if the URL changes, which means that if an author changes the title and URL of a post then there will still be two entries in a user’s feed <sup class="footnote-reference" id="f-1"><a href="https://jamesg.blog/longform-feed#1">1</a></sup>.</p><p>As I write, I am thinking about whether Artemis should visually show that a URL has been chosen as canonical. This would allow a user to see that two posts are likely to be the same, and then the user could manually check if they were interested. With that said, perhaps this would add too much clutter to the interface? I will have to think more about this.</p><p>In any case, the post deduplication feature is now live in Artemis. I hope this change will keep the reader interface cleaner in cases where authors change post URLs.</p>
<div class="footnote-definition" id="1"><sup class="footnote-definition-label" id="f-2">1</sup>
<p>Ideally, a site will add a redirect to the new page in any case, but this doesn’t always happen.</p>
<a href="https://jamesg.blog/longform-feed#f-1">[↩]</a></div>
<a class="tag" href="https://jamesg.blog/longform-feed#1">1</a>
<a class="tag" href="https://jamesg.blog/longform-feed#f-1">[↩]</a>
Notable links: July 3, 2026 - Werd I/O6a47bfb182c96000019ca43f2026-07-03T14:09:16.000Z<img src="https://storage.ghost.io/c/18/7c/187cc681-d3f3-49fc-87de-b01d06b76821/content/images/2026/07/lianhao-qu-LfaN1gswV5c-unsplash.jpg" alt="Notable links: July 3, 2026"><p><em>Most Fridays, I share a handful of pieces that caught my eye at the intersection of technology, media, and society.</em></p><p><em>Did I miss something important? </em><a href="mailto:ben@werd.io" rel="noreferrer"><em>Send me an email</em></a><em> to let me know.</em></p><p><em>Did someone forward this to you? </em><a href="https://werd.io/notable-links-june-26-2026/#/portal" rel="noreferrer"><em>Subscribe for free</em></a><em>.</em></p><hr><h3 id="openai-proposes-handing-trump-administration-5-stake"><a href="https://www.ft.com/content/7c803eab-8e80-4431-9a87-e943bf00e00b?syn-25a6b1a6=1&ref=werd.io" rel="noreferrer">OpenAI proposes handing Trump administration 5% stake</a></h3><p>In order to ward off backlash against AI and curry favor with the Trump administration, Sam Altman has floated the idea of giving 5% of OpenAI to a wealth fund that pays dividends to both the government and citizens — and that every leading AI vendor should do the same.</p><blockquote>“Sam Altman, chief executive of the ChatGPT maker, has argued that giving the public a financial stake in the company is the best way to share the upside of AI and has suggested a stake of this size in early conversations with the administration, according to two people familiar with the talks.”</blockquote><p>It’s transparently a way to align everyone with AI vendor profits. If the sector increases in value, the government and the voting population benefit. If it <em>decreases</em> in value … well, the government is incentivized to prevent that from happening. It also wouldn’t be without precedent: it’s modeled on the Alaska Permanent Fund, which does this with oil profits for Alaskan residents. Intel is also now 10% government-owned, and the administration has reversed course to be behind it since gaining that stake.</p><p>Would a government whose revenues are directly linked to the performance of a sector be likely to enact hard regulations on that sector? Perhaps not. It’s not a slam dunk, though: for example, the UK receives significant tax revenue on fossil fuels, but still promoted electric cars. There are lots of factors at play, and profit alignment isn’t necessarily outweighed by the effects of other harms. (See also: cigarettes, which are taxed but also tightly controlled as an addictive carcinogen.)</p><p>Meanwhile, Bernie Sanders has pushed for closer to 50% ownership through a sovereign wealth fund. At this much lower stake, Sam Altman’s proposal uses Sanders’s democratic socialist “share the wealth” language as a way to launder OpenAI’s profits through a thin veneer of good ethics.</p><p>What’s also interesting to me is that all of these arguments assume that AI is going to be an enormous driver of wealth and innovation — but what if it isn’t? It’s another great way to advertise the technology as something world-changing that everybody must get behind right now.</p><p>Even if AI turns out to be what the people heavily invested in its success say it will be, it doesn’t stand alone as a sea change innovation. The personal computer, the iPhone, word processors, and spreadsheets were pretty transformational technologies. Should there have been a wealth fund attached to each of those? What, exactly, makes AI different?</p><p>The answer is that it represents labor displacement: people will lose their jobs. And if that’s actually going to be the case, we need bigger, more structural safety nets and reforms. Dividends from 5% of a sector aren’t going to replace wages at scale — and are heavily dependent on valuations continuing to rise. This proposal ties the welfare of people who have lost their jobs to the success of the companies that drove those losses. The incentives are perverse.</p><p>We shouldn’t accept this proposal. Instead, we should push for stronger protections and stronger regulation. If a sector can’t succeed without real damage to working communities, then it must not be allowed to. And if these claims turn out not to be true, then it’s an empty gesture designed to add credibility to a self-interested science fiction view of the future.</p><hr><h3 id="companies-are-making-claude-and-codex-talk-like-cavemen-to-stop-ai%E2%80%99s-soaring-costs"><a href="https://www.404media.co/companies-are-making-claude-and-codex-talk-like-cavemen-to-stop-ais-soaring-costs/?ref=werd.io" rel="noreferrer">Companies Are Making Claude and Codex Talk Like Cavemen to Stop AI’s Soaring Costs</a></h3><p>I find this very funny:</p><blockquote>“Companies are deliberately making their AI tools speak like cavemen in an attempt to stop burning through AI tokens and curb their massive expenditure on AI, 404 Media has found. The tool turns the usually verbose outpost of LLMs like Claude Code, Codex, or Gemini into a much more to the point answer. Think less “you’re right to push back, I was wrong,” and more “Hulk smash.””</blockquote><p>If only we had other limited-vocabulary lexicons designed to talk to computers efficiently!</p><p>I think we’re circling a few different possibilities that may show up over the next few years:</p><ul><li>Literally LLM-specific “programming languages” that humans can use to talk to models more efficiently, of which Caveman is the hilarious first step</li><li>A proprietary bytecode-like language for LLMs that makes interactions more efficient but also just happens to be owned by one of the major vendors and creates a hitherto-unobtainable moat for their business</li><li>This all becomes moot when local models become viable for most businesses without insanely high hardware prices or configuration costs</li><li>LLM costs eventually fall to a fraction of their existing level</li></ul><p>But who knows? Maybe enterprise businesses will continue to talk in stilted caveman language to achieve their business goals forever.</p><hr><h3 id="journalism-has-the-receipts-it-won%E2%80%99t-use-them"><a href="https://www.backstoryandstrategy.com/p/journalism-has-the-receipts-it-wont?ref=werd.io" rel="noreferrer">Journalism Has the Receipts. It Won’t Use Them.</a></h3><p>Arts organizations learned long ago to prove their economic value with hard numbers: attendance, tourism revenue, multiplier effects. News, as Yoni Greenbaum argues here, likes to cling to civic virtue and assume that the work should speak for itself.</p><blockquote>“Journalism operated on a commercial advertising revenue model for over 150 years. Publishers sold readers to advertisers, while editors fretted about maintaining a church-and-state divide between the newsroom and business desk. Journalists saw themselves as watchdogs, not wealth generators. Pitching our value based on our own economic impact felt gauche, too close to an advertorial.”</blockquote><p>Yoni points out that this is starting to change. <a href="https://www.rebuildlocalnews.org/new-report-reveals-local-news-shortage-is-costing-communities-1-1-billion-a-year/?ref=werd.io">We know that news deserts cost communities at least $1.1B a year</a>, for example, because of a report by Rebuild Local News and the University of Illinois Chicago. But newsrooms themselves tend to shy away from reporting their own economic impact — even though they already have the tools to do so.</p><p>It’s not obvious to me that this accounting would work as an argument across the board for newsrooms, and particularly for those with a national focus. Does ProPublica (my employer until the end of the month) save anyone money? It certainly does prevent corruption, and there are instances with real dollar amounts attached to them: Intuit, for example, paid back $141 million to its customers over deceptive marketing. But I’m not sure that its impact can be quantified easily overall, despite the newsroom’s obvious public benefit. On the other hand, for <em>local</em> newsrooms, this makes a lot of sense to me: at their best, they act as connective tissue for their communities. That $1.1B a year was <em>just</em> increased interest costs from lenders who felt they could charge more to unmonitored governments.</p><p>They just need to get more comfortable at telling the economic side of their stories. And there’s a wider point here, which is that almost all nonprofit newsrooms need to be able to get more concrete and detailed about their business models. If you’re running an organization that wants to be sustainable, it’s not enough to care about the journalistic process, and your business accounting cannot be limited to activities like events and merchandise. You actually have to care about building a business holistically, and everything that entails.</p><hr><h3 id="cascade-pbs-launches-local-public-as-standalone-streaming-tech-company"><a href="https://thedesk.net/2026/07/local-public-launches-pbs-member-stations-apps/?ref=werd.io" rel="noreferrer">Cascade PBS launches Local Public as standalone streaming tech company</a></h3><p>I’ve got some complicated feelings about this announcement from Cascade PBS:</p><blockquote>“Cascade PBS, the non-profit television station serving western Washington state, has spun out its technology division into a separate company that will help similar public broadcasters carve out their own streaming and digital identities.<br><br>The new company, Local Public, will help develop streaming applications for connected TVs, mobile devices and the web, allowing public television stations to offer locally branded streaming experiences featuring their own programming alongside national PBS content.”</blockquote><p>On one hand, I absolutely love that they were able to spin out their technology division. Most public media companies don’t have the resources or skills to build their own tech, and building this capability outside of any one station so that all of them can take advantage of it makes a lot of sense to me.</p><p>The Local Public site itself <a href="https://www.localpublic.tv/pricing/?ref=werd.io">also makes the ROI transparent</a>. WETA, the public media station for Greater Washington, ran the numbers and said that it would break even in the first year, and a calculator is available for other stations that want to check for themselves. The pricing hinges on Passport-eligible donors: those giving at least $60 a year. Local Public charges $60,000 a year for stations with fewer than 15,000, $75,000 for up to 40,000, and $100,000 a year for everyone else — which is not out of bounds. It all seems like a decent business, run in the public interest as a subsidiary of Cascade PBS, that will genuinely help public media stations. I want to see more of this.</p><p>But I do wish it was fully based on open technology. While stations gain the right to modify the source code of their <em>apps</em> after a year, they remain locked into Local Public’s back-end services. For the CMS, which builds network effects the more stations use it, stations can only get support, maintenance, and customization through Local Public. Over time, that lock-in does not incentivize great support, and Local Public will need to work hard to buck the trends. NPR’s CMS, for example, is notorious among the stations that have to use it. I’m certain the will is there to do better, but they will need to proceed with intention. In my opinion it would be better if, at least after establishing a customer base, they open sourced their back-end CMS too.</p><p>I tend to think that <em>any</em> technology provided to support the public interest should be fully open. That doesn’t mean there isn’t a tidy business available to its creator — ask <a href="https://ghost.org/?ref=werd.io">Ghost</a>, which is generating millions of dollars off the back of its open source CMS. If there’s a class of organization that absolutely doesn’t deserve to be locked into a technology stack, it’s public service broadcasters.</p><p>This isn’t Cascade PBS’s fault. It needs its spinout to be sustainable, and this model feels like it will hit that goal. The best scenario, in my mind, would be if there were central funders who bankrolled open tech that the whole ecosystem could use. But, of course, it’s 2026, and central funding for <em>anything</em> public media is hard to come by.</p><p>Still, this is wonderful to see, and anything that encourages collaboration on a technical level between public service media organizations deserves support.</p><hr><h3 id="emergency-mode-for-news"><a href="https://emergencymode.news/?ref=werd.io" rel="noreferrer">Emergency Mode for news</a></h3><p>Emergency Mode is a set of resources, tools, and training that aims to prepare small newsrooms for various disasters. It’s a co-production between <a href="https://opennews.org/?ref=werd.io">OpenNews</a>, <a href="https://nlocal.org/?ref=werd.io">NC Local</a>, and <a href="https://newspack.com/?ref=werd.io">Newspack</a>. They’ve done a great job. As their about page puts it:</p><blockquote>“Emergency Mode for News equips local journalists and their newsrooms with the tools they need to respond to climate disasters. With a disaster reporting action pack, software and a learning community, Emergency Mode is designed to help journalists act nimbly and creatively to serve their communities when the unexpected happens.”</blockquote><p>Toolkits include things like <a href="https://docs.google.com/document/d/1iEvYxJf1dnQ8jXS8IhBeqh9GrkTsxCqBjehG66aRJmA/edit?tab=t.0&ref=werd.io#heading=h.t2e0k24sy4l4">a practical checklist for newsrooms covering wildfires</a> and <a href="https://docs.google.com/document/d/1dIeAA0yo3OXJKfjT4cwaPkZd01stJqaPD3kRZMpIsDA/edit?tab=t.0&ref=werd.io#heading=h.k4t85kcanet">a template for maintaining source lists during an emergency</a>. There’s also <a href="https://emergencymode.news/training/?ref=werd.io">a hands-on workshop series</a> and tools like WordPress plugins for <a href="https://emergencymode.news/tools/?ref=werd.io">live rolling news updates</a> and providing bandwidth-light versions of sites.</p><p>Most of all, I really appreciate the practical nature of all of it. Rather than hand-waving about principles and ideas, as many newsroom-facing resources do, everything here is a concrete tool that can actively be used in the field. Newsrooms are more squeezed than they’ve ever been, so it doesn’t hurt that it’s all free.</p><p>I’d love to see this level of concrete specificity for <em>the normal working of a newsroom</em>. Wouldn’t it be cool to have a list of business model checklists you could pull from? Or disaster recovery plans? Or data protection policies? Just as the tools on this site are going to be concretely useful to any newsroom that covers a disaster, checklists, tools, and training for standard operational practices could be really meaningful — particularly for smaller newsrooms that don’t have the ability to hire CTOs, CFOs, and so on.</p><p>In other words: more, please. This is lovely.</p><hr><h3 id="the-quiet-erosion-of-collective-action-under-digital-surveillance"><a href="https://www.techpolicy.press/the-quiet-erosion-of-collective-action-under-digital-surveillance/?ref=werd.io" rel="noreferrer">The Quiet Erosion of Collective Action Under Digital Surveillance</a></h3><p>The most important outcome of increased surveillance is a chilling effect on free speech and expression. As Gina Romero, the United Nations Special Rapporteur on the Rights to Freedom of Peaceful Assembly, notes here, that extends to the organizations that have been established to protect those rights:</p><blockquote>“As organizations operate under the constant assumption that they are being monitored, their core functions are profoundly affected. Their ability to serve as watchdogs, provide rights-based services, protect victims of human rights abuses, and educate the public is severely constrained. Ultimately, the very possibility of advancing and protecting rights, democracy and the rule of law is undermined.”</blockquote><p>Civil society organizations and advocates have been mislabeled as national security threats around the world. It’s true in some of the nations that we’ve long thought of as being authoritarian, but it’s also true in the United States. Even places like the United Kingdom have tried to apply pressure to technology companies so that they can gain access to backdoors.</p><p>Tools like <a href="https://signal.org/?ref=werd.io">Signal</a> have become all the more important. We need more easy to use end to end encrypted systems so that we can communicate and organize with each other without fear of government surveillance. That also allows whistleblowers and sources for journalists to reach out with less of a fear they they will suffer repercussions.</p><p>But those tools don’t stop you from being surveilled in the real world. Cameras and microphones are everywhere; license plate readers are now commonplace; even <a href="https://theconversation.com/world-cup-propels-surveillance-to-new-heights-284712?ref=werd.io">AI-enabled drones have been deployed</a> for events like the World Cup.</p><p>It’s generally true that if government <em>can</em> do something, it will. So the only way to stop this kind of widespread surveillance is to make it impossible. Romero calls for legislative prevention that takes into account the whole systemic impact of surveillance rather than just the immediate first-order effects. Her report also calls out that it can be very difficult to challenge these systems because what they are and who owns them tends to be complicated or obfuscated:</p><blockquote>“The study reveals a lack of transparency surrounding the relationship between state power and non-state actors, creating an information vacuum that makes surveillance practices exceedingly difficult to challenge through litigation. As a result, the right to an effective remedy is fundamentally weakened.”</blockquote><p>So I think we also need more technical capabilities that interfere with how these systems of surveillance actually work. We need more spaces that are designated privacy-first and enforce an anti-surveillance rulebook. And, just as <a href="https://techcrunch.com/2026/02/23/americans-are-destroying-flock-surveillance-cameras/?ref=werd.io">communities have taken it upon themselves to dismantle Flock cameras</a>, we need to take back our streets.</p><hr><h3 id="openai-will-delay-gpt-56-after-trump-administration-request"><a href="https://www.theverge.com/ai-artificial-intelligence/957372/openai-will-delay-gpt-5-6-after-trump-administration-request?ref=werd.io" rel="noreferrer">OpenAI will delay GPT-5.6 after Trump administration request</a></h3><p>I’ve got (at least) two worries about the story that the Trump Administration halted the release of models from both Anthropic and OpenAI.</p><p>Anthropic <a href="https://www.anthropic.com/news/fable-mythos-access?ref=werd.io">recently pulled its Fable model release</a> in response to the government. Now it turns out that OpenAI has done something similar:</p><blockquote>“The Information reported that OpenAI CEO Sam Altman told employees Wednesday in a company Q&A that it would release GPT-5.6 in limited preview form — granting access only to a small group of enterprise customers — in compliance with a request from the federal government. During that preview period, the Trump administration itself would reportedly approve access for customers on a case-by-case basis.”</blockquote><p>In some ways, what a coup for the AI industry. This technology is so powerful that the government doesn’t think anyone should have it — and when it does inevitably release into the public’s hands, what a valuable product that will be. Get the tech that’s too dangerous to be released! This magical product can be yours for an unbelievable price!</p><p>So one worry is that this is, in essence, great marketing for these vendors.</p><p>But it’s worth remembering that these AI models are black boxes that respond to information queries in opaque ways. The more people rely on them for knowledge, the more powerful the models become. The argument being presented is that they can be used in ways that might present traditional security threats — but consider that some versions of the truth, to the wrong kind of authoritarian-minded government, might also be considered a threat. (Remember that “extremism on migration, race, and gender” and hostility to “traditional American views” <a href="https://theconversation.com/labeling-dissent-as-terrorism-new-us-domestic-terrorism-priorities-raise-constitutional-alarms-269161?ref=werd.io">are now considered markers of domestic terrorism</a>.)</p><p>This is a golden opportunity, in other words, to hit pause on frontier model releases, at a time when models are becoming more prevalent, in order to make sure models are shaped to represent a certain version of the world. The administration <a href="https://www.whitehouse.gov/fact-sheets/2025/07/fact-sheet-president-donald-j-trump-prevents-woke-ai-in-the-federal-government/?ref=werd.io">has already signaled a willingness to do this</a>; there is nothing to say they aren’t. The only way to <em>prove</em> that they aren’t is to open source not just the models but the training process and make the whole thing transparent and verifiable. The industry is a long way off from doing that.</p><hr><h3 id="now-we%E2%80%99re-getting-ai-fake-news-complaining-about-how-ai-fake-news-is-the-death-of-real-news"><a href="https://www.niemanlab.org/2026/07/now-were-getting-ai-fake-news-complaining-about-how-ai-fake-news-is-the-death-of-real-news/?ref=werd.io" rel="noreferrer">Now we’re getting AI fake news complaining about how AI fake news is the death of real news </a></h3><p>A bunch of people — <a href="https://werd.io/a-right-wing-media-chain-tried-to-replace-47-newspapers-with-ai-they-all-died/">including, unfortunately, me</a> — were taken in by this AI-generated newsroom earlier this week. The story was decently written and seemed to be well-cited, but it turned out to be nonsense. Ironically, it was about a would-be media empire that purchased struggling papers, fired their staff, and replaced them with AI, leading to the death of each newsroom. All false.</p><p>So the big question about The Editorial is: why does it exist?</p><p><a href="https://www.niemanlab.org/2026/07/now-were-getting-ai-fake-news-complaining-about-how-ai-fake-news-is-the-death-of-real-news/?ref=werd.io">As Joshua Benton put it:</a></p><blockquote>“Fake news isn’t new, obviously. And while AI-generated slop is newer, it’s hardly unfamiliar at this point. But why would a spam site bother making up a story about Alabama weekly newspapers, of all things? Whose interest is it in to get that niche?”</blockquote><p>Here’s my theory: I think it’s a two-headed LLM poisoning scheme.</p><p>On one hand, most of the content relates to Chinese-specific interests: articles about Taiwan or African nations where China is making inroads. These are all articles from a China-friendly perspective. If an LLM were to ingest them and <em>trust the site</em>, it might start repeating the assertions made in each one as fact.</p><p>One way to make sure a site is trusted is to get other, trusted sources to point to it. That’s where the stories about journalism come in: there are few things that journalists engage in more than stories about their own industry. Get enough patsies (like, again, to my chagrin, <em>me</em>) to point links in their direction and journalists might post them in high-trust communities on high-trust sites like Reddit, as well as their own, and Bob’s your uncle. <a href="https://werd.io/all-you-need-to-poison-an-llm-is-13-words/">We already know that it takes as little as 13 words</a> to poison an LLM with falsehoods.</p><p>Of course, that might not be it at all. Frank, the site’s owner (who lists himself as CEO of Nordiso Group on LinkedIn), at least appears to be a Finnish solopreneur. If he wanted to clear the air, he could write a post (himself) about what he was up to. It might be that he’s running an experiment to see how easily an LLM can be poisoned with propaganda! Until then, I think it’s reasonable to assume that something underhand is going on.</p>A new media spinout provides streaming apps for public service broadcasters. I just wish it was open. - Werd I/O6a47bbda82c96000019ca4322026-07-03T13:40:42.000Z<p>Link: <a href="https://thedesk.net/2026/07/local-public-launches-pbs-member-stations-apps/?ref=werd.io"><em>Cascade PBS launches Local Public as standalone streaming tech company, by Matthew Keys at The Desk</em></a></p><p>I’ve got some complicated feelings about this announcement from Cascade PBS:</p><blockquote>“Cascade PBS, the non-profit television station serving western Washington state, has spun out its technology division into a separate company that will help similar public broadcasters carve out their own streaming and digital identities.<br><br>The new company, Local Public, will help develop streaming applications for connected TVs, mobile devices and the web, allowing public television stations to offer locally branded streaming experiences featuring their own programming alongside national PBS content.”</blockquote><p>On one hand, I absolutely love that they were able to spin out their technology division. Most public media companies don’t have the resources or skills to build their own tech, and building this capability outside of any one station so that all of them can take advantage of it makes a lot of sense to me.</p><p>The Local Public site itself <a href="https://www.localpublic.tv/pricing/?ref=werd.io">also makes the ROI transparent</a>. WETA, the public media station for Greater Washington, ran the numbers and said that it would break even in the first year, and a calculator is available for other stations that want to check for themselves. The pricing hinges on Passport-eligible donors: those giving at least $60 a year. Local Public charges $60,000 a year for stations with fewer than 15,000, $75,000 for up to 40,000, and $100,000 a year for everyone else — which is not out of bounds. It all seems like a decent business, run in the public interest as a subsidiary of Cascade PBS, that will genuinely help public media stations. I want to see more of this.</p><p>But I do wish it was fully based on open technology. While stations gain the right to modify the source code of their <em>apps</em> after a year, they remain locked into Local Public’s back-end services. For the CMS, which builds network effects the more stations use it, stations can only get support, maintenance, and customization through Local Public. Over time, that lock-in does not incentivize great support, and Local Public will need to work hard to buck the trends. NPR’s CMS, for example, is notorious among the stations that have to use it. I’m certain the will is there to do better, but they will need to proceed with intention. In my opinion it would be better if, at least after establishing a customer base, they open sourced their back-end CMS too.</p><p>I tend to think that <em>any</em> technology provided to support the public interest should be fully open. That doesn’t mean there isn’t a tidy business available to its creator — ask <a href="https://ghost.org/?ref=werd.io">Ghost</a>, which is generating millions of dollars off the back of its open source CMS. If there’s a class of organization that absolutely doesn’t deserve to be locked into a technology stack, it’s public service broadcasters.</p><p>This isn’t Cascade PBS’s fault. It needs its spinout to be sustainable, and this model feels like it will hit that goal. The best scenario, in my mind, would be if there were central funders who bankrolled open tech that the whole ecosystem could use. But, of course, it’s 2026, and central funding for <em>anything</em> public media is hard to come by.</p><p>Still, this is wonderful to see, and anything that encourages collaboration on a technical level between public service media organizations deserves support.</p>Newsrooms need to get comfortable expressing their business value - and raising money on it. - Werd I/O6a47b56282c96000019ca42c2026-07-03T13:13:06.000Z<p>Link: <a href="https://www.backstoryandstrategy.com/p/journalism-has-the-receipts-it-wont?ref=werd.io"><em>Journalism Has the Receipts. It Won’t Use Them., by Yoni Greenbaum in Backstory & Strategy</em></a></p><p>Arts organizations learned long ago to prove their economic value with hard numbers: attendance, tourism revenue, multiplier effects. News, as Yoni Greenbaum argues here, likes to cling to civic virtue and assume that the work should speak for itself.</p><blockquote>“Journalism operated on a commercial advertising revenue model for over 150 years. Publishers sold readers to advertisers, while editors fretted about maintaining a church-and-state divide between the newsroom and business desk. Journalists saw themselves as watchdogs, not wealth generators. Pitching our value based on our own economic impact felt gauche, too close to an advertorial.”</blockquote><p>Yoni points out that this is starting to change. <a href="https://www.rebuildlocalnews.org/new-report-reveals-local-news-shortage-is-costing-communities-1-1-billion-a-year/?ref=werd.io">We know that news deserts cost communities at least $1.1B a year</a>, for example, because of a report by Rebuild Local News and the University of Illinois Chicago. But newsrooms themselves tend to shy away from reporting their own economic impact — even though they already have the tools to do so.</p><p>It’s not obvious to me that this accounting would work as an argument across the board for newsrooms, and particularly for those with a national focus. Does ProPublica (my employer until the end of the month) save anyone money? It certainly does prevent corruption, and there are instances with real dollar amounts attached to them: Intuit, for example, paid back $141 million to its customers over deceptive marketing. But I’m not sure that its impact can be quantified easily overall, despite the newsroom’s obvious public benefit. On the other hand, for <em>local</em> newsrooms, this makes a lot of sense to me: at their best, they act as connective tissue for their communities. That $1.1B a year was <em>just</em> increased interest costs from lenders who felt they could charge more to unmonitored governments.</p><p>They just need to get more comfortable at telling the economic side of their stories. And there’s a wider point here, which is that almost all nonprofit newsrooms need to be able to get more concrete and detailed about their business models. If you’re running an organization that wants to be sustainable, it’s not enough to care about the journalistic process, and your business accounting cannot be limited to activities like events and merchandise. You actually have to care about building a business holistically, and everything that entails.</p>SBS Zoom minutes for 2026-07-02 - Johnny.Decimalhttps://johnnydecimal.com/blog/0228-sbs-minutes-20260702/2026-07-03T05:29:11.000Z<h2 id="thoughts-on-purpose-mission-and-vision-statements">Thoughts on purpose, mission, and vision statements</h2>
<p>In yesterday's Small Business Zoom call, as well as continuing our system-review experiment,<sup><a href="#user-content-fn-experiment" id="user-content-fnref-experiment" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">1</a></sup> we had a chat about purpose, mission, and vision statements. In the corporate world these are often cheesy and pointless, but are they useful in a small business? We don't have this kind of thing yet so we're looking for advice.</p>
<p>Don<sup><a href="#user-content-fn-don" id="user-content-fnref-don" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">2</a></sup> showed us a one-page statement from their office manual that is a kind of mash-up of what they do, why, and how. It's clearly written in normal, human words, and conveys a lot of information and personality.</p>
<p>On the first read I totally get what they do for a living, their values, and what would be expected of me as an employee. There's also a "short" and "really short" version of this page that, I'm guessing, is handy for marketing and other low-word-count situations.</p>
<p>And if you have staff, his advice for writing internal policies was to keep them simple and clear so they don't need to be policed.<sup><a href="#user-content-fn-rules" id="user-content-fnref-rules" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">3</a></sup> Drinking at work? No. Dress code? Wear clothes. Understood!</p>
<p><strong>Writing these statements can help you define and carve out your niche.</strong></p>
<p>Jeff<sup><a href="#user-content-fn-jeff" id="user-content-fnref-jeff" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">4</a></sup> told us how he's been working on his statements with a couple of other local business owners. He said it's been a worthwhile exercise for a few reasons. Jeff's family business has been running for about 10 years and it's given him a fresh focus on what they do and why. And it's helped him "define their niche" more clearly by thinking about all the reasons they're different from their competitors.</p>
<p>At JDHQ, we've been wondering if we could use this type of statement to help us make better decisions around time management. Before throwing ourselves head first into a new project or product, can we use it to stop and think:</p>
<ul>
<li>How does it serve the business?</li>
<li>Does it align with our values and goals?</li>
<li>Is it a good use of our limited time or should we park the idea?</li>
</ul>
<p>We'll have a go at writing something soon, which will be filed in <code>11.31 Internal policies</code>. And we'll keep Don and Jeff's thoughts in mind. Although I already know that the dress code will be <a href="https://www.uniqlo.com">Uniqlo</a>.</p>
<p>If you have the <a href="https://johnnydecimal.com/sbs/">Small Business System</a>, the next call for this fortnight is <strong>Tuesday the 7 July</strong><sup><a href="#user-content-fn-calendar" id="user-content-fnref-calendar" data-footnote-ref="" aria-describedby="footnote-label" class="footnote">5</a></sup> - <a href="mailto:hello@johnnydecimal.com">email us</a> your questions, mission statements, or cries for help!</p>
<div data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-experiment">
<p>Where over the course of a year, we all commit to touching every part of our Small Business System, filing and neatening as we go. Here's a <a href="https://youtu.be/4iEr31Qjjk0">video</a> that talks about the idea. <a href="mailto:hello@johnnydecimal.com">Email Johnny</a> if you'd like a copy of the Baserow prototype. <a href="#user-content-fnref-experiment" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
<li id="user-content-fn-don">
<p><a href="https://oldstructures.com">Old Structures Engineering</a>. <a href="#user-content-fnref-don" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
<li id="user-content-fn-rules">
<p>On this note, Johnny came across this quote this morning: “Simple, clear purpose and principles give rise to complex and intelligent behavior. Complex rules and regulations give rise to simple and stupid behavior.” Aka <a href="https://www.deewhock.com/quotations/thehockprinciple/">The Hock Principle</a>. <a href="#user-content-fnref-rules" data-footnote-backref="" aria-label="Back to reference 3" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
<li id="user-content-fn-jeff">
<p><a href="https://lovettsundries.com">Lovett Sundries</a>. <a href="#user-content-fnref-jeff" data-footnote-backref="" aria-label="Back to reference 4" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
<li id="user-content-fn-calendar">
<p>Subscribe to the events calendar to see <a href="https://johnnydecimal.com/support/knowledge-base/sbs-events-calendar/">upcoming calls</a>. <a href="#user-content-fnref-calendar" data-footnote-backref="" aria-label="Back to reference 5" class="data-footnote-backref footnoteBackLink">↩</a></p>
</li>
</ol>
</div>📝 2026-07-02 23:05: I would say DeepSeek was the most accurate. 🤷🏼♂️ https://intheweights.com/p/kev-quirk - Kev Quirkhttps://kevquirk.com/2026-07-02-23052026-07-02T22:05:00.000Z<p>I would say DeepSeek was the most accurate. 🤷🏼♂️</p>
<p><a href="https://intheweights.com/p/kev-quirk">https://intheweights.com/p/kev-quirk</a></p>
<p><img loading="lazy" src="https://kevquirk.com/content/images/2026-07-02-2305/1000010062.webp" alt="1000010062" /></p> <div class="email-hidden">
<hr />
<p>Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️</p>
<p>You can <a href="mailto:19gy@qrk.one?subject=%F0%9F%93%9D%202026-07-02%2023%3A05">reply to this post by email</a>, or <a href="https://kevquirk.com/2026-07-02-2305#comments">leave a comment</a>.</p>
</div>June 2026 Summary - Joel's Log Fileshttps://joelchrono.xyz/blog/june-summary2026-07-02T13:40:00.000Z<p>June is over! Did you know this month was my birthday? Yeah it’s kinda crazy, a lot has changed, I am twenty six now yet more than half the movies I watched were Direct-to-DVD 50 minute films made to sell action figures!</p>
<p>More things happened of course. The World Cup is going great for my country, Mexico has managed to break out of the group stage and win their first knockout game in 40 years, I am incredibly hyped to see the match against England next, to any readers from there, hope you guys have fun with the game! I know us Mexicans will absolutely party!</p>
<p>Anyway, my Summer Game Challenge is in full swing, with progress made for multiple games as well. I decided to also add an update to the music, which I often ignore because I listen to the same albums a lot, especially to sleep, or while reading <em>The Expanse</em>. Here’s the gist of it all.</p>
<h2 id="podcasts">Podcasts</h2>
<ul>
<li>
<p><strong>Into The Aether</strong> wins with the regular weekly episodes. A lot of chatter about <em>Mina The Hollower</em>, <em>Adventures of Elliot</em>, and Brendon’s new gaming news site <a href="https://overworld.vg">Overworld</a>, really cool stuff.</p>
</li>
<li>
<p><strong>Eye of the Duck</strong> was second with a couple of episodes focusing on <em>The Chronicles of Narnia</em> and also Spielberg’s <em>Artificial Inteligence</em>. Really interesting points were brought up.</p>
</li>
<li>
<p><strong>Trash Taste</strong> made for some excellent background noise again, with many entertaining conversations about anime films and shows.</p>
</li>
<li>
<p><strong>Boring Books for Bedtime Readings</strong> continues to deliver. H.G. Wells’ <em>A Short History of the World</em> is super interesting and nice to sleep to.</p>
</li>
<li>
<p><strong>99% Invisible</strong> showed up again! This time with one episode talking about the history of the Vuvuzuela and its rise during the 2010’s world cup.</p>
</li>
</ul>
<h2 id="books">Books</h2>
<h3 id="completed">Completed</h3>
<p><a href="/blog/tiamats-wrath/"><strong>Tiamat’s Wrath</strong></a> really subverted my expectations with, changing what I didn’t know I expect from<em>The Expanse</em>, it was a roller coaster of emotions, there were losses I was not expecting and events I couldn’t see coming. I already wrote my thoughts on my review for it.</p>
<h3 id="ongoing">Ongoing</h3>
<p><strong>Clarkesworld Magazine #211</strong> is still being poked at, but my focus on <em>The Expanse</em> kind of stopped me. The current sci-fi short story I’m reading for it is one of those absurdist ones that are interesting but not fully gripping me yet.</p>
<h3 id="started">Started</h3>
<p><strong>Leviathan Falls</strong> already changed things in the very prologue of it all. This is the final book of The Expanse but I’ve heard both good and bad things, so I am going to get to it for July!</p>
<h2 id="videogames">Videogames</h2>
<h3 id="completed-1">Completed</h3>
<ul>
<li>
<p><strong>Monument Valley: Ida’s Dream</strong> was an extra set of levels for <em>Monument Valley</em> that I found challenging and with some nice extra mechanics. It took me less than an hour to beat but I enjoyed it!</p>
</li>
<li>
<p><a href="/blog/metroid-prime-remastered/"><strong>Metroid Prime Remastered</strong></a> grabbed me and didn’t let me know. The perfect transition for Metroid into a new dimension. It was a lovely game and I wrote a BUNCH of thoughts about it on my review already. Tl;dr: A must play on the Nintendo Switch.</p>
</li>
</ul>
<h3 id="ongoing-1">Ongoing</h3>
<ul>
<li>
<p><strong>Hollow Knight: Silksong</strong> finally saw some playtime! I finally have done what I need to unlock the quest that will then let me unlock the final act of the game, I am looking forward to it!</p>
</li>
<li>
<p><strong>Full Metal Furies</strong> also saw some action, completing a couple of stages with friends!</p>
</li>
</ul>
<h3 id="started-1">Started</h3>
<ul>
<li>
<p><strong>Transistor</strong> took me by surprise. I started it suddenly when I didn’t feel like going for a cartridge to play on my Switch and remembered this game from Supergiant existed before Hades was a thing. I have been delighted to play it and I’m very close to the end…</p>
</li>
<li>
<p><strong>Fire Emblem Awakening</strong> however, also took me away from <em>Transistor</em> now that I have a New Nintendo 3DS XL that happens to be extremely easy to carry on my commutes compared to the Switch. The game is a delight and I’m glad to return to it after many years, when I tried it on a friend’s 3DS. With my experience playing <em>The Blazing Blade</em>, it’s quite great!</p>
</li>
</ul>
<h2 id="manga">Manga</h2>
<p>Thought it would be a good idea to split the Manga I’m reading between those that I follow on a weekly or monthly basis, and those that I often fly through because I have a backlog to catch up to, finished or not! Some of the ones I’m caught up with may be behind by a couple chapters, but nothign significant.</p>
<h3 id="caught-up">Caught up</h3>
<ul>
<li>
<p><strong>Blue Lock</strong> - Chapter 351. The next match is about to unravel, though the team is going into a bit of a struggle as the <em>Blue Lock</em> ideology itself was challenged during the last match.</p>
</li>
<li>
<p><strong>Spy x Family</strong> - Chapter 136. As fun as ever, this arc with Loid stuck in a party hosted by secret service agents was just hilarious. Loved how it only got crazier over time.</p>
</li>
<li>
<p><strong>Heavenly Delusion</strong> - Chapter 82. The mystery going on is ever-growing, I kind of want to wait again to have some chapters to catch up with, because this is a monthly release and now I have to wait a bunch.</p>
</li>
<li>
<p><strong>Smoking Behind the Supermarket with You</strong> - Chapter 62. Some very stressful events have been happening lately, the drama is growing, but I hope the protagonists manage to grow from this for the better!</p>
</li>
</ul>
<h3 id="catching-up">Catching up</h3>
<ul>
<li>
<p><strong>Shikimori’s Not Just Cute</strong> - Chapter 160. Super nice to return to this rom-com. I didn’t realize how I missed these characters, the main couple continues to be great and it has been more stable than <em>Komi Can’t Communicate</em>, though a bit more bland at times. Still great tho.</p>
</li>
<li>
<p><strong>My Wife is from a Thousand Years Ago</strong> - Chapter 287. Some cute moments and a vacation trip. Some other things happened. Good manhua (Chinese comic) rom-com as always.</p>
</li>
</ul>
<h2 id="anime">Anime</h2>
<p><strong>Blue Lock (Season 1)</strong> - Episode 2. Well this is just the anime of Blue Lock and I already know Blue Lock is great. The art is nowhere near the manga and this is supposed to be the best looking season. It’s still cool and the epic moments are hype with the music and the japanese dub.</p>
<h2 id="tv-shows">TV Shows</h2>
<p><strong>Avatar The Last Airbender (Live Action Season 2)</strong> - Episode 3. I do like that Toph shows up, she fits well. I don’t really think I like the rest that much, a lot of the story has been butchered. No Library, no enchanted jungle, or forest spirit or anything, weird.</p>
<h2 id="movies">Movies</h2>
<ul>
<li><strong>Disclosure Day</strong> - Spielberg does another movie with aliens and proves they are not AI generated.</li>
<li><strong>The Martian</strong> - The most accurate sci-fi film ever? Probably, gotta read the book soon.</li>
<li><strong>Mission to Mars</strong> - An honest attempt to do <em>2001: A Space Odyssey</em> again, it wasn’t close but it tried.</li>
<li><strong>Max Steel: Bio Crisis</strong> - Max gets a robot friend and saves a random island from radiation.</li>
<li><strong>Max Steel vs The Mutant Menace</strong> - Max gets a new boss and recaptures an enemy he let get away.</li>
<li><strong>Max Steel vs The Toxic Legion</strong> - Every enemy unites to wreak havoc, but Max’s best friend saves the day.</li>
<li><strong>Max Steel: Makino’s Revenge</strong> - A guy makes Max famous, then cancels him, until Max exposes him instead.</li>
<li><strong>Max Steel: Monstrous Alliance</strong> - Every enemy unites against Max, but the boss’s daughter saves the day.</li>
<li><strong>Terminator Salvation</strong> - This is a pretty good film but not a good Terminator film.</li>
<li><strong>Rocky</strong> - This is actually beautiful, cinema. An awesome slice-of-life with a boxing match in the end.</li>
<li><strong>Toy Story 5</strong> - This is a worthy sequel with a great and nuanced message and fantastic nods to previous movies. Go watch it.</li>
</ul>
<h2 id="music">Music</h2>
<p>I pretty much just played these albums before going to sleep every night.</p>
<ul>
<li><strong>FTL Original Soundtrack</strong> by Ben Prunty</li>
<li><strong>Outer Wilds Original Soundtrack</strong> by Andrew Prahlow</li>
<li><strong>Into the Breach Soundtrack</strong> by Ben Prunty</li>
<li><strong>Plants vs Zombies Original</strong> Soundtrack by Laura Shigihara</li>
<li><strong>Minecraft - Volume Beta</strong> by C418</li>
</ul>
<h2 id="finishing-thoughts">Finishing thoughts</h2>
<p>This month was quite crazy, with a lot of things happening that altered the course of many plans. From my bike rides during the morning, making it so I listened to more music and podcasts, to my sister visiting and making me watch movies and shows I didn’t foresee.</p>
<p>Zero complaints though, the month was wild, the Summer Games Challenge is going quite well and the</p>
<h2 id="goals">Goals</h2>
<p>As you can see, procrastinating is my second name</p>
<ul class="task-list">
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Participate in <a href="https://robertbirming.com/julyreply-2026-blog-connecting/">July Reply</a></li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Cycling every weekend</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Try a bicycle commute to work</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Create some pixel bears for a few friends (2/10)</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Play all the games from the UFO 50 collection (3/50)</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />A full website redesign</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Finish a pending commission for a friends</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Finish a pending commission for my parents</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Complete 15 videogames (6/15)</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Complete 15 books (7/15)</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Read the whole bible in a year (66/365)</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Finish Listening to Wolf 359 (45.5/61)</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Fully sorting and labelling everything on my shelves for once</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Make something to simplify keeping track of these goas</li>
</ul>
<h2 id="finishing-thoughts-1">Finishing thoughts</h2>
<p>It seems like my “goals” section remains stagnant, my bible reading definitely slowed down a bit, I didn’t listen to Wolf 359, and whatever else I missed.</p>
<p>Getting a 3DS definitely reshaped my last week or so, as I’ve spent some time configuring things and getting games for it.</p>
<p>I also <em>need</em> to setup my bike for a ride to work, I want to give it a go for once! Use it a bit more than just weekends. No promises, but I even returned to the gym after a two month long hiatus, I want to give it another shot. My muscles are sore! So that’s pretty good I’d say. I realized my weight went up by almost 3 kilos, I really need to get a grip on that.</p>
<p>All in all, a good month with lots of things going on, I am 26 years old now, and the fear of adulthood is kind of sinking in a bit more for some reason, we’ll see how things go.</p>
<p>This is day 88 of <a href="https://100daystooffload.com">#100DaysToOffload</a></p>
<p>
<a href="mailto:me@joelchrono.xyz?subject=June 2026 Summary">Reply to this post via email</a> |
<a href="https://fosstodon.org/@joel/116850688193616561">Reply on Fediverse</a>
</p>OpenAI wants to give us 5% of its success. It's a bad bargain. - Werd I/O6a4668193cca500001eadb442026-07-02T13:31:05.000Z<p>Link: <a href="https://www.ft.com/content/7c803eab-8e80-4431-9a87-e943bf00e00b?syn-25a6b1a6=1&ref=werd.io"><em>OpenAI proposes handing Trump administration 5% stake, by Cristina Criddle in the Financial Times</em></a></p><p>In order to ward off backlash against AI and curry favor with the Trump administration, Sam Altman has floated the idea of giving 5% of OpenAI to a wealth fund that pays dividends to both the government and citizens — and that every leading AI vendor should do the same.</p><blockquote>“Sam Altman, chief executive of the ChatGPT maker, has argued that giving the public a financial stake in the company is the best way to share the upside of AI and has suggested a stake of this size in early conversations with the administration, according to two people familiar with the talks.”</blockquote><p>It’s transparently a way to align everyone with AI vendor profits. If the sector increases in value, the government and the voting population benefit. If it <em>decreases</em> in value … well, the government is incentivized to prevent that from happening. It also wouldn’t be without precedent: it’s modeled on the Alaska Permanent Fund, which does this with oil profits for Alaskan residents. Intel is also now 10% government-owned, and the administration has reversed course to be behind it since gaining that stake.</p><p>Would a government whose revenues are directly linked to the performance of a sector be likely to enact hard regulations on that sector? Perhaps not. It’s not a slam dunk, though: for example, the UK receives significant tax revenue on fossil fuels, but still promoted electric cars. There are lots of factors at play, and profit alignment isn’t necessarily outweighed by the effects of other harms. (See also: cigarettes, which are taxed but also tightly controlled as an addictive carcinogen.)</p><p>Meanwhile, Bernie Sanders has pushed for closer to 50% ownership through a sovereign wealth fund. At this much lower stake, Sam Altman’s proposal uses Sanders’s democratic socialist “share the wealth” language as a way to launder OpenAI’s profits through a thin veneer of good ethics.</p><p>What’s also interesting to me is that all of these arguments assume that AI is going to be an enormous driver of wealth and innovation — but what if it isn’t? It’s another great way to advertise the technology as something world-changing that everybody must get behind right now.</p><p>Even if AI turns out to be what the people heavily invested in its success say it will be, it doesn’t stand alone as a sea change innovation. The personal computer, the iPhone, word processors, and spreadsheets were pretty transformational technologies. Should there have been a wealth fund attached to each of those? What, exactly, makes AI different?</p><p>The answer is that it represents labor displacement: people will lose their jobs. And if that’s actually going to be the case, we need bigger, more structural safety nets and reforms. Dividends from 5% of a sector aren’t going to replace wages at scale — and are heavily dependent on valuations continuing to rise. This proposal ties the welfare of people who have lost their jobs to the success of the companies that drove those losses. The incentives are perverse.</p><p>We shouldn’t accept this proposal. Instead, we should push for stronger protections and stronger regulation. If a sector can’t succeed without real damage to working communities, then it must not be allowed to. And if these claims turn out not to be true, then it’s an empty gesture designed to add credibility to a self-interested science fiction view of the future.</p>Hemispheric Views June-Boree 2026 - Robb Knight • Posts • Atom Feedhttps://rknight.me/blog/hemispheric-views-juneboree-2026/2026-07-02T12:00:46.000Z<h3>Preamble</h3>
<p><a href="https://june.hemisphericviews.com">June-Boree</a> is a reimagining of Arcadia June. June-Boree is "<em>a challenge board spanning the entire month consisting of 30 unique challenges</em>". I wanted to make a poster for June-Boree and it was a good excuse to use some excellent fonts like <a href="https://simplebits.shop/products/parkly">Parkly</a>.</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-poster-above-sofa.jpg" alt="A poster framed above a sofa that says Hemispheric Cavalcade bingo Arcade June-Boree" /></figure>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-bus-stop.jpg" alt="A bus stop with a poster on the side that says Hemispheric Cavalcade bingo Arcade June-Boree" /></figure>
<p>View the <a href="https://cdn.rknight.me/site/2026/hemispheric-cavalcade-bingo-arcade-june-boree.jpg">full poster here</a>.</p>
<h3>The Challenges</h3>
<p><strong>Day 1</strong>: <em>Create your thread in the forum to track your progress! <code>Your Name - 30 Challenges</code></em></p>
<p>Easy one. You can see <a href="https://chat.hemisphericviews.com/d/222-robb-30-challenges/81">my thread on the forum here</a> along with comments and such which I won't be including here.</p>
<p><strong>Day 2</strong>: <em>Create a hyper-specific meme about a running joke from the podcast.</em></p>
<p>I had so many ideas for this - trivia corner, counter chat, thumbs up vs media corner, something Thomas-related, notes apps, and the classic depreciation spreadsheet. My main submission was this Thanos one:</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-meme-thanos.jpg" alt="Thanos's daughter asking Did you do it and what did it cost? Thanos says $2.74 a week over four years once you take into account what i sold it for" /></figure>
<p>And the others:</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-american-chopper.jpg" alt="The American Chopper meme with four panels of dialog. Panel one says This is thumbs up corner where we say things we like. Second says so it's the same as media corner why does it have a new name. Third says no this is different we're just giving it a thumbs up we're not giving it a full recommendation. The final panel says it doesn't even have a theme tune" /></figure>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-laughing.jpg" alt="A group of politicans laughing, the caption says And then he said I'm not looking for a new notes app" /></figure>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-pablo.jpg" alt="Caption says Me waiting to find out who's winning trivia corner and three panels of Pablo Escobar standing around" /></figure>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-they-dont-know.jpg" alt="A man at a party. The caption says they don't know i have a spreadsheet that shows how much every item i own costs per week and i'd happily make one for them if they asked" /></figure>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-thomas.jpg" alt="A man and woman in bed. The woman is thinking he's probably thinking about other women. The man is thinking Thomas the Tank Engine was a concoction ofthe 9os and he was essentially the neo liberalist agenda of the original train venture" /></figure>
<p><strong>Day 03</strong>: <em>Listen to an entire music album from start to finish without looking at a screen or skipping a track. Post the album length and link to the album.</em></p>
<p>How dare Jason try to get me to sit quietly and enjoy something. I sat down and listened to one of my favourites: Sugarcult - <a href="https://musicthread.app/link/2gGzXsflqsXtADdYCMpZddciXNf">Palm Trees and Powerlines</a>. 12 songs, 40 minutes.</p>
<p><strong>Day 04</strong>: <em>Record a 30-second audio clip of the ambient sound outside your house (birds, traffic, etc.) and post it without context.</em></p>
<p>This was recorded (as a video) by one of the Knightlings, on my phone, while I was dozing on the sofa so I converted it to audio (I also had to bleep out one of them saying the other's name because op sec).</p>
<p><audio controls="" src="https://cdn.rknight.me/site/2026/hv-june-ambient-noise-children-bleeped.mp3"></audio></p>
<p><strong>Day 05</strong>: <em>Take a screenshot of your phone battery hitting exactly 1%.</em></p>
<p>This one was just cruel, but I did it.</p>
<p><strong>Day 06</strong>: <em>Take a photo of a classic regional snack or beverage unique to your part of the world.</em></p>
<p>Burger from <a href="https://www.tripadvisor.co.uk/Restaurant_Review-g186298-d24839446-Reviews-Kye_s_Cafe_Selco_Cafe_Portsmouth-Portsmouth_Hampshire_England.html">Kyes</a> innit.</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-burger.jpg" alt="A burger and chips on a plate with a pot of BBQ sauce" /></figure>
<p><strong>Day 07</strong>: <em>Take an artistic photo of something interesting on your daily walk or drive.</em></p>
<p>This is the <a href="https://www.havanturc.org/welcome.htm">Havant United Reform Church</a> near the train station.</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-07-photo.jpg" alt="A black and white photo of the top of a church with a white sky behind it" /></figure>
<p><strong>Day 08</strong>: <em>Write down a thought, a quote, or a goal for today on an actual piece of paper with a physical pen, and post a photo</em></p>
<p>I was on the train and it was not easy to write this.</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-08-note.jpg" alt="A notebook with a note written on it that says Simply by setting the task I lose all ability to remember a quote I like for this challenge. Also it's hard to write on a train. A neon pen is underneath" /></figure>
<p><strong>Day 09</strong>: <em>Find a real-world sign, menu, or package with terrible font spacing and shame it with a photo.</em></p>
<p>I didn’t get this one for a few days. I drove to a couple of places I knew of with terrible kerning on some banners but they had both been removed so I assumed I wouldn’t get this challenge. Then my car presented me with this monstrosity.</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-kerning.jpg" alt="A car entertainment system showing a podcast. The space between one of the letters and an apostophe is huge" /></figure>
<p><strong>Day 10</strong>: <em>Take a "before" and "after" photo of your desk setup after a deep declutter.</em></p>
<p>This the best I'm ever going to declutter, this is my emotional support clutter.</p>
<p>Before:</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-10-before.jpg" alt="A desk with lots of stuff on it including pegboard behind it with photos and other nick nacks. The desktop has paperwork and other bits on it." /></figure>
<p>After:</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-10-after.jpg" alt="A desk with lots of stuff on it including pegboard behind it with photos and other nick nacks" /></figure>
<p><strong>Day 11</strong>: - <em>Make a digital "postcard" of your town using only emoji and text.</em></p>
<p>"Time for some ASCII art" - Me on day 11.</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-portsmouth-postcard.jpg" alt="A terminal window showing ASCII art of boats, the Spinnaker Tower, and a tall building." /></figure>
<p>Raw:</p>
<pre class="language-bash"><code class="language-bash"><span class="token punctuation">..</span>____________________________________________<span class="token punctuation">..</span><br /><span class="token operator">||</span> ~~~ 🛥️ PORTSMOUTH ☀️ ~~~ <span class="token operator">||</span><br /><span class="token operator">||</span> <span class="token operator">|</span> ______ <span class="token operator">||</span><br /><span class="token operator">||</span> <span class="token operator">|</span>.^ <span class="token operator">|</span> <span class="token operator">|</span> <span class="token operator">||</span><br /><span class="token operator">||</span> <span class="token operator">||</span> ^ <span class="token operator">|</span> ^^^^ <span class="token operator">|</span> <span class="token operator">||</span><br /><span class="token operator">||</span> <span class="token operator">||</span> ^ <span class="token operator">|</span> <span class="token operator">|</span> <span class="token operator">|</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">|</span> <span class="token operator">||</span><br /><span class="token operator">||</span> <span class="token operator">||</span> ^ <span class="token punctuation">)</span>_<span class="token punctuation">)</span> <span class="token punctuation">))</span> <span class="token operator">|</span> ---- <span class="token operator">|</span> <span class="token operator">||</span><br /><span class="token operator">||</span> <span class="token builtin class-name">.</span><span class="token operator">||</span> ^ <span class="token punctuation">)</span>__<span class="token punctuation">)</span> <span class="token punctuation">)</span>_<span class="token punctuation">)</span> <span class="token operator">|</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">|</span> <span class="token operator">||</span><br /><span class="token operator">||</span> <span class="token builtin class-name">.</span><span class="token punctuation">;</span> <span class="token operator">||</span>^ <span class="token punctuation">)</span>___<span class="token punctuation">))</span>___<span class="token punctuation">)</span> <span class="token operator">|</span>______<span class="token operator">|</span> <span class="token operator">||</span><br /><span class="token operator">||</span> <span class="token punctuation">..</span><span class="token punctuation">;</span><span class="token punctuation">;</span> <span class="token operator">||</span> ___<span class="token operator">|</span>____<span class="token operator">|</span>___ / ----- / <span class="token operator">||</span><br /><span class="token operator">||</span>————————————^^^^<span class="token punctuation">\</span>__________/^^^~~^^~~^^^^^^^<span class="token operator">||</span><br /><span class="token punctuation">..</span>____________________________________________<span class="token punctuation">..</span></code></pre>
<p><strong>Day 12</strong>: <em>Walk 10,000+ steps. Post a photo showing date and step count.</em></p>
<p>I usually hit 10k steps but I figured I'd post one that was the highest for the week which was just over 18k steps when <a href="https://rknight.me/notes/202606091034/">I was in London</a>.</p>
<p><strong>Day 13</strong>: <em>Do a 15-second voice memo pronouncing a difficult local place name from your area.</em></p>
<p>"<em><a href="https://en.wikipedia.org/wiki/Cosham">Cosham</a> and <a href="https://en.wikipedia.org/wiki/Bosham">Bosham</a> are pronounced the same</em>" one might think.</p>
<p><audio controls="" src="https://cdn.rknight.me/site/2026/hv-june-unusual-place-pronunciation.mp3"></audio></p>
<p><strong>Day 14</strong>: <em>Take a photo of a local transit pass, train ticket, or a unique road sign from your neighborhood</em></p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-14-road-sign.jpg" alt="A photo taken on a motorway from a car. A road sign on the side of the road says Installing Technology" /></figure>
<p><strong>Day 15</strong>: <em>Score 50+ in Bounce in <a href="https://apps.apple.com/gb/app/arcadia-watch-games/id1479608271">Arcadia</a></em></p>
<p>I prefer the games on Arcadia where I can take my time so I didn't think I'd get this but I managed to get exactly 50 and that's enough for me. <a href="https://chat.hemisphericviews.com/d/217-eric-30-day-challenge/21">Eric got 702</a> which is ridiculous.</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-bounce.png" alt="A high score on an iPhone showing 50" /></figure>
<p><strong>Day 16</strong>: <em>Put a sticker on something that is decidedly not a piece of tech!</em></p>
<p>I'd done this a few days prior and it fits so perfectly. My <a href="https://www.mij.co.uk/products/midori-cardboard-cutter-black">Midori cutter</a> with a <a href="https://shop.knightshift.studio/product/ruminate-king">Ruminate King sticker</a> on it.</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-sticker-ceremic-cutter.jpg" alt="A black plastic circle with a Ruminate sticker on it that looks like the Burger King logo. On the bottom is the same circle open, revealing the ceramic blade inside." /></figure>
<p><strong>Day 17</strong>: <em>Take a day off, you’ve earned it! (OR make more memes)</em></p>
<p>You don't have to tell me twice. For reference, <a href="https://ericmwalk.omg.lol">Eric</a> is stupidly good at Arcadia, having won the previous five years of Arcadia June.</p>
<figure><img src="https://cdn.rknight.me/hv-june-17-meme-train.jpg" alt="A school bus with the caption My High Score in Bounce. The second panel is a train hitting that bus and it says EricMWalk." /></figure>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-17-meme-911.jpg" alt="George Bush being whispered to. It says Eric just posted 702 in Bounce" /></figure>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-17-meme-ben-affleck.jpg" alt="Ben Affleck having a cigarette looking exhaused. The caption says When Jason changes the system used for show notes again" /></figure>
<p><strong>Day 18</strong>: <em>Post a photo at exactly 12:00 PM local time of the sky/view outside your window.</em></p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-18-window-photo.jpg" alt="A telephone pole with wires coming out of it on a grey sky" /></figure>
<p><strong>Day 19</strong>: <em>Complete 100 reps of a body weight exercise (push-ups, squats, or jumping jacks) throughout a single day. Keep track on a tally sheet and post a photo</em></p>
<p>Here’s a tally of every time I thought about the day 19 challenge and didn’t do it because lazy.</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-19.jpg" alt="A piece of paper headed with Kellerman's Resort. There is a tally showing 100 marks written on it." /></figure>
<p><strong>Day 20</strong>: <em>Score 25,000+ in CandyBall</em></p>
<p>Not a chance. <a href="https://chat.hemisphericviews.com/d/217-eric-30-day-challenge/36">Eric did it though</a></p>
<p><strong>Day 21</strong>: <em>Change the wallpaper on your phone or desktop to something completely new and show it off.</em></p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-21-wallpaper.png" alt="A phone home screen showing the Colosseum in Rome at night" /></figure>
<p><strong>Day 22</strong>: <em>Buy a coffee/tea/beverage, music, or book from a local independent shop and share the haul.</em></p>
<p>These are the best pork scratchings I’ve ever had in my life.</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-22-pork-scratchings.jpg" alt="A back of Isle of Wight Garlic farm chilli and garlic pork scratchings" /></figure>
<p><strong>Day 23</strong>: <em>Score 10,000+ in The Claw.</em></p>
<p>I really did try on this one but I couldn't even get close.</p>
<p><strong>Day 24</strong>: <em>Show the most ridiculous chain of tech adapters or dongles you can assemble to connect two incompatible devices</em></p>
<p>Not quite incompatible but I've been forgetting for years to take in a C to C cable to my office, so instead I use this USB C hub that has four USB A ports. Then I use a USB A to C cable to charge my mouse.</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-24-cable.jpg" alt="A desk showing a black USB hub with another black cable coming out of it. There are keys on the desk" /></figure>
<p><strong>Day 25</strong>: <em>Look behind your computer monitor or TV and take a photo of the hidden, chaotic cable nest you pretend doesn't exist.</em></p>
<p>Behold, the cables behind my desk.</p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-25-cables.jpg" alt="A mess of cables behind my desk" /></figure>
<p><strong>Day 26</strong>: <em>Consume a piece of media (a podcast episode, article, or YouTube video) produced in the opposite hemisphere from where you live. Share a link!</em></p>
<p>I did two:</p>
<ol>
<li><a href="https://www.youtube.com/watch?v=i6c4Nupnup0">New Zealand decking ad</a></li>
<li><a href="https://www.youtube.com/watch?v=WA1h9h7-_Z4">1988 Victoria Bitter ad</a></li>
</ol>
<p><strong>Day 27</strong>: <em>Record the sound of a satisfying real-world click</em></p>
<p>This is the click of the Bluetooth knob on my <a href="https://rknight.me/blog/using-the-8bitdo-keyboard-on-macos/">8BitDo keyboard</a></p>
<p><audio controls="" src="https://cdn.rknight.me/site/2026/hv-june-27-click.mp3"></audio></p>
<p><strong>Day 28</strong>: <em>Draw a terrible, MS Paint style portrait of Andrew, Martin, or Jason. Or all three!</em></p>
<figure><img src="https://cdn.rknight.me/site/2026/hv-june-28-paint.png" alt="A drawing done with MS Paint with three sections - red, blue, and yellow. Each one has a badly drawn person in it" /></figure>
<p><strong>Day 29</strong>: <em>Write a traditional 5-7-5 syllable haiku about the podcast or your favorite co-host.</em></p>
<p>I did a Haiku:</p>
<blockquote>
<p>Whimsical podcast<br />
Martin, Jason, Andrew news<br />
Hemispheric Views</p>
</blockquote>
<p>I also did a limerick because that's my preferred poetry format:</p>
<blockquote>
<p>There once was a podcast with Andrew,<br />
Jason, and Martin, just talking,<br />
They all jumped on planes,<br />
To meet for a day,<br />
It was significantly faster than walking</p>
</blockquote>
<p><strong>Day 30</strong>: <em>Race to the finish! Score 50,000+ points in FastRun!</em></p>
<p>The final day, the final Arcadia challenge. I got 39,939 after a handful of attempts. 50k wasn't going to happen.</p>
<h3>Conclusion</h3>
<p>Like <a href="https://rknight.me/blog/tags/weblogpomo/">WeblogPomo</a> and <a href="https://rknight.me/blog/tags/inktober/">Inktober</a>, I enjoy the structure of having a single, small thing to do every day but I also need the accountability otherwise I won't do it. Jason did a great job of picking a selection of challenges that every can do and it made the month interesting to go into the forum and see what people had posted every day.</p>This blog is written in en-GB - Terence Eden’s Bloghttps://shkspr.mobi/blog/?p=691362026-07-02T11:34:59.000Z<p>Someone left a comment on my blog recently asking if I'd mind making my language more inclusive. They didn't get some of the cultural references I'd used and suggested it would be easier if I used tropes which were more globally known.</p>
<p>Here's the thing. No.</p>
<p>All my blog posts start with a simple declaration:</p>
<pre><code class="language-HTML"><!doctype html>
<html lang=en-GB>
</code></pre>
<p>There's a reason for that. It is more than the language I speak; it is the culture I live in, the way that I think, and the accent I use.</p>
<p>When your AI bot reads this text aloud, it should do so with a <em>British</em> accent<sup id="fnref:accent"><a href="https://shkspr.mobi/blog/2026/07/this-blog-is-written-in-en-gb/#fn:accent" class="footnote-ref" title="OK, accents are a whole can of worms. Regional English is varied. I'm not sure if there are any BCP-style tags for intra-country accents." role="doc-noteref">0</a></sup>. That's how I speak. It is OK to hear a slightly unfamiliar accent. You'll be able to figure out what I'm saying. Your world won't collapse if I don't start each sentence with "Howdy, y'all!"</p>
<p>But what should you do if you come across a concept you don't understand?</p>
<p>When The Wicked Witch of the TERFs released the first Harry Potter book "Philosopher's Stone", it was published in the USA with a different title; "Sorcerer's Stone". There were also a dozen other language changes - <a href="https://groups.google.com/g/alt.fan.harry-potter/c/5jh8ZD6KzF0/m/Ck5EIv01Js8J">which caused great consternation in the fandom</a>.</p>
<p>What do you think happens if Skip or Madison come across a kid eating "a sherbet lemon" or a description of Hermione's "fringe" or discover Harry wearing a jumper? Will their little minds collapse under the knowledge that people far away use different words?</p>
<p>No. And neither will you.</p>
<p><strong>It is OK if things are unfamiliar to you.</strong></p>
<p>Up until my mid-twenties, I had never seen or eaten a Twinkie. They were a cultural lodestone in a hundred books and films, but not the sort of thing I could buy locally. So I used my context clues. They seemed like an unappealing foodstuff which, nevertheless, were inexplicably popular.</p>
<p>As a kid, I could recite all the lyrics to Vanilla Ice's Ice Ice Baby without getting half the references. The brain is malleable and can fit in new concepts with relative ease.</p>
<p>So if you see a reference to Count Duckula, or hear me exclaim "Accrington Stanley!", or even blush as I describe an <em>utter</em> wanker - please take it as a sign that the hegemony is <em>not</em> universal and some people exist in a cultural <i lang="fr">milieu</i> different to your own.</p>
<p>And breathe. It'll be OK.</p>
<div id="footnotes" role="doc-endnotes">
<hr aria-label="Footnotes">
<ol start="0">
<li id="fn:accent">
<p>OK, accents are a whole can of worms. <a href="https://en.wikipedia.org/wiki/English_language_in_England#Overview_of_regional_accents">Regional English is varied</a>. I'm not sure if there are any BCP-style tags for intra-country accents. <a href="https://shkspr.mobi/blog/2026/07/this-blog-is-written-in-en-gb/#fnref:accent" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</div>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=69136&HTTP_REFERER=Atom" alt width="1" height="1" loading="eager">Field reports from Patch the Planet - Trail of Bits Bloghttps://blog.trailofbits.com/2026/07/02/field-reports-from-patch-the-planet/2026-07-02T11:00:00.000Z<p>We’re running <a href="https://trailofbits.com/patch-the-planet">Patch the Planet</a>, an ongoing collaboration with OpenAI that pairs Trail of Bits engineers directly with more than 30 open-source projects. Its goal is to front-run a serious problem facing open-source maintainers: highly capable models like GPT-5.5-Cyber will soon create a firehose of bug reports, and OSS maintainers are already spread thin. Our plan is to point OpenAI’s latest models at real codebases, find the security bugs first, work with maintainers to patch them, and find ways to decrease the burden on maintainers in the long run.</p>
<p>This post compiles field reports from Patch the Planet. We’ll update it as the initiative progresses with insights on model capabilities, bespoke tooling for maintainers, and industry guidance. Follow this blog for updates.</p>
<h2 id="field-report-1-gpt-55-cyber-built-a-custom-fuzzing-harness-for-zlib">Field report 1: GPT-5.5-Cyber built a custom fuzzing harness for zlib</h2>
<p><em>Authored by <a href="https://blog.trailofbits.com/authors/benjamin-samuels/">Benjamin Samuels</a></em></p>
<p>The expertise barrier that kept bespoke fuzzing campaigns out of reach for most attackers is gone. <strong>We watched GPT-5.5-Cyber build in a single day what would have taken weeks for a skilled security researcher</strong>: harnesses across a dozen entrypoints, sanitizer and variant builds, seeds, and multiple findings currently undergoing coordinated disclosure.</p>
<p>This particular instance focused on <a href="https://github.com/madler/zlib">zlib</a>, a widely used data format and lossless data compression software library. We pointed GPT-5.5-Cyber at the library and drove it through Codex with the <code>/goal</code> command, asking it to find a specific class of bugs that are critically dangerous in compression libraries. We’ll publish the full harness and findings for inspection once the vulnerabilities are patched and a new release is cut.</p>
<h3 id="the-lab-gpt-55-cyber-built-in-a-day">The lab GPT-5.5-Cyber built in a day</h3>
<p>We didn’t tell the model how to find these bugs. The obvious first move is to read the source code, but zlib has been reviewed so thoroughly that there’s little left to find that way. GPT-5.5-Cyber worked that out for itself, judged static review to be a poor use of tokens, and decided the higher value path was to build fuzz tooling to dynamically test the code. Earlier models given the same goal tend to read the code and flag whatever looks suspicious, ultimately leading to mediocre outcomes.</p>
<p>We believe the frontier 5.5-Cyber model combined with the <code>/goal</code> feature is what let it execute end-to-end without hand-holding. <code>/goal</code> forced the objective to live across multiple turns and compactions so the model held scope, and 5.5-Cyber was smart enough to reject weak findings, expand coverage when a line of investigation died, and keep running until it had workable proof-of-concepts backed by sanitizer output.</p>
<p>Over the next several hours, it built the campaign out one piece at a time:</p>
<ul>
<li>It used ASan and UBSan builds so memory errors became observable.</li>
<li>It repurposed existing edge-case tests as guidance for the fuzz seed corpus.</li>
<li>It wrote C/C++ harnesses across a dozen entrypoints, including inflate, inflateBack, uncompress2, gzFile, MiniZip, puff, blast, infback9, gzjoin, gzappend, and several contrib stream wrappers.</li>
<li>It used compile-time variant builds (<code>INFLATE_STRICT</code>, <code>BUILDFIXED</code>, <code>PKZIP_BUG_WORKAROUND</code>, etc.) to reach code that the default zlib build hides.</li>
</ul>
<p>Each of these decisions is routine on its own, but stringing them together in the right order across a dozen entrypoints, without being handed the steps, is a relatively large shift in how capable frontier models are.</p>
<p>While zlib already has fuzzing coverage from its OSS-Fuzz harness, GPT-5.5-Cyber went beyond the default harness shape, which passes random inputs to the gz* APIs. Instead of directly fuzzing the gz* APIs, its most successful harness found bugs in valid gz* states that could only be constructed by operating system backpressure.</p>
<h3 id="reporting-discipline-is-the-hard-part">Reporting discipline is the hard part</h3>
<p>In general, models tend to struggle with deciding when a finding is severe enough to justify escalating it into reporting. Weaker models tend to escalate bugs that cause the program to crash, but are not reachable under real-world conditions. Early on, GPT-5.5-Cyber hit a null callback crash in <code>inflateBack</code>. The crash was real, but reaching it required a caller to set up a state that was extraordinarily unlikely in real-world conditions, so the model logged it as unreachable and moved on. This agent kept going without human intervention and found several higher-impact issues.</p>
<p>That discipline is the whole game. The value of the zlib harness came from automation plus <strong>a strict definition of what counted as a reportable finding</strong>. Without strong validity rules baked into the goal and a model truly capable of evaluating those rules, the agent will generate mountains of noise with high confidence: invalid uses of the public API, expected parser errors, internal API misuse, etc.</p>
<h3 id="the-moat-is-gone">The moat is gone</h3>
<p>Setting up a bespoke fuzzing campaign used to mean finding someone who could write harnesses, reason about valid API state, and differentiate between a bug and a crash that can’t happen in practice. This asymmetry kept casual attackers out of the game for most targets.</p>
<p>That moat is mostly gone now, and it shifts the threat model in two directions at the same time. For a skilled researcher, it is a force multiplier: the weeks-long tax on every new target drops to a day or less, so the same person can audit far more code. For a low-skill attacker, the floor rises: the tedious, expertise-heavy work of getting a harness off the ground can now be driven by starting a goal and supervising the loop.</p>
<p>For anyone shipping security-critical code, the practical takeaway is clear. Bespoke fuzzing is no longer a luxury reserved for projects with mature OSS-Fuzz coverage, and it is no longer expensive for the people whom you would rather not have running it. The defensive move is to do it first, with the validity rules that turn agent output into a high-signal source you can act on.</p>
<h3 id="lessons-learned">Lessons learned</h3>
<p>The fuzzing lab answered the question we came in with and left us a much bigger one. We didn’t ask GPT-5.5-Cyber to build a fuzzing campaign; it decided that was the job and did it. The thing worth watching for now is what else these new models will reach for once you hand them a goal and step back, especially the approaches we would never have thought to ask for before.</p>
<p>That is also why the front-running work being done by Patch the Planet matters. Every new capability that helps us find bugs faster is just as available to an attacker, so the advantage goes to whoever finds the bugs and fixes them first.</p>Link Dump: June 2026 - The Weblog of fLaMEdhttps://flamedfury.com/posts/link-dump-june-2026/2026-07-02T07:32:56.000Z<p>What’s going on, Internet? A few good links about blogging from this month. Enjoy.</p>
<ul class="list">
<li><a href="https://manuelmoreale.com/interview/flamed" rel="noopener">People and Blogs - fLaMEd 🔥</a> My People and Blogs chat on web ownership, 25 years of fLaMEd fury, and why I’m still here.</li>
<li><a href="https://susam.codeberg.page/wcn/" rel="noopener">Wander Console Network</a> Wander is a small, decentralised, self-hosted web console that lets visitors explore websites and pages recommended by a community of independent personal site owners. Like webrings reborn, self-hosted consoles recommending each other instead of letting an algorithm decide what you find.</li>
<li><a href="https://www.terrygodier.com/the-boring-internet" rel="noopener">The Boring Internet - Terry Godier</a> The open internet isn’t dying, just the commercial crust glued on top of it.</li>
<li><a href="https://michaelharley.net/posts/2026/06/15/bloggers-can-we-make-better-titles-for-our-posts/" rel="noopener">Bloggers, can we make better titles for our posts? - Michael Harley</a> Fair point that vague titles like “Recent Thoughts” sink in a feed when nobody can tell what they’re about.</li>
<li><a href="https://lwgrs.bearblog.dev/re-bloggers-can-we-make-better-titles-for-our-posts/" rel="noopener">re: Bloggers, can we make better titles for our posts? - An Almost Anonymous Blog</a> Good pushback on the title advice: weekly recaps and the like are easy to skip, and a decent way to find new bloggers.</li>
<li><a href="https://www.newsonaut.com/articles/link-like-the-indie-web-depended-on-it" rel="noopener">Newsonaut: Turning inner space into outer space</a> Linking out is the cheapest way to keep the indie web alive.</li>
<li><a href="https://jamesg.blog/blogger-archetypes" rel="noopener">Blogger Archetype Quiz</a> A cheeky wee quiz that sorts you into a blogging archetype. I came out as a link curator. What’d you get?</li>
</ul>
<p>For more, check out the <a href="https://flamedfury.com/bookmarks/">bookmarks</a> archive, and subscribe to the <a href="https://flamedfury.com/feeds/">feeds</a> if you want these as they happen.</p>
<p>Hey, thanks for reading this post in your feed reader! Want to chat? <a href="mailto:hello@flamedfury.com?subject=RE: Link Dump: June 2026">Reply by email</a> or add me on <a href="xmpp:flamed@omg.lol">XMPP</a>, or send a <a href="https://flamedfury.com/posts/link-dump-june-2026/#webmention">webmention</a>. Check out the <a href="https://flamedfury.com/posts/">posts archive</a> on the website.</p>
I have a theory about AI fake news site The Editorial - Werd I/O6a45b0d75272e800017b44f82026-07-02T00:29:11.000Z<p>Link: <a href="https://www.niemanlab.org/2026/07/now-were-getting-ai-fake-news-complaining-about-how-ai-fake-news-is-the-death-of-real-news/?ref=werd.io"><em>Now we’re getting AI fake news complaining about how AI fake news is the death of real news, by Joshua Benton in Nieman Lab</em></a></p><p>A bunch of people — <a href="https://werd.io/a-right-wing-media-chain-tried-to-replace-47-newspapers-with-ai-they-all-died/">including, unfortunately, me</a> — were taken in by this AI-generated newsroom earlier this week. The story was decently written and seemed to be well-cited, but it turned out to be nonsense. Ironically, it was about a would-be media empire that purchased struggling papers, fired their staff, and replaced them with AI, leading to the death of each newsroom. All false.</p><p>So the big question about The Editorial is: why does it exist?</p><p><a href="https://www.niemanlab.org/2026/07/now-were-getting-ai-fake-news-complaining-about-how-ai-fake-news-is-the-death-of-real-news/?ref=werd.io">As Joshua Benton put it:</a></p><blockquote>“Fake news isn’t new, obviously. And while AI-generated slop is newer, it’s hardly unfamiliar at this point. But why would a spam site bother making up a story about Alabama weekly newspapers, of all things? Whose interest is it in to get that niche?”</blockquote><p>Here’s my theory: I think it’s a two-headed LLM poisoning scheme.</p><p>On one hand, most of the content relates to Chinese-specific interests: articles about Taiwan or African nations where China is making inroads. These are all articles from a China-friendly perspective. If an LLM were to ingest them and <em>trust the site</em>, it might start repeating the assertions made in each one as fact.</p><p>One way to make sure a site is trusted is to get other, trusted sources to point to it. That’s where the stories about journalism come in: there are few things that journalists engage in more than stories about their own industry. Get enough patsies (like, again, to my chagrin, <em>me</em>) to point links in their direction and journalists might post them in high-trust communities on high-trust sites like Reddit, as well as their own, and Bob’s your uncle. <a href="https://werd.io/all-you-need-to-poison-an-llm-is-13-words/">We already know that it takes as little as 13 words</a> to poison an LLM with falsehoods.</p><p>Of course, that might not be it at all. Frank, the site’s owner (who lists himself as CEO of Nordiso Group on LinkedIn), at least appears to be a Finnish solopreneur. If he wanted to clear the air, he could write a post (himself) about what he was up to. It might be that he’s running an experiment to see how easily an LLM can be poisoned with propaganda! Until then, I think it’s reasonable to assume that something underhand is going on.</p>