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.
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__gridat1.35fr 1fr,align-items: start. Collapses to a single column under60rem."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 single1frcolumn, so the five read as a list.The counters moved up, between the eyebrow and the
<h1>.The headline dropped from
--step-4to--step-3— roughly two-thirds.No max-widths left in the hero.
max-width: 14chis off theh1andmax-width: 40remis 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 width | 5rem | 3.4rem | 0.68 |
| Narrow | 2.6rem | 2rem | 0.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 boundSo 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.
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:
ss -lptnH "sport = :4403" # pid of the real listener
readlink /proc/<pid>/cwd # prove it is yours before killing itThat 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 localhostSo whether http://localhost:4403 works depends entirely on which family the
client picks:
| Client address | Result |
http://[::1]:4403 | 200 |
http://127.0.0.1:4403 | connection refused |
http://localhost:4403 | depends 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.1toserve-variants.shso 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.
Related
[[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
