Skip to content

fix(compiler): map color: inherit to the inherited-color variable - #391

Open
YevheniiKotyrlo wants to merge 7 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/color-inherit
Open

fix(compiler): map color: inherit to the inherited-color variable#391
YevheniiKotyrlo wants to merge 7 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/color-inherit

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Two independent defects, both in how the inherited-color variable is handled. The first is the title's: text-inherit (color: inherit) renders correctly on web but falls back to React Native's default text color on native, so a dark surface with an inheriting label becomes black-on-black:

<View className="bg-primary text-primary-foreground">
  <Text className="text-inherit">Label</Text>  {/* correct on web, black-on-black on native */}
</View>

The second I found while fixing the first, and it is worse: main today crashes a render with RangeError: Maximum call stack size exceeded for several ordinary colors. That one is independent of inherit and reproduces on stock 3.0.7 — details below.

A third, smaller divergence rides along on the same lines and is worth naming up front, because it is the one place the currentColor spelling React authors write reaches the compiler's own keyword handling: through a custom property. --brand: currentColor publishes the literal string "currentColor" as the variable's value on main, and every consumer renders that string as a color. The case fold added here resolves it. See The same keywords on a custom property, which also records — rather than hides — a route split this PR opens on inherit.

Cause — web keeps the CSS, native drops it

The two runtimes handle className differently, and only native breaks:

  • Webweb/api.tsx's useCssElement assigns { $$css: true, className } to style, so react-native-web applies the class name as a real CSS class. color: inherit is then resolved by the browser's own cascade — nothing is compiled.
  • Native — there is no CSS engine, so compiler/declarations.ts compiles each value to an RN style object. Its token/ident branch treats inherit as unsupported and drops it with a warning, so the <Text> gets no color at all and RN's default wins.

Fix 1 — map the inherited-color keywords to the variable

The native runtime already carries the mechanism. Because React Native doesn't inherit color across elements, every color rule publishes --__rn-css-color down its subtree, and parseUnparsed already resolves currentcolor to it. color: inherit just needs the same path — and it isn't an approximation: per CSS Color 3 §4.4, "if the currentColor keyword is set on the color property itself, it is treated as color: inherit." Same value, same descriptor.

const keyword = value.toLowerCase();
if (
  keyword === "currentcolor" ||
  ((keyword === "inherit" || keyword === "unset") && property === "color")
) {
  return inheritedColorLookup();
}

unset is included because it computes to inherit on inherited properties and color is inherited.

The .toLowerCase() is load-bearing on two surfaces, not one. On a declared property it serves INHERIT / UNSET / INITIAL, which lightningcss hands through unfolded. It also serves the only route by which the currentColor spelling reaches this branch at all: a custom property's value arrives as raw tokens, so --brand: currentColor is met here as a literal string rather than as a parsed CssColor. Written directly, color: currentColor never gets this far — lightningcss parses it and parseColor handles it, and the fold there is lightningcss's rather than ours.

Fix 2 — a color that READS the inherited color must never publish itself

This is the crash, and it is not caused by fix 1 — it is reachable on main right now.

A rule's color is handed to descendants as an unresolved descriptor. So if a rule both reads --__rn-css-color and publishes itself as --__rn-css-color, a descendant resolving it walks straight back into the same name and recursion runs until the stack is exhausted. There were two guards against that and both inspected only the top level of the descriptor, which misses every value that buries the read one level down:

color: color-mix(in srgb, currentcolor, blue)
color: light-dark(currentcolor, blue)
color: rgb(from currentcolor r g b)
color: var(--brand, currentcolor)

A parent with any of those plus a descendant reading the inherited color takes the app down. readsInheritedColor walks the whole descriptor tree instead, and one publishInheritedColor replaces both guards — parseUnparsedDeclaration for the keyword path and parseFontColorDeclaration for the parsed-CssColor path, which had its own narrower type !== "currentcolor" test and let light-dark(currentcolor, …) through.

Withholding the publish leaves the nearest ancestor naming a color of its own as the one descendants inherit. That is exactly right for inherit, unset and currentcolor — see the open question below for the case where it is an approximation.

Two smaller things ride along, because they sit on the same lines:

  • revert / revert-layer reached the style as the literal string "revert" and were published under --__rn-css-color, so a descendant reading the inherited color got color: "revert". React Native has no cascade origins to roll back to, so they now drop with a warning like initial.
  • parseFontColorDeclaration parsed the color twice. light-dark() pushes an extra prefers-color-scheme: dark rule as a side effect of parsing, so color: light-dark(a, b) emitted that rule twice. It parses once now.

Fix 3 — a declaration's style target is scoped to its own iteration

applyDeclarations walked the target path into a fresh binding per declaration, but that binding was the function's target parameter, reassigned each time around the loop. The delayed and transform closures capture it and run after every declaration has been walked, so they saw whatever nested object the LAST declaration ended on.

.parent { color: red }
.child  { color: inherit; box-shadow: 1px 1px blue }
- { color: { color: true }, boxShadow: [...] }   /* the internal placeholder, in the style */
+ { color: "#f00",          boxShadow: [...] }

A transform key fails worse — the values land inside the shadow object and the transform array keeps its boolean placeholders:

  .my-class { translate: 10px 20px; box-shadow: 1px 1px blue }

- { transform: [{ translateX: true }, { translateY: true }],
-   boxShadow: [{ …, transform: [{ translateX: 10 }, { translateY: 20 }] }] }
+ { transform: [{ translateX: 10 }, { translateY: 20 }], boxShadow: [{ … }] }

color: currentcolor in place of color: inherit reaches the same bug, so this is a general runtime defect, not an inherit one — it is the one part of this PR I would happily split out, and its tests are written not to depend on inherit so that a split is clean.

Keyword coverage on color

value result
inherit, unset, currentcolor (any case) var(--__rn-css-color), the inherited color
initial dropped — its own semantics (the platform default); a follow-up
revert, revert-layer dropped, on every property — RN has no cascade origin to roll back to
inherit / initial on a non-color property dropped — no per-property inheritance context exists on native
unset on a non-color property not dropped — there it means initial, and the literal is what clears the color

The same keywords on a custom property — two routes, and only one of them resolves

A custom property is a non-color property as far as the keyword arm is concerned, so by the table above it drops. What makes it worth its own section is that the same CSS reaches two different outcomes depending on a rule elsewhere in the sheet. inlineVariables keys on a custom property's declaration COUNT: a name declared exactly once is folded into its consumer at compile time and its declaration deleted, so the keyword is met on color and the property context exists. Declare the same name twice and the fold is defeated, var(--brand) survives as a runtime lookup, and the keyword is met on the custom property, where there is nothing to inherit from.

Measured, .parent { color: red } above .child { --brand: K; color: var(--brand) }, where route B adds a second .other { --brand: K } to defeat the inliner. The cell is the child's rendered props.style:

--brand: route main (f70c402) this PR
inherit A, folded undefined — the whole .child rule compiles away { color: "#f00" }
inherit B, unfolded {} {}
currentcolor A, folded { color: "#f00" } { color: "#f00" }
currentcolor B, unfolded { color: "#f00" } { color: "#f00" }
currentColor A, folded { color: "#f00" } { color: "#f00" }
currentColor B, unfolded { color: "currentColor" } { color: "#f00" }

Two things follow, and I would rather state both than let a reader infer the flattering one:

  • --brand: currentColor is a second divergence this PR closes, and it is the camelCase spelling's only non-trivial home. On main the unfolded route publishes the literal string as the variable's value and the element renders it; the .toLowerCase() is what makes the two routes agree.
  • The inherit split is NEW as of this PR. On main both routes drop inherit, symmetrically and equally wrongly — the folded one by compiling the rule away entirely, the unfolded one by leaving a rule whose lookup resolves to nothing. This change reaches the folded route only, so afterwards A resolves and B still drops. The unfolded defect is old; the disagreement between the two is not. Closing it is not a keyword-table change — a custom property would have to carry the property context of whatever consumes it, which is a resolver change — so B is pinned at its current output, with the CSS-correct answer named in the test.

The published-value rows on the compiler plane, same construction (two declaring rules, hence two published entries):

--brand: main publishes this PR publishes
inherit, initial nothing nothing
revert, revert-layer ["revert", "revert"] — the literal nothing
INHERIT ["INHERIT", "INHERIT"] — the literal nothing
currentcolor two var(--__rn-css-color) lookups two var(--__rn-css-color) lookups
currentColor ["currentColor", "currentColor"] — the literal two var(--__rn-css-color) lookups
unset ["unset", "unset"] ["unset", "unset"]

unset is deliberately absent from the drop rows and stays a literal, for the same reason it is absent from the keyword table: on anything that is not color it means initial, and the literal is what the runtime clears a value with — the same contract background-color: unset relies on.

Why these rows are not in the keyword table. That table's template is .child { ${property}: ${keyword}; }, one declaration. For a custom property that compiles to {} because the inliner deleted a once-declared name, so a row there would be vacuously green even if the ident branch resolved inherit on a custom property. The rows use the two-definition construction in an adjacent block instead, cross-referenced from the table. The currentcolor rows are the vacuity guard for the drop rows: same construction, non-empty expected result, so an inliner change that ate the declarations turns them red rather than silently hollowing out the drops.

Test plan

Every defect above is covered on both planes — the compiler output and a real render — and each test was mutation-proved: I broke the thing it guards, watched it go red for the right reason, and reverted. Reverting each fix in turn reddens, measured across the whole suite on the current head:

revert compiler red native red
the inherit mapping (fix 1) 13 32 (+1 vendor)
the lowercase keyword fold 6 5
the descriptor-tree walk (fix 2) 6 6, all RangeError
parseFontColorDeclaration's publish guard 1 1, RangeError
treating any var() as a read (over-eager walk) 3 1
the revert / revert-layer drop 8 2
the double parse of light-dark() 2 0 — see below
the target scoping (fix 3) 0 — see below 5

The crash is proven red-to-green rather than pinned as a toThrow: the census renders a parent → mid → child chain for each input and asserts the resolved colors, and on the unfixed code six of those renders die with RangeError: Maximum call stack size exceeded.

Two rows are deliberately zero, and both are measurements rather than omissions. The light-dark() double parse is compiler-only: restoring it puts three rules on the element instead of two and changes no rendered style, so only the two compiler assertions redden. The target scoping is native-only: it lives in src/native/styles/calculate-props.ts, which the compiler never executes, so reverting it reddens five render tests and not one compiler test.

The vendor text-inherit test (vendor/tailwind/typography.test.tsx) had encoded the dropped-declaration behaviour (props: {}, warnings: { color: "inherit" }) and now asserts the resolved label color, identical to the passing text-current. Its decoration-inherit sibling still drops, which confirms the color-only scope.

One pin asserts an absolute value, because the comparison it would otherwise make cannot fail

PIN: currentColor (camelCase) asserts the compiled output literally rather than comparing stylesheetFor("currentColor") against stylesheetFor("currentcolor"). Both of those spellings are folded by lightningcss, before this package runs, so the two sides of that equality move together whatever this package then does with the result — the assertion is structurally incapable of failing.

The counterfactual, since a claim like that is worth measuring rather than reasoning about. Mutate parseColor's case "currentcolor": return inheritedColorLookup() to return "#000":

  • the absolute form goes red;
  • the equality form stays green, while its own sibling pin one screen up goes red.

Note that a revert-based check cannot detect this class at all, by construction: reverting asks whether an assertion notices the new behaviour disappearing, never whether it notices anything. All four PIN: tests are green under a full revert of this branch, which is exactly what a pin of pre-existing behaviour should do — the other three say so in their comments, and none of them is evidence that the assertion can fail.

Gates

yarn test on the current head: Test Suites: 2 failed, 4 skipped, 53 passed, 55 of 59 total, Tests: 3 failed, 21 skipped, 1151 passed, 1175 total.

The 3 are react-native › plugin › 7., react-native-web › plugin › 6. and 17. — Windows-only babel-plugin-tester cases that are not this branch's: they fail identically, same three names, with both changed source files reverted to f70c402, which is how every revert measurement below was taken. That is the stable baseline on a warm cache; on a cold one this machine also produces a load-dependent tail of whole-suite run failures (a jest transform-cache EPERM, which subtracts that suite's tests from the total rather than failing an assertion) that vanish on a re-run. Read the Test Suites: line, not just Tests:, before comparing totals. yarn typecheck and yarn lint pass.

Known limits

--brand: initial makes the consuming rule vanish silently on the folded route. Measured on this branch:

compile(`.child { --brand: initial; color: var(--brand); }`).stylesheet();
// {}         — and warnings() is {} too: no rule, no diagnostic

compile(`.child { --brand: initial; color: var(--brand); }
         .other { --brand: initial; }`).stylesheet();
// the rule survives as a runtime lookup, and a warning IS emitted

This is one layer earlier than anything here: inlineVariables carries its own "the value is initial → substitute undefined" special case, which fires before the compiler's keyword arm ever runs, and the route-dependence is the same declaration-count fold as above. It is adjacent to this PR rather than part of it, and fixing it needs a semantics decision first — what should initial mean for an unregistered custom property, given there is no @property descriptor to take an initial value from — so I have left it alone rather than attaching a rider to a keyword change.

The unfolded custom-property route still drops inherit, as the table above records. Pinned, not fixed, with the CSS-correct expectation named in the test comment.

Open questions

  1. A derived color is a knowingly-approximate result. Withholding the publish is exactly right for inherit, unset and currentcolor, and an approximation for a value that DERIVES from the inherited color: with color: color-mix(in srgb, currentcolor, blue) on a middle element, descendants see the ancestor's color rather than the mixed one. Publishing the derived value is only possible once resolution happens in the publisher's own scope, which the compiler cannot do. Two census entries pin the approximation rather than hide it — happy to change the call.
  2. Overlap with fix(compiler): make a light-dark() extra rule a rule in its own right #420. Covering light-dark() on the render plane turned up a divergence — under dark an ancestor renders #00f while a descendant declaring inherit gets #f00 — and fix(compiler): make a light-dark() extra rule a rule in its own right #420 already owns it as its defect 2. I have pinned it here as a known divergence rather than fixed it, so whichever of the two lands second updates the expectation. The double parse in parseFontColorDeclaration is fixed on both branches too (fix(compiler): make a light-dark() extra rule a rule in its own right #420's defect 6), so that function will conflict on merge and either copy can be dropped — they are the same change. Happy to rebase onto fix(compiler): make a light-dark() extra rule a rule in its own right #420, or to drop the double-parse hunk here, whichever suits the merge order.
  3. rgb(from currentcolor r g b) is pinned at its current non-implemented output ("rgb(from, #f00, r, g, b)"), deliberately — it is in the census for the crash, not for relative-color support. Implementing relative color syntax must update that expectation.
  4. Fix 3 is a general runtime defect riding in a compiler-scoped PR. It is independently reproducible with currentcolor and I am happy to split it into its own PR.
  5. revert / revert-layer change from emitting a literal to being dropped. Observable, and outside the title's scope — but leaving them was actively harmful, since they were published as the inherited color.
  6. Is the inherit route split acceptable as a landing state? It is strictly better than main — one of the two routes now works where neither did — but it is a disagreement that did not exist before, and I would rather you decide that than discover it. The alternative is holding this until a custom property can carry its consumer's property context, which is a much larger change.

Tailwind's `text-inherit` (`color: inherit`) was dropped by the compiler and
rendered React Native's default text color (black on Android) instead of the
parent's color; web inherited correctly via real CSS.

lightningcss emits `color: inherit` as an UnparsedProperty (the keyword is not
a CssColor), and parseUnparsed's token/ident branch discarded it with a
warning. Per CSS Color 4, `currentcolor` used as the value of `color` is
defined as `inherit`, so both resolve to the --__rn-css-color variable every
color rule already publishes to its subtree -- no new machinery.

- parseUnparsed: resolve the inherited-color keywords -- `inherit`, `unset`
  (CSS Cascade: `unset` computes to `inherit` on the inherited `color`
  property), and `currentcolor` -- to the variable, matched case-insensitively
  (`currentColor`, `INHERIT`), instead of dropping them.
- parseUnparsedDeclaration: fix the self-reference guard, which compared against
  a stale "-css-color" literal that never matched the emitted "__rn-css-color".
  Without it an inheriting rule publishes a circular
  --__rn-css-color: var(--__rn-css-color), breaking resolution for its subtree.

`inherit` on non-color properties, and `initial`, remain dropped with a warning
(no per-property inheritance context exists). Adds compiler + native-render
tests, including the parent -> child -> grandchild chain that proves the
self-reference fix, and corrects the vendor text-inherit test that had encoded
the dropped-declaration behaviour.
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review July 26, 2026 14:53
mubeess added a commit to mubeess/react-native-css that referenced this pull request Jul 31, 2026
- Hoist allowed Set to module scope (no per-call realloc)
- Comment lightningcss case normalization and nativewind#391 asymmetry
- Add negative test: match-parent drops with warning (no over-match)
- Document native View->Text inheritance caveat in README
`color: inherit` is a READER of --__rn-css-color, the variable every colour
rule publishes to its subtree. A rule that reads that variable must not also
publish itself as it: the descriptor is handed to descendants unresolved, so a
descendant resolving it walks straight back into the same name and recursion
runs until the stack is exhausted — `RangeError: Maximum call stack size
exceeded` from a render.

The guard compared the top level of the value only, which misses every value
that buries the read one level down:

    color: var(--brand, inherit)
    color: color-mix(in srgb, currentcolor, blue)
    color: rgb(from currentcolor r g b)
    color: light-dark(currentcolor, blue)

so a parent with any of those plus a descendant `text-inherit` took the app
down. `readsInheritedColor` walks the whole descriptor tree instead, and the
publish is one `publishInheritedColor` used by both sites that publish —
`parseUnparsedDeclaration` for the keyword path and `parseFontColorDeclaration`
for the parsed-CssColor path, which had its own narrower `currentcolor` check
and let `light-dark(currentcolor, …)` through.

Withholding the publish leaves the nearest ancestor naming a colour of its own
as the one descendants inherit. That is exactly right for `inherit`, `unset`
and `currentcolor`, and an approximation for a value DERIVED from the inherited
colour, where descendants see the ancestor's colour rather than the derived
one — publishing the derived value needs resolution in the publisher's own
scope, which the compiler cannot do.

Also here, because they sit on the same two lines:

- `revert` / `revert-layer` reached the style as the literal string "revert",
  and were published under --__rn-css-color, so a descendant reading the
  inherited colour got `color: "revert"`. React Native has no cascade origins
  to roll back to, so they drop with a warning like `initial`.
- `parseFontColorDeclaration` parsed the colour twice, once for the declaration
  and once for the variable. `light-dark()` pushes an extra
  `prefers-color-scheme: dark` rule as a side effect of parsing, so
  `color: light-dark(a, b)` emitted that rule twice. It parses once now.

The two compiler tests asserting `currentcolor` behaviour are relabelled as
pins: `color: currentcolor` never reaches the restructured ident branch,
because lightningcss parses it into a CssColor that `parseColor` handles. That
branch's `currentcolor` clause is live for the UNPARSED properties — box-shadow,
filter: drop-shadow(), custom properties — and removing it fails
`src/__tests__/native/{box-shadow,filters}.test.tsx`.
`applyDeclarations` walks the target path into a fresh binding per declaration,
but that binding was the function's `target` PARAMETER, reassigned each time
around the loop. A delayed or transform closure captures it, and those closures
run after every declaration has been walked — so they saw whatever nested
object the LAST declaration ended on.

A rule with a delayed `color` and a `box-shadow` is the shape that shows it: the
shadow declaration walks into `["&", "boxShadow", "[0]", "color"]`, so the
colour's closure reads `getDeepPath(shadowObject, "color")`, never matches the
placeholder it minted against the style root, and leaves the internal
`{ color: true }` in the style. `text-shadow` does the same.

    .parent { color: red }
    .child  { color: inherit; box-shadow: 1px 1px blue }

    before: { color: { color: true }, boxShadow: [...] }
    after:  { color: "#f00",          boxShadow: [...] }

The binding is now declared inside the loop, so each declaration's closures keep
the target that declaration resolved. `color: currentcolor` in place of
`color: inherit` reaches the same bug, so this is not specific to the keyword.
The name was spelled as a literal at the publish site and at each of the three
places that compile a read of it, so the publish and the reads could drift
apart silently. One constant, and one `inheritedColorLookup()` the three read
sites call. It returns a fresh tuple per call because a descriptor is owned by
the rule it lands in.
Each defect on this branch should be observable from the compiler output AND
from a render. Auditing what was here found four places where only one plane
watched, plus one guard direction nothing watched at all. Every test below was
mutation-proved: the thing it guards was broken, the test was watched go red for
the right reason, and the break was reverted.

Compiler plane

- `revert` / `revert-layer` drop on EVERY property, not just `color`. The drop
  arm is keyword-first and only the resolving arm is gated on `color`, but the
  whole census sat on `color`. A property axis pins the arm that actually
  exists. Reverting the widened drop reddens 6 tests, 4 of them new.
- `unset` on a non-color property is deliberately NOT dropped — it means
  `initial` there and the literal is what clears a colour. That exception was
  documented in a comment and asserted nowhere, so adding `unset` to the drop
  list was a silent change.
- A value that CONTAINS a `var()` which is not the inherited colour still
  publishes. The publish census held only values resolving to a plain string, so
  a walk answering "reads the inherited colour" for ANY `var()` passed it
  untouched — no crash, just every descendant of a `color: var(--brand)` rule
  quietly ceasing to inherit. That mutation now reddens 4 tests; before it
  reddened none.

Native plane

- `color: inherit` with no coloured ancestor at all, which resolves against the
  root seed rather than the nearest publisher.
- An ancestor whose colour is itself a variable, inlined and uninlined. The
  uninlined case is asserted as an equality against the ancestor rather than a
  literal, because the class is that the two agree.
- `revert-layer` beside `revert`, and a `light-dark()` ancestor across a colour
  scheme change.
- `color: currentcolor` beside a box-shadow — the stranded-target defect with no
  `inherit` in the input, so the runtime fix stays pinned if it is split out.
- A transform key before a nested declaration. The two existing tests covered
  the delayed closure; the transform closure had nothing, and it fails worse:
  the translate values land INSIDE the shadow object and the transform array
  keeps its boolean placeholders.

Two planes measured, not assumed

- The double-parse of `light-dark()` is compiler-only. Restoring it puts three
  rules on the element instead of two and changes no rendered style; across all
  1160 tests only the two compiler assertions redden.
- The `calculate-props` target scoping is native-only. Reverting it reddens five
  render tests and not one compiler test.

A divergence found while covering `light-dark()` is pinned rather than fixed:
under dark, a descendant inherits the ancestor's LIGHT colour, because
`light-dark()` publishes --__rn-css-color from its light branch while the extra
dark rule carries the dark declaration. It reproduces with the double-parse
restored, so it predates this branch. The expectation records it so a fix has to
come back and change it.
The pinned divergence is that PR's defect 2, not a new finding, and the double
parse this branch fixes is its defect 6 — so the comment names the owner and the
merge order rather than implying either is unclaimed.
…split

`PIN: currentColor (camelCase) resolves like currentcolor` compared
`stylesheetFor("currentColor")` against `stylesheetFor("currentcolor")`,
and lightningcss folds both spellings into the same CssColor before this
package sees either — so both sides of the equality moved together and
the assertion could not fail. Returning "#000" from `parseColor`'s
currentcolor case turns its sibling pin red and leaves that one green.
It now asserts the compiled output, so the same mutation fails it.

The spelling this package folds itself is the one that reaches the ident
branch, and a custom property is where that happens. That surface splits
by whether `inlineVariables` folded the name, which it does only for a
name declared once:

- declared once, `--brand: inherit; color: var(--brand)` compiles as
  `color: inherit` and resolves to the inherited-color variable;
- declared twice, the fold is defeated and the keyword is met on a
  custom property, which carries no property context, so it drops and
  the lookup resolves to nothing.

Both routes dropped before `color: inherit` mapped to the variable, so
the divergence between them is new even though the unfolded route's
defect is not. Both are pinned, along with the keyword rows for a custom
property: `inherit` / `initial` / `revert` / `revert-layer` drop there
while `currentcolor` resolves, because that arm is keyword-only rather
than gated on the property. The `currentcolor` rows double as the
vacuity guard for the drop rows — same construction, so an empty result
there would mean the inliner had eaten the declaration.
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — The child renders the default foreground, ignoring the ancestor it declares it inherits from.

FIXED — The child text is the SAME red as its parent — color: inherit resolved.

before — stock 3.0.7 after — with this PR

Both frames come from the same device in the same run (Android 36 emulator, 1140×2400 @ 480dpi), differing only in whether this PR is applied.

Each frame carries a build-probe width=<dp> line — a rem-derived box that resolves differently on a patched build. The capture harness reads it off the device and refuses to save a frame whose probe disagrees with the variant it claims, so a before image cannot silently be a second after.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant