diff --git a/apps/mobile/src/components/agents/attachment-preview-strip.tsx b/apps/mobile/src/components/agents/attachment-preview-strip.tsx
index 1853d94e7e..690b7aa840 100644
--- a/apps/mobile/src/components/agents/attachment-preview-strip.tsx
+++ b/apps/mobile/src/components/agents/attachment-preview-strip.tsx
@@ -383,7 +383,7 @@ function AttachmentChip({
'overflow-hidden rounded-md border border-border bg-card',
isImage ? 'h-16 w-20' : 'h-12 w-48',
description.showRetry && 'border-destructive',
- isErrored && !description.showRetry && 'border-destructive/60'
+ isErrored && !description.showRetry && 'border-danger-tile-border'
)}
>
{description.showRetry ? (
diff --git a/apps/mobile/src/components/agents/permission-card.tsx b/apps/mobile/src/components/agents/permission-card.tsx
index c7d592bf73..7170e1c438 100644
--- a/apps/mobile/src/components/agents/permission-card.tsx
+++ b/apps/mobile/src/components/agents/permission-card.tsx
@@ -110,7 +110,7 @@ export function PermissionCard({
{presentation.errorMessage ? (
-
+
{presentation.errorMessage}
) : null}
diff --git a/apps/mobile/src/components/agents/question-card.tsx b/apps/mobile/src/components/agents/question-card.tsx
index 6c56bc37a0..d3eec82b6e 100644
--- a/apps/mobile/src/components/agents/question-card.tsx
+++ b/apps/mobile/src/components/agents/question-card.tsx
@@ -207,7 +207,7 @@ export function QuestionCard({
{presentation.errorMessage ? (
-
+
{presentation.errorMessage}
) : null}
diff --git a/apps/mobile/src/components/pr-review/pr-review-overview-parts.tsx b/apps/mobile/src/components/pr-review/pr-review-overview-parts.tsx
index 832fac0fa6..381c1976aa 100644
--- a/apps/mobile/src/components/pr-review/pr-review-overview-parts.tsx
+++ b/apps/mobile/src/components/pr-review/pr-review-overview-parts.tsx
@@ -68,10 +68,10 @@ export function describePrState(args: {
return { labelKey: 'prReview.overview.stateOpen', tone: 'muted', icon: GitPullRequest };
}
-// Theme colors are CSS variables — Tailwind opacity modifiers like
-// `bg-good/10` don't work on them. The chip uses a flat muted background
-// and lets the foreground color carry the tone so it stays legible in
-// both themes without needing per-tone backgrounds.
+// Theme colors are CSS variables, so Tailwind opacity modifiers on them are
+// silently dropped. The chip uses a flat muted background and lets the
+// foreground color carry the tone so it stays legible in both themes
+// without needing per-tone backgrounds.
const TONE_FG_CLASS = {
good: 'text-good',
warn: 'text-warn',
diff --git a/tools/nativewind/check-classes.mjs b/tools/nativewind/check-classes.mjs
index 384be42e9f..a1c9d7e830 100644
--- a/tools/nativewind/check-classes.mjs
+++ b/tools/nativewind/check-classes.mjs
@@ -51,6 +51,19 @@ const RULES = [
pattern: new RegExp(`\\b(?:${COLOR_UTILITIES})-black/\\d+\\b`, 'g'),
advice: 'compiles to an unparseable #NaN colour; use a concrete value, e.g. bg-[#00000066]',
},
+ {
+ // A `/opacity` modifier on a theme colour compiles to
+ // `color-mix(in oklab, var(--color-…) n%, transparent)`. NativeWind v5
+ // cannot decompose the variable, so the runtime drops the declaration and
+ // the element paints nothing, with no build error. The `*-tile-bg` /
+ // `*-tile-border` pairs pre-bake the alpha instead.
+ pattern: new RegExp(
+ `\\b(?:${COLOR_UTILITIES})-(?:background|foreground|card|popover|primary|secondary|muted-soft|muted|accent-soft|accent|destructive|border|input|ring|ink2|good|warn|info|danger|agent-[a-z]+)(?:-foreground)?/\\d+\\b`,
+ 'g'
+ ),
+ advice:
+ 'the /opacity modifier is dropped on CSS-variable theme colours; use the paired *-tile-bg / *-tile-border token or a concrete colour',
+ },
{
// `text-align` only accepts auto/left/right/center/justify, so the
// logical keywords are dropped and the text keeps its default alignment.
@@ -226,6 +239,21 @@ function checkCompilerStillDropsThese() {
if (!JSON.stringify(alignment).includes('text-align')) {
fail('check-classes: text-align: start is no longer dropped — drop the text-start/end rule');
}
+
+ // A `/opacity` modifier on an `@theme inline` variable is emitted as a
+ // deferred colorMix over the variable; the runtime cannot decompose it and
+ // drops the declaration. When the compiler resolves it instead, this stops
+ // matching and the rule above can go.
+ const themeOpacity = firstValue(
+ '@theme inline { --color-destructive: var(--destructive); }\n' +
+ ':root { --destructive: #b0483a; }\n' +
+ '.x { background-color: color-mix(in oklab, var(--color-destructive) 10%, transparent); }'
+ );
+ if (!themeOpacity.includes('colorMix')) {
+ fail(
+ `check-classes: color-mix over an @theme inline variable now compiles to ${themeOpacity} instead of a deferred colorMix — drop the theme-token opacity rule`
+ );
+ }
}
checkCompilerStillDropsThese();