@@ -861,10 +861,6 @@ fn build_element_css(doc: &Document, styles: &StyleCollector, dev: bool) -> Stri
861861 // Generated rules always go in `@layer htmlang`, so unlayered user CSS
862862 // (`@style`, `@raw`) overrides them regardless of specificity.
863863 let styles_css = styles.to_css_formatted(dev);
864− // Fold literal values declared via `@theme` / `@let --name` back into
865− // `var(--name)` references so the generated CSS actually uses the
866− // custom properties emitted in `:root`.
867− let styles_css = substitute_css_vars(&styles_css, &root_vars);
868864
869865 // Emit the :root block first so the cascade picks up the custom
870866 // properties before the class rules consume them.
@@ -2214,99 +2210,6 @@ fn css_px(value: &str) -> String {
22142210 }
22152211}
22162212
2217−/// Rewrite literal values that match a declared CSS custom property (from
2218−/// `@theme` or `@let --name value`) to `var(--name)` references. Matches are
2219−/// anchored to CSS value boundaries so e.g. `#3b82f6` inside a longer hex or
2220−/// inside an identifier is not replaced. The `:root` block is emitted before
2221−/// this call runs, so its declarations are not affected.
2222−fn substitute_css_vars(css: &str, vars: &[(String, String)]) -> String {
2223− if css.is_empty() || vars.is_empty() {
2224− return css.to_string();
2225− }
2226− // Prefer longer values first so that if two vars share a prefix, the
2227− // longer (more specific) match wins.
2228− let mut pairs: Vec<(&str, String)> = vars
2229− .iter()
2230− .filter(|(name, value)| name.starts_with("--") && !value.is_empty())
2231− .map(|(name, value)| (value.as_str(), format!("var({})", name)))
2232− .collect();
2233− pairs.sort_by_key(|(v, _)| std::cmp::Reverse(v.len()));
2234− if pairs.is_empty() {
2235− return css.to_string();
2236− }
2237−
2238− let is_boundary_before = |b: Option<u8>| match b {
2239− None => true,
2240− Some(c) => matches!(c, b':' | b' ' | b',' | b'(' | b';' | b'{' | b'\n' | b'\t'),
2241− };
2242− let is_boundary_after = |b: Option<u8>| match b {
2243− None => true,
2244− Some(c) => matches!(c, b';' | b'}' | b',' | b' ' | b')' | b'\n' | b'\t'),
2245− };
2246−
2247− let bytes = css.as_bytes();
2248− let mut out = String::with_capacity(css.len());
2249− let mut i = 0;
2250− let mut prev: Option<u8> = None;
2251− // Only declaration values are rewritten — never selectors or at-rule
2252− // preludes such as `@media (min-width:768px)`. `blocks` records, for
2253− // each open `{`, whether it holds declarations (a style rule) or nested
2254− // rules (an at-rule like `@media` / `@layer`).
2255− let mut blocks: Vec<bool> = Vec::new();
2256− let mut prelude_start = 0;
2257− let mut in_value = false;
2258− while i < bytes.len() {
2259− match bytes[i] {
2260− b'{' => {
2261− let prelude = css[prelude_start..i].trim_start();
2262− blocks.push(!prelude.starts_with('@'));
2263− in_value = false;
2264− prelude_start = i + 1;
2265− }
2266− b'}' => {
2267− blocks.pop();
2268− in_value = false;
2269− prelude_start = i + 1;
2270− }
2271− b';' => {
2272− in_value = false;
2273− prelude_start = i + 1;
2274− }
2275− b':' if blocks.last() == Some(&true) => in_value = true,
2276− _ => {}
2277− }
2278− if in_value && is_boundary_before(prev) {
2279− let mut matched = false;
2280− for (val, repl) in &pairs {
2281− let vb = val.as_bytes();
2282− if i + vb.len() <= bytes.len() && &bytes[i..i + vb.len()] == vb {
2283− let next = bytes.get(i + vb.len()).copied();
2284− if is_boundary_after(next) {
2285− out.push_str(repl);
2286− prev = repl.as_bytes().last().copied();
2287− i += vb.len();
2288− matched = true;
2289− break;
2290− }
2291− }
2292− }
2293− if matched {
2294− continue;
2295− }
2296− }
2297− // Advance by one UTF-8 code point.
2298− let start = i;
2299− i += 1;
2300− while i < bytes.len() && (bytes[i] & 0xC0) == 0x80 {
2301− i += 1;
2302− }
2303− out.push_str(&css[start..i]);
2304− prev = bytes.get(i - 1).copied();
2305− }
2306− out
2307−}
2308−
2309−
23102213/// Format a `line-height` value. CSS accepts either a unitless multiplier
23112214/// (e.g. `1.5`) or a length (e.g. `24px`). Plain integers in htmlang source
23122215/// are ambiguous: `[line-height 24]` was historically emitted as `24` (which