Decision log

DL-001 to DL-031. Rendered from DL.md at each commit.

Numbered decisions for hanneszetterblom.com, newest last. An entry is written before the work it covers starts.

DL-001 (2026-09-21) Stack: plain HTML, CSS and one JS file

Decision: Build the site as hand-written HTML, one stylesheet and one vanilla JS file with no framework, build step or dependencies, and migrate to Astro when notes exceed five.

Rationale: One page and a few notes do not need a build. With no build there is nothing between an edit and a deploy that can break, and the Content-Security-Policy can forbid all inline and third-party code.

Alternatives rejected: Astro from the start (a build step and dependencies with no benefit at this size); a hosted site builder (no repo, no log, no control of the markup).

DL-002 (2026-09-21) Domain: .com canonical, .ai redirects

Decision: hanneszetterblom.com is the canonical host, and www and hanneszetterblom.ai (if registered) redirect to it with a 301.

Rationale: The site has to rank for the name, and one canonical host keeps every link and signal on one URL. The .com form is what people type when they guess an address.

Alternatives rejected: .ai as canonical (reads as a product, not a person); serving several hosts without redirects (duplicate URLs).

Amended 2026-09-22 by DL-022: the www redirect runs as a Cloudflare Redirect Rule at the zone, not from a _redirects file.

DL-003 (2026-09-21) Hosting: Cloudflare Pages through Git

Decision: Host on Cloudflare Pages with the Git integration, no build command, and the repo root as the output directory.

Rationale: A push to main is the deploy, so the repo is the only source of what is live. The _headers and _redirects files keep the security headers and the www redirect in the repo, reviewed like any other change.

Alternatives rejected: GitHub Pages (no custom response headers, so no CSP or HSTS); direct uploads from a local CLI (deploys outside Git history).

Amended 2026-09-22 by DL-022: only _headers stays in the repo; the _redirects file is deleted.

DL-004 (2026-09-21) One accent color, three uses

Decision: The accent color appears in exactly three places: the four Right now numbers, the single call-to-action link, and link underlines on hover.

Rationale: A reader gives the page 60 to 120 seconds. Color marks the two things that matter in that time, the live numbers and the one action, and every further use would weaken both.

Alternatives rejected: accent on headings and links (marks everything, so marks nothing); no accent (the numbers and the action do not stand out).

Amended 2026-09-22 by DL-015: the heatmap is a fourth use.

Superseded 2026-09-22: the four Right now numbers no longer exist. The accent uses are the heatmap cells, the call to action and link hover; the linked figures in prose are ordinary links, ink with the accent on hover.

DL-005 (2026-09-21) This log is public

Decision: DL.md is public in the repo and linked from the footer of every page.

Rationale: The site claims a way of working: numbered decisions, written before the work. A public log makes that claim checkable instead of asserted.

Alternatives rejected: a private log with a summary on the page (not checkable); no log for a site this small (the site would not be an instance of the system it describes).

Superseded 2026-09-22 by DL-026: the log stays public as a page at /decisions/; the repository is private.

DL-006 (2026-09-21) Left off the page: credentials, adjectives, a full CV

Decision: The page has no credentials list, no adjectives about Hannes and no full CV; the bio is one line.

Rationale: Every claim on the page is tied to an artifact or a number, and a credentials list or a self-description is neither. The one-line bio gives the timeline without turning the page into a CV.

Alternatives rejected: a CV section or PDF (invites reading titles instead of artifacts); testimonials and skill lists (not verifiable on the page).

Amended 2026-09-22 by DL-021: the one-line bio became the era block.

DL-007 (2026-09-21) Contrast ruling: light ink-3 is #66707A

Decision: The light-mode ink-3 token is #66707A, and the spec value #7C868F is rejected for failing WCAG AA.

Rationale: #7C868F measures 3.36:1 on bg and 3.71:1 on panel, below the 4.5:1 that the quality floor requires for all text. #66707A measures 4.57:1 on bg and 5.04:1 on panel. Dark-mode ink-3 already passes (5.28:1 on bg, 4.86:1 on panel) and is unchanged.

Alternatives rejected: keeping the spec value (fails AA and the Lighthouse contrast audit); using ink-2 for tertiary text (removes the third text level).

DL-008 (2026-09-21) Typeface: Familjen Grotesk replaces IBM Plex Sans

Decision: Familjen Grotesk (400, 500, 600 and 400 italic) replaces IBM Plex Sans everywhere, and Literata (400 and 400 italic, with its optical size axis) replaces IBM Plex Serif for note bodies.

Rationale: Plex now appears in circulated prompt templates as the "technical" default. Familjen Grotesk has tabular figures by default (every digit advances 680 of 1200 units at weights 400 to 600), so the count-up does not jitter and stays enabled.

Alternatives rejected: Inter, Geist, Space Grotesk, keeping Plex.

DL-009 (2026-09-21) Accent: instrument amber replaces blue

Decision: The accent is instrument amber, #8A5209 in light mode and #E3A94E in dark mode; the accent-bg token is removed and the call to action is a plain underlined text link in the accent.

Rationale: Blue, indigo and purple are where generated sites land. Amber is the color of readouts, and it is used only where the page shows live data and for the one action. Both values pass WCAG AA: 5.78:1 on bg and 6.38:1 on panel in light mode, 8.99:1 on bg and 8.27:1 on panel in dark mode.

Alternatives rejected: keeping blue; no accent at all.

Amended 2026-09-30 by DL-031: the timeline bars are an accent use, since they encode measured dates like the heatmap cells.

DL-010 (2026-09-21) Readout: a vertical list with a source per value

Decision: The Right now values are a vertical readout list, one row per value with its source under the label, replacing the four-cell grid.

Rationale: Horizontal stat rows are a listed generated-design tell and carry no provenance. A row per value has room for a source line, so every number says where it comes from.

Alternatives rejected: the grid; a single line.

DL-011 (2026-09-22) Uneven sections

Decision: The three system sections have different sizes and shapes: system 1 carries a four-lane diagram, three metric lines and an excerpt from this log, system 2 a diagram and a table, system 3 two lines under a small heading.

Rationale: Three identical templates read as a skeleton. The material sets each section's size.

Alternatives rejected: three identical templates (heading, summary, diagram, three bullets, link).

DL-012 (2026-09-22) Centered column

Decision: The reading column is centered at 42rem with left-aligned text. This reverses ruling 3 of HALT 2, which kept the column left-anchored.

Rationale: Eight of ten reference sites center the column and left-align the text. Distinctiveness lives in the one decision, not the margin.

Alternatives rejected: the left-anchored column (ruling 3); centered text.

DL-013 (2026-09-22) No count-up

Decision: The Right now values render immediately; the 600 ms count-up is removed.

Rationale: The animation read as a stat banner in the audit.

Alternatives rejected: keeping the count-up behind the reduced-motion check.

DL-014 (2026-09-22) Readout scope

Decision: The readout shows TimeBox, decisions and last ship only; the odds data point count is removed from the page and from the stats.json contract.

Rationale: The publication's dataset is off-topic on this domain and belongs on the publication.

Alternatives rejected: keeping the odds data point count as a fourth value.

DL-015 (2026-09-22) The record is the record

Decision: The heatmap runs from the first logged day, 7 April 2026, and shows the September gap.

Rationale: A live page that hides its own gaps is not a live page.

Alternatives rejected: starting the series after the gap; a rolling window that would drop the gap as it ages out.

DL-016 (2026-09-22) Body text 18px

Decision: Body text is 18px on desktop and 17px under 560px; the root font size stays 16px so the hero, readout and heading sizes in rem are unchanged.

Rationale: The target is 75 to 85 characters per line. Measured on the rendered page at 18px, full lines in the 42rem column carry 81 to 95 characters, 87 on average, which scales to about 98 at the previous 16px. The target is not reached yet; reaching it takes 20px in this column or a 38rem column at 18px, which is a separate ruling.

Alternatives rejected: narrowing the column to about 34rem (would shrink the heatmap and the readout with it).

DL-017 (2026-09-22) Light only

Decision: The site renders in one light palette; the dark tokens and the prefers-color-scheme blocks are removed and color-scheme is light.

Rationale: One palette is one set of contrast ratios to verify and one appearance in every screenshot, preview and viewer.

Alternatives rejected: keeping both palettes; dark by default.

DL-018 (2026-09-22) No diagrams, tables, TODO blocks, empty sections or nav

Decision: The page has no diagrams, no tables, no dashed TODO blocks, no empty sections and no navigation. A section whose content is missing is omitted, and nothing labelled TODO renders.

Rationale: A one-page site reads top to bottom. The diagrams, the table and the nav were structure standing in for content, and a visible TODO on a live page is an unfinished page.

Alternatives rejected: keeping the diagrams as illustrations; the v1 convention of visible TODO blocks as placeholders.

Amended 2026-09-30 by DL-031: the timeline encodes dates the way the heatmap encodes hours; decorative diagrams stay out.

DL-019 (2026-09-22) Context before data

Decision: The page order is the person first (Me in 10 seconds), the work second, the heatmap third, then beliefs, method and the offer.

Rationale: A number means nothing until the reader knows who logged it and why. The heatmap lands after the person and the work.

Alternatives rejected: the v1.1 order with the heatmap directly under the header.

DL-020 (2026-09-22) Two faces

Decision: Literata (400, 400 italic, optical size) for prose; Familjen Grotesk (400, 500, 600) for the h1, section headings, era years, dates, numbers, labels and the heatmap.

Rationale: The serif carries the reading. The grotesk marks everything that is a label or a figure, so numbers in prose read as data.

Alternatives rejected: Familjen Grotesk for everything (DL-008 as first applied); one serif for everything.

Amended 2026-09-22: numbers and dates inside sentences render in Literata like the surrounding text; the grotesk stays on the era years, the timestamp line, the heatmap labels and the footer.

DL-021 (2026-09-22) Era block after sive.rs, beliefs list after nat.org

Decision: The person is introduced by an era block (a year column and one sentence per era), and the page carries a list of beliefs with one sub-point each.

Rationale: The ten-site study found the person first, proof as linked nouns, and a worldview list as the memorable element.

Alternatives rejected: a one-line bio; a CV section; a page with proof and no worldview.

DL-022 (2026-09-22) The www redirect is a zone Redirect Rule

Decision: The redirect from www.hanneszetterblom.com to the apex runs as a Cloudflare Redirect Rule at the zone (hostname www.hanneszetterblom.com, 301, path and query preserved), and the _redirects file is deleted.

Rationale: Cloudflare Pages reads _redirects sources as paths, not hostnames, so the file's www rule never fired and www served the site with 200. The zone rule was verified from outside after it was set.

Alternatives rejected: keeping the inert _redirects file in the repo; a Bulk Redirect list.

DL-023 (2026-09-22) Stale-asset incident: cache headers and stamped asset URLs

Decision: /style.css and /script.js are served with Cache-Control: public, max-age=0, must-revalidate from _headers, and every HTML page references them as /style.css?v=STAMP and /script.js?v=STAMP, where STAMP is the short hash of the two files' staged contents, written by the pre-commit hook in .githooks. /assets/* and the root images are cached for one week.

Rationale: After the v2.0 merge the new HTML went live while browsers kept the v1.1 stylesheet, and the page rendered broken for four hours. The custom domain served style.css with max-age=14400, the zone's default 4-hour Browser Cache TTL, although the HTML carried max-age=0; Cloudflare applies whichever is higher. A stylesheet must never outlive the HTML that references it. The stamp changes the URL whenever the file changes, so a fresh page cannot pick up a cached old asset; the header stops the browser from holding the file past a revalidation, and takes effect on the custom domain once the zone's Browser Cache TTL is set to Respect Existing Headers.

Alternatives rejected: the commit's own short hash as the stamp (it cannot be known before the commit exists without a build step); a version constant bumped by hand (a step that can be forgotten).

DL-024 (2026-09-22) Copy rewrite: the facts that make the page mine

Decision: The header sentence, the era block, What I am building, the beliefs list, How I work and the offer are rewritten with the specifics: the April 2023 investor-deck slide, the four regime changes, the Clar work, and the audit method becoming doctrine.

Rationale: Mechanisms without the facts read generic; the 2023 slide, the four regime changes and the Clar specifics are the evidence that makes the page mine.

Alternatives rejected: keeping the v2.0 copy; moving the facts into a separate CV section.

DL-025 (2026-09-22) Era lines tightened

Decision: The 2015-2025 era line describes the work rather than its size, and the figure from a former employer's confidential M&A process is removed from the Clar line.

Rationale: The reader is deciding whether to work with me, and the work answers that; a size figure only invites the question of scale. The acquisition figure came from a confidential process at a former employer.

Alternatives rejected: keeping the size figures; keeping the acquisition figure (not mine to publish).

DL-026 (2026-09-22) The decision log is served at /decisions/; the repository is private

Decision: DL.md is rendered as a page at /decisions/ in the notes style, linked from What I am building and from every footer, and the repository is private.

Rationale: The log is the public artifact; the repository is not. A rendered page keeps the log readable without exposing the working files, and the pre-commit hook re-renders it whenever DL.md changes.

Alternatives rejected: keeping the repository public for the sake of one file; a log excerpt on the home page instead of the full log.

DL-027 (2026-09-30) Fact-check pass: every claim against its source

Decision: The page's claims are checked against their sources and every overstatement is rewritten: the 2023 slide, the betting era (2017-2025, three markets, a team of up to seven, four regime changes, two loss months by April 2023), recruiting rather than hiring at Clar, a watch app built rather than written, lower bounds for the publication's counts, KTH as an affiliation rather than an alma mater in the JSON-LD, and an og image that carries the current header sentence.

Rationale: A page whose premise is that the record is the record cannot carry claims its own sources contradict: the deck names a 52-person product team becoming less useful, not a market leader's content team becoming redundant, and the publication's About page, which this page links, says 2017-2025 and three markets. The team size stays because the header's "the machinery was people" needs it, and lower bounds stay true between deploys where exact counts go stale.

Alternatives rejected: keeping the v2.2 wording; exact counts for the publication; profit figures (size again, which DL-025 took off the page).

Amended 2026-09-30 by DL-029: the betting era runs 2016-2025 and is told by its phases, without the four regime changes or the loss months.

DL-028 (2026-09-30) Focused hours published nightly from the database

Decision: A GitHub Actions job runs tools/update-stats.py every night at 02:40 UTC and whenever the job itself changes; it reads every session from the TimeBox database with the publishable key, applies the dashboard's day rules, and rewrites stats.json, the seed block and the static figures in index.html through the last complete day, where the heatmap now ends; the week figure becomes the last seven days, and decisions_logged leaves the contract.

Rationale: The page said live while it showed 22 September for eight days, and it drew the days after that as empty although 13 hours were logged in them: an instrument that invents gaps is worse than one that hides them. A nightly commit keeps the site static, the CSP unchanged and the figures readable without JavaScript, and the job fails closed on implausible rows, so a bad row never reaches the page.

Alternatives rejected: a Pages Function reading the database per request (server code for a figure that changes a few times a day); fetching the database from the browser (a new external request); updating by hand (the failure that happened).

DL-029 (2026-09-30) The betting era by phase

Decision: The betting era runs 2016-2025 and names its markets (Sweden, Norway, Canada), its phases (sports betting from 2017 to 2021, slots from 2021 to 2024 through Excel models of game variance and Monte Carlo pricing of each bonus, both from 2024) and the work that ran alongside it (sales, KTH, Clar); the four regime changes and the loss-month sentence leave the line.

Rationale: The line now says how the income was made in each phase, which is the work DL-025 asked it to describe, and the loss-month count goes with the other result figures DL-025 took off the page. The wording holds to what can be stated as fact: variance measured from collected data is an estimate, so the models mapped it rather than calculated it exactly, and they are named rather than rated.

Alternatives rejected: keeping 2017-2025 and the loss-month sentence; the per-bonus volatility rule (high volatility for a withdrawable win, low volatility on the highest-RTP games) as too granular for the page; "exact" volatility and "advanced" models (an overstatement and a self-rating).

DL-030 (2026-09-30) Copy pass for voice, same facts

Decision: Every section is rewritten for concreteness and rhythm without new claims: a sharper header sentence that says nine years to match DL-029, the betting era in DL-029's facts but shorter, the bracelet brand named, a shorter Clar line, the CRM audit told as a sequence, the watch app described by what it does, a plainer offer, and section headings that name what they hold.

Rationale: The v2.3 copy was accurate but read like a CV. Wording changed and facts did not, so every figure keeps the source DL-027 checked it against, and the betting facts stay Hannes's own (DL-029).

Alternatives rejected: adding claims for colour; keeping the v2.3 wording.

DL-031 (2026-09-30) Layout v3: display header, margin headings, timeline

Decision: The header sentence is set at display size; from 1100px each section heading sits in a left column beside the 42rem text column and stays in view while its section scrolls; 2014 to now opens with a timeline of every track on a shared 2014-2026 axis, the betting business in its three phases; the heatmap fills the text column.

Rationale: The page had one visual idea and an empty margin at desktop widths. The timeline shows at a glance what the era list can only state, that the tracks ran in parallel, and it uses the heatmap's language: amber marks measured facts and the one action, nothing else.

Alternatives rejected: bars inside each era row (too far apart to compare); a dark or cream theme; cards, icons or animation.