Dex

A blog?

Style guide

Everything on this page renders from the live stylesheet. If a token changes, this page changes with it — it cannot drift from the site the way a screenshot would.

Colour

Eight tokens, defined once in assets/css/tokens.css and redefined under prefers-color-scheme: dark. No other file in the repo contains a colour literal. Toggle your OS between light and dark to see both.

Every value below is asserted by test/tokens.test.mjs: ink tokens clear 4.5:1 against --bg, --ink clears 7:1, --accent clears 4.5:1, and --on-accent clears 4.5:1 against --accent. A palette change that breaks legibility fails the test rather than shipping.

  • --bgpage background
  • --bg-sunkrecessed: code, pre
  • --bg-liftraised: chips
  • --inkbody text
  • --ink-mutedmetadata, summaries
  • --rulehairlines, borders
  • --accentlinks, focus rings
  • --on-accenttext on --accent
  • --syn-keywordcode: keywords, operators
  • --syn-stringcode: strings
  • --syn-namecode: names, functions
  • --syn-numbercode: numbers

Code is body-sized text, so the four syntax colours carry the same 4.5:1 obligation as everything else. Three gruvbox syntax colours fail it and are substituted with the nearest passing member of the same family — faded_green 4.3:1, faded_aqua 4.4:1, and bright_red 4.3:1. Comments use --ink-muted rather than gruvbox gray, which measures 4.0:1 and cannot legally carry text.

Type

Three roles. Body text uses no webfont, so reading never waits on a third party.

Eyebrow — JetBrains Mono, uppercase, tracked

Display — Newsreader

Second level

Third level

Body copy is the system sans stack. It should feel unremarkable, which is the point: the display face and the mono metadata do the work, and the reading column stays out of the way. The measure is capped at 34rem so a line never runs long enough to lose your place on the return sweep.

2026 · 09metadata row3 min

Elements

A link in running text, some inline code, and bold and italic.

A blockquote, for the times a quotation earns its own space.

// A fenced code block.
const tokens = readFileSync('assets/css/tokens.css', 'utf8');
Column Column
Table cell Table cell
Table cell Table cell

Markdown

kramdown with GFM input — the GitHub Pages default. _config.yml sets no markdown: key, so changing processor would mean changing that. Everything below was established by building a probe post and reading the rendered HTML.

Footnotes

Footnotes work.1 They parse into correct role="doc-noteref" markup and an endnote block at the foot of the page. They looked broken for a while only because nothing styled them — the endnote list inherited body size and colour, so it read as a stray numbered list rather than apparatus.2

The definition needs a colon. [^1]: text works; [^1] text does not, and fails in the worst way — kramdown finds no definitions, so it leaves the markers and the definition lines as literal text in the middle of the post. No error, no warning, and it only shows up when you read the published page.

Task lists

  • - [x] renders a checked box
  • - [ ] renders an empty one
  • the list bullet is suppressed, or each item shows a disc and a checkbox

Definition lists

A kramdown extension, not GFM.

Term
The definition, indented behind a rule.
Another term
Its definition.

Abbreviations

Also kramdown. The HTML spec defines this.

Code

Rouge tokenises fenced blocks and emits span classes; the four --syn-* tokens colour them.

// a comment
const greeting = "hello";
function shout(text, times = 3) {
  return Array.from({ length: times }, () => text.toUpperCase()).join(" ");
}

A second language, because Rouge emits different token classes per lexer and a class with no rule renders in body colour — highlighting that looks broken rather than absent. Exercising two lexers here means the test catches that.

class Kettle:
    """Boil water, eventually."""
    def __init__(self, litres: float = 1.7):
        self.litres = litres
        self.on = False

    def boil(self):
        if self.litres > 0:
            self.on = True
        return f"boiling {self.litres}L"

Attribute lists

{: .lede} after a block applies one of this page’s classes to it — handy for promoting a paragraph without writing HTML.

Not supported

Syntax What happens
Bare URL https://example.com stays plain text — wrap in <> or use [text](url)
Emoji :smile: passes through literally; needs the jemoji plugin
Maths $$a^2$$ becomes MathJax \(a^2\), but no MathJax is loaded, so it shows as literal backslash-parens

Favicon

favicon at 16px favicon at 32px favicon at 64px 16 · 32 · 64

A keyhole, or a chess pawn. The ambiguity is deliberate — escape rooms read the first, games read the second, and neither needs explaining.

assets/favicon.svg is the only file besides tokens.css that contains colour literals, because an external SVG referenced from <link rel="icon"> cannot see the page’s custom properties. test/tokens.test.mjs asserts the duplicated values match the palette in both schemes, so the icon cannot drift.

The glyph fills 78% of the tile height, and that number is load-bearing. An earlier version used 56%. At 16px — the only size most people ever see — that left a head barely 3px across, and the shape collapsed into an unreadable blob. Clearance that looks composed at 64px is wasted resolution at favicon size. A test fails if the scale drops below 1.1.

Some clearance still earns its place: past roughly 90% the glyph looks cramped inside the rounded corners at the large sizes used for bookmarks and PWA icons. 78% is the compromise.

Jelly components

Loaded from jelly-ui.com and enhancement only. Each is shown twice: as it upgrades, and with the upgrade suppressed so you can inspect the fallback without taking the CDN down.

Note that a default <jelly-chip> paints its body on a <canvas> from --jelly-color-background-neutral. Any Jelly token left unbridged in tokens.css silently keeps Jelly’s own blue-grey default, so the bridge list is asserted by test.

Upgraded

hardwareesp32

Fallback, as it renders if package.js never loads

hardwareesp32

  1. The marker is accent-coloured and bracketed so it reads as a reference. ↩

  2. The arrow returns you to where you were reading. ↩