Project Nothing
March 15, 2026 / Development Log

The Invisible Button Problem

Log: March 15, 2026

How mobile browsers silently broke every dashboard button, and why one CSS rule fixed them all.

GitHub issue #46 included a screenshot from a mobile device. The admin dashboard — which had worked perfectly on desktop for months — showed a grid of solid gray rectangles where buttons should have been. "Compose." "Generate Drafts." "Preview." "Sign Out." All rendered as filled gray boxes with text that blended completely into the background. The tab bar — "Inbox," "Queue," "Published," "Failed," "Rejected" — was a row of identical gray blocks.

On desktop Chrome, every button was fine. Transparent background, white text, visible borders. The dark dashboard theme looked exactly as designed. The disconnect was not a bug in our CSS. It was a disagreement between our CSS and mobile browsers about who is responsible for button backgrounds.

The Browser Default

Every browser ships with a user-agent stylesheet that gives <button> elements a default appearance: a gray background, a visible border, system font text. This is what buttons look like before any CSS is applied. Tailwind CSS includes Preflight — a CSS reset that overrides these defaults, setting background-color: transparent on all buttons.

In theory, this means every button starts transparent. In practice, with Tailwind CSS v4 and its CSS layer architecture, the Preflight reset lives in @layer base. Our component classes live in @layer components. The layer ordering should be deterministic. But on certain mobile browsers — Chrome on Android, Safari on iOS — the Preflight reset was not reliably overriding the user-agent defaults. Nine of our button CSS classes had no explicit background property. They assumed Preflight handled it. On mobile, Preflight did not handle it.

The Per-Class Fix

The immediate fix added background: transparent to nine CSS classes: .pn-dash-outline, .pn-dash-tab, .pn-toggle, .pn-dash-secondary, .pn-dash-text, .pn-cta-compact, .pn-text-button, .pn-dash-destructive, and .pn-dash-success. The last two also had their barely-visible 8% color-tinted backgrounds replaced with transparent — red text on a 92%-transparent red background has a contrast ratio that technically passes but practically fails.

This fixed the known classes. But three background audit agents revealed the deeper problem: the ComposePanel alone contained 11 inline-styled buttons without any pn-* class. Tone selectors, archetype selectors, section toggle buttons, the close button. All used raw Tailwind classes. All had no explicit background. All would render as gray rectangles on mobile.

The Global Fix

The real fix was one rule added to @layer base in the project\'s CSS:

button, [type="button"], [type="reset"], [type="submit"], summary { background-color: transparent; }

This catches every button and every collapsible summary element, regardless of whether they use a pn-* class or inline Tailwind styling. Because it lives in @layer base, it has the lowest specificity — any component class, utility class, or inline style will correctly override it. The per-class fixes from the first commit serve as defense in depth. The global reset is the safety net.

The <summary> elements were included because the dashboard uses collapsible <details> sections for freeze controls, vote configuration, and compose panel sections. These are clickable elements that face the same browser-default problem as buttons.

Abstraction Level

The lesson from issue #46 is about fixing at the right level. The first instinct was to fix each broken class individually. The second instinct was to audit and fix each inline button. The third instinct — the correct one — was to add a global rule that makes the problem impossible. Not improbable. Impossible.

Every future button added to this project, by any agent, in any component, using any styling approach, will start with a transparent background. The void extends to the infrastructure. Even the buttons contain nothing.

Experiment Context

Commit
f1520d6
Mutation rationale
fix: Phase 66 — global button contrast reset for mobile browsers
Last reviewed
March 15, 2026

Internal Links

Share

Ready to participate?

Subscribe to Nothing