<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Design on Erik Failing</title><link>https://erikfailing.com/tags/design/</link><description>Recent content in Design on Erik Failing</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sat, 31 Oct 2026 11:01:45 -0500</lastBuildDate><atom:link href="https://erikfailing.com/tags/design/index.xml" rel="self" type="application/rss+xml"/><item><title>Building the Color Accents on This Site</title><link>https://erikfailing.com/posts/building-color-accents/</link><pubDate>Fri, 30 Oct 2026 19:30:00 -0500</pubDate><guid>https://erikfailing.com/posts/building-color-accents/</guid><description>Three versions of a text-coloring system, from automatic keyword matching to hand-placed accents, and what each one taught me.</description><content:encoded><![CDATA[<p>The colored words on this site went through three versions before they actually did their job.
I started with a <span class="accent accent-static accent-grad">fully automatic keyword matcher</span>, then made it per-page, and ended up
deleting most of it for something way simpler. This is the builder&rsquo;s side of that, including the
parts that didn&rsquo;t work.</p>
<h2 id="the-goal">The Goal</h2>
<p>I play a lot of ASCII and text-based games, and one of my favorite details in <span class="accent accent-qud accent-grad">Caves of Qud</span> is
how it colors certain words: an item name in a gradient, a faction in its own color. It makes a
wall of text easy to scan. I wanted the same thing here, with a few constraints:</p>
<ul>
<li><strong>Professional, not zany.</strong> It&rsquo;s a site people look at when deciding whether to hire me.</li>
<li><strong>Readable for everyone.</strong> Every color has to pass <span class="accent accent-clarity">WCAG AAA contrast</span> on the dark background,
and screen readers have to read the words normally.</li>
<li><strong>No inline styles.</strong> The site has a strict <span class="accent accent-ward">Content-Security-Policy</span> (<code>style-src 'self'</code>), so
every color has to come from a class in a stylesheet.</li>
</ul>
<h2 id="version-1-automatic-keyword-matching">Version 1: Automatic Keyword Matching</h2>
<p>The first design was a keyword table in a data file, applied automatically at build time. Add
&ldquo;fuzzing&rdquo; to the table, and every page that mentions fuzzing gets it colored. That&rsquo;s
<span class="accent accent-trophy">retroactive and zero effort per post</span>, which sounded great.</p>
<p>The hard part was that Hugo only gives you the rendered HTML, and I had to color text without ever
touching markup. A keyword inside a link, a code block, a heading or an <code>href</code> attribute must stay
alone. Go&rsquo;s regex engine has <span class="accent accent-glitch accent-grad">no lookbehind</span>, so you can&rsquo;t just write &ldquo;match this word unless
it&rsquo;s inside <code>&lt;code&gt;</code>.&rdquo; I ended up splitting each page into tokens (tags vs. plain text), tracking
which protected element I was inside, and only running replacements on plain text:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go-html-template" data-lang="go-html-template"><span class="line"><span class="cl"><span class="cp">{{-</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">findRE</span><span class="w"> </span><span class="s">`&lt;!--[\s\S]*?--&gt;|&lt;[^&gt;]*&gt;|[^&lt;]+`</span><span class="w"> </span><span class="nx">$html</span><span class="w"> </span><span class="cp">-}}</span>
</span></span><span class="line"><span class="cl">  <span class="cp">{{-</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nx">hasPrefix</span><span class="w"> </span><span class="na">.</span><span class="w"> </span><span class="s">&#34;&lt;&#34;</span><span class="w"> </span><span class="cp">-}}</span>  <span class="cm">{{/* a tag: update which protected element we&#39;re in */}}</span>
</span></span><span class="line"><span class="cl">  <span class="cp">{{-</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="k">eq</span><span class="w"> </span><span class="nx">$protTag</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="w"> </span><span class="cp">-}}</span>  <span class="cm">{{/* plain text outside code/links/headings: safe to edit */}}</span>
</span></span></code></pre></div><p>Matches got swapped for private-use placeholder characters first, so a short keyword couldn&rsquo;t
match inside a longer one that was already colored. Then there were the small surprises: Hugo&rsquo;s
typographer turns <code>'</code> into <code>&amp;rsquo;</code>, so &ldquo;Let&rsquo;s Encrypt&rdquo; never matched, and <code>&amp;</code> becomes <code>&amp;amp;</code>.</p>
<p>The gradients had their own constraint. Coloring each letter separately means one <code>&lt;span&gt;</code> per
letter, and current testing shows that <span class="accent accent-ghostly accent-grad">screen readers handle per-letter spans badly</span>
(spelling words out, or reading only the first letter). So every gradient is a single element
with <code>background-clip: text</code>, which paints the gradient inside the glyphs and leaves the text alone.</p>
<p><span class="accent accent-point">It worked, but it colored words, not ideas</span>. Pages about topics on the list got crowded, and
pages about anything else got nothing.</p>
<h2 id="version-2-per-page-picks">Version 2: Per-Page Picks</h2>
<p>The fix seemed obvious: let each page pick its own terms in front matter, with the shared table
kept as a library so &ldquo;fuzzing&rdquo; is the same color everywhere. I added build checks too: the build
<span class="accent accent-ward">fails on a typo&rsquo;d term</span> and warns when a picked term never gets colored.</p>
<p>Then the library grew to about 110 terms, including tools and languages in their brand colors
(Python&rsquo;s blue and yellow, Rust&rsquo;s copper, Claude&rsquo;s terracotta). A lot of brand colors are too dark
for a near-black page, so a script <span class="accent accent-arcane">lightened each one in OKLCH until it passed 7:1</span>. Raising
only the lightness keeps the hue and saturation, so reds stay red instead of fading to pink.</p>
<p>Two problems showed up once real posts used it:</p>
<ol>
<li><strong>Too many gradients.</strong> When everything shimmers, nothing stands out. I cut them to
<span class="accent accent-trophy">about 20% of styles</span> and made the rest solid colors.</li>
<li><strong>First mentions cluster.</strong> An intro paragraph previews the whole post, so it holds the first
mention of half the picked terms and lights up like a Christmas tree.</li>
</ol>
<p>I also shipped a bug here worth admitting: a Hugo pipe passes its value as the <em>last</em> argument, I
wrote it as if it were the first, and <span class="accent accent-glitch accent-grad">every match on the site silently disappeared</span>. A
coverage report I&rsquo;d written for checking density caught it before anything went live.</p>
<h2 id="version-3-just-mark-the-point">Version 3: Just Mark the Point</h2>
<p>Eventually I realized I was solving the wrong problem. A machine can match terms reliably, but it
can&rsquo;t tell which phrase in a paragraph is <em>the point</em>, and that&rsquo;s what I actually wanted readers to
catch. So I deleted the matcher, the library and the front matter picks, and replaced all of it
with <span class="accent accent-point">an inline marker you place by hand</span>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">The {=flow: upload step=} {=glitch: skips anything starting with a dot=} by default.
</span></span><span class="line"><span class="cl">If the build fails, {=point: the live site stays on the last good version=}.
</span></span></code></pre></div><p>There are two tiers. Colored accents mark the tool, company or idea a paragraph is about, picked
from a palette where every style has a short note on when to use it. A key-point style (bright
italics, no color) marks the one takeaway of a section, at most once per section. The
guideline is about <span class="accent accent-clarity">one accent per paragraph</span>, on the phrase that carries the idea.</p>
<p>The syntax took a couple of tries. My first version reused link syntax, <code>[phrase](=style)</code>, with a
render hook, which worked great on the site and <span class="accent accent-crimson">looked like broken links on GitHub</span>. Double
brackets (<code>[[style: phrase]]</code>) never fired, because Markdown&rsquo;s link parser claims <code>[</code> first. What
worked was Hugo&rsquo;s passthrough extension, which hands anything between custom delimiters to a render
hook untouched:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">markup</span><span class="p">.</span><span class="nx">goldmark</span><span class="p">.</span><span class="nx">extensions</span><span class="p">.</span><span class="nx">passthrough</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">enable</span> <span class="p">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="p">[</span><span class="nx">markup</span><span class="p">.</span><span class="nx">goldmark</span><span class="p">.</span><span class="nx">extensions</span><span class="p">.</span><span class="nx">passthrough</span><span class="p">.</span><span class="nx">delimiters</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="nx">inline</span> <span class="p">=</span> <span class="p">[[</span><span class="s1">&#39;{=&#39;</span><span class="p">,</span> <span class="s1">&#39;=}&#39;</span><span class="p">]]</span>
</span></span></code></pre></div><p>I skipped <code>::</code> and <code>&lt;&lt;</code> as delimiters because they show up in normal writing about code
(<code>std::vector</code>, heredocs). The render hook splits off the style name, <span class="accent accent-ward">fails the build on an
unknown style</span>, and runs the inner text back through Markdown so bold and code inside an accent
still work. After converting, I diffed every rendered accent and link against a saved baseline,
and the output was byte-for-byte identical.</p>
<h2 id="what-id-take-from-it">What I&rsquo;d Take From It</h2>
<p>I think the main lesson is to automate what&rsquo;s actually mechanical and leave judgment to people.
Contrast checks, style validation and &ldquo;is this markup well-formed&rdquo; are perfect for the build to
enforce. Picking what matters in a paragraph isn&rsquo;t, and version 1 and 2 were me spending a lot of
clever code avoiding that.</p>
<p>The final system is a palette file, a CSS template that generates one class per style, and two
small render hooks. <span class="accent accent-point">That&rsquo;s less template code than the first version&rsquo;s matcher alone</span>, and
it does the job better. I built it with Claude Code doing a lot of the implementation and testing, which made
it cheap to try (and throw away) the first two versions lol.</p>
]]></content:encoded></item></channel></rss>