Patrick Lidstone
Self-hosted

Development log

A login page as a conformance suite

· Path OS

The target for the browser is chatgpt's login page. The reason it matters is an application rather than a benchmark: one of the things I want on this machine is a genuine Codex client that signs in with a ChatGPT account, and that sign-in goes through that page. It is a reasonable thing for the machine to need and an unreasonable thing to render. It turns out to be a better conformance suite than any test file, because it uses the whole modern toolbox at once and it does not care whether you have implemented it.

Six faults, each one of which made the page wrong in a way that looked like a different fault.

A ratio written as a division

line-height: 1.5 means one and a half times the font size. Tailwind v4 does not write it that way. It writes line-height: calc(1.5 / 1), because its generator carries the ratio through a custom property and the division is how it keeps the value unitless on the way through.

Every path through this engine's arithmetic ended in pixels, so that arrived as one and a half pixels. The line boxes were one pixel tall and one pixel apart, and every line of text was drawn on top of its neighbours. It read as a font fault, then a flex fault, then a centring fault, and it was none of them.

The arithmetic now says whether its answer carried a unit, and line-height asks for a number before it asks for a length. The other half of that matters as much: an expression with a unit anywhere in it is a length, so calc(1rem + 2px) is still refused as a number rather than quietly becoming a multiplier of eighteen.

Six attempts at the wrong button

This is the part worth writing down. Six attempts at that fault went into the box tree and the flex algorithm first. Each one reproduced a button of my own invention, found a real defect in it - a text run in a flex row claiming its container's stated width was real, which is fixed - and left the screen exactly as wrong as before.

The markup was imagined rather than read. The label is one text node, Continue with Google, and not the Continue with plus <span>Google</span> that every one of those attempts had assumed. Six rounds of correct work on a page that does not exist.

The browser now publishes the source it parsed, and remote.sh html prints a window of it centred on a piece of text. That ended the argument, and there was no other way to get it: this page cannot be fetched from the bench at all, because the Mac gets a 403 from Cloudflare where the X1 gets a 200.

The window arithmetic lives in the API crate where it is tested, rather than in the kernel, where a byte offset landing inside a multi-byte character would panic a machine somebody was already in the middle of diagnosing.

Colours, mixed 1,063 times

color-mix() is now understood. Tailwind v4 writes it wherever a colour has an alpha on it, and this one stylesheet contains 1,063 of them. An unparsed colour is a declaration thrown away, which is the same failure as the oklch work and has the same symptom: a panel painted with whatever was underneath it instead of the colour that was asked for.

Mixing is done on premultiplied components, which makes the dominant case exact - mixing a colour with transparent is that colour at that alpha whatever space is named. in srgb mixes sRGB and everything else mixes in oklab, which needed the inverse of the conversion the earlier oklch work added. A test mixes six colours with themselves to prove the pair really is an inverse, rather than trusting that it is.

Icons that were never drawn

Three separate reasons the buttons were bare.

An absolutely positioned box belongs to its nearest positioned ancestor. The block path has always known that. flex, grid and the atomic-inline path noted their absolute children but never opened a list of their own, so an absolute inside a positioned flex or grid container was placed against whatever positioned ancestor happened to be open above it - in practice the document. The sign-in buttons are exactly that shape: the icon wrapper is position:relative with display:grid, so every icon was placed against the page at the page's origin. The box tree read off the machine showed it plainly, the wrapper inside the button and its child at the root with no size at all.

Then the sprite sheets. Most sites ship their icons as one file and name each one with <use href="/sprites.svg#id">. The reference is into another document and nothing fetched it. The engine now lists the files a document's <use> elements name, deduplicated, and the browser fetches them - after the stylesheets and before the module graph, because a sprite is one small file that makes the icons appear whereas a module graph is twenty-eight files that cannot change the screen until every one has landed. Doing the quick visible thing first is the whole difference between a page that fills in and one that sits blank. A sprite file that fails costs its icon and nothing else.

And fill-rule, which is how a hole is drawn. The two rules disagree in exactly one place: where contours overlap. A ring is an outer contour and an inner one, and whether the middle is filled depends entirely on this. That sheet declares evenodd on 242 shapes, so it is not a corner of the format, it is most of an icon set. Even-odd is the parity of the crossings, and parity composes, so each contour is rasterised on its own and the coverages combined with a + b - 2ab, which is exclusive-or for the solid interiors and blends sensibly along the antialiased edges between them.

How much that last one actually changes is worth being straight about. Rendering all forty multi-contour evenodd paths in that sheet under both rules, two come out visibly different. Most icon sets wind their inner contours the opposite way, which makes the rule moot - the phone handset that prompted the work is one of those and drew correctly before the change. About one in twenty genuinely needs it.

And one that only appeared after logging in

Inside the <use> handling the document is rebound to the sprite sheet, and the use element's x and y were being read through it using a node id that belongs to the page. A page with more nodes than the sheet indexes past the end of the sheet. The conversation page, after a login that worked, took the browser down on the X1. The offset now comes from the use site's own document, as the fill already did, and a test puts three hundred paragraphs in front of the icon so the old read is out of range.

What is not built

Self-intersecting contours are approximated rather than handled: such a contour's own interior is taken by the non-zero rule rather than by parity. Icons do not contain them, and the alternative is writing a rasteriser of our own.

A status line sat on the URL global-modals-….css# for most of a night, because a fragment was being sent to the server. That is fixed, and it is a fair illustration of where the time actually goes.

Codex itself does not yet sign in on the machine. It works by completing the ChatGPT sign-in on the Mac and handing the tokens to the X1 over a one-time pinned TLS pairing endpoint, after which the Mac can go away - model discovery, inference and token refresh all run on the laptop. That is a stopgap standing in for the browser doing the whole thing, which is what the work above is for.