The engine this page runs on serves five sites. Four of them look like a book: paper, a serif face, justified paragraphs, a drop cap at the start of the text. The fifth — this one — looks like a technical site. The same code, the same version, one field of difference in the config.
theme: "dev",Below I describe what lies behind that field, because the road to it went through three mistakes, and each teaches something different.
Mistake one: a skin as a stylesheet
The first reflex was to write a second stylesheet and swap it depending on the instance. The reflex is wrong for a reason that only shows at the third change: two stylesheets have to be maintained in parallel. A fix to a line colour in one place does not reach the other, and after a month the sites differ in things nobody decided.
A skin is not a set of rules. It is a set of VALUES that the rules read. The components do not even know that such a thing as a theme exists — they take tokens.
[data-theme="dev"] {
--paper: oklch(0.16 0.008 250);
--ink: oklch(0.95 0.003 250);
--ink-soft: oklch(0.78 0.006 250);
--rule: oklch(0.32 0.008 250);
--prose-size: 1.05rem;
}The attribute sits on the html element and comes straight from the instance config. An instance that declares nothing gets the book skin — exactly what it had before this feature was added. That is a condition, not a courtesy: four working sites must not notice that anything changed beneath them.
Mistake two: a variable nobody reads
The most interesting of the three. I wanted the technical skin to use a sans-serif face everywhere the book skin uses a serif. It seemed like a one-liner: override the serif font variable with a sans-serif stack in the skin selector and every component would follow.
They did not. The entry title stayed serif, even though the rule was correct and visible in the browser tools.
The cause lies in how Tailwind treats the `@theme inline` directive. The variable's value is then pasted straight into the generated utility classes instead of remaining a reference. The `font-serif` class does not say “use the variable's value” — it contains the font name directly. Overriding the variable has nothing to change.
[data-theme="dev"] .font-serif,
[data-theme="dev"] .prose-literary,
[data-theme="dev"] h1,
[data-theme="dev"] h2,
[data-theme="dev"] h3 {
font-family: var(--font-sans);
}The conclusion is broader than Tailwind itself: a layer that optimises by pasting in values stops being an intermediate layer. Everything you build on it at run time stops working — and you find out from a screenshot, not from an error message.
Mistake three: a rule that reaches too far
The dark header needed its icons lightened, because they are painted in ink colour and disappeared on the black background. We wrote a rule setting the colour on the header and footer, plus an exception for the page content, so as not to catch it by accident.
The exception turned out worse than the problem. A selector made of an attribute and an element name has higher specificity than a single utility class, so the text colour on a button in the content stopped working. The result: two buttons on the home page had invisible labels, and only the third one showed, because its colour was written in the style attribute.
A frame rule has no business reaching beyond the frame. Instead of an exception — a narrower scope, and the problem goes away together with the exception.
What does not belong to the skin
The last thing is a design matter, not a technical one. Not everything that has a colour should be a token.
In the console blocks, the black of the Unix shell and the navy of PowerShell are hard-coded. That is not an oversight: those two colours are a quotation from reality. The reader recognises the system by them before reading the first line, and the instance has no right to redefine that, because it is not the one who decides it.
The opposite example: the accent colour. In the book skin it was a dark green and worked as the background for the whole header band. After switching to the dark skin the same token had to brighten to mint — and the band with the CV header turned into a glowing slab half the screen high.
The lesson is not “pick a different shade”. It is: an accent should be an accent — a line, a label, a single button. A colour that is a surface one time and a signal the next will sooner or later fail in one of those roles.
The balance
The technical skin is about a hundred lines of CSS, one field in the config type and three places in the components where it is checked which one is active. The four other instances noticed nothing — checked after deployment, live, on each of them.
That is the whole measure of success for a change like this. Not that the new variant looks good, but that the old ones look exactly as they did yesterday.