After we’d shipped the colored borders for in-maintenance and completed sites on the multi-site list, real-world usage produced an unexpected report: “the running site somehow looks weak.” Followed by: “the green border that’s supposed to stick for 24 hours doesn’t show up.”
The first one was surprising. The running site has a pulsing blue border — it should be obvious. But when I looked at the screen again, yes, it did look weak relative to its neighbors. The problem turned out not to be the running-state color itself, but a hierarchy among the three states that had quietly inverted. This post walks through that visual-hierarchy collapse and rebuild, along with the “design oscillation” we’d accidentally introduced one round earlier.
At a glance, “running” looked weaker than “pending”
The multi-site list has three states:
- Pending — sites queued up to be processed next
- Running — the site currently being processed
- Completed — sites finished within the last 24h
The old CSS painted them like this:
/* Old: no background for running, faint background for pending */
.site-running {
border-color: #2563eb;
/* no background-color */
animation: pulse 2.2s ...;
}
.site-pending {
border: 1px dashed #2563eb;
background-color: rgba(37, 99, 235, 0.02);
}
“Running has a pulse, so it doesn’t need a background. Pending gets a subtle fill.” Sounds reasonable on paper. In actual use it produced the opposite effect.
Pending sites had a dashed border + a faint blue background — making their whole area speak. Running sites had no fill and asserted only through their border line. Filled areas are louder than lines, so the visual hierarchy was actually pending > running. “The running site looks weak” wasn’t a perception bug. It was a design bug.
Express the hierarchy through background alpha
The fix consolidated everything onto a single principle: assign a background color to all three states, and use alpha values to encode the hierarchy.
.site-running {
border-color: #2563eb;
background-color: rgba(37, 99, 235, 0.08); /* loudest */
animation: pulse 2.2s ease-in-out infinite;
box-shadow: 0 0 0 6px rgba(37, 99, 235, 0.0); /* pulse a touch stronger */
}
.site-completed {
border-color: #10b981;
background-color: rgba(16, 185, 129, 0.05); /* middle */
box-shadow: inset 0 0 0 1px rgba(16, 185, 129, 0.25);
}
.site-pending {
border: 1px dashed #2563eb;
background-color: rgba(37, 99, 235, 0.02); /* faintest */
}
The three alphas line up 0.08 > 0.05 > 0.02. That makes the running > completed > pending visual hierarchy numerically guaranteed. The regression test reads exactly that:
// test sketch
function test_running_alpha_is_higher_than_pending() {
const r = getComputedAlpha('.site-running');
const p = getComputedAlpha('.site-pending');
assert(r > p, 'running should be louder than pending');
}
It’s tempting to say “as long as the tones feel right, that’s fine.” But encoding the hierarchy in numeric alpha values pulls visual-design intent into a form a machine can verify. Someone swapping the values later? CI catches it.
The second report — “the 24h green border doesn’t show up”
The same feedback included “the completion green border isn’t appearing.” I thought it was a separate bug, but it turned out to be a side effect from the previous round.
As written up in Detecting the running site from streaming logs, to fix the log-rewind false-detection bug, we’d abolished the “mark previous done on site switch” entirely and moved completion marking to a single batch at run-end. The false positives went away — but so did the green flash on the previous site during normal forward progression.
Normal forward run: when the system goes from site A to site B, the user expects “site A turns green right at that moment.” With our previous fix, nothing turned green until the whole run finished. The blue pulse on the running site was the only visual signal in between.
Fix: “mark complete only on forward moves”
We wanted to keep the rewind defense but also bring back the mid-run green marking. The two looked mutually exclusive, but distinguishing forward moves via the execution-order index made both possible.
function _setRunningSiteId(siteId) {
const newIdx = _runningSiteIdOrder.indexOf(siteId);
const curIdx = _runningSiteIdIndex;
const isForwardMove = newIdx === -1 || newIdx >= curIdx;
if (isForwardMove && _runningSiteId && _runningSiteId !== siteId) {
_markSiteCompleted(_runningSiteId); // mark previous done only on forward moves
}
// Rewinds (newIdx < curIdx): do nothing (keep the previous-round defense)
_runningSiteIdIndex = newIdx;
_runningSiteId = siteId;
}
For site IDs not in the planned order (partial runs, unexpected sites), newIdx === -1 falls back to “treat as forward.” The result:
- Normal forward run (A → B): A turns green ✅
- Log rewind (A reappears): ignored ✅
Both goals satisfied. “Rewind defense” and “restore forward-move state changes” had looked binary at first — adding one line for movement-direction detection is what reconciled them. Clean convergence for what had felt like an oscillating design.
Reflection — don’t treat “design oscillation” as failure
Looking at the timeline:
- V33 initial cut: mark previous done on every switch
- V43’s fix: kill switch-time marking to stop log-rewind false positives → batch-mark at run-end
- This round: “the 24h green disappears” side-effect surfaced → restore forward-only marking to satisfy both
You could say “we should have nailed it from the start.” But the other framing is that minimal fixes for real-world problems, then minimal fixes for the side effects of those fixes is a healthy progression. Designing the complete “rewind defense + forward-detection” combo upfront — before hearing actual usage feedback — is close to impossible.
Lessons — verify visual hierarchy with alpha values
Three principles to keep:
- Filled area beats line in visual loudness. What looked like a confident pulsing border on the running state got out-shouted by a pending state with a quiet background fill. In a UI with three states, the most important state needs the strongest fill — not just the strongest border
- Express visual hierarchy with numeric alphas — then test those numbers. Write
0.08 > 0.05 > 0.02into the CSS, assertrunning > completed > pendingin a test, and any future change that flips the values fails CI. Translate design intent into a form machines can guard - “Design oscillation” is a healthy progression, not failure. Side effects from a fix get fixed by the next round. Choices that initially look binary (rewind defense vs forward-move marking) often reconcile with one extra distinguishing axis. Chasing the perfect version upfront is less responsive to real usage than oscillating toward it
Reports of the form “X should stand out but doesn’t” usually trace back to the relationship between X and its neighbors, not to X’s color itself. When you’re building a UI that puts three states side by side, deciding upfront — and encoding numerically — which one should be loudest saves the rebuilding later.