<!doctype html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>Holger — setting up</title>
<style>
/* HOLGER'S WELCOME PAGE (A12, owner 2026-10-23): gunnar-ui's `auth/welcome.html`, as it is,
in Holger's look; its script is `/welcome.js` (no inline script: the bundle's CSP).
Gunnar's notes below are kept as Gunnar wrote them.
Self-contained, like `login.html` and for the same reason: this page is
served before the console's stylesheet exists to a visitor who has no
session at all, and its ONE inline script is hashed into the CSP at
startup. The tokens are the login page's, copied rather than re-chosen —
two pre-login pages that disagreed about what "dim" means would look like
two different products to the one person seeing both in a row. */
:root {
color-scheme: light;
/* The ground is the LOGIN PAGE's, written out rather than re-derived: that
page's #f4f4f4 is the measured mean of the backdrop webp's right edge,
and a measured value must be copied or the documents drift apart again.
This one had drifted to #F8F6F4 — four steps lighter and four steps
warmer — so a visitor crossing from the door to the shop watched the
page change colour. The ink and the lines below were already
token-for-token the login page's.
The CARD is `#f4f4f4` too, written out again rather than pointed at
`--bg`. It used to be pure white, and the login page paints no pure
white anywhere a visitor looks, so a white card here was a second white
on the way from the door to the till. The card is told apart from the
page by what it already carried, its `--line` border and its shadow
over the dimmed backdrop, not by being whiter. The rename field
and the progress track, `--raised`, move down by exactly what the card
moved, 1 -> 0.9672 in OKLab lightness, so they keep the step they had
against the card they sit on. That lands on #e5e5e5: the login page's own control
fill, less its trace of warmth. */
--bg: #f4f4f4;
--surface: #f4f4f4;
--raised: oklch(0.9214 0 0);
--line: oklch(0.885 0.005 75);
--line-strong: oklch(0.775 0.006 75);
--ink: oklch(0.225 0.008 70);
--ink-dim: oklch(0.435 0.008 70);
--ink-mute: oklch(0.565 0.007 70);
--pass: oklch(0.52 0.15 155);
--bad: oklch(0.53 0.20 25);
--sans: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
--mono: ui-monospace, "JetBrains Mono", SFMono-Regular, Menlo, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100dvh; display: flex; align-items: center; justify-content: center;
padding: 1.5rem; background: var(--bg); color: var(--ink);
font: 14.5px/1.55 var(--sans); -webkit-font-smoothing: antialiased;
}
/* ── The backdrop ──────────────────────────────────────────────────────
The SAME face as the login page, `gunnar_silver.webp`, and on purpose.
This used to be `gunnar_snarl.webp` — "the third face of the badger", on
the argument that a different moment should not look like the same page
failing to navigate. The owner ruled the other way on 2026-09-14, from a
phone mid-purchase: the old illustration read as a different, dated
product at exactly the point a customer is deciding whether to trust it
with a card. One face across the door and the till. Same mechanism as
`login.html` — one custom property naming the asset, served at the root by
the `data-trunk rel="copy-file"` line in `crates/gunnar-ui-web/index.html`. */
:root { --backdrop-image: url("/holger.webp"); }
/* `fixed` so it cannot scroll on a short viewport, `pointer-events:none` so
it can never eat a click. The card here sits dead centre — there is no
column of its own to make room for, the way the login page gives the
picture a half of the viewport — so the backdrop steps well back: the
panel really is over the subject, and on a white page an undimmed
photograph behind a card is a card floating in someone's holiday snap. */
.backdrop {
position: fixed; inset: 0; z-index: 0; pointer-events: none;
background: var(--backdrop-image) center/cover no-repeat var(--bg);
opacity: .34; filter: saturate(.95);
}
.card {
position: relative; z-index: 1;
width: min(94vw, 440px); padding: 1.75rem 1.6rem;
background: var(--surface); border: 1px solid var(--line); border-radius: 10px;
box-shadow: 0 0 0 1px oklch(0 0 0 / .06), 0 18px 45px -18px oklch(0 0 0 / .28);
}
h1 {
display: flex; align-items: center; gap: .55rem; margin: 0 0 .15rem;
font-size: 1.25rem; font-weight: 640; letter-spacing: -.02em;
}
.sub { margin: 0 0 1.3rem; color: var(--ink-mute); font-size: .85rem; }
#headline { margin: 0 0 .5rem; font-size: 1rem; font-weight: 560; }
#detail { margin: 0; color: var(--ink-dim); font-size: .86rem; overflow-wrap: anywhere; }
#detail.err { color: var(--bad); font-family: var(--mono); font-size: .8rem; }
#detail.ok { color: var(--pass); }
/* ── The calm progress line ────────────────────────────────────────────
A bar that fills and drains rather than a spinner or a percentage. There
is no percentage to be had: the console is waiting on a webhook whose
arrival it cannot predict, and a bar that claimed 60% would be inventing
one. It is decoration that says "still going", which is exactly what is
true. It goes away the moment the page has an answer. */
.progress {
margin-top: 1.25rem; height: 3px; border-radius: 2px;
background: var(--raised); overflow: hidden;
}
.progress > i {
display: block; height: 100%; width: 40%; border-radius: 2px;
background: linear-gradient(90deg, transparent, var(--ink-mute), transparent);
animation: sweep 1.9s cubic-bezier(.45,0,.55,1) infinite;
}
@keyframes sweep { from { transform: translateX(-110%); } to { transform: translateX(360%); } }
.progress[hidden] { display: none; }
.foot { margin-top: 1.35rem; padding-top: 1rem; border-top: 1px solid var(--line);
color: var(--ink-mute); font-size: .78rem; }
.foot a { color: var(--ink); }
.foot[hidden] { display: none; }
/* ── The one field this page can show ──────────────────────────────────
Only when monetize says the refusal was about the NAME. A name that was
free when it was picked can be taken in the minute somebody spends typing
a card number, and the alternative to this field is a refund by hand and
a customer with nothing. It is the signup page's field, in the signup
page's shape, because it is the same question asked again. */
#rename { margin-top: 1.1rem; }
#rename[hidden] { display: none; }
#rename input {
width: 100%; padding: .6rem .7rem; border-radius: 7px;
border: 1px solid var(--line-strong); background: var(--raised); color: var(--ink);
font: 15px/1.3 var(--mono); letter-spacing: .01em;
}
#rename input:focus { outline: 2px solid var(--ink-mute); outline-offset: 1px; }
#rename button {
margin-top: .7rem; width: 100%; padding: .62rem .7rem; border-radius: 7px;
border: 1px solid var(--ink); background: var(--ink); color: var(--surface);
font: 600 14px/1.3 var(--sans); cursor: pointer;
}
#rename button[disabled] { opacity: .55; cursor: default; }
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; }
.progress > i { animation: none; width: 100%; opacity: .5; }
}
</style>
</head>
<body>
<div class="backdrop" aria-hidden="true"></div>
<main class="card">
<h1>
<img src="/holger-mark.webp" width="20" height="20" alt="" aria-hidden="true">
Holger
</h1>
<p class="sub">Thank you.</p>
<p id="headline">Setting up your account …</p>
<p id="detail" role="status" aria-live="polite">Checking your sign-up …</p>
<div class="progress" id="progress"><i></i></div>
<form id="rename" hidden autocomplete="off">
<input id="u" name="u" type="text" inputmode="latin" autocapitalize="none"
autocorrect="off" spellcheck="false" maxlength="32" required
aria-label="Username">
<button id="go" type="submit">Use this name</button>
</form>
<p class="foot" id="foot" hidden></p>
</main>
<script src="/welcome.js"></script>
</body>
</html>