← Back to work CRAFT LOG

Depth by border, not shadow

31 August 2026 · 5 min read

A rebrand is not repainting.

I ran one this month on an internal AI product. An agent tool I redesigned from someone else's prototype palette into ours. The easy read is that I swapped a few colours. The real work was a set of decisions about what the interface should say, and I can defend each one.

Here are four.

Depth by border, not shadow

I deleted every drop shadow in the UI. All of them. Depth is built with borders and a background shift instead.

A shadow is a guess about a light source that is not in the room. It blurs an edge to fake distance.

A border states the edge. It has a position, a colour, and a width you can read.

The cards taught me the rule. A card that hovers to a brighter wash reads as disabled, not lifted. So I made the cards hover to an indigo border instead. The edge sharpens. Nothing dims.

There is a second reason, and it is the one I care about more. An agent reads a border. border: 1px solid is a fact in the markup. A shadow that implies a panel is not.

When depth lives in the structure, the structure carries the meaning, and anything reading the DOM gets it for free. Shadows are for true overlays only. A menu that floats over the page can cast one. A card sitting in a grid cannot.

One indigo, two roles

The brand ships one indigo. #6366f1. On white it measures 4.47:1.

WCAG AA wants 4.5:1 for text. It misses by 0.03.

So one colour cannot do both jobs. It cannot decorate a surface and also carry a white button label. The label would fail contrast, and nobody would catch it by eye.

I split it. #4f46e5 is the same hue, deeper, at 6.29:1. That one takes the interactive fills and the small labels, the places where text has to pass. The lighter one decorates. One colour, two jobs, and the accessible one carries the words.

Both sit on the same OKLCH hue, so on screen it reads as one indigo that deepens where it must. Nobody sees two blues. The accessibility is invisible, which is the whole point of doing it this way.

This is a token decision, not a palette one. The name says which job. action gets the deep one. Decoration gets the light one. An agent reading the token knows which to reach for without measuring anything, which is what semantic tokens are for. A hex on its own would have handed it the 4.47 and let it ship the near-miss with confidence.

The stage rail, not the progress bar

I deleted the progress bar.

It said 62%. A 6-pixel bar filling to a percentage is true of every SaaS product ever shipped. It tells you how far along, and nothing else.

I replaced it with an 11-segment rail. Eleven segments, filled to the current stage, in that stage's own ink. It says stage 8 of 11, and here is the shape of what is left. Same pixels. More said.

The bar answers "how much". The rail answers "where, and what remains". Those are different questions, and the second is the one a person watching a build actually has. A filling bar hides the structure of the work behind a single number. A segmented rail is the structure.

There is a payoff on the grid, too. Each card fills its rail in its own stage colour, so a wall of them reads light-to-dark by maturity. You see the state of the whole workspace before you read a word.

The card also becomes a small copy of the pipeline on the detail screen. The two views rhyme instead of contradict.

Replay a rebrand with asserts

The last decision is not visual. It is about how a rebrand ships.

Upstream keeps handing me raw exports with none of this applied. So I built a script that replays the mechanical parts onto a fresh export. Typography, the tokens, the logo swap, emoji to inline SVG, the spacing scale.

Every step asserts on its anchor before it runs. If the text it expects to find is not there, the script stops and says which step and why. It does not skip the step and carry on.

That is the whole design. A brand change that half-applies is worse than one that fails outright. A failure is loud. A half-apply is silent.

A missing anchor with no assert is a screen that quietly went out with the old logo still on it. Nobody notices until a customer does.

It earned its keep. When a later build introduced a new corner radius and a third logo slot, the asserts fired on both. I re-derived the anchors and moved on.

I did not weaken the check to make it pass. A check you soften to get a green is not a check.

This is the same idea I use to build this site. A patch that cannot find the thing it is patching should fail loud, not silent. I trust the build more because it is allowed to break.

The part that isn't automatable

Four decisions. Not one of them was "make it our colour".

Each is a judgment about what the interface should communicate, and each carries a reason I can say out loud. Depth is an edge, not a guess. One indigo, split so the readable half carries the text. A rail that shows the shape of the work, not a number. A script that fails loud so a brand cannot half-ship.

An AI can repaint. Hand it a hex and it will apply the new one everywhere the old one sat. Fast, and without a complaint.

What it cannot do is decide that a shadow is a small lie. That 0.03 of contrast is worth a second token. That a percentage was the wrong question all along. Those calls came from doing the work, on this product, with these constraints in front of me.

That is the scarce part. The rest of my work sits on that difference.

The contrast ratios here were measured on white with the WCAG 2.1 relative luminance formula. Check my numbers. Back to work