Gunnar

nordisk/holger-ui

assets/paid.html

<!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 — your order</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; }
  /* ── Gunnar's FollowOrder (A16): the numbered steps, the pay area, "Cancel order" ── */
  .steps { display: flex; gap: .9rem; margin: 0 0 1.1rem; padding: 0; list-style: none;
           color: var(--ink-mute); font-size: .8rem; flex-wrap: wrap; }
  .steps li.current { color: var(--ink); font-weight: 600; }
  .steps li.done { color: var(--pass); }
  .steps[hidden], #change[hidden], #room[hidden], .pay[hidden], .fault[hidden], .cancelled[hidden],
  #headline[hidden], #detail[hidden] { display: none; }
  .cancelled { margin: 0 0 1.1rem; padding: .8rem .9rem; border: 1px solid var(--line); border-radius: 7px; }
  .cancelled h3 { margin: 0 0 .3rem; font-size: .95rem; }
  .cancelled p { margin: 0; color: var(--ink-dim); font-size: .86rem; }
  #change { margin: .6rem 0 0; font-weight: 600; font-size: .86rem; }
  #room { margin: .6rem 0 0; font-weight: 600; font-size: .86rem; }
  .pay { margin-top: 1.1rem; display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; }
  .pay p { margin: 0; font-size: .86rem; }
  .pay p.muted { color: var(--ink-mute); }
  .pay p.bad, .fault { color: var(--bad); }
  .fault { margin: .8rem 0 0; font-size: .82rem; }
  .pay button {
    padding: .55rem .9rem; border-radius: 7px; border: 1px solid var(--line-strong);
    background: var(--raised); color: var(--ink); font: 600 14px/1.3 var(--sans); cursor: pointer;
  }
  .pay button.primary { border-color: var(--ink); background: var(--ink); color: var(--surface); }
  .pay 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>
    <div id="cancelled" class="cancelled" hidden>
      <h3>Payment cancelled</h3>
      <p>Nothing was charged. Your order is kept for a while: try again below, or cancel it.</p>
    </div>
    <ol class="steps" id="steps"><li class="current">1. Choose</li><li>2. Pay at Stripe</li><li>3. Room added</li></ol>
    <p id="headline">Checking your order …</p>
    <p id="detail" role="status" aria-live="polite"></p>
    <p id="change" hidden></p>
    <p id="room" aria-live="polite" hidden></p>
    <div class="progress" id="progress"><i></i></div>
    <div class="pay" id="pay" hidden></div>
    <p class="fault" id="fault" role="alert" hidden></p>
    <p class="foot" id="foot" hidden></p>
  </main>
<script src="/paid.js"></script>
</body>
</html>