==> read the decisions
ADR-0025: Navigation is one bar, the same on every host, and a choice made anywhere reaches the account
Status: Accepted (2026-10-16). Implemented 2026-10-16. Amends ADR-0013 (the ladder: a choice this browser made and the account has not seen wins), ADR-0017 (the bar has no arrangements) and ADR-0024 (the profile opens the account pages in the same tab; every sign-out lands on the app's front door). Amended 2026-10-16 by ADR-0026: the static docs show sign-out like the docs' per-request pages; a signed-in reader holds a docs session on every docs page (a silent sign-in before paint). The homepage still has none.
Context
Section titled “Context”Four hosts render the platform -- the homepage on the apex, the app, the identity host, the docs -- and each grew its bar on its own day:
- Preferences are lost between hosts. A theme chosen on the identity host, the docs or the
homepage is written to the
cs-prefscookie only. The app, signed in, renders from the account's older settings and rewrites the cookie with them (ADR-0013): the newer choice is gone. - The same controls in two modalities. Theme, mode and language are flat on the homepage and the
docs, behind a cog on the app and the identity host (ADR-0017's
inlineandstacked). - The order of the buttons is whatever each layout added last, and the set differs by host for no stated reason: the profile and the docs icon only in the app, the terminal button only on the homepage and the docs, no way from the identity host to the app.
- The trail ("clusterstuck / ...") means a different thing per host: the user's name in the app, "docs" on the docs, the page on the identity host; its first crumb has pointed at the apex and at the current host's root on different days.
- Tabs: the profile opens the account pages in a new tab, the docs icon opens the docs in the same.
- Landings: after an app sign-out the app's front door, after an identity sign-out its sign-in
page, after ending the current session from the account page the same; a host's root has the bare
clusterstucktitle, so four tabs read the same.
Decision
Section titled “Decision”The bar
Section titled “The bar”- One bar, one arrangement.
Bar.astrorenders the same thing on every host; only the data differs (the trail, who is signed in, what the host can do). No variants. - Three regions: start, the trail (and, after it, at most one status of the host's: the homepage's countdown); centre, the host's search (the docs only); end, the controls.
- The end is always in this order, left to right: places, settings, session.
.
book-open-- the docs. Always. .terminal-- the app (the user's projects). Signed in. . the chip -- signed in,userand the username, to the profile: the bar's one statement of who you are. Signed out,loginand "sign in", to the app's front door, which signs in. .settings-cog-- the preferences panel. Always. .power-- sign out. Only where the page holds a session of its own host (the app, the identity host, the docs' signed-in pages); the homepage and the static docs have none to end. - Places are buttons on the bar; preferences are behind the cog. Places are used constantly, preferences rarely: every host puts theme, mode and language (where the host has more than one language) in the cog's panel, in that order, and nothing else there.
- A place button is never hidden for being where you are. Its place is marked instead
(
aria-current, the primary border): the terminal anywhere in the app but the profile, the chip on the profile, the book on the docs. A button that moves between pages is a button that has to be found again. "Sign in" is an action, not a place: it is absent on the identity host's own sign-in and enrolment pages, where the reader is already doing it (operator, 2026-10-16). - Sign-out shows only for someone signed in: a host's session cookie that the hint does not back (an ended session's) is nothing to sign out of.
- "Signed in" is the session where the host has one, the
cs-accounthint elsewhere: the app asks its session; the homepage, the identity host and the docs read the hint (the static docs in their pre-paint script, as today). A stale hint costs one redirect to a sign-in.
The trail
Section titled “The trail”- At most three crumbs: the brand, the host, the page.
clusterstucklinks to the homepage from every host; the host crumb (app,docs,id, the host's own name, never translated) links to the host's root; the page crumb is where you are and links nowhere. The last crumb is never a link. On a host's root the host crumb is the last; on the homepage the brand is. - The page crumb is the page's name in the page's language:
profile, a project's title (in the app and in the docs alike),sign in,enrol,account, a docs page's title. Deeper structure is the page's own (the docs' sidebar), never the trail. The user's name leaves the trail: the chip says it. - The title is the last crumb:
<last crumb> | clusterstuck, so a host's root isapp | clusterstuck,docs | clusterstuck; only the homepage isclusterstuck.
Between hosts
Section titled “Between hosts”- A thing that lives on two hosts links its two pages to each other, at the top of each, with the
target host's icon (
terminalthe app,book-openthe docs,keythe identity host): a project's page in the app and its reference in the docs; the profile and the account pages. - Every link opens in the same tab. The reader opens tabs; a page never does. This holds for
external links too, with one exception the operator chose: a
[S]link (it plays sound, as in Homestuck) opens its own tab, so the page it was on stays where it was. - A sign-in returns to where it started; one that started nowhere (the chip, a direct visit to the sign-in page) lands in the app.
- Every sign-out, from any host, lands on the app's front door (
app./?signedout=1), which says so and offers to sign in again. That includes ending the current session from the account page.
Preferences: the newest choice wins
Section titled “Preferences: the newest choice wins”- The cookie remembers which of its fields this browser chose and the account has not seen yet (pending). Every host writes the cookie, and marks the field pending.
- The app writes the account. A page of the app, on load and after each choice, sends the pending fields to the account and clears each one the account took (if the cookie still holds the value it sent). It is the one writer, as in ADR-0007.
- The ladder, signed in, per field: a pending choice in the cookie, then the account's stored value, then the cookie's, then derived, then the default. The cookie is rewritten to the result with its pending fields kept. Anonymous, unchanged: the cookie, then derived, then the default. So a choice made on the identity host renders in the app at once and is in the account a moment later; on another device, the account's value shows at its next page.
- No clocks. Ordering is by arrival at the account: the last browser to carry its choice there wins, which is the newest choice whenever the browsers involved are visited in turn.
- An open tab catches up when it is shown again (
visibilitychange,pageshow): theme and mode in place, the language by swapping the page.
Consequences
Section titled “Consequences”- The bar is the same on every host; the
inlineandstackedarrangements, and ADR-0017's sentence about them, go. The homepage's hues are one click away instead of on the bar. - The cookie changes shape (
v2, a fifth field);v1is still read, as nothing pending, so no cutover. The shared library'sprefs.tschanges first. - A pending choice survives signing out and is carried into the next account signed in on that browser. On a shared browser that is someone else's theme: accepted, it is a theme.
- A browser that chose long ago and returns signed in after another device chose differently puts its old choice back. Accepted: it is the choice that browser last showed.
- Following a project's reference from its page ends the project's running UI; Back starts it again.
- The homepage gains a sign-in button. Enrolment stays by invitation; the button leads to a sign-in page that already exists.
- The language swap no longer waits for the account write: the server reads the pending field.
Alternatives rejected
Section titled “Alternatives rejected”- A timestamped cookie, newest timestamp wins. Browsers' clocks disagree with each other and with the server; a slow clock loses a newer choice silently, and comparing per field needs a time per field in the account.
- Every host that knows the account writes it. The homepage and the static docs hold no credential; the identity host would need a settings route and CSRF of its own; the docs without a docs session cannot. The cookie has to carry the choice anyway, so it is the one path.
- Signed in, the cookie always wins. An old device's cookie would overwrite a newer choice from another one on every visit; pending limits it to what this browser chose since it last synced.
- Every control flat on every bar. Eight controls beside the trail and the docs' search, and the theme picker is wide; the bar wraps on a phone. Preferences are not navigation.
- Keeping two arrangements. That is the inconsistency this replaces.
- An account button beside the profile. Two person-shaped buttons ask the reader to know which host keeps what; the profile is the one page about you, and it links to the account pages.
- Sign-out on the homepage and the static docs. They hold no session; a button there could only clear the hint and leave the real session running, or send the browser elsewhere to sign out.
- Landing on the homepage after sign-out. It does not say what happened; the front door does.
- New tabs for other hosts or external sites. A page that opens tabs takes a decision that is the reader's, and Back stops working.