The Transparent Card That Vanished in Light Mode
Cards styled bg-card/20 through /90 nearly vanished in light mode because Tailwind v4's opacity modifier compiles to color-mix with a token variable.
TL;DR
Tailwind's opacity modifier compiles bg-card/50 into a color-mix() blend against the --card CSS variable, so the same class renders differently per theme. Dark mode contrast hid the issue; light mode made white blends at 20–90% opacity melt into the background. The fix: a light-mode CSS override forcing solid card colors, with shadows for depth instead of alpha.
I deployed to production, opened the site in Chrome, switched to light mode, and saw nothing but a flat #f8fafc background. The portfolio cards that looked crisp in dark mode had almost vanished. The elements were there, the borders were there, but their surfaces melted into the background.
I opened DevTools and inspected one of the cards. The classes were bg-card/20 through bg-card/90, all using Tailwind's opacity modifier. The confusing part: the --card variable had the correct value, a clean white hex. The page background was a light greenish-white too. So it was white cards on a near-white background, blended at 20 to 90 percent opacity. The result: ghost cards — present in the DOM, invisible to the eye.
I checked the previous state too. Those cards used semi-transparent surfaces so the aurora background behind them would bleed through slightly, giving a glass effect. This trick works in dark mode because the dark background contrasts with the transparent white blend. Once the background turned light, that blend fused right into it.
My first guess was that the --card variable was set wrong. Maybe a wrong hex, maybe copied from the dark mode palette. I opened frontend/src/styles/globals.css and double-checked: it was white too. Not a token problem.
Why does everything look fine in dark mode, but fall apart in light mode? If the color was wrong, both should break.
Opacity Modifier Isn't Just Alpha
I dug into Tailwind v4's source code. Turns out the opacity modifier doesn't write the alpha channel directly. The withAlpha() function in packages/tailwindcss/src/utilities.ts compiles bg-card/50 into a color-mix declaration [1], following this pattern:
/* compiled result of Tailwind v4 for bg-card/50 */
background-color: color-mix(in oklab, var(--card) 50%, transparent);
Tailwind's official docs market this modifier as a way to control background color opacity [2]. And that's technically what happens: mixing a color with transparent gives an alpha multiplier effect, meaning the output is the same card color at 50 percent transparency.
The problem isn't there. The problem is that the value being mixed is var(--card), a CSS variable pointing to a theme token. In dark mode that variable points to a dark color; in light mode, to white. The same bg-card/50 class produces two different computed colors depending on the theme. When I overhauled the light theme to use a solid bright background, all those semi-transparent blends shifted meaning without me touching a single class.
The math is straightforward. White at 20 percent over #f8fafc produces a color that differs from the background by only a few points. The 90 percent blend is still too close. MDN summarizes this limit bluntly: color-mix() cannot turn a semi-transparent color fully opaque; for that you have to swap the color directly, not blend it [3]. The modifier controls transparency, not contrast.
The Fix I Chose
Three options were on the table. First, redesign the cards to stop relying on transparent surfaces. Second, switch the --card token to an rgb format with explicit alpha. Third, force card surfaces solid in light mode via a CSS override.
I picked the third because it had the narrowest blast radius. A light mode override for the bg-card classes from level 20 through 90, redirecting them to background-color: var(--card) !important, plus a subtle gray shadow so the cards still lift off the background:
.light .bg-card\/50 {
background-color: var(--card) !important;
box-shadow: 0 1px 3px rgb(15 23 42 / 0.06), 0 1px 2px rgb(15 23 42 / 0.04);
}
This override wins over the original utility because the two-class selector (.light combined with bg-card/50) is more specific than a single utility class. I kept the !important from the original commit as a safety net, so this rule stays on top if declaration order ever changes down the line. Why not change the token to rgb? Because that token is used by many components across both themes — changing its default means rewriting the contract for every consumer. A light-mode-specific override touches one file, one block, and leaves dark mode completely untouched.
The takeaway I'm carrying forward: Tailwind's opacity modifier isn't a final color contract when the token is a CSS variable. What it promises is a blend formula, not a result. The same formula can look great in one theme and disappear in another. If a card surface genuinely needs to be visible, write the solid color explicitly, then separate the card from the background with a shadow, not alpha. This fix continues a pattern from [when I reset a faded accent color in light mode](/en/blog/accent-mint-dua-mode): dark themes forgive a lot of color choices; light themes don't.