Lossless Changelog
product

The Hero Grows a Right-Hand Column, and the Dev Server Lies Twice About Where It Lives

The landing page now leads with the copy on the left and the five curated filters on the right, one row, no max-widths. Getting there took four small CSS changes and one genuinely instructive detour: a local server can be running, healthy, returning 200 — and still be serving the wrong build at an address your browser cannot reach. Both halves of that lie are worth writing down, because both were reported as success first.

Michael StatonMusic Editoraugmented with Claude Code on Claude Opus 5 (1M context)

Changelog — 2026-09-12 (#1)

Why Care?

A catalogue's landing page has one job: get you filtering. Ours was making you scroll to find out that was even an option — headline, lede, counters, two buttons, and then, below all of it, the five filters that are actually the fastest way into 1,765 tools.

The hero is now one row. Copy on the left, filters on the right, both visible at once. The headline came down a step so it stops shouting over the thing it is introducing, and the counters moved up under the eyebrow where they read as credentials rather than as an afterthought.

The second half of this entry is about local development, and it earns its space because the failure it describes is one we have now hit three times on this project in three different costumes. A server that is up is not a server that is serving what you think, where you think.

What's New?

  • The hero is a two-column grid. .shell.hero__grid at 1.35fr 1fr, align-items: start. Collapses to a single column under 60rem.

  • "Start with a filter" moved into the right-hand track, and its list went from a reflowing repeat(auto-fit, minmax(15rem, 1fr)) card grid to a single 1fr column, so the five read as a list.

  • The counters moved up, between the eyebrow and the <h1>.

  • The headline dropped from --step-4 to --step-3 — roughly two-thirds.

  • No max-widths left in the hero. max-width: 14ch is off the h1 and max-width: 40rem is off the stats. The grid tracks hold the line length now.

The hero, before and after

BEFORE                              AFTER
┌────────────────────────────┐      ┌──────────────────┬─────────────────┐
│ The Lossless Group         │      │ The Lossless…    │ START WITH A    │
│ ╔══════════════════════╗   │      │ 1765  744  13 10 │ FILTER          │
│ ║  THE TOOLS WE'VE     ║   │      │ ─────────────────│ ┌─────────────┐ │
│ ║  FOUND WORTH SAVING  ║   │      │ ╔══════════════╗ │ │ AI Toolkit  │ │
│ ╚══════════════════════╝   │      │ ║ The tools    ║ │ ├─────────────┤ │
│ 1765 catalogued tools…     │      │ ║ worth saving ║ │ │ Agentic AI  │ │
│ ───────────────────────    │      │ ╚══════════════╝ │ ├─────────────┤ │
│ 1765  744  13  10          │      │ 1765 catalogued… │ │ Founder…    │ │
│ [Explore] [Verticals]      │      │ [Explore] [Vert] │ ├─────────────┤ │
│ START WITH A FILTER        │      │                  │ │ Must Have   │ │
│ ┌────────┬────────┬──────┐ │      │                  │ ├─────────────┤ │
│ │ AI Kit │ Agentic│ …    │ │      │                  │ │ Check It Out│ │
└────────────────────────────┘      └──────────────────┴─────────────────┘

Two-thirds, and why that is a scale step and not a calc

The ask was "about two-thirds as big." The literal implementation is calc(var(--step-4) * 0.66), and it would have worked.

We used --step-3 instead, because the house runs a modular type scale and an off-scale heading is a heading that will not match any other heading on the site. The two land in the same place where it matters:

--step-4 (was)--step-3 (now)ratio
Full width5rem3.4rem0.68
Narrow2.6rem2rem0.77

At desktop widths that is two-thirds to within a rounding error. At narrow widths it is closer to three-quarters, because the two clamp() curves do not scale proportionally — an honest divergence from the literal request, taken so the heading stays on the scale. If a true two-thirds at every width turns out to matter, the calc is a one-line swap.

The pattern: up, healthy, 200 — and wrong

Three local servers were running for a side-by-side variant comparison. The session's job was to iterate on one of them. Two separate lies followed, and both were initially reported as success.

Lie one: the process you killed is not the process holding the port

Swapping the static preview on :4403 for a hot-reloading dev server meant stopping the old one. The recorded pid was killed, cleanly. But pnpm preview is a wrapper — the thing actually bound to the socket is its astro child, and killing the parent orphaned it:

kill 134248  (pnpm)   →  gone
              └─ 134264 (node astro.js preview --port 4403)  →  ALIVE, still bound

So the new dev server found :4403 busy and — silently, with no warning and a zero exit code — auto-incremented to :4404. The verification step then curled :4403, got a 200 and a complete page, and confirmed the new layout had shipped. It had not. That was the old build, from a throwaway worktree, wearing the right port number.

This is the same defect class as [[2026-08-23_04]]'s closing lesson and the one serve-variants.sh was hardened against last week: the response was valid, the page was real, and the conclusion drawn from it was false. The script already knew to read the bound port back from the server's own log rather than trust the one it asked for. The manual path around the script did not inherit that discipline, and paid for it immediately.

The rule, generalised: never assert on the port you requested. Assert on the port the server reported.

BASH
bound=$(grep -oE 'localhost:[0-9]+' dev.log | head -1 | cut -d: -f2)

And when freeing a port, identify what is actually holding it rather than what you think is:

BASH
ss -lptnH "sport = :4403"          # pid of the real listener
readlink /proc/<pid>/cwd            # prove it is yours before killing it

That second line matters. The whole reason serve-variants.sh stopped killing ports in the first place is that the process on a port is often someone else's — a dev server the operator started deliberately. Checking cwd turns "kill whatever is in the way" into "kill the thing I orphaned."

Lie two: reachable for me, refused for you

With the right build finally on the right port, the servers were reported as up. The operator's browser disagreed.

Astro binds to localhost, and on this box that resolves to the IPv6 loopback only:

LISTEN  [::1]:4401        LISTEN  [::1]:4402        LISTEN  [::1]:4403

/etc/hosts carries both entries, which is what makes this ambiguous rather than simply broken:

127.0.0.1 localhost
::1       localhost

So whether http://localhost:4403 works depends entirely on which family the client picks:

Client addressResult
http://[::1]:4403200
http://127.0.0.1:4403connection refused
http://localhost:4403depends on the client's resolution order

curl on this machine happened to pick IPv6 and reported healthy. A browser that picks IPv4 gets nothing. Both are looking at the same running server. "It works on my machine" was, in this case, "it works on my resolver."

The durable fix is to bind explicitly rather than let localhost arbitrate — --host 127.0.0.1 on the preview and dev invocations in serve-variants.sh. Noted, not yet applied.

Why this keeps happening here

Three times now, on three different surfaces, this project has produced a defect that passes every automated check and is obvious the instant a human loads the page. The build passes. curl returns 200. The HTML is well-formed. And the thing on screen is wrong.

The handoff called browser drives "the only rung that sees this class of defect" and said they should exist before the next feature rather than after it. This session is one more data point for that, with a wrinkle: the drive has to assert against the address and build under test, not against a URL that merely resembles it. A browser drive pointed at :4403 would have confirmed the old build just as confidently as curl did.

What's next

  • Apply --host 127.0.0.1 to serve-variants.sh so the bind is not left to resolver order.

  • The Playwright drive named in [[VARIANT-NOTES]] — now with the added requirement that it resolve its target port from the server log.

  • Decide whether two-thirds should hold at every width, or only where it reads.

  • [[2026-09-08_01]] — the full corpus, and the five defects a 10% sample hid

  • [[2026-08-23_04]] — what the two spikes agreed on, and the lesson that cost the most

  • [[Use-Spikes-to-Converge-on-Spec]] — the handoff this work is still executing against