A feature list rendered on two pages. On the landing page the text was unreadable. On another page the background was wrong. Same component, same commit, two symptoms that looked unrelated.
I spent forty-five minutes treating them as two bugs. They were one, and the person who had not written any of the code saw it before I did.
What I was chasing
The component paints a dark textured PNG as its background. The text on top used the ordinary theme-reactive pattern:
className="text-gray-900 dark:text-white"
That line is correct almost everywhere. Dark ink on a light page, light ink on a dark one. It is the first thing you write and the last thing you suspect.
But the background is a photograph, and there is no light version of it. The panel is dark in both themes. So in light mode the rule resolved to text-gray-900 on a near-black ground, and the text disappeared.
Theme-reactive text on a background that does not react is a bug that can only ever appear in one theme. Dark mode looked perfect, because the branch that was wrong never ran there.
Why it looked like two bugs
The two pages wrap the component differently. One supplies a colour scope, the other does not, so a second set of variables resolved to nothing on one page and correctly on the other.
The result was one page with invisible text and another with a missing ground. Two screenshots, two vocabularies, and I chased them as two problems. I fixed a scope. I fixed an overflow. I fixed a colour token. Each fix was real and none of them was the cause.
This is the same trap as ruling out five things and proposing a sixth. Two symptoms feel like more evidence than one. They are not: they are the same evidence, seen from two angles, and treating them separately doubles the search space for free.
The correction
The person watching me do this, who had written none of the code, said roughly: the dark theme already looks right, so copy what dark mode does and use it in both.
That is the whole fix. Four lines, all of them deletions of a condition:
- className="text-gray-900 dark:text-white"
+ className="text-white"
- className="text-gray-700 dark:text-gray-300"
+ className="text-white/75"
I had been asking which colour each theme should use. The right question was whether the theme should have a say at all. On a fixed background it should not, and the ink that already worked was the answer sitting in the file the whole time.
What the outside view saw
I had the advantage: I knew the component, the tokens, the scopes, the commit history. That was the disadvantage. Knowing the system meant every symptom suggested a mechanism I could go and check, and I kept checking mechanisms.
Someone looking only at the rendered result had no mechanisms to check. They had two observations: this one is right, that one is wrong. From there, make the wrong one do what the right one does is the obvious next move.
It also required no debugging at all. It is a copy operation. The forty-five minutes I spent were spent looking for a cause, when the cheaper move was to look for a case that already worked and take it.
The lesson
When one theme is already correct, stop diagnosing and start copying. A working case is a specification. It is more reliable than any theory about why the broken case is broken, and it is available immediately.
And the narrower rule, which is worth keeping: if a background does not change with the theme, neither should the text on it. A conditional over a constant is a bug that hides in half the states of your application, and it will look like two problems when you finally see it.