<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Duncan Hunter</title>
    <link>https://www.duncanhunter.com.au/</link>
    <description>I make large frontends behave and lead the teams that build them. A lot of that work now runs through AI agents, with me keeping the judgement. This is where I write down what works, what breaks and what I would do again.</description>
    <language>en-AU</language>
    <lastBuildDate>Mon, 26 Oct 2026 09:40:52 GMT</lastBuildDate>
    <atom:link href="https://www.duncanhunter.com.au/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>A blog that builds the next blog post</title>
      <link>https://www.duncanhunter.com.au/writing/a-blog-that-builds-the-next-post/</link>
      <guid isPermaLink="true">https://www.duncanhunter.com.au/writing/a-blog-that-builds-the-next-post/</guid>
      <pubDate>Mon, 26 Oct 2026 00:00:00 GMT</pubDate>
      <description>I built this site in a day, alongside my eight-hour enterprise job. My own time came to six to ten rounds of five to fifteen minutes, in between runs of thirty …</description>
      <content:encoded><![CDATA[<p>I built this site in a day, alongside my eight-hour enterprise job. My own time came to six to ten rounds of five to fifteen minutes, in between runs of thirty to sixty minutes where the agents implemented things. This post is about how that worked, where it went wrong, and the pipeline that then wrote the post itself.</p>
<p>The site is meant to show two things rather than claim them: design systems and AI. The theme picker in the header is a plain web component driven by a real token pipeline. The breadcrumb prompt is a bit of fun. Both would have taken me weeks by hand.</p>
<h2 id="most-of-the-effort-was-up-front">Most of the effort was up front</h2>
<p>I already had a dark, developer-style blog. The first thing I did was brainstorm with the agent about what I actually wanted, and as soon as I saw a terminal-based option I loved it. I love the terminal and I love themes, so the site has nine. You don&#39;t need hundreds.</p>
<p>I also cut things that added nothing. There was a lab page in the plan; it is built but not in the navigation. There was a &quot;now&quot; page; it went. The agent then went off and found almost everything I had ever made, talks, courses and workshops, with working links. That alone would have taken me forever.</p>
<h2 id="a-spec-a-plan-and-one-outside-review">A spec, a plan, and one outside review</h2>
<p>The agent wrote a spec from the brainstorm, I approved it, and it wrote an implementation plan: sixteen tasks, each with the exact values to use and a list of tests. Before any code existed I had a second model, Codex, review the plan. It found 34 issues.</p>
<p>I did not read all 34. I let it roll, and the plan came back as version two with every one resolved. The rule I hold to is one review cycle, then move. If you let agents review each other indefinitely they loop forever.</p>
<p>What I care about instead is deterministic checks. The design tokens live in JSON and a build step refuses any theme where body text falls under the contrast ratio. A Lighthouse run gates the end of the build. When I ran Lighthouse myself and pasted the results back, the agent fixed the findings: a logo link whose accessible name did not match its visible text, and breadcrumb links distinguished by colour alone.</p>
<h2 id="one-task-one-brief-one-report">One task, one brief, one report</h2>
<p>Each task ran the same way. A controlling agent wrote a brief with the exact requirements, a fresh implementer worked in a git worktree and wrote a report, and a reviewer read the code rather than the report. When a brief and reality disagreed, the controller made a ruling and wrote it in a log with the cost if it was wrong.</p>
<p>One real ruling: the plan had a 3:1 contrast rule for borders, which failed in all nine themes. The controller removed it, because borders are decorative and the accessibility rule is about text, and logged &quot;low-contrast borders, reversible&quot; as the cost. I found out at the next checkpoint and agreed.</p>
<h2 id="what-the-agents-got-wrong">What the agents got wrong</h2>
<p>The build log is honest, so this can be too.</p>
<ul>
<li>A brief said a function keeps an <code>alias</code> field. After alias resolution the field was gone. The very first test failed on it.</li>
<li>A brief marked several colours as known contrast failures. Against the real palette, none failed. The marks had been copied from a test fixture.</li>
<li>The headline typing effect was built with <code>clip-path</code> on an inline element, which hid every wrapped line after the first in Chrome.</li>
<li>The first image run dropped the EXIF orientation, so photos of me shipped sideways and upside down.</li>
</ul>
<p>The pattern: the agents did exactly what the brief said. A wrong brief produces confident wrong code, and the fix each time was a test that pins the real value, not a better prompt.</p>
<h2 id="where-i-overruled-them">Where I overruled them</h2>
<p>The checkpoints are where I earned my keep. In one batch I sent ten corrections: I live in the Blue Mountains, not Melbourne; the product is called Foodzone, with no version number; the prompt had an <code>@</code> in it that made it read like an email address; the lede said &quot;I spent a decade&quot;, which was wrong, because I still do the work. The agents could not have known any of that. Facts, taste and naming are mine. Syntax is theirs.</p>
<h2 id="the-pipeline-that-wrote-this-post">The pipeline that wrote this post</h2>
<p>Every post on this site, this one included, is made the same way the code was.</p>
<ol>
<li>Claude Code writes an interview brief: the purpose, the audience, numbered evidence pulled from the build log or a session transcript, and ten questions that cite it.</li>
<li>I paste the brief into the Claude app, switch to voice, and talk for fifteen to twenty minutes with a slightly sceptical interviewer.</li>
<li>I paste the conversation back. Claude Code drafts the post in my voice. Anything only I can fill is left as a comment that the renderer strips, so a placeholder can never ship.</li>
<li>I edit and approve.</li>
</ol>
<p>For this post I talked for about twenty minutes and pasted back the closing summary. The draft came from that summary and the build log. You can read the brief that produced it <a href="https://github.com/duncanhunter/duncan-hunter-site/blob/main/content/interviews/a-blog-that-builds-the-next-post.brief.md" rel="noopener">on GitHub</a>.</p>
<h2 id="the-point-is-fun">The point is fun</h2>
<p>AI does not have to mean purple slop. A terminal breadcrumb, a token-driven theme picker and a site that sounds like me came out of a day of directing, checking and saying no.</p>
<p>My advice, if you want one of your own: think about what you are passionate about, then just ask the agent. You can even ask it to help you work out what you want.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Building the theme picker as a web component</title>
      <link>https://www.duncanhunter.com.au/writing/building-the-theme-picker/</link>
      <guid isPermaLink="true">https://www.duncanhunter.com.au/writing/building-the-theme-picker/</guid>
      <pubDate>Mon, 26 Oct 2026 00:00:00 GMT</pubDate>
      <description>Working outline</description>
      <content:encoded><![CDATA[<aside class="box" aria-label="Working outline"><p class="box-title">Working outline</p><p>This is the plan for the post, published early so the series is visible. The full write-up follows a voice interview, the way part 1 was written.</p>
</aside>
<!-- Outline only. Each section is written from the voice interview in content/interviews/building-the-theme-picker.md once it exists. The live element goes in this post. --><h2 id="why-a-custom-element">Why a custom element</h2>
<ul>
<li>One file, <code>public/js/theme-picker.js</code>, ES modules, no library and no build step, tying back to the Web Components Foundations course.</li>
<li>Progressive enhancement: with JavaScript off the control is simply absent and the page renders in the default theme.</li>
</ul>
<h2 id="one-controller-many-pickers">One controller, many pickers</h2>
<ul>
<li>A module-level singleton from <code>createController()</code>: every dispatch runs reduce, save, the <code>data-*</code> attributes on <code>&lt;html&gt;</code>, then <code>data-switching</code> for the motion.</li>
<li>The <code>theme-change</code> event, bubbling and composed, fired from every connected instance so the popup and the <code>/tokens/</code> page stay in sync.</li>
</ul>
<h2 id="no-flash-the-inline-init-script">No flash: the inline init script</h2>
<ul>
<li>An ES5 script under 700 characters in <code>&lt;head&gt;</code> that applies the stored attributes before first paint, generated from the same constants as the element.</li>
<li>The parity test that runs it under <code>node:vm</code> across 480 combinations, and the 12.25rem by 2.25rem box reserved so the nav does not move when the element upgrades.</li>
</ul>
<h2 id="shadow-dom-tokens-and-the-focus-trap">Shadow DOM, tokens and the focus trap</h2>
<ul>
<li>Custom properties inherit across the shadow boundary, which is why a token system and web components fit together; <code>adoptedStyleSheets</code> only, which sets the browser floor (Safari 16.4+, Firefox 119+); older browsers keep the default theme and never upgrade the element.</li>
<li>The non-modal dialog: focus to the checked theme on open, Tab and Shift+Tab wrap with each radio group as one stop, Escape closes and refocuses, a <code>composedPath</code> check closes on outside clicks.</li>
</ul>
<h2 id="the-panel-that-trailed-the-page">The panel that trailed the page</h2>
<ul>
<li>The bug: a transition on every shadow element made the panel settle about 600ms after a switch while the page settled at about 270ms.</li>
<li>The fix: registered <code>--color-*</code> properties already animate and inherit, so the extra transition now applies only where <code>CSS.registerProperty</code> is missing.</li>
</ul>
<h2 id="the-12-288-byte-budget">The 12,288-byte budget</h2>
<ul>
<li>The spec guessed about 250 lines; the element landed at 12,207 bytes and 216 lines, then 12,276 bytes after the carry-overs.</li>
<li>What was cut to stay inside it, including the mono face option removed end to end.</li>
</ul>
<h2 id="directing-the-agents">Directing the agents</h2>
<ul>
<li>What was prompted, what came back wrong (the <code>CSS</code> constant that shadowed the global, the panel running off screen at 360px), and what was corrected.</li>
<li>What Duncan would not accept, and why.</li>
</ul>

]]></content:encoded>
    </item>
    <item>
      <title>How this site&apos;s themes work</title>
      <link>https://www.duncanhunter.com.au/writing/how-this-sites-themes-work/</link>
      <guid isPermaLink="true">https://www.duncanhunter.com.au/writing/how-this-sites-themes-work/</guid>
      <pubDate>Mon, 26 Oct 2026 00:00:00 GMT</pubDate>
      <description>Working outline</description>
      <content:encoded><![CDATA[<aside class="box" aria-label="Working outline"><p class="box-title">Working outline</p><p>This is the plan for the post, published early so the series is visible. The full write-up follows a voice interview, the way part 1 was written.</p>
</aside>
<!-- Outline only. Each section is written from the voice interview in content/interviews/how-this-sites-themes-work.md once it exists. --><h2 id="why-mygov-made-me-care-about-tokens">Why myGov made me care about tokens</h2>
<ul>
<li>The motivation: one set of components at Services Australia serving many teams and frameworks, and what happens to all of them when a single value changes.</li>
<li>Public-level war stories only: naming, governance, versioning, and the change that broke something downstream.</li>
</ul>
<h2 id="three-tiers-primitive-semantic-component">Three tiers: primitive, semantic, component</h2>
<ul>
<li>Primitives are raw palette values per theme, semantics are theme-independent meaning (17 paths, including five named hues), and components alias semantics only.</li>
<li>The colour rule the whole site follows: CSS and the picker reference semantic or component tokens, never a primitive or a hex literal, and a test enforces it.</li>
</ul>
<h2 id="one-source-in-dtcg-format-four-outputs">One source in DTCG format, four outputs</h2>
<ul>
<li><code>tokens/*.json</code> in the Design Tokens Community Group 2025.10 format, and the dependency-free <code>build-tokens.mjs</code> that emits <code>tokens.css</code>, <code>tokens.json</code>, a manifest and the table in this post.</li>
<li>Why everything renders in memory first, so a contrast failure leaves the last good output untouched.</li>
</ul>
<h2 id="the-contrast-gate">The contrast gate</h2>
<ul>
<li>Every text-on-surface pair in every theme checked at full precision, because <code>#647a86</code> on white is 4.49686 and would pass at 4.5 if rounded.</li>
<li>What failed and how it was fixed: only foregrounds moved, by HSL lightness in steps of 0.001, so Monokai Pro pink went from 4.30 to 4.53 and Solarized pink from 3.80 to 4.50.</li>
</ul>
<h2 id="themes-and-modes">Themes and modes</h2>
<ul>
<li>Eight dark themes plus Paper, <code>data-theme</code> and <code>data-mode</code> defined so that a first visit with no JavaScript gets Gruvbox dark on any operating system.</li>
<li>Why choosing a dark theme always sets dark mode, and how system mode follows the OS only when asked.</li>
</ul>
<h2 id="section-tints-three-layers-no-pixel-coloured-twice">Section tints: three layers, no pixel coloured twice</h2>
<ul>
<li>The tint contract: the theme layer, the section layer and the content layer each own different pixels, and exactly four places use the section tint.</li>
<li>The CSS test that counts those four rules, and the Talks heading that briefly made a fifth.</li>
</ul>
<h2 id="when-a-token-changes">When a token changes</h2>
<ul>
<li>On this site: the line colour that failed 3:1 in all nine themes, and the ruling that borders are decorative.</li>
<li>Back at myGov: how a change like that is versioned and rolled out across teams.</li>
</ul>

]]></content:encoded>
    </item>
    <item>
      <title>From dietitian to developer</title>
      <link>https://www.duncanhunter.com.au/writing/from-dietitian-to-developer/</link>
      <guid isPermaLink="true">https://www.duncanhunter.com.au/writing/from-dietitian-to-developer/</guid>
      <pubDate>Wed, 04 Mar 2015 00:00:00 GMT</pubDate>
      <description>I trained as a clinical dietitian. From 2010 to 2014 I was a director of Nutrition Experts, and in 2011 I published nutrition research.</description>
      <content:encoded><![CDATA[<p>I trained as a clinical dietitian. From 2010 to 2014 I was a director of Nutrition Experts, and in 2011 I published nutrition research.</p>
<p>In 2014 I did FireBootCamp, SSW&#39;s developer bootcamp, and in November 2014 I started at SSW as a software architect.</p>
<p>In March 2015 Pluralsight turned that story into a short video.</p>
<figure class="yt"><iframe title="Becoming a Developer Using Pluralsight" width="560" height="315" loading="lazy" allow="autoplay; encrypted-media; picture-in-picture" allowfullscreen referrerpolicy="strict-origin-when-cross-origin" srcdoc="&lt;!doctype html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;&lt;meta charset=&quot;utf-8&quot;&gt;&lt;title&gt;Becoming a Developer Using Pluralsight&lt;/title&gt;&lt;style&gt;html,body{margin:0;height:100%;background:#000}a{display:grid;place-items:center;height:100%;color:#fff;font:600 18px system-ui;text-decoration:none}img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.8}span{position:relative;padding:.6em 1em;border:2px solid #fff;border-radius:4px}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;a href=&quot;https://www.youtube-nocookie.com/embed/aQvmHst5958?autoplay=1&quot;&gt;&lt;img src=&quot;/images/yt/aQvmHst5958.jpg&quot; alt=&quot;&quot;&gt;&lt;span&gt;▶ Play&lt;/span&gt;&lt;/a&gt;&lt;/body&gt;&lt;/html&gt;"></iframe><figcaption><a href="https://www.youtube.com/watch?v=aQvmHst5958" rel="noopener">Watch on YouTube: Becoming a Developer Using Pluralsight</a></figcaption></figure>
<p>If you are thinking about the same move: the field you come from is not wasted. Knowing what the software is for is half the job.</p>
<h2 id="reading-this-in-2026">Reading this in 2026</h2>
<p>This post is dated to the video, but I wrote it in 2026, looking back. In 2017 I became a Pluralsight author myself, on the platform the video is about. The nutrition side never left either: I still build Foodzone, a nutrition analysis platform for Australian nutritionists.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
