Changelog
Kolofon's version history, release by release, straight from git — commit timestamps included. What went in, what broke, and what had to be fixed a quarter of an hour later.
Kolofon · 16 September 2026 · provisional

0.13.17 — counters without zeros
Two counters on the entry page stop showing discouraging zeros. The view counter appears only from the tenth view; the comments section loses its count line — comments show only when there are any. Under a freshly published entry stood two numbers, both working against it. “1 view” in the byline and “0 comments” above the comments section. Each tells the reader the same thing: no one is here. That is exactly the signal a fresh text should not be sending. From 0.13.17 the view counter appears only from a threshold — ten views. Below it, it disappears entirely. We are not hiding the measurement, only the display: a view is still recorded exactly as before, we simply do not show the number until it grows large enough to give nothing away. The threshold lives in one engine constant, `MIN_VIEWS_SHOWN`. The comments section loses its count line. There is no more “0 comments” or “3 comments” — the number is not shown at all. Comments appear only when there are any, and below them, as before, stands the invitation to write one. Along the way the engine sheds the last string hardcoded into it: the fallback “Comments” shown while loading. The plural inflection of the word stopped being needed and goes too. Both changes have one justification. A counter is an ornament, not a function; when it reads a handful or zero, it stops being an ornament and becomes a record of emptiness. Better to show nothing than a number that puts people off. When there is something to count, the numbers come back on their own — from the threshold where they begin to mean something.
Kolofon · 14 September 2026 · provisional

0.13.16 — an account page without prices
Two tiles opened the sign-in form: a free one with a struck-through price, and a greyed-out paid one marked “coming soon”. A price list in front of the address field asks the reader to settle the cost before they… The account page used to open with two tiles. The first: basic account, free, with nineteen struck through beside it. The second, greyed out and not clickable: Premium, sixty-nine, “coming soon”. They were there for a sensible reason — so the reader would know the free account was not a trimmed-down version of something kept out of sight. The reason turned out to be weaker than the cost. A price list placed before the address field forces the question of money to be settled before the real question is even asked: what is this account for. A struck-through price adds a note of clearance sale, and a greyed tile marked “coming soon” promises something that does not exist and could not be bought today even by someone willing. A page whose job is to take an address and hand back the reader's record was opening with an advert. As of 0.13.16 the tiles are gone, and seven strings with them — `planBasicName`, `planBasicPrice`, `planBasicPriceWas`, `planBasicNote`, `planPremiumName`, `planPremiumPrice`, `planPremiumNote` — so instances no longer declare them in their `ui` block. This is not hiding by blanking the configuration: the markup is simply absent, and the string guard does not ask about keys the engine no longer knows. The wall for posts beyond the basic account is untouched. `wallPremiumBody` and `wallPremium` are a separate mechanism — they act on a post, not on the sign-in pa
Kolofon · 13 September 2026 · provisional

0.13.15 — search results you can actually see
Results were appended below the field with no motion and no scroll. On a phone that looked like a broken button — the list sat below the fold and the page stayed put. The report came in the way the best reports do: I pressed search and never noticed anything appear. The results were on the page — all three, each with its paragraph. But they were appended below the field with no motion at all, and the page did not move, so on a phone, where the field sits halfway down the screen, the whole list landed below the fold. The button looked broken. Since 0.13.15 two things happen once the answer arrives. The page scrolls to the field, so the question and the first result stand together, with `scroll-mt-24` keeping clear of the header bar — without it the bar would cover the very field being scrolled to. The results then slide in one by one, seventy milliseconds apart, a short rise out of transparency. The motion lasts three tenths of a second, because its job is to say look here, not to entertain. The empty-result message arrives exactly the same way. No answer is also an answer, and it needs to be noticed too — otherwise silence after a button press means both nothing was found and nothing happened. When the system reports reduced motion, only the scroll remains, performed as a jump. An element that slides and fades is precisely what that setting exists to prevent — but hiding the result off-screen is not accessibility either, just a different failure.
Kolofon · 13 September 2026 · provisional

0.13.14 — the example list takes a frozen array
The string added in 0.13.13 was typed `string[]`, while instance config sits behind `as const`. The first instance that wanted to use the list failed the type check. The example list introduced in 0.13.13 was typed `string[]`. Instance config sits entirely behind `as const` — that is what keeps configuration values from being swapped at runtime — so the array arrives from there as `readonly`. The type check rejected every instance that wanted to use the new list, and the only way around it would have been dropping `as const` from the whole file. Since 0.13.14 `searchExamples` takes `readonly string[]`. The engine only ever reads that list, so nothing is lost. The fix surfaced at the first use, before 0.13.13 had gone up anywhere, and ships as its own number. A release that is already out describes what it did; fixes are not glued onto it.
Kolofon · 13 September 2026 · provisional

0.13.13 — the search hint types itself
Semantic search wants a sentence, and an empty field looks like every other search box on the internet — so it gets a word. Instead of asking for a sentence in words, the engine now shows one, typing itself. The semantic search added in 0.13.7 does not match words: the query goes to a model and is compared against the paragraphs of entries. It works best when a reader writes a full sentence about what brought them here. The problem is that an empty search field looks like every other search field on the internet, and those take a single keyword — precisely the input this search handles worst. The first instinct was an instruction in the hint: a full sentence, in your own words. True, and useless. It reads like a classroom exercise, and people look at the field rather than at the label above it. Since 0.13.13 the empty field suggests differently. An example query types itself in, character by character, holds for a moment, erases itself and gives way to the next one. Nothing is demanded; the shape of a good query is simply visible. The animation stops at the first touch — a click, a caret, any key — and never comes back, even if the reader leaves the field empty. A hint that resumes under someone's cursor is nagging. When the system reports reduced motion (`prefers-reduced-motion`), or the field already has content, there is no animation at all: the plain static hint stays, the same one that ships in the HTML and remains for anyone with JavaScript off. The sentences are interface strings, so they belong to the instance (`ui.searchExamples`). The engine knows none of them — the defau
Kolofon · 11 September 2026 · provisional

0.13.5 — a fourth skin: atelier
The brief fit in one sentence: clean white, elegant, with touches of brushed copper and bottle green. The engine got a fourth skin and not a single new component. A second professional business card went up on the engine, and the brief for its look fit in one sentence: the page should be clean, white and elegant, with touches of brushed copper and bottle green. None of the three existing skins could do that. “book” has warm paper, “dev” is dark, and “docs” is cool and navy. From 0.13.5 there is a fourth, `theme: "atelier"`. The background is true white. Green serves as ink, titles and the footer. Copper appears only in details: as the rule under the header bar and the drop cap in a post, because over a large area it stops looking precious. Headings are set in Bodoni Moda and text in Jost, since a serif with that much contrast tires the eye in a long paragraph at a small size. The instance loads the typefaces in `fontsHref`; without them the skin falls back to system fonts and still works. The brushed finish is two layers in one variable: a metal gradient and fine scratches on top. The first screenshot showed right away what not to do with it. In the `background` shorthand a given size applies only to the last layer, so the rule under the quote came out right, while the scratches spread across the whole paragraph as a grey box. Rules are now drawn with a pseudo-element. As with “dev” and “docs”, this is tokens and the cascade only. No component knows the skin exists, and the other three did not change by a single byte.
Kolofon · 11 September 2026 · provisional

0.13.4 — back to the default version
Switching to English worked. Switching back to Polish did not: the server read the language from the Referer header and sent the reader back to /en. The owner tried the switcher in production: Polish to English worked, English to Polish did not. The click seemed to lead to the Polish page and ended up on the English one again. Server functions go to a shared address with no prefix, so since 0.13.0 they got their language from a header, with the Referer header as a fallback. That fallback, though, applied to every request, not only to server functions. Clicking “Polski” on an English page sent a Referer of `…/en/…`, and the server treated the request for the Polish page as English. The tests did not see it because they used `curl` without a Referer. From 0.13.4 the header and the Referer count only under the server-function address, and a page gets its language from the path alone. Checked by clicking: four switches in a row, both ways.
Kolofon · 11 September 2026 · provisional

0.13.3 — R2 media under the prefix
A recording under /en/audio/… returned 404. The router strips the prefix from what it sees, but the media route read its key from the request itself. The second language merge covered a site that keeps its recordings and some photos in an R2 bucket rather than in the repository. A quarter of an hour after deployment a recording under `/en/audio/…` returned 404. Media routes take the object key from `request.url`, because params in server handlers are sometimes empty. The router's `rewrite` strips the `/en` prefix from what the router sees, but not from the request itself — the bucket was asked for `en/audio/…`, which is not there. The local test missed it for two reasons at once: repository files were already handled by another mechanism, and the local R2 is empty. From 0.13.3 `server.ts` strips the prefix from the address of every file request, and the language stays in the request context. Checked against an object put into the local R2.
Kolofon · 11 September 2026 · provisional

0.13.2 — no engine name in the browser
An instance can choose not to credit the engine. The footer credit went away, and since 0.9.0 so did the name in the bundle — but it stayed in class names, events and a header nobody read. Since 0.9.0 an instance with the credit switched off does not ship the engine name in its JavaScript bundle: the decision is made at build time and the dead branch disappears. That closed one place. Reviewing the 0.13.1 pull requests showed there were others. In the bundles of uncredited instances the engine name sat in the names of two animation classes and their keyframes, in four browser event names, and from 0.13.0 it was about to arrive in the HTTP language header. None of these places are visible on the page. All of them are visible to anyone who opens the file — which is exactly the person the credit is switched off for. There is a new guard, `check-engine-name.mjs`: after the build it goes through everything that is sent to the browser and stops if an uncredited instance carries the engine name. A credited instance passes straight away. An instance that used the old classes in its own content has to rename them — the only change required on its side.
Kolofon · 11 September 2026 · provisional

0.13.1 — the migration creates its own column
The 0.13.0 migration was meant to give pre-release events a language. In production it did not: the column is created by another module, and that one wakes up later. The first `status` after deploying 0.13.0 showed twelve old events with the page language “none”. Migration `0013` had recorded itself as done, moved the view counters correctly, and left the events alone. The cause is ordering. The `events.page_lang` column is created by the log module, but only on the first event write. The migration runs on the first view-counter read, which comes earlier. An `UPDATE` on a column that did not exist fell into a `catch` meant for a missing table, and nobody saw it. The dress rehearsal on a copy of production passed because it ran in a different order: the database merge script created the column before the first page view hit the copy. The test checked the correct scenario and said nothing about the one that actually happened. Filling in the events moved out of `0013` into a separate migration, `0013b`, which creates the column itself first. Databases where `0013` already ran get `0013b` on the next start. Both orders were checked for real this time: on a copy of current production and on a clean pre-0.13 database, with no script. The rule from here on: a migration creates every column it touches instead of relying on which module happens to wake up first in the isolate.
Kolofon · 11 September 2026 · provisional

0.13.0 — two languages, one deployment
Up to 0.12 a language version was a separate instance: its own subdomain, repository and database. From 0.13.0 one deployment serves several versions — the folder decides an entry's language, the path prefix decides the… For a month the English version of this site lived next to the Polish one like a neighbour through the wall: its own subdomain, its own repository, its own database, its own deployment. Every fix went out twice, and every new entry meant two pushes and two checks. 0.13.0 ends that arrangement. Both versions now run in one deployment, and the English one lives under `/en`. Two things, and neither is a new config field. **The folder decides an entry's language**: `src/content/posts` is the default version, `src/content/en/posts` is English. **The address decides a page's language**: a version registered with the URL `https://…/en` gets the `/en` prefix, because that is what its `url` says. Every Polish address stayed exactly where it was. The harder half was not routing. It was that `cfg()` is called in hundreds of places and until now always returned the same thing. Now it has to know which version the request is in. A global variable was out straight away: one Worker isolate handles requests concurrently, and two renders interleave at every `await`. On the server the language rides in AsyncLocalStorage, in the browser it is the prefix in the address bar. The test: eighty concurrent requests in Polish and English, not one string from the wrong version. The first idea for the prefix, the router's `basepath`, did not survive contact with TanStack Start: the server overwrites it on
Kolofon · 11 September 2026 · provisional

0.12.0 — the themes release
The number moves from 0.11 to 0.12 because the way the engine is used has changed, not merely how it behaves. An instance declares a theme and a few settings; the engine does the rest. The 0.11 line began with a site that looked the same everywhere and ended with something else: an instance declares what it wants to be, and the engine adjusts to that. This is a change in how the engine is used, not in what it does — which is precisely why the number moves up a level. Three themes: **book** — paper and old-style type, for a site built around reading prose. **dev** — dark and technical. **docs** — light, cool, documentation-like. They are not variants of one another; they are three different promises made to a reader in the first second. Alongside them, two prompts built from a single pattern — one for installing the app, one for enabling notifications — and propagation of releases to instances through pull requests, with guards deciding whether a pull request is ready or stays a draft. Bumping straight to 1.0 was tempting: themes sound like product maturity. But a 1.0 says “this is finished and stable”, and there are still parts of the engine I know will change. There is a stronger reason pointing the other way. **None of these fields is required.** An instance that declares nothing looks and behaves exactly as it did in 0.11.0 — verified after every one of the last ten releases, live, on every instance. A release that breaks nothing and demands nothing is not a major release. It is the next step. A number should describe reality, not ambition.
Kolofon · 11 September 2026 · provisional

0.11.69 – 0.11.78 — themes, instance settings and a nudge about notifications
Ten releases in two days. A third theme, five new fields in instance configuration, a notification prompt, and one fix in propagation that only surfaced on the second release in a row. Ten releases in two days, nearly all of them from one source: a new instance that was meant to look unlike the others. Every time something was missing, the choice was the same — write an exception into the instance, or turn it into a field in engine configuration. Ten times out of ten the second option won. Themes are no longer a pair. `book` and `dev` are joined by `docs`: light, cool, with a navy accent and a dark bar as its only frame. That is what documentation looks like — white space, a narrow column, plenty of air, colour used strictly as a signal. One criterion guided it: the new theme has to differ from both of the others at a glance, rather than read as a variant of either. A site for reading prose, a technical calling card and a product's documentation are three different promises. The `dev` theme was finished off along the way. It had kept light paper under the text, so moving from a dark landing screen to a white list of posts looked like two different sites glued together. Everything is dark now, and syntax colours in code blocks go through variables — the same palette cannot serve both paper and graphite. Each came out of a specific piece of friction, not out of an idea for a feature. **tileImageFit** — a grid tile cropped images to a fixed height. For photographs that is right: the frame is a decision anyway, and an even grid reads better than faithful proporti
Kolofon · 10 September 2026 · provisional

0.11.40 – 0.11.68 — twenty-nine releases, one entry
The changelog stopped on 8 September and stayed there for three days while the engine kept moving. Rather than twenty-nine entries written backwards, here is one that says what actually changed. The rule in this project is that every change to engine code is a new release, and every release gets a changelog entry. The first half held. The second stopped on 8 September at 0.11.39 and did not move for three days, during which twenty-nine further versions shipped. Writing twenty-nine entries after the fact was an option. They would have been dishonest — composed from memory, each pretending to be a note taken at the moment of release. This entry stands in their place and pretends to be nothing other than what it is: catching up. THE REGISTRATION WALL AND READER ACCOUNTS (0.11.40 – 0.11.59) The largest block, and the only one that changes what a reader sees on the first screen. A post can carry a threshold: the rest is for readers with an account. The account is free, an email address creates it, and signing in happens through a link in the inbox — no password, because a password that does not exist cannot be stolen. A dozen or so fixes grew around this, each of them out of one specific situation. The teaser now shows a fraction of the text instead of breaking off mid-sentence. An image behind the wall fades into a gradient and carries a button. Returning after sign-in leads back to the post you started from rather than to the home page — first through an address in the link, then, once that proved unreliable, through an address stored alongside the token on the server. Co
Kolofon · 8 September 2026 · provisional

0.11.39 — a login that is not a case number
A new account was given a login along the lines of p_anon4720. It reveals nothing and is unambiguous — it just stands under the first comment, before the reader has had a chance to name themselves. The login handed out at sign-up came from a pattern: the first letter of the address, `_anon`, four digits. That produced `p_anon4720`. The pattern has its virtues — it reveals nothing, it cannot be mistaken for a person's signature, and an account has something to be called from its first second, so the comment form does not refuse over an empty field. It also has a fault that stays invisible until you look at it as a reader. People open an account in order to say something, and the first comment follows the registration immediately — signed with exactly that. A string of this shape reads like a ticket reference, or an identifier in a system one has ended up inside. Changing the login was always possible, but it took a step that someone writing for the first time will not take first. From this release an instance can supply its own pool: `community.autoNames`. The engine draws from it, checks whether the name is free, and adds a counter when it is not — `quiet_water`, then `quiet_water_2`. A hundred words run out sooner than they look like they would, so the counter is not decoration but the condition on which the pool keeps working as accounts accumulate. The words belong to the instance rather than the engine, and that is not an administrative detail. The login is seen by every reader under a comment, it is in the language of the content and it carries the tone of the site —
Kolofon · 8 September 2026 · provisional

0.11.38 — a series withdrawn
Hiding a series had two degrees and both left its own address standing. The third takes the address too — while keeping the way back to a single deleted field. An author is sometimes in a position where a published text has to stop being visible. Not because it is bad, and not in order to destroy it — it simply must not stand on display right now. Until this release the engine had two answers. 'nav' takes a series out of the menu while its entries keep flowing in the stream and the feed. 'all' takes it out of everywhere, leaving its own address standing — because the rule was that hiding is not deletion, and a link sent a year ago should still reach the text. The rule is a good one and it stays. What it does not cover is the case where being unreachable is precisely the intention: the series is to disappear from the reader's view, not merely come off the billboard. Hence a third degree, `hidden: "gone"`. The series index answers 404, every one of its entries answers 404, the stable key `/i/<key>` no longer leads there, and the series drops out of the feed, the sitemap, the offline bundle and notifications. It is the only variant that breaks the 'hiding, not deletion' rule, which is why it has its own name rather than being a quiet extension of the previous one. The cut sits in one place — the function that returns a series' published entries. It was tempting to add the condition in the index route and the entry route, but routes accumulate, and a missed path here is not a cosmetic defect: it is a text on display that was meant to be d
Kolofon · 8 September 2026 · provisional

0.11.37 — a dot without alarm
The marker for things not yet looked at glowed red. Red in an interface means something is wrong — and nothing was wrong here, something had simply arrived. A review of colours in the engine gave a short result: red occurred exactly twice, and both times it was the same dot. Small, in the corner of a glyph in the header, lit whenever something had arrived for the reader that they had not looked at yet. Both occurrences were written into the components by hand, outside the palette. The palette does have a rule, and the rule is written down in the stylesheet: the only colour beyond grey is the tile that sends you somewhere else. Everything else is ink, paper and mist. The dot stood beside that rule — not because anyone had argued for an exception, but because the dot was made quickly and took the colour one reaches for without thinking. That is the real reason for the change, and it matters more than tidiness in the palette. A red point in the corner of an icon is a sign lifted straight from services that hold attention through unease: you have unread items, someone is waiting, something is slipping away, come back now. The sign is so well drilled that it is read faster than whatever it sits on. The trouble is that here it tells a lie. Nothing is slipping away, nothing expires and nobody is waiting. Something arrived — that is all. So the dot is ink now. It is visible thanks to the paper-coloured ring that separates it from the glyph, not thanks to shouting. A reader who misses it for three days loses nothing; that was the only funct
Kolofon · 8 September 2026 · provisional

0.11.36 — an address is not a person
The event log knew nothing but the IP address. A mobile address changes daily, and a whole household sits behind one home connection — so counting addresses was not counting people. The first serious traffic report on an instance turned up something unpleasant: not one of its numbers could be defended. The event log recorded an IP address and nothing else, and an address is not a person. On a mobile network it changes daily — one reader over a week looks like seven people in the table. Behind a home connection sits a household — three people look like one. The browser identifier that settles this had existed since 0.11.4, only in other tables: beside entry opens and beside stars. Tying it to the log meant guessing by time — looking for a view that landed in the same second as a star on the same entry. It works, but only where a star was given, which is a handful of events. From this release the log knows the identifier outright. What matters is what it does not do: it does not create one. A view reads the identifier only if it already exists, and it comes into being at the first entry actually read. Merely arriving on the site gives nobody anything, and a reader with no storage, or none to give, stays an address in the log as before. The identifier is a random bearer string with nothing about the person in it — which is why we store nothing beside it. The second change concerns ratings. An instance may hold preliminary scores entered by the author while setting the content up. Unmarked, they counted towards the average shown to readers — the author's own s
Kolofon · 8 September 2026 · provisional

0.11.35 — the guard that stopped failing
The header test died with ERR_MODULE_NOT_FOUND, because it needs playwright and playwright is not among the dependencies. A guard that always fails teaches you that guard failures are to be ignored. The header broke three times, and each time it was a person on a phone who noticed, not a test. Hence a guard that renders the page at eight viewport widths and checks that the site name, the navigation and the icons do not overlap. A review found it had been ending in a missing-package error for several releases, and nobody had noticed, because nobody was running it. The obvious fix would have been the wrong one. Adding playwright to the dependencies means downloading a browser — a hundred and fifty megabytes or so — into every fresh working environment, for a test run once every few releases. A test that renders a page belongs to the continuous integration run, where the browser installs once and is cached. Run without the package, the script now skips with a message and exits successfully instead of dying. This is not hiding an error: a guard that always fails teaches a team that guard failures are to be ignored — and then it stops working on the day it actually catches something. A second layer arrives for use before a commit, cheap and local. All three header failures shared a cause in the layout classes: the icon group without the class that prevents shrinking pushed the row wider than the screen, and the name without permission to shrink pushed the icons off it. Checking statically that those classes are in place costs milliseconds and catches exactly that kind of regres
Kolofon · 8 September 2026 · provisional

0.11.34 — the address that never arrived
The function issuing the login link took the client address from an optional parameter its caller never passed. The empty column in the database was the milder of two consequences. The client address reached the function issuing the link through an optional parameter. The caller never passed it, so the parameter was empty always, from the day the mechanism was written. The database showed it plainly: the address column beside login tokens held not a single value. The empty column is the milder consequence. The same address goes to the anti-bot check, where it binds the token to the machine that received it — which is precisely the part that catches a token copied and used elsewhere. The check worked, but without that part. The address is now taken from the request inside the function itself, rather than at the caller. The parameter remains as an override, but nobody has to remember it any more — and remembering is what failed. Fourteen type errors came along with it, all pre-existing. Nine share one cause: since TypeScript 5.7, Uint8Array is generic and by default admits a shared buffer, which Web Crypto will not accept. The rest were a missing field in a comment handler's input declaration, a hand-written props shape instead of the router's own type, `as const` on a conditional expression, and six uses of `any`. The formatter configuration belongs to the engine as of this release. It had none before and wrapped differently from every instance — see 0.11.33.
Kolofon · 8 September 2026 · provisional

0.11.33 — formatting the sources
The engine had no formatter configuration of its own and wrapped at eighty columns, while every instance had one and wrapped at a hundred. As long as nobody formatted anything, it did not show. This release changes not one statement. It changes whitespace and line breaks in the engine sources — and it had to go out on its own, ahead of any other change. The reason is mechanical. An instance receives engine files through engine-sync, by copying. Had the engine stayed unformatted and the instance been reformatted, the first synchronisation would have undone the instance's formatting — and the other way round. Two sides fixing the same file in two different ways is a loop you do not escape by being careful. A separate commit, because mixing whitespace with substantive fixes makes history unreadable. Whoever one day goes looking for why a line reads the way it does should land on a release that says outright: there is no decision here, only whitespace.
Kolofon · 7 September 2026 · provisional

0.11.29 — a login that is also a pen name
A reader now receives a login while the account is being created, not afterwards. A machine assigns it — the first letter of the address, an underscore, `anon` and four digits — and the reader trades it for their own… Until now an account had an address, a role and a tally of stars, but no name: the signature field stayed empty, because a name taken from the provider would have signed the first comment with details nobody meant to reveal. The reasoning was sound, the consequence was not: an empty signature waited for the first comment in order to refuse it, and the reader learned that signatures exist at the moment they wanted to write a sentence, not to create an account. So a machine assigns the login now, as the account is created. The shape is public and deliberately plain: the first letter of the address, an underscore, the word `anon` and four random digits — `k_anon8161`. One letter from the address narrows nothing down, and it keeps the login from being a bare number. The draw checks whether it landed on something free and repeats until it does. That shape is also reserved: a reader cannot choose it, because a login pretending to be assigned is a ready-made tool for impersonating a fresh account or squatting on somebody's future name. The login is also the pen name — one field, not two, and it is described that way everywhere it appears. The rules stand beside the field as you type, not in the message after a failed attempt: three to thirty-two characters, letters from any alphabet, digits, spaces and four punctuation marks. No space at either edge and never two in a row — both are r
Kolofon · 7 September 2026 · provisional

0.11.32 — the name on top, the identity below
The account page opened with “Signed in as” and an email address. The address is the account's identity, but the reader's name is their login — and the login is what everybody else sees. The greeting was written when an account had no name: the address was the only thing a reader could be called. Since 0.11.29 the name exists, and it is the name that appears under comments, so greeting somebody by address meant introducing them with something nobody but they can see. The login is at the top now. The address moved down into the table, beside the role and the tally of stars — where facts about the account live, rather than facts about the person. The login's own row in that table is gone, because it repeated itself twice within two lines; the button for changing it stays where it was. The division is simple: above, what the reader is to others; below, what the account is to the site. The address is still visible only to the owner of the session and still never leaves this page — what changed is its place, not its reach.
Kolofon · 7 September 2026 · provisional

0.11.31 — “allowed to choose” is not “exists”
The sign-in form refused a login it had assigned itself. It was checking it with the very function that guards what a reader may not choose — and an assigned login is precisely what a reader may not choose. The login `p_anon4720` exists, sits on an account and was assigned by this engine. Typed into the sign-in form, it was told it looked like neither an address nor a login. It looked like both — except that the function checking it was answering a different question. `checkDisplayName` answers the question “may I give myself this name”. It rejects names reserved for the editors and rejects the shape of an automatically assigned login, so that nobody can pose as a fresh account. Both rules are right and both stay. Signing in, however, poses an entirely different question — “is this even shaped like a login” — because every existing login has to open its account, including one you could never have chosen. The consequence was the worst available: the door shut precisely for those who had changed nothing yet, which is every new account. An account carried a login from its first second exactly so that it would not begin with an absence — and it was that login refusing entry. The shape is now checked by `looksLikeLogin`: length, permitted characters, no space at the edges and none doubled. Nothing about reservations, nothing about protected patterns. Separating those two questions is the whole fix; the entry exists because confusing “may be chosen” with “exists” is easy to repeat anywhere one name serves two purposes at once.
