sten.wtf
Back to blog
Product

Soft Glass: rebuilding every surface on sten.wtf

Six weeks ago sten.wtf looked like a terminal. Today almost every surface is glass. Here is what changed, why we did it in that order, and the handful of things the rewrite broke on the way.

8 min read

The old sten.wtf was a console. Cascadia Code everywhere, square corners, flat navy panels, > prefixes on section labels. It was honest about what the product is — an operations tool — and it was fast. It also made every page look like a config file, and it made a lot of genuinely different things look identical.

Soft Glass is the replacement: a dark navy base, frosted panels with real depth, rounded geometry, and a display typeface that is not a monospace. The rollout is now essentially complete. This post is the honest version of how it went.

We rebuilt element types, not pages

The obvious way to redesign a dashboard is page by page. We did the opposite for the first pass and it was the single best decision in the project: buttons, cards, and modals were rebuilt as primitives, sitewide, before any individual page was touched.

That meant upgrading three components in place — Button, Card, Modal — rather than replacing them. Every existing call site kept working and simply started rendering glass. Around twenty hand-rolled fixed-overlay modals got migrated onto a shared shell in the same pass, which deleted more code than the redesign added.

After that, per-page work became cosmetic rather than structural. A page migration stopped meaning "rewrite every control" and started meaning "delete the console CSS and let the primitives show through."

The order we went in

Roughly: primitives, then toggles and toasts and notices, then the sidenav, then page by page — Overview, auth, purchase, secure, accounts, settings, wallet, the account detail view, the inbox. Public pages came last: the landing, the FAQ, the leaderboard, error pages, the shared account page.

Doing the shared vocabulary first meant the long tail got cheaper as we went, not more expensive. The last few pages were mostly deletion.

Two universal layers

Two things are rendered once, for every route, rather than per page.

The first is the atmosphere — a single fixed background layer with pre-rendered radial gradients and a fine grain. Before it, half a dozen pages each carried their own gradient blobs and scanline overlays that did not quite agree with each other. Now there is exactly one background on the site, and page shells are transparent so it shows through. The rule that falls out of that is worth stating plainly: a page shell must never declare its own opaque background, because that punches a hole in the atmosphere on that one route and nowhere else — the kind of bug that survives review because the page looks fine in isolation.

The second is the shader — an animated aurora that sits above the page rather than below it, with a bloom that follows the cursor and a ripple on click. Putting a full-viewport layer on top of a product is normally a terrible idea, and it rests entirely on two CSS properties. It never receives pointer events, so it cannot swallow a click. And it blends with screen, which can only lighten — a black source pixel is a mathematical no-op. It adds glow where it paints and is invisible everywhere else. Nothing underneath it can ever be dimmed or veiled.

Blur is not free, and repeated rows are where it bites

backdrop-filter is the whole look, and it is the most expensive property in the stylesheet. The accounts list can render eight hundred rows. Giving each one its own frosted backdrop is a slideshow.

So the rule across the codebase is that repeated-row surfaces use a flat card variant with no per-row blur, no per-row spring animation, and content-visibility: auto. Glass is for surfaces you have a bounded number of: panels, modals, the dock, toasts. Toggle switches deliberately have no blur for the same reason — they live in long lists of bot commands and shader parameters.

The related discipline is entrance animation. Individual list items get CSS transitions, never a JavaScript spring per row. Springs are for things there is one of.

The lens periphery, and three rules we learned the hard way

A late addition blurs the far left and right edges of the viewport, so the site has a soft optical periphery. It took four attempts.

  • Backdrop-filter replaces the backdrop. It cannot add a blurred copy over a sharp one. A full-viewport blur necessarily blurs your text. Masking is not a refinement of the idea — masking is the entire idea.
  • The mask must be edge bands, not a radial.A radial gradient measures distance from the centre, so a card sitting 124px from the left edge lands deep in the gradient purely because it is also near the bottom. We shipped the landing page's corner cards unreadable exactly that way.
  • No top or bottom band. Side gutters are stable. Content scrolls through the horizontal edges continuously, so any band there eventually blurs live text.

The rim width is measured rather than chosen: the closest text to a side edge on any public route at 1440×900 is 48 pixels, so the blur has to be done well before that.

Redesigns surface real bugs

This is the part nobody puts in the announcement. Rebuilding a surface means reading it properly, often for the first time in a year, and that finds things.

  • The auth flow remounted the login form when two-factor activated, which silently reset in-place 2FA back to the credentials step. It had been doing that for as long as the step existed.
  • The tip modal parsed amounts against the wrong exponent, so a typed "1" sent a thousandth of a SOL. That one was a money bug, and it was found by rewriting a form.
  • Email subjects encoded per RFC 2047 rendered as raw =?utf-8?B?…?= gibberish. Decoding at the source fixed it in the inbox and on shared account pages at once.
  • Inbox unread counts came only from live socket events, so a page reload silently zeroed them. They are a real server-side read watermark now.
  • In the Module Builder, a click inside a nested block bubbled to the outermost container — invisible on desktop, where you drag, but click-to-add is the only path on touch. A block could not be placed inside a conditional branch on a phone. At all.

None of those were design problems. They were found because a redesign is a forced code review with a deadline.

Every pass ships guard tests

A design system decays quietly. Someone adds a page, reaches for the nearest-looking class, and six months later there are two vocabularies again.

So each migration lands with tests that assert the shape of the result: the demolished console classes are actually gone, the new recipes exist, the primitives are the ones being used, the atmosphere is not punched through, the shader layer keeps its two load-bearing properties. They are not testing that the page looks good. They are testing that the decisions survive the next person who did not read the spec.

What is left

The API documentation page is still console, deliberately — it is reference material, and a monospace reference page is not a design failure. Tables and long-form forms outside the migrated routes still wear flat-navy chrome. And there is a final token de-duplication pending: the landing page keeps its own scoped copy of the design tokens alongside the global set, which is one copy too many.

Everything else — the dashboard, purchase, wallet, accounts, settings, secure, inbox, the public pages, the error states — is glass.