beck_core/
style.rs

1//! What can reach a `class=`, enumerated.
2//!
3//! [`docs/104-styling-and-the-component-library.md`](../../../../../docs/104-styling-and-the-component-library.md)
4//! §104.4:
5//!
6//! > The compiler already knows every string that can reach a `class=` attribute, across imported
7//! > modules, because it resolved them. […] **Exact extraction.** No false positives (a
8//! > `def truncate` is a definition, not a token), no false negatives across a module boundary, and
9//! > no configuration.
10//!
11//! That is the claim; this is the analysis behind it. It answers two questions about a program and
12//! neither of them is about Tailwind: **which class names can this page carry**, and **where does
13//! the program build one the compiler cannot know**. The first is what a stylesheet emitter needs
14//! (§8.5.4's next styling item). The second is what makes the first honest, because a scanner that
15//! silently misses a name produces a page missing a rule, which is the failure §104.3 measured in
16//! Tailwind's own scanner over this tree.
17//!
18//! # Why a list is the shape that can be enumerated
19//!
20//! `class=["btn", "primary" if hot else "plain"]` has four leaves and every one is a literal, so the
21//! set is `{btn, primary, plain}` and the analysis is a fold over the tree. `class="btn " + variant`
22//! has one leaf that is a value, and no analysis recovers what it can hold. The two are the same
23//! page and only one of them can be styled without a safelist, which is the whole reason
24//! [`beck_macro`]'s `ui:` lowering learned to take a list.
25//!
26//! **So this module refuses rather than guesses**, and says which of the two a program wrote. A
27//! refusal here is not an error: nothing is rejected, and the caller decides what to do with a site
28//! it cannot enumerate. `beck explain style` prints them, and the emitter that follows will need a
29//! deliberate escape hatch for the genuine cases (§104.4's `@style(dynamic)`), which is a decision
30//! rather than a default.
31
32use std::collections::{BTreeMap, BTreeSet};
33use std::fmt::Write;
34use std::sync::Arc;
35
36use beck_diag::{Diagnostic, Diagnostics, Span};
37
38use crate::check::{Def, Program};
39use crate::core::{children, Const, Core, CoreKind, Prim};
40
41/// How far a class expression is followed through named definitions.
42///
43/// `examples/todo.beck` writes `class=done_class(t)` and the body one call away is the `if` whose
44/// two arms are the answer, which is the depth this needs and the shape §104.4 predicted programs
45/// would already be written in. A bound rather than a budget: what it stops is a cycle of mutually
46/// recursive definitions, not a slow analysis.
47const DEPTH: usize = 8;
48
49/// Every class a program's pages can carry, and every place one could not be worked out.
50#[derive(Clone, Debug, Default)]
51pub struct Styles {
52    /// The class names, deduplicated and in order. A `class=""` contributes nothing rather than an
53    /// empty token, because that is what the browser does with it.
54    pub classes: BTreeSet<Arc<str>>,
55    /// The `class=` sites whose value is not enumerable, in the order they were found.
56    pub dynamic: Vec<Dynamic>,
57    /// Every literal that reaches a `class=`, with where it was written.
58    ///
59    /// [`Styles::classes`] is the set a sheet is emitted from and has no positions, because a name
60    /// written in three places is one rule. This is the other question — *where did this name come
61    /// from* — and it is what a diagnostic needs, so it keeps one entry per site rather than one
62    /// per name.
63    pub named: Vec<Named>,
64}
65
66/// One literal class token, where it was written.
67#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct Named {
69    pub class: Arc<str>,
70    /// The definition the literal is in — which is not always the one the `class=` is in, because
71    /// the analysis follows a call.
72    pub in_def: Arc<str>,
73    pub span: Span,
74}
75
76/// One `class=` the analysis could not work out, and why.
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct Dynamic {
79    /// The definition the site is in, so a reader can find it without a span map.
80    pub in_def: Arc<str>,
81    pub span: Span,
82    pub because: Because,
83}
84
85/// Why a class expression could not be enumerated.
86///
87/// Three, and they are distinguished because a reader does something different about each: the
88/// first is a rewrite the language already invites, the second is a design question about where the
89/// name comes from, and the third is a limit of this analysis rather than of the program.
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91pub enum Because {
92    /// A string built with `+`, so the name exists only at run time.
93    Concatenated,
94    /// A value computed rather than named — a field, a parameter, a lookup, a call to a
95    /// primitive.
96    FromData,
97    /// A shape this analysis does not enter: a call through a value, a `let`-bound name, a
98    /// definition deeper than the analysis follows.
99    NotFollowed,
100}
101
102impl Because {
103    /// The sentence a diagnostic or a report prints, in the voice the rest of `beck explain` uses.
104    pub fn because(&self) -> &'static str {
105        match self {
106            Because::Concatenated => {
107                "it is built with `+`, so the name exists only while the page is being rendered. A \
108                 list of alternatives is the shape that can be styled: `class=[\"btn\", \"primary\" \
109                 if hot else \"plain\"]`"
110            }
111            Because::FromData => {
112                "it is computed rather than named — a field, a lookup, a call to a primitive — so \
113                 the names it can take are in the data rather than in the program"
114            }
115            Because::NotFollowed => {
116                "it is behind a shape this analysis does not enter — a call through a value, a \
117                 `let`-bound name, or a chain of definitions deeper than it follows"
118            }
119        }
120    }
121}
122
123/// Enumerate every class a program's `class=` attributes can carry.
124pub fn classes(program: &Program) -> Styles {
125    let mut out = Styles::default();
126    for (name, def) in &program.defs {
127        sites(&def.body, name, &program.defs, &mut out);
128    }
129    out
130}
131
132/// Find every `html_attr("class", …)` under an expression and enumerate each one's value.
133fn sites(c: &Core, in_def: &Arc<str>, defs: &BTreeMap<Arc<str>, Def>, out: &mut Styles) {
134    if let CoreKind::Prim {
135        op: Prim::HtmlAttr,
136        args,
137    } = &c.kind
138    {
139        if args.len() == 2 && is_class(&args[0]) {
140            let mut seen = BTreeSet::new();
141            if let Err(because) = tokens(&args[1], defs, &mut seen, DEPTH, in_def, out) {
142                out.dynamic.push(Dynamic {
143                    in_def: in_def.clone(),
144                    span: args[1].span,
145                    because,
146                });
147            }
148        }
149    }
150    for child in children(c) {
151        sites(child, in_def, defs, out);
152    }
153}
154
155/// Whether this expression is the literal attribute name `class`.
156fn is_class(c: &Core) -> bool {
157    matches!(&c.kind, CoreKind::Const(Const::Str(s)) if &**s == "class")
158}
159
160/// The set of class names an expression can evaluate to, or why that set is not knowable.
161fn tokens(
162    c: &Core,
163    defs: &BTreeMap<Arc<str>, Def>,
164    seen: &mut BTreeSet<Arc<str>>,
165    depth: usize,
166    in_def: &Arc<str>,
167    out: &mut Styles,
168) -> Result<(), Because> {
169    if depth == 0 {
170        return Err(Because::NotFollowed);
171    }
172    match &c.kind {
173        // A literal, split the way the browser splits it: `class="a b"` is two tokens, and an
174        // empty string is none rather than one empty one.
175        CoreKind::Const(Const::Str(s)) => {
176            for token in s.split_whitespace() {
177                let class: Arc<str> = Arc::from(token);
178                out.classes.insert(class.clone());
179                out.named.push(Named {
180                    class,
181                    in_def: in_def.clone(),
182                    span: c.span,
183                });
184            }
185            Ok(())
186        }
187        // What the `ui:` lowering makes of a list, and the list itself.
188        CoreKind::Prim {
189            op: Prim::StrJoin,
190            args,
191        } if args.len() == 2 => tokens(&args[0], defs, seen, depth - 1, in_def, out),
192        CoreKind::ListLit(items) => items
193            .iter()
194            .try_for_each(|i| tokens(i, defs, seen, depth - 1, in_def, out)),
195        // Every branch can happen, so every branch contributes. The condition cannot reach the
196        // attribute and is not looked at.
197        CoreKind::If { then, alt, .. } => {
198            tokens(then, defs, seen, depth - 1, in_def, out)?;
199            tokens(alt, defs, seen, depth - 1, in_def, out)
200        }
201        CoreKind::Match { arms, .. } => arms
202            .iter()
203            .try_for_each(|a| tokens(&a.body, defs, seen, depth - 1, in_def, out)),
204        // A definition, whether it is named or called. The arguments are not followed: if the
205        // answer depended on one, the body would read a parameter and this would refuse there.
206        CoreKind::Global(name) => follow(name, defs, seen, depth, out),
207        CoreKind::App { func, .. } => match &func.kind {
208            CoreKind::Global(name) => follow(name, defs, seen, depth, out),
209            CoreKind::Lam { body, .. } => tokens(body, defs, seen, depth - 1, in_def, out),
210            _ => Err(Because::NotFollowed),
211        },
212        CoreKind::Prim {
213            op: Prim::Add,
214            args,
215        } if args.len() == 2 => {
216            // Two literals added is still two literals, and constant-folding is not this module's
217            // job — but the *reason* a reader gets should name the concatenation rather than the
218            // shape underneath it, so this arm exists to say `Concatenated` and not `NotFollowed`.
219            let _ = args;
220            Err(Because::Concatenated)
221        }
222        // Everything else that answers with a value rather than with a name. A primitive is here
223        // rather than under `NotFollowed` because there is nothing to follow: `str_upper(x)` has an
224        // answer and the answer is not in the program.
225        CoreKind::Var(_) | CoreKind::Field { .. } | CoreKind::Prim { .. } => Err(Because::FromData),
226        _ => Err(Because::NotFollowed),
227    }
228}
229
230/// Follow a named definition once, refusing a cycle rather than chasing it.
231fn follow(
232    name: &Arc<str>,
233    defs: &BTreeMap<Arc<str>, Def>,
234    seen: &mut BTreeSet<Arc<str>>,
235    depth: usize,
236    out: &mut Styles,
237) -> Result<(), Because> {
238    if !seen.insert(name.clone()) {
239        return Err(Because::NotFollowed);
240    }
241    let Some(def) = defs.get(name) else {
242        return Err(Because::NotFollowed);
243    };
244    // A definition's body is a `Lam` when it takes arguments; what answers is what it returns.
245    let body = match &def.body.kind {
246        CoreKind::Lam { body, .. } => body,
247        _ => &def.body,
248    };
249    // The literals are in *this* definition, whatever called it: a diagnostic that pointed at
250    // the caller would send a reader to a line with no class name on it.
251    let answer = tokens(body, defs, seen, depth - 1, name, out);
252    seen.remove(name);
253    answer
254}
255
256// -------------------------------------------------------------------------------------------
257// A misspelled utility
258// -------------------------------------------------------------------------------------------
259
260/// Warn about a class that is one slip away from a utility.
261///
262/// [`docs/104`](../../../../../docs/104-styling-and-the-component-library.md) §104.4: "a
263/// misspelling is a diagnostic. `rounded-ful` gets a `B0…` with a did-you-mean, because
264/// Levenshtein over a known table is what the compiler already does for field names. **This is the
265/// whole difference between Tailwind and a language that absorbed it.**"
266///
267/// # Why it is a warning and not an error
268///
269/// The class vocabulary is **open**, which is what makes this different from `B0217` and `B0218`
270/// one file over: every attribute must be an HTML attribute, and every event must be one the
271/// client listens for, so an unknown one is wrong. An unknown *class* is not wrong — it is the
272/// program's own name, and this tree has eight of them. So the compiler cannot say "this is a
273/// mistake"; it can only say "this is one edit from something that would have had a rule", which
274/// is a warning with a suggestion in it.
275///
276/// # The threshold, and the margin it has
277///
278/// Distance 1 always, and distance 2 from eight characters up. `rounded-ful`, `bg-emerald-550`,
279/// `text-4xxl`, `flexx`, `font-mediumm` and `justify-arround` are all one edit from a real utility
280/// and `items-centre` is two, so the misspellings are inside it. The other population is further
281/// away than the rule needs: the nearest utility to any class this tree's own programs write is
282/// **three** edits — `card` to `grid`, `here` to `h-px`, `mine` to `inline` — so the rule has a
283/// margin of one rather than sitting on the boundary.
284///
285/// `style.rs::a_misspelled_utility_is_a_diagnostic` asserts the **margin** rather than the
286/// outcome, which is the difference between a gate that can fail and one that cannot: a family
287/// added to the table that lands two edits from somebody's own class name turns it red before
288/// anybody's build starts warning about a name they chose.
289pub fn check_classes(program: &Program, diags: &mut Diagnostics) {
290    for site in &classes(program).named {
291        if is_utility(&site.class) {
292            continue;
293        }
294        let allowed = usize::from(site.class.len() >= 8) + 1;
295        let Some((_, near)) = nearest_utility(&site.class).filter(|(d, _)| *d <= allowed) else {
296            continue;
297        };
298        diags.push(
299            Diagnostic::warning(
300                "B0222",
301                format!(
302                    "`{}` is not a utility, and is one slip from one",
303                    site.class
304                ),
305                site.span,
306            )
307            .with_primary_label("no rule is emitted for this class")
308            .with_note(
309                "a class this compiler does not know is a class of your own and is left alone — \
310                 `beck explain style` lists which of a page's are which. This one is close enough \
311                 to a utility to be worth asking about",
312            )
313            .with_fix(format!("did you mean `{near}`?")),
314        );
315    }
316}
317
318/// The utility nearest a name, and how far away it is.
319///
320/// Over [`enumerate`]'s closed names, which is the whole table but for the open scales: `p-2.75` is
321/// a utility and no misspelling of it is close to a *name*, because the thing that went wrong there
322/// is a number.
323///
324/// It answers with the distance rather than applying the threshold, so a caller can ask **how far**
325/// — which is what turns [`check_classes`]'s gate from "did it warn" into "how much room does the
326/// rule have", and the second is the one that can fail before a user notices.
327pub fn nearest_utility(name: &str) -> Option<(usize, &'static str)> {
328    let mut best: Option<((usize, usize), &'static str)> = None;
329    for candidate in CLOSED.iter() {
330        let d = distance(name, candidate);
331        // Ties broken towards the candidate of the same length, because a substitution is a
332        // likelier slip than a deletion: `bg-emerald-550` is one edit from `bg-emerald-50` and
333        // from `bg-emerald-500`, and only the second is a shade somebody meant.
334        let rank = (d, name.len().abs_diff(candidate.len()));
335        if best.is_none_or(|(b, _)| rank < b) {
336            best = Some((rank, candidate));
337        }
338    }
339    best.map(|((d, _), name)| (d, name))
340}
341
342/// Levenshtein distance, iterative over one row.
343///
344/// The same measure `beck_macro::vocabulary` uses for attribute names and the checker for field
345/// names, written again here rather than shared because the two crates do not depend on each other
346/// in that direction and a distance function is six lines.
347fn distance(a: &str, b: &str) -> usize {
348    let b: Vec<char> = b.chars().collect();
349    let mut row: Vec<usize> = (0..=b.len()).collect();
350    for (i, x) in a.chars().enumerate() {
351        let mut corner = row[0];
352        row[0] = i + 1;
353        for (j, y) in b.iter().enumerate() {
354            let next = (row[j] + 1)
355                .min(row[j + 1] + 1)
356                .min(corner + usize::from(x != *y));
357            corner = row[j + 1];
358            row[j + 1] = next;
359        }
360    }
361    row[b.len()]
362}
363
364/// Every closed name the table knows, built once.
365static CLOSED: std::sync::LazyLock<Vec<String>> =
366    std::sync::LazyLock::new(|| enumerate().0.into_iter().collect());
367
368// -------------------------------------------------------------------------------------------
369// Which names are utilities, and what they mean
370// -------------------------------------------------------------------------------------------
371
372/// One utility's CSS: where it sits, what it selects, and what it declares.
373///
374/// [`docs/104`](../../../../../docs/104-styling-and-the-component-library.md) §104.4 takes
375/// Tailwind's **design system** and refuses its delivery mechanism. This is that design system, as
376/// a total function from a name to a rule — which is the same shape Tailwind's own compiler has,
377/// and the reason its output can be the oracle.
378#[derive(Clone, Debug, PartialEq, Eq)]
379pub struct Rule {
380    /// The at-rules this sits inside, outermost first: `@media (width >= 48rem)`.
381    pub at: Vec<&'static str>,
382    /// The selector, with the class name escaped as CSS requires.
383    pub selector: String,
384    /// The declarations, in the order they are written.
385    pub decls: Vec<(&'static str, String)>,
386}
387
388impl Rule {
389    /// Every theme token this rule reads, so a sheet can define the ones it needs and no others.
390    fn tokens(&self, out: &mut BTreeSet<&'static str>) {
391        for (_, value) in &self.decls {
392            let mut rest = value.as_str();
393            while let Some(at) = rest.find("var(--") {
394                rest = &rest[at + 4..];
395                let end = rest
396                    .find(|c: char| !c.is_ascii_alphanumeric() && c != '-')
397                    .unwrap_or(rest.len());
398                if let Some((name, _)) = THEME.iter().find(|(n, _)| *n == &rest[..end]) {
399                    out.insert(name);
400                }
401                rest = &rest[end..];
402            }
403        }
404    }
405}
406
407/// Whether this class is a Tailwind utility Beck knows.
408///
409/// **Defined as "there is a rule for it"**, which is not a tidiness: a predicate and a generator
410/// that could disagree would put a class in a stylesheet with no rule under it, or refuse one the
411/// emitter can render — and the first is a page missing a style with every gate green, which is the
412/// failure the whole arrangement exists to prevent. There is one table and this reads it.
413pub fn is_utility(name: &str) -> bool {
414    rule(name).is_some()
415}
416
417/// The rule for one class, or `None` if it is not a utility this knows.
418///
419/// # It is a subset, and the gate measures which one
420///
421/// Tailwind's surface is enormous and this covers the families a page is actually built from —
422/// layout, spacing, colour, type, borders, flex and grid — with the variants in front of them.
423/// **What matters is the direction of the error.** A name this accepts must be one Tailwind emits
424/// the same rule for; a name Tailwind refuses must be refused here. A name Tailwind accepts and
425/// this does not is a *gap*, counted rather than tolerated silently, and
426/// `style.rs::the_utility_table_agrees_with_tailwind` is where all three are asserted against
427/// Tailwind's own output rather than against a table somebody typed in.
428///
429/// # What made a fixed table wrong, found by asking
430///
431/// Tailwind 4's spacing is multiplicative — `calc(var(--spacing) * n)` — so `p-2.75` and `gap-13.5`
432/// are rules, and any list of steps would have refused them. So the spacing families here take a
433/// number rather than a member of a set, which is a thing the oracle said and a person would not
434/// have. Asking it about the *rule* rather than only about the name said three more: `1` is
435/// `var(--spacing)` and not `calc(var(--spacing) * 1)`, `0` is `0px` rather than `0`, and `auto` is
436/// a padding value in no family at all — which this table used to accept in seventeen names the
437/// candidate list had never asked about.
438pub fn rule(name: &str) -> Option<Rule> {
439    let mut parts: Vec<&str> = name.split(':').collect();
440    let base = parts.pop()?;
441    let mut at = Vec::new();
442    let mut pseudo = String::new();
443    for v in &parts {
444        let (media, suffix) = variant(v)?;
445        at.extend(media);
446        pseudo.push_str(suffix);
447    }
448    let (decls, shape) = base_rule(base)?;
449    let class = format!(".{}{pseudo}", escape(name));
450    Some(Rule {
451        at,
452        selector: match shape {
453            // `space-x-4` spaces an element's *children*, so the rule selects them rather than it.
454            Shape::Between => format!(":where({class} > :not(:last-child))"),
455            Shape::Element => class,
456        },
457        decls,
458    })
459}
460
461/// What a utility's rule selects.
462#[derive(Clone, Copy, Debug, PartialEq, Eq)]
463enum Shape {
464    /// The element carrying the class.
465    Element,
466    /// Every child of it but the last — `space-x` and `space-y`, which are a gap written as a
467    /// margin so that it collapses at the edge.
468    Between,
469}
470
471/// A variant, as the at-rule it opens and the pseudo-class it appends.
472///
473/// Stacking is Tailwind's own and so is the order: the leftmost variant is the outermost at-rule,
474/// and every pseudo-class is appended to the one selector.
475fn variant(v: &str) -> Option<(Option<&'static str>, &'static str)> {
476    Some(match v {
477        "hover" => (Some("@media (hover: hover)"), ":hover"),
478        "focus" => (None, ":focus"),
479        "focus-visible" => (None, ":focus-visible"),
480        "focus-within" => (None, ":focus-within"),
481        "active" => (None, ":active"),
482        "visited" => (None, ":visited"),
483        "disabled" => (None, ":disabled"),
484        "checked" => (None, ":checked"),
485        "first" => (None, ":first-child"),
486        "last" => (None, ":last-child"),
487        "odd" => (None, ":nth-child(odd)"),
488        "even" => (None, ":nth-child(even)"),
489        "empty" => (None, ":empty"),
490        "dark" => (Some("@media (prefers-color-scheme: dark)"), ""),
491        "motion-safe" => (Some("@media (prefers-reduced-motion: no-preference)"), ""),
492        "motion-reduce" => (Some("@media (prefers-reduced-motion: reduce)"), ""),
493        "print" => (Some("@media print"), ""),
494        "sm" => (Some("@media (width >= 40rem)"), ""),
495        "md" => (Some("@media (width >= 48rem)"), ""),
496        "lg" => (Some("@media (width >= 64rem)"), ""),
497        "xl" => (Some("@media (width >= 80rem)"), ""),
498        "2xl" => (Some("@media (width >= 96rem)"), ""),
499        _ => return None,
500    })
501}
502
503/// A class name as a CSS identifier.
504///
505/// `:` and `.` are punctuation in a selector and are escaped one by one; a **leading digit** is not
506/// escapable that way and takes CSS's hex form, `\32 ` — a code point and a terminating space. That
507/// space is part of the escape rather than whitespace in the selector, which is what made an
508/// earlier reader of the oracle stop at it and lose every `2xl:` rule.
509fn escape(name: &str) -> String {
510    let mut out = String::with_capacity(name.len() + 4);
511    for (i, c) in name.chars().enumerate() {
512        match c {
513            '0'..='9' if i == 0 => out.push_str(&format!("\\3{c} ")),
514            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => out.push(c),
515            _ if !c.is_ascii() => out.push(c),
516            _ => {
517                out.push('\\');
518                out.push(c);
519            }
520        }
521    }
522    out
523}
524
525/// The declarations of one utility, without its variants.
526fn base_rule(base: &str) -> Option<(Vec<(&'static str, String)>, Shape)> {
527    let one = |property: &'static str, value: &str| {
528        Some((vec![(property, value.to_string())], Shape::Element))
529    };
530    if let Some(decls) = WORDS.iter().find(|(n, _)| *n == base) {
531        return Some((
532            decls.1.iter().map(|(p, v)| (*p, v.to_string())).collect(),
533            Shape::Element,
534        ));
535    }
536    if let Some(rest) = base.strip_prefix("text-") {
537        if TEXT_SIZES.contains(&rest) {
538            return Some((
539                vec![
540                    ("font-size", format!("var(--text-{rest})")),
541                    (
542                        "line-height",
543                        format!("var(--tw-leading, var(--text-{rest}--line-height))"),
544                    ),
545                ],
546                Shape::Element,
547            ));
548        }
549        return colour(rest).and_then(|v| one("color", &v));
550    }
551    if let Some(rest) = base.strip_prefix("font-") {
552        if WEIGHTS.contains(&rest) {
553            return Some((
554                vec![
555                    ("--tw-font-weight", format!("var(--font-weight-{rest})")),
556                    ("font-weight", format!("var(--font-weight-{rest})")),
557                ],
558                Shape::Element,
559            ));
560        }
561        if ["sans", "serif", "mono"].contains(&rest) {
562            return one("font-family", &format!("var(--font-{rest})"));
563        }
564        return None;
565    }
566    if let Some(rest) = base.strip_prefix("rounded-") {
567        return match rest {
568            "none" => one("border-radius", "0"),
569            "full" => one("border-radius", "calc(infinity * 1px)"),
570            "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" => {
571                one("border-radius", &format!("var(--radius-{rest})"))
572            }
573            _ => None,
574        };
575    }
576    if let Some(rest) = base.strip_prefix("border-") {
577        if ["0", "2", "4", "8"].contains(&rest) {
578            return Some((
579                vec![
580                    ("border-style", "var(--tw-border-style)".to_string()),
581                    ("border-width", format!("{rest}px")),
582                ],
583                Shape::Element,
584            ));
585        }
586        return colour(rest).and_then(|v| one("border-color", &v));
587    }
588    if let Some(rest) = base.strip_prefix("items-") {
589        return match rest {
590            "start" => one("align-items", "flex-start"),
591            "center" => one("align-items", "center"),
592            "end" => one("align-items", "flex-end"),
593            "baseline" => one("align-items", "baseline"),
594            "stretch" => one("align-items", "stretch"),
595            _ => None,
596        };
597    }
598    if let Some(rest) = base.strip_prefix("justify-") {
599        return match rest {
600            "start" => one("justify-content", "flex-start"),
601            "center" => one("justify-content", "center"),
602            "end" => one("justify-content", "flex-end"),
603            "between" => one("justify-content", "space-between"),
604            "around" => one("justify-content", "space-around"),
605            "evenly" => one("justify-content", "space-evenly"),
606            "stretch" => one("justify-content", "stretch"),
607            _ => None,
608        };
609    }
610    if let Some(rest) = base.strip_prefix("flex-") {
611        return match rest {
612            "row" => one("flex-direction", "row"),
613            "row-reverse" => one("flex-direction", "row-reverse"),
614            "col" => one("flex-direction", "column"),
615            "col-reverse" => one("flex-direction", "column-reverse"),
616            "wrap" => one("flex-wrap", "wrap"),
617            "nowrap" => one("flex-wrap", "nowrap"),
618            "wrap-reverse" => one("flex-wrap", "wrap-reverse"),
619            "1" => one("flex", "1"),
620            "auto" => one("flex", "auto"),
621            "initial" => one("flex", "0 auto"),
622            "none" => one("flex", "none"),
623            _ => None,
624        };
625    }
626    if let Some(rest) = base.strip_prefix("overflow-") {
627        return match rest {
628            "auto" | "hidden" | "clip" | "visible" | "scroll" => one("overflow", rest),
629            _ => None,
630        };
631    }
632    for (family, property) in COLOURED {
633        if let Some(rest) = base.strip_prefix(family) {
634            return colour(rest).and_then(|v| one(property, &v));
635        }
636    }
637    for (family, properties, screen) in SIZED {
638        if let Some(rest) = base.strip_prefix(family) {
639            let value = match rest {
640                "full" => "100%".to_string(),
641                "min" => "min-content".to_string(),
642                "max" => "max-content".to_string(),
643                "fit" => "fit-content".to_string(),
644                "auto" if !family.starts_with("max-") => "auto".to_string(),
645                "screen" if *screen => match family.contains('h') {
646                    true => "100vh".to_string(),
647                    false => "100vw".to_string(),
648                },
649                _ => spacing(rest)?,
650            };
651            return Some((
652                properties.iter().map(|p| (*p, value.clone())).collect(),
653                Shape::Element,
654            ));
655        }
656    }
657    for (family, kind) in SPACED {
658        let Some(rest) = base.strip_prefix(family) else {
659            continue;
660        };
661        let Some(rest) = rest.strip_prefix('-') else {
662            continue;
663        };
664        let value = match rest {
665            "auto" => match kind {
666                Spaced::Padding { .. } | Spaced::Between { .. } => return None,
667                Spaced::Margin { property } => return one(property, "auto"),
668            },
669            _ => spacing(rest)?,
670        };
671        return Some(match kind {
672            Spaced::Padding { property } | Spaced::Margin { property } => {
673                (vec![(*property, value)], Shape::Element)
674            }
675            // `--tw-space-x-reverse` is `0` unless something flips it, so the margin lands on the
676            // start of every child but the first — written as a pair so that `flex-row-reverse`
677            // can invert it without a second class.
678            Spaced::Between {
679                reverse,
680                start,
681                end,
682            } => (
683                match value == "0px" {
684                    // Nothing to reverse about a gap of nothing, and Tailwind says so by writing
685                    // the margin out rather than the `calc` around it.
686                    true => vec![
687                        (reverse, "0".to_string()),
688                        (start, "0".to_string()),
689                        (end, "0".to_string()),
690                    ],
691                    false => vec![
692                        (reverse, "0".to_string()),
693                        (start, format!("calc({value} * var({reverse}))")),
694                        (end, format!("calc({value} * calc(1 - var({reverse})))")),
695                    ],
696                },
697                Shape::Between,
698            ),
699        });
700    }
701    None
702}
703
704/// A multiple of the spacing scale, as the value Tailwind gives it.
705///
706/// Three special cases, each of which the oracle said and none of which a person would have
707/// written down: zero is `0px` rather than `0`, one is the variable itself rather than a `calc`
708/// multiplying by one, and `px` is the literal pixel rather than a multiple of anything.
709fn spacing(rest: &str) -> Option<String> {
710    if rest == "px" {
711        return Some("1px".to_string());
712    }
713    if !number(rest) {
714        return None;
715    }
716    Some(match rest.parse::<f64>().ok()? {
717        0.0 => "0px".to_string(),
718        1.0 => "var(--spacing)".to_string(),
719        _ => format!("calc(var(--spacing) * {rest})"),
720    })
721}
722
723/// Utilities that are one name and take no argument.
724const WORDS: &[(&str, &[(&str, &str)])] = &[
725    ("flex", &[("display", "flex")]),
726    ("inline-flex", &[("display", "inline-flex")]),
727    ("grid", &[("display", "grid")]),
728    ("inline-grid", &[("display", "inline-grid")]),
729    ("block", &[("display", "block")]),
730    ("inline-block", &[("display", "inline-block")]),
731    ("inline", &[("display", "inline")]),
732    ("hidden", &[("display", "none")]),
733    ("contents", &[("display", "contents")]),
734    ("flow-root", &[("display", "flow-root")]),
735    ("table", &[("display", "table")]),
736    ("static", &[("position", "static")]),
737    ("relative", &[("position", "relative")]),
738    ("absolute", &[("position", "absolute")]),
739    ("fixed", &[("position", "fixed")]),
740    ("sticky", &[("position", "sticky")]),
741    ("italic", &[("font-style", "italic")]),
742    ("not-italic", &[("font-style", "normal")]),
743    ("underline", &[("text-decoration-line", "underline")]),
744    ("overline", &[("text-decoration-line", "overline")]),
745    ("line-through", &[("text-decoration-line", "line-through")]),
746    ("no-underline", &[("text-decoration-line", "none")]),
747    ("uppercase", &[("text-transform", "uppercase")]),
748    ("lowercase", &[("text-transform", "lowercase")]),
749    ("capitalize", &[("text-transform", "capitalize")]),
750    ("normal-case", &[("text-transform", "none")]),
751    (
752        "truncate",
753        &[
754            ("overflow", "hidden"),
755            ("text-overflow", "ellipsis"),
756            ("white-space", "nowrap"),
757        ],
758    ),
759    (
760        "border",
761        &[
762            ("border-style", "var(--tw-border-style)"),
763            ("border-width", "1px"),
764        ],
765    ),
766    ("rounded", &[("border-radius", "0.25rem")]),
767    (
768        "sr-only",
769        &[
770            ("position", "absolute"),
771            ("width", "1px"),
772            ("height", "1px"),
773            ("padding", "0"),
774            ("margin", "-1px"),
775            ("overflow", "hidden"),
776            ("clip-path", "inset(50%)"),
777            ("white-space", "nowrap"),
778            ("border-width", "0"),
779        ],
780    ),
781    (
782        "outline-none",
783        &[("--tw-outline-style", "none"), ("outline-style", "none")],
784    ),
785];
786
787/// The families whose argument is a colour, and the property each sets.
788const COLOURED: &[(&str, &str)] = &[
789    ("bg-", "background-color"),
790    ("fill-", "fill"),
791    ("stroke-", "stroke"),
792    ("ring-", "--tw-ring-color"),
793    ("outline-", "outline-color"),
794    ("decoration-", "text-decoration-color"),
795];
796
797/// The families whose argument is a length: the properties each sets, and whether `screen` is one
798/// of its values.
799///
800/// Longest first, so `min-w-` is tried before `w-`.
801const SIZED: &[(&str, &[&str], bool)] = &[
802    ("size-", &["width", "height"], false),
803    ("min-w-", &["min-width"], true),
804    ("min-h-", &["min-height"], true),
805    ("max-w-", &["max-width"], true),
806    ("max-h-", &["max-height"], true),
807    ("w-", &["width"], true),
808    ("h-", &["height"], true),
809];
810
811/// What a spacing family sets, and therefore whether `auto` is one of its values.
812enum Spaced {
813    Padding {
814        property: &'static str,
815    },
816    Margin {
817        property: &'static str,
818    },
819    Between {
820        reverse: &'static str,
821        start: &'static str,
822        end: &'static str,
823    },
824}
825
826/// The families whose argument is a multiple of the spacing scale.
827///
828/// Longest first, so `gap-x` is tried before `gap` and `-x-2` is never read as a number.
829const SPACED: &[(&str, Spaced)] = &[
830    (
831        "gap-x",
832        Spaced::Padding {
833            property: "column-gap",
834        },
835    ),
836    (
837        "gap-y",
838        Spaced::Padding {
839            property: "row-gap",
840        },
841    ),
842    (
843        "space-x",
844        Spaced::Between {
845            reverse: "--tw-space-x-reverse",
846            start: "margin-inline-start",
847            end: "margin-inline-end",
848        },
849    ),
850    (
851        "space-y",
852        Spaced::Between {
853            reverse: "--tw-space-y-reverse",
854            start: "margin-block-start",
855            end: "margin-block-end",
856        },
857    ),
858    (
859        "px",
860        Spaced::Padding {
861            property: "padding-inline",
862        },
863    ),
864    (
865        "py",
866        Spaced::Padding {
867            property: "padding-block",
868        },
869    ),
870    (
871        "pt",
872        Spaced::Padding {
873            property: "padding-top",
874        },
875    ),
876    (
877        "pr",
878        Spaced::Padding {
879            property: "padding-right",
880        },
881    ),
882    (
883        "pb",
884        Spaced::Padding {
885            property: "padding-bottom",
886        },
887    ),
888    (
889        "pl",
890        Spaced::Padding {
891            property: "padding-left",
892        },
893    ),
894    (
895        "ps",
896        Spaced::Padding {
897            property: "padding-inline-start",
898        },
899    ),
900    (
901        "pe",
902        Spaced::Padding {
903            property: "padding-inline-end",
904        },
905    ),
906    (
907        "mx",
908        Spaced::Margin {
909            property: "margin-inline",
910        },
911    ),
912    (
913        "my",
914        Spaced::Margin {
915            property: "margin-block",
916        },
917    ),
918    (
919        "mt",
920        Spaced::Margin {
921            property: "margin-top",
922        },
923    ),
924    (
925        "mr",
926        Spaced::Margin {
927            property: "margin-right",
928        },
929    ),
930    (
931        "mb",
932        Spaced::Margin {
933            property: "margin-bottom",
934        },
935    ),
936    (
937        "ml",
938        Spaced::Margin {
939            property: "margin-left",
940        },
941    ),
942    (
943        "ms",
944        Spaced::Margin {
945            property: "margin-inline-start",
946        },
947    ),
948    (
949        "me",
950        Spaced::Margin {
951            property: "margin-inline-end",
952        },
953    ),
954    ("gap", Spaced::Padding { property: "gap" }),
955    ("inset", Spaced::Margin { property: "inset" }),
956    ("top", Spaced::Margin { property: "top" }),
957    ("right", Spaced::Margin { property: "right" }),
958    ("bottom", Spaced::Margin { property: "bottom" }),
959    ("left", Spaced::Margin { property: "left" }),
960    (
961        "p",
962        Spaced::Padding {
963            property: "padding",
964        },
965    ),
966    ("m", Spaced::Margin { property: "margin" }),
967];
968
969const TEXT_SIZES: &[&str] = &[
970    "xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "8xl", "9xl",
971];
972
973const WEIGHTS: &[&str] = &[
974    "thin",
975    "extralight",
976    "light",
977    "normal",
978    "medium",
979    "semibold",
980    "bold",
981    "extrabold",
982    "black",
983];
984
985/// The palette, as the names rather than the values: what a shade *is* belongs to [`THEME`].
986const PALETTE: &[&str] = &[
987    "slate", "gray", "zinc", "neutral", "stone", "red", "orange", "amber", "yellow", "lime",
988    "green", "emerald", "teal", "cyan", "sky", "blue", "indigo", "violet", "purple", "fuchsia",
989    "pink", "rose",
990];
991
992const SHADES: &[&str] = &[
993    "50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950",
994];
995
996/// A colour as the value a declaration takes: a keyword, or a reference to a theme token.
997///
998/// The three keywords that are not tokens are not an inconsistency — `transparent` and `inherit`
999/// are CSS's own and there is nothing to theme about them, and `current` is the spelling
1000/// difference between Tailwind's name and CSS's `currentcolor`.
1001fn colour(rest: &str) -> Option<String> {
1002    match rest {
1003        "transparent" => return Some("transparent".to_string()),
1004        "current" => return Some("currentcolor".to_string()),
1005        "inherit" => return Some("inherit".to_string()),
1006        "white" | "black" => return Some(format!("var(--color-{rest})")),
1007        _ => {}
1008    }
1009    let (name, shade) = rest.rsplit_once('-')?;
1010    match PALETTE.contains(&name) && SHADES.contains(&shade) {
1011        true => Some(format!("var(--color-{rest})")),
1012        false => None,
1013    }
1014}
1015
1016/// A multiple of the spacing scale: a decimal number, because Tailwind 4's scale is multiplicative
1017/// rather than a list of steps.
1018fn number(rest: &str) -> bool {
1019    !rest.is_empty()
1020        && rest.chars().all(|c| c.is_ascii_digit() || c == '.')
1021        && rest.chars().filter(|c| *c == '.').count() <= 1
1022        && rest.chars().next().is_some_and(|c| c.is_ascii_digit())
1023        && rest.chars().last().is_some_and(|c| c.is_ascii_digit())
1024}
1025
1026/// Every **closed** utility name this table knows, and every variant that can go in front of one.
1027///
1028/// The spacing scale is multiplicative and therefore open — `p-4` is here and `p-2.75` is not — so
1029/// this is the part of the table that can be listed. Its caller is the gate, which asks the oracle
1030/// about every one of them rather than about a list somebody wrote: a name accepted here and never
1031/// asked about is a page missing a rule with every gate green, which is how `size-screen`,
1032/// `max-w-auto` and fifteen `-auto` paddings survived a green run of the table's own differential.
1033pub fn enumerate() -> (Vec<String>, Vec<&'static str>) {
1034    let mut names: Vec<String> = Vec::new();
1035    let mut add = |name: String| {
1036        if rule(&name).is_some() {
1037            names.push(name);
1038        }
1039    };
1040    for (word, _) in WORDS {
1041        add(word.to_string());
1042    }
1043    let colours: Vec<String> = ["white", "black", "transparent", "current", "inherit"]
1044        .iter()
1045        .map(|k| k.to_string())
1046        .chain(
1047            PALETTE
1048                .iter()
1049                .flat_map(|p| SHADES.iter().map(move |s| format!("{p}-{s}"))),
1050        )
1051        .collect();
1052    for family in COLOURED.iter().map(|(f, _)| *f).chain(["text-", "border-"]) {
1053        for colour in &colours {
1054            add(format!("{family}{colour}"));
1055        }
1056    }
1057    for size in TEXT_SIZES {
1058        add(format!("text-{size}"));
1059    }
1060    for weight in WEIGHTS.iter().chain(&["sans", "serif", "mono"]) {
1061        add(format!("font-{weight}"));
1062    }
1063    for radius in [
1064        "none", "xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "full",
1065    ] {
1066        add(format!("rounded-{radius}"));
1067    }
1068    for width in ["0", "2", "4", "8"] {
1069        add(format!("border-{width}"));
1070    }
1071    for (family, values) in [
1072        (
1073            "items-",
1074            &["start", "center", "end", "baseline", "stretch"][..],
1075        ),
1076        (
1077            "justify-",
1078            &[
1079                "start", "center", "end", "between", "around", "evenly", "stretch",
1080            ][..],
1081        ),
1082        (
1083            "flex-",
1084            &[
1085                "row",
1086                "row-reverse",
1087                "col",
1088                "col-reverse",
1089                "wrap",
1090                "nowrap",
1091                "wrap-reverse",
1092                "1",
1093                "auto",
1094                "initial",
1095                "none",
1096            ][..],
1097        ),
1098        (
1099            "overflow-",
1100            &["auto", "hidden", "clip", "visible", "scroll"][..],
1101        ),
1102    ] {
1103        for value in values {
1104            add(format!("{family}{value}"));
1105        }
1106    }
1107    // The closed arguments of the open families: their keywords, and **the scale Tailwind's own
1108    // documentation lists**. The scale is multiplicative and therefore infinite, so this is a
1109    // sample by construction — but it is the sample somebody typing `gap-` expects to be offered,
1110    // which is why it is the documented steps rather than three round numbers. Every one of them is
1111    // in `compiler/style/candidates.txt`, which is what
1112    // `style.rs::every_name_the_table_accepts_was_asked_about` holds.
1113    for (family, _, _) in SIZED {
1114        for value in ["full", "auto", "screen", "min", "max", "fit", "px"] {
1115            add(format!("{family}{value}"));
1116        }
1117        for step in SCALE {
1118            add(format!("{family}{step}"));
1119        }
1120    }
1121    for (family, _) in SPACED {
1122        for value in ["px", "auto"] {
1123            add(format!("{family}-{value}"));
1124        }
1125        for step in SCALE {
1126            add(format!("{family}-{step}"));
1127        }
1128    }
1129    (names, VARIANTS.to_vec())
1130}
1131
1132/// The steps of the spacing scale Tailwind's documentation lists.
1133///
1134/// Not the scale — that is `calc(var(--spacing) * n)` for any `n` and has no end — but the part of
1135/// it a person browsing a completion list is looking for. [`rule`] accepts any number whether or
1136/// not it is here.
1137const SCALE: &[&str] = &[
1138    "0", "0.5", "1", "1.5", "2", "2.5", "3", "3.5", "4", "5", "6", "7", "8", "9", "10", "11", "12",
1139    "14", "16", "20", "24", "28", "32", "36", "40", "44", "48", "52", "56", "60", "64", "72", "80",
1140    "96",
1141];
1142
1143/// Every variant this knows, in front of any utility.
1144const VARIANTS: &[&str] = &[
1145    "hover",
1146    "focus",
1147    "focus-visible",
1148    "focus-within",
1149    "active",
1150    "visited",
1151    "disabled",
1152    "checked",
1153    "first",
1154    "last",
1155    "odd",
1156    "even",
1157    "empty",
1158    "dark",
1159    "motion-safe",
1160    "motion-reduce",
1161    "print",
1162    "sm",
1163    "md",
1164    "lg",
1165    "xl",
1166    "2xl",
1167];
1168
1169/// The condition guarding [`stylesheet`]'s fallback for browsers with no registered custom
1170/// properties. Tailwind's own, captured by `compiler/style/generate.sh` rather than transcribed.
1171const SUPPORTS: &str = "((-webkit-hyphens: none) and (not (margin-trim: inline))) or \
1172                        ((-moz-orient: inline) and (not (color:rgb(from red r g b))))";
1173
1174/// One theme token's value, or `None` if the theme does not define it.
1175pub fn theme(token: &str) -> Option<&'static str> {
1176    THEME.iter().find(|(n, _)| *n == token).map(|(_, v)| *v)
1177}
1178
1179/// Every token the theme defines, so the gate can hold each to Tailwind's own value.
1180pub fn theme_tokens() -> &'static [(&'static str, &'static str)] {
1181    THEME
1182}
1183
1184/// Every registered custom property the utilities use, and what each is.
1185pub fn properties() -> &'static [(&'static str, &'static str)] {
1186    PROPERTIES
1187}
1188
1189/// The condition guarding the sheet's fallback for browsers with no registered custom properties.
1190pub fn supports() -> &'static str {
1191    SUPPORTS
1192}
1193
1194/// The theme, as Tailwind 4.3.3 defines it.
1195///
1196/// **Values rather than names**, which is the half [`is_utility`] never needed and a sheet cannot
1197/// do without. A ramp is not derivable from anything — `oklch(50.8% 0.118 165.612)` is a decade of
1198/// somebody's taste — so this is transcribed from the oracle's own output by
1199/// `compiler/style/generate.sh`, and `style.rs::the_theme_is_tailwinds` holds every entry against
1200/// it. That gate cannot catch the transcription that produced the table, only an edit to it
1201/// afterwards and a version that moves underneath it, and saying so is the point: it is a
1202/// regression gate rather than a derivation.
1203///
1204/// [`docs/08`](../../../../../docs/08-roadmap.md) §8.5.4's styling item 5 is what makes this a Beck
1205/// value a program can change; until then it is the default and the only one.
1206const THEME: &[(&str, &str)] = &[
1207    ("--color-amber-100", "oklch(96.2% 0.059 95.617)"),
1208    ("--color-amber-200", "oklch(92.4% 0.12 95.746)"),
1209    ("--color-amber-300", "oklch(87.9% 0.169 91.605)"),
1210    ("--color-amber-400", "oklch(82.8% 0.189 84.429)"),
1211    ("--color-amber-50", "oklch(98.7% 0.022 95.277)"),
1212    ("--color-amber-500", "oklch(76.9% 0.188 70.08)"),
1213    ("--color-amber-600", "oklch(66.6% 0.179 58.318)"),
1214    ("--color-amber-700", "oklch(55.5% 0.163 48.998)"),
1215    ("--color-amber-800", "oklch(47.3% 0.137 46.201)"),
1216    ("--color-amber-900", "oklch(41.4% 0.112 45.904)"),
1217    ("--color-amber-950", "oklch(27.9% 0.077 45.635)"),
1218    ("--color-black", "#000"),
1219    ("--color-blue-100", "oklch(93.2% 0.032 255.585)"),
1220    ("--color-blue-200", "oklch(88.2% 0.059 254.128)"),
1221    ("--color-blue-300", "oklch(80.9% 0.105 251.813)"),
1222    ("--color-blue-400", "oklch(70.7% 0.165 254.624)"),
1223    ("--color-blue-50", "oklch(97% 0.014 254.604)"),
1224    ("--color-blue-500", "oklch(62.3% 0.214 259.815)"),
1225    ("--color-blue-600", "oklch(54.6% 0.245 262.881)"),
1226    ("--color-blue-700", "oklch(48.8% 0.243 264.376)"),
1227    ("--color-blue-800", "oklch(42.4% 0.199 265.638)"),
1228    ("--color-blue-900", "oklch(37.9% 0.146 265.522)"),
1229    ("--color-blue-950", "oklch(28.2% 0.091 267.935)"),
1230    ("--color-cyan-100", "oklch(95.6% 0.045 203.388)"),
1231    ("--color-cyan-200", "oklch(91.7% 0.08 205.041)"),
1232    ("--color-cyan-300", "oklch(86.5% 0.127 207.078)"),
1233    ("--color-cyan-400", "oklch(78.9% 0.154 211.53)"),
1234    ("--color-cyan-50", "oklch(98.4% 0.019 200.873)"),
1235    ("--color-cyan-500", "oklch(71.5% 0.143 215.221)"),
1236    ("--color-cyan-600", "oklch(60.9% 0.126 221.723)"),
1237    ("--color-cyan-700", "oklch(52% 0.105 223.128)"),
1238    ("--color-cyan-800", "oklch(45% 0.085 224.283)"),
1239    ("--color-cyan-900", "oklch(39.8% 0.07 227.392)"),
1240    ("--color-cyan-950", "oklch(30.2% 0.056 229.695)"),
1241    ("--color-emerald-100", "oklch(95% 0.052 163.051)"),
1242    ("--color-emerald-200", "oklch(90.5% 0.093 164.15)"),
1243    ("--color-emerald-300", "oklch(84.5% 0.143 164.978)"),
1244    ("--color-emerald-400", "oklch(76.5% 0.177 163.223)"),
1245    ("--color-emerald-50", "oklch(97.9% 0.021 166.113)"),
1246    ("--color-emerald-500", "oklch(69.6% 0.17 162.48)"),
1247    ("--color-emerald-600", "oklch(59.6% 0.145 163.225)"),
1248    ("--color-emerald-700", "oklch(50.8% 0.118 165.612)"),
1249    ("--color-emerald-800", "oklch(43.2% 0.095 166.913)"),
1250    ("--color-emerald-900", "oklch(37.8% 0.077 168.94)"),
1251    ("--color-emerald-950", "oklch(26.2% 0.051 172.552)"),
1252    ("--color-fuchsia-100", "oklch(95.2% 0.037 318.852)"),
1253    ("--color-fuchsia-200", "oklch(90.3% 0.076 319.62)"),
1254    ("--color-fuchsia-300", "oklch(83.3% 0.145 321.434)"),
1255    ("--color-fuchsia-400", "oklch(74% 0.238 322.16)"),
1256    ("--color-fuchsia-50", "oklch(97.7% 0.017 320.058)"),
1257    ("--color-fuchsia-500", "oklch(66.7% 0.295 322.15)"),
1258    ("--color-fuchsia-600", "oklch(59.1% 0.293 322.896)"),
1259    ("--color-fuchsia-700", "oklch(51.8% 0.253 323.949)"),
1260    ("--color-fuchsia-800", "oklch(45.2% 0.211 324.591)"),
1261    ("--color-fuchsia-900", "oklch(40.1% 0.17 325.612)"),
1262    ("--color-fuchsia-950", "oklch(29.3% 0.136 325.661)"),
1263    ("--color-gray-100", "oklch(96.7% 0.003 264.542)"),
1264    ("--color-gray-200", "oklch(92.8% 0.006 264.531)"),
1265    ("--color-gray-300", "oklch(87.2% 0.01 258.338)"),
1266    ("--color-gray-400", "oklch(70.7% 0.022 261.325)"),
1267    ("--color-gray-50", "oklch(98.5% 0.002 247.839)"),
1268    ("--color-gray-500", "oklch(55.1% 0.027 264.364)"),
1269    ("--color-gray-600", "oklch(44.6% 0.03 256.802)"),
1270    ("--color-gray-700", "oklch(37.3% 0.034 259.733)"),
1271    ("--color-gray-800", "oklch(27.8% 0.033 256.848)"),
1272    ("--color-gray-900", "oklch(21% 0.034 264.665)"),
1273    ("--color-gray-950", "oklch(13% 0.028 261.692)"),
1274    ("--color-green-100", "oklch(96.2% 0.044 156.743)"),
1275    ("--color-green-200", "oklch(92.5% 0.084 155.995)"),
1276    ("--color-green-300", "oklch(87.1% 0.15 154.449)"),
1277    ("--color-green-400", "oklch(79.2% 0.209 151.711)"),
1278    ("--color-green-50", "oklch(98.2% 0.018 155.826)"),
1279    ("--color-green-500", "oklch(72.3% 0.219 149.579)"),
1280    ("--color-green-600", "oklch(62.7% 0.194 149.214)"),
1281    ("--color-green-700", "oklch(52.7% 0.154 150.069)"),
1282    ("--color-green-800", "oklch(44.8% 0.119 151.328)"),
1283    ("--color-green-900", "oklch(39.3% 0.095 152.535)"),
1284    ("--color-green-950", "oklch(26.6% 0.065 152.934)"),
1285    ("--color-indigo-100", "oklch(93% 0.034 272.788)"),
1286    ("--color-indigo-200", "oklch(87% 0.065 274.039)"),
1287    ("--color-indigo-300", "oklch(78.5% 0.115 274.713)"),
1288    ("--color-indigo-400", "oklch(67.3% 0.182 276.935)"),
1289    ("--color-indigo-50", "oklch(96.2% 0.018 272.314)"),
1290    ("--color-indigo-500", "oklch(58.5% 0.233 277.117)"),
1291    ("--color-indigo-600", "oklch(51.1% 0.262 276.966)"),
1292    ("--color-indigo-700", "oklch(45.7% 0.24 277.023)"),
1293    ("--color-indigo-800", "oklch(39.8% 0.195 277.366)"),
1294    ("--color-indigo-900", "oklch(35.9% 0.144 278.697)"),
1295    ("--color-indigo-950", "oklch(25.7% 0.09 281.288)"),
1296    ("--color-lime-100", "oklch(96.7% 0.067 122.328)"),
1297    ("--color-lime-200", "oklch(93.8% 0.127 124.321)"),
1298    ("--color-lime-300", "oklch(89.7% 0.196 126.665)"),
1299    ("--color-lime-400", "oklch(84.1% 0.238 128.85)"),
1300    ("--color-lime-50", "oklch(98.6% 0.031 120.757)"),
1301    ("--color-lime-500", "oklch(76.8% 0.233 130.85)"),
1302    ("--color-lime-600", "oklch(64.8% 0.2 131.684)"),
1303    ("--color-lime-700", "oklch(53.2% 0.157 131.589)"),
1304    ("--color-lime-800", "oklch(45.3% 0.124 130.933)"),
1305    ("--color-lime-900", "oklch(40.5% 0.101 131.063)"),
1306    ("--color-lime-950", "oklch(27.4% 0.072 132.109)"),
1307    ("--color-neutral-100", "oklch(97% 0 none)"),
1308    ("--color-neutral-200", "oklch(92.2% 0 none)"),
1309    ("--color-neutral-300", "oklch(87% 0 none)"),
1310    ("--color-neutral-400", "oklch(70.8% 0 none)"),
1311    ("--color-neutral-50", "oklch(98.5% 0 none)"),
1312    ("--color-neutral-500", "oklch(55.6% 0 none)"),
1313    ("--color-neutral-600", "oklch(43.9% 0 none)"),
1314    ("--color-neutral-700", "oklch(37.1% 0 none)"),
1315    ("--color-neutral-800", "oklch(26.9% 0 none)"),
1316    ("--color-neutral-900", "oklch(20.5% 0 none)"),
1317    ("--color-neutral-950", "oklch(14.5% 0 none)"),
1318    ("--color-orange-100", "oklch(95.4% 0.038 75.164)"),
1319    ("--color-orange-200", "oklch(90.1% 0.076 70.697)"),
1320    ("--color-orange-300", "oklch(83.7% 0.128 66.29)"),
1321    ("--color-orange-400", "oklch(75% 0.183 55.934)"),
1322    ("--color-orange-50", "oklch(98% 0.016 73.684)"),
1323    ("--color-orange-500", "oklch(70.5% 0.213 47.604)"),
1324    ("--color-orange-600", "oklch(64.6% 0.222 41.116)"),
1325    ("--color-orange-700", "oklch(55.3% 0.195 38.402)"),
1326    ("--color-orange-800", "oklch(47% 0.157 37.304)"),
1327    ("--color-orange-900", "oklch(40.8% 0.123 38.172)"),
1328    ("--color-orange-950", "oklch(26.6% 0.079 36.259)"),
1329    ("--color-pink-100", "oklch(94.8% 0.028 342.258)"),
1330    ("--color-pink-200", "oklch(89.9% 0.061 343.231)"),
1331    ("--color-pink-300", "oklch(82.3% 0.12 346.018)"),
1332    ("--color-pink-400", "oklch(71.8% 0.202 349.761)"),
1333    ("--color-pink-50", "oklch(97.1% 0.014 343.198)"),
1334    ("--color-pink-500", "oklch(65.6% 0.241 354.308)"),
1335    ("--color-pink-600", "oklch(59.2% 0.249 0.584)"),
1336    ("--color-pink-700", "oklch(52.5% 0.223 3.958)"),
1337    ("--color-pink-800", "oklch(45.9% 0.187 3.815)"),
1338    ("--color-pink-900", "oklch(40.8% 0.153 2.432)"),
1339    ("--color-pink-950", "oklch(28.4% 0.109 3.907)"),
1340    ("--color-purple-100", "oklch(94.6% 0.033 307.174)"),
1341    ("--color-purple-200", "oklch(90.2% 0.063 306.703)"),
1342    ("--color-purple-300", "oklch(82.7% 0.119 306.383)"),
1343    ("--color-purple-400", "oklch(71.4% 0.203 305.504)"),
1344    ("--color-purple-50", "oklch(97.7% 0.014 308.299)"),
1345    ("--color-purple-500", "oklch(62.7% 0.265 303.9)"),
1346    ("--color-purple-600", "oklch(55.8% 0.288 302.321)"),
1347    ("--color-purple-700", "oklch(49.6% 0.265 301.924)"),
1348    ("--color-purple-800", "oklch(43.8% 0.218 303.724)"),
1349    ("--color-purple-900", "oklch(38.1% 0.176 304.987)"),
1350    ("--color-purple-950", "oklch(29.1% 0.149 302.717)"),
1351    ("--color-red-100", "oklch(93.6% 0.032 17.717)"),
1352    ("--color-red-200", "oklch(88.5% 0.062 18.334)"),
1353    ("--color-red-300", "oklch(80.8% 0.114 19.571)"),
1354    ("--color-red-400", "oklch(70.4% 0.191 22.216)"),
1355    ("--color-red-50", "oklch(97.1% 0.013 17.38)"),
1356    ("--color-red-500", "oklch(63.7% 0.237 25.331)"),
1357    ("--color-red-600", "oklch(57.7% 0.245 27.325)"),
1358    ("--color-red-700", "oklch(50.5% 0.213 27.518)"),
1359    ("--color-red-800", "oklch(44.4% 0.177 26.899)"),
1360    ("--color-red-900", "oklch(39.6% 0.141 25.723)"),
1361    ("--color-red-950", "oklch(25.8% 0.092 26.042)"),
1362    ("--color-rose-100", "oklch(94.1% 0.03 12.58)"),
1363    ("--color-rose-200", "oklch(89.2% 0.058 10.001)"),
1364    ("--color-rose-300", "oklch(81% 0.117 11.638)"),
1365    ("--color-rose-400", "oklch(71.2% 0.194 13.428)"),
1366    ("--color-rose-50", "oklch(96.9% 0.015 12.422)"),
1367    ("--color-rose-500", "oklch(64.5% 0.246 16.439)"),
1368    ("--color-rose-600", "oklch(58.6% 0.253 17.585)"),
1369    ("--color-rose-700", "oklch(51.4% 0.222 16.935)"),
1370    ("--color-rose-800", "oklch(45.5% 0.188 13.697)"),
1371    ("--color-rose-900", "oklch(41% 0.159 10.272)"),
1372    ("--color-rose-950", "oklch(27.1% 0.105 12.094)"),
1373    ("--color-sky-100", "oklch(95.1% 0.026 236.824)"),
1374    ("--color-sky-200", "oklch(90.1% 0.058 230.902)"),
1375    ("--color-sky-300", "oklch(82.8% 0.111 230.318)"),
1376    ("--color-sky-400", "oklch(74.6% 0.16 232.661)"),
1377    ("--color-sky-50", "oklch(97.7% 0.013 236.62)"),
1378    ("--color-sky-500", "oklch(68.5% 0.169 237.323)"),
1379    ("--color-sky-600", "oklch(58.8% 0.158 241.966)"),
1380    ("--color-sky-700", "oklch(50% 0.134 242.749)"),
1381    ("--color-sky-800", "oklch(44.3% 0.11 240.79)"),
1382    ("--color-sky-900", "oklch(39.1% 0.09 240.876)"),
1383    ("--color-sky-950", "oklch(29.3% 0.066 243.157)"),
1384    ("--color-slate-100", "oklch(96.8% 0.007 247.896)"),
1385    ("--color-slate-200", "oklch(92.9% 0.013 255.508)"),
1386    ("--color-slate-300", "oklch(86.9% 0.022 252.894)"),
1387    ("--color-slate-400", "oklch(70.4% 0.04 256.788)"),
1388    ("--color-slate-50", "oklch(98.4% 0.003 247.858)"),
1389    ("--color-slate-500", "oklch(55.4% 0.046 257.417)"),
1390    ("--color-slate-600", "oklch(44.6% 0.043 257.281)"),
1391    ("--color-slate-700", "oklch(37.2% 0.044 257.287)"),
1392    ("--color-slate-800", "oklch(27.9% 0.041 260.031)"),
1393    ("--color-slate-900", "oklch(20.8% 0.042 265.755)"),
1394    ("--color-slate-950", "oklch(12.9% 0.042 264.695)"),
1395    ("--color-stone-100", "oklch(97% 0.001 106.424)"),
1396    ("--color-stone-200", "oklch(92.3% 0.003 48.717)"),
1397    ("--color-stone-300", "oklch(86.9% 0.005 56.366)"),
1398    ("--color-stone-400", "oklch(70.9% 0.01 56.259)"),
1399    ("--color-stone-50", "oklch(98.5% 0.001 106.423)"),
1400    ("--color-stone-500", "oklch(55.3% 0.013 58.071)"),
1401    ("--color-stone-600", "oklch(44.4% 0.011 73.639)"),
1402    ("--color-stone-700", "oklch(37.4% 0.01 67.558)"),
1403    ("--color-stone-800", "oklch(26.8% 0.007 34.298)"),
1404    ("--color-stone-900", "oklch(21.6% 0.006 56.043)"),
1405    ("--color-stone-950", "oklch(14.7% 0.004 49.25)"),
1406    ("--color-teal-100", "oklch(95.3% 0.051 180.801)"),
1407    ("--color-teal-200", "oklch(91% 0.096 180.426)"),
1408    ("--color-teal-300", "oklch(85.5% 0.138 181.071)"),
1409    ("--color-teal-400", "oklch(77.7% 0.152 181.912)"),
1410    ("--color-teal-50", "oklch(98.4% 0.014 180.72)"),
1411    ("--color-teal-500", "oklch(70.4% 0.14 182.503)"),
1412    ("--color-teal-600", "oklch(60% 0.118 184.704)"),
1413    ("--color-teal-700", "oklch(51.1% 0.096 186.391)"),
1414    ("--color-teal-800", "oklch(43.7% 0.078 188.216)"),
1415    ("--color-teal-900", "oklch(38.6% 0.063 188.416)"),
1416    ("--color-teal-950", "oklch(27.7% 0.046 192.524)"),
1417    ("--color-violet-100", "oklch(94.3% 0.029 294.588)"),
1418    ("--color-violet-200", "oklch(89.4% 0.057 293.283)"),
1419    ("--color-violet-300", "oklch(81.1% 0.111 293.571)"),
1420    ("--color-violet-400", "oklch(70.2% 0.183 293.541)"),
1421    ("--color-violet-50", "oklch(96.9% 0.016 293.756)"),
1422    ("--color-violet-500", "oklch(60.6% 0.25 292.717)"),
1423    ("--color-violet-600", "oklch(54.1% 0.281 293.009)"),
1424    ("--color-violet-700", "oklch(49.1% 0.27 292.581)"),
1425    ("--color-violet-800", "oklch(43.2% 0.232 292.759)"),
1426    ("--color-violet-900", "oklch(38% 0.189 293.745)"),
1427    ("--color-violet-950", "oklch(28.3% 0.141 291.089)"),
1428    ("--color-white", "#fff"),
1429    ("--color-yellow-100", "oklch(97.3% 0.071 103.193)"),
1430    ("--color-yellow-200", "oklch(94.5% 0.129 101.54)"),
1431    ("--color-yellow-300", "oklch(90.5% 0.182 98.111)"),
1432    ("--color-yellow-400", "oklch(85.2% 0.199 91.936)"),
1433    ("--color-yellow-50", "oklch(98.7% 0.026 102.212)"),
1434    ("--color-yellow-500", "oklch(79.5% 0.184 86.047)"),
1435    ("--color-yellow-600", "oklch(68.1% 0.162 75.834)"),
1436    ("--color-yellow-700", "oklch(55.4% 0.135 66.442)"),
1437    ("--color-yellow-800", "oklch(47.6% 0.114 61.907)"),
1438    ("--color-yellow-900", "oklch(42.1% 0.095 57.708)"),
1439    ("--color-yellow-950", "oklch(28.6% 0.066 53.813)"),
1440    ("--color-zinc-100", "oklch(96.7% 0.001 286.375)"),
1441    ("--color-zinc-200", "oklch(92% 0.004 286.32)"),
1442    ("--color-zinc-300", "oklch(87.1% 0.006 286.286)"),
1443    ("--color-zinc-400", "oklch(70.5% 0.015 286.067)"),
1444    ("--color-zinc-50", "oklch(98.5% 0 none)"),
1445    ("--color-zinc-500", "oklch(55.2% 0.016 285.938)"),
1446    ("--color-zinc-600", "oklch(44.2% 0.017 285.786)"),
1447    ("--color-zinc-700", "oklch(37% 0.013 285.805)"),
1448    ("--color-zinc-800", "oklch(27.4% 0.006 286.033)"),
1449    ("--color-zinc-900", "oklch(21% 0.006 285.885)"),
1450    ("--color-zinc-950", "oklch(14.1% 0.005 285.823)"),
1451    ("--default-font-family", "var(--font-sans)"),
1452    ("--default-mono-font-family", "var(--font-mono)"),
1453    ("--font-mono", "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace"),
1454    ("--font-sans", "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\""),
1455    ("--font-serif", "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif"),
1456    ("--font-weight-black", "900"),
1457    ("--font-weight-bold", "700"),
1458    ("--font-weight-extrabold", "800"),
1459    ("--font-weight-extralight", "200"),
1460    ("--font-weight-light", "300"),
1461    ("--font-weight-medium", "500"),
1462    ("--font-weight-normal", "400"),
1463    ("--font-weight-semibold", "600"),
1464    ("--font-weight-thin", "100"),
1465    ("--radius-2xl", "1rem"),
1466    ("--radius-3xl", "1.5rem"),
1467    ("--radius-4xl", "2rem"),
1468    ("--radius-lg", "0.5rem"),
1469    ("--radius-md", "0.375rem"),
1470    ("--radius-sm", "0.25rem"),
1471    ("--radius-xl", "0.75rem"),
1472    ("--radius-xs", "0.125rem"),
1473    ("--spacing", "0.25rem"),
1474    ("--text-2xl", "1.5rem"),
1475    ("--text-2xl--line-height", "calc(2 / 1.5)"),
1476    ("--text-3xl", "1.875rem"),
1477    ("--text-3xl--line-height", "calc(2.25 / 1.875)"),
1478    ("--text-4xl", "2.25rem"),
1479    ("--text-4xl--line-height", "calc(2.5 / 2.25)"),
1480    ("--text-5xl", "3rem"),
1481    ("--text-5xl--line-height", "1"),
1482    ("--text-6xl", "3.75rem"),
1483    ("--text-6xl--line-height", "1"),
1484    ("--text-7xl", "4.5rem"),
1485    ("--text-7xl--line-height", "1"),
1486    ("--text-8xl", "6rem"),
1487    ("--text-8xl--line-height", "1"),
1488    ("--text-9xl", "8rem"),
1489    ("--text-9xl--line-height", "1"),
1490    ("--text-base", "1rem"),
1491    ("--text-base--line-height", "calc(1.5 / 1)"),
1492    ("--text-lg", "1.125rem"),
1493    ("--text-lg--line-height", "calc(1.75 / 1.125)"),
1494    ("--text-sm", "0.875rem"),
1495    ("--text-sm--line-height", "calc(1.25 / 0.875)"),
1496    ("--text-xl", "1.25rem"),
1497    ("--text-xl--line-height", "calc(1.75 / 1.25)"),
1498    ("--text-xs", "0.75rem"),
1499    ("--text-xs--line-height", "calc(1 / 0.75)"),
1500];
1501
1502// -------------------------------------------------------------------------------------------
1503// The sheet
1504// -------------------------------------------------------------------------------------------
1505
1506/// The stylesheet a program's pages need, and **nothing else**.
1507///
1508/// [`docs/104`](../../../../../docs/104-styling-and-the-component-library.md) §104.4's first
1509/// sentence, mechanised: "`beck build` walks the typed tree, collects the class strings that reach
1510/// a `class=`, and emits the sheet. No false positives, no false negatives across a module
1511/// boundary, and no configuration." [`classes`] is the walk and this is the sheet.
1512///
1513/// What it contains, in order:
1514///
1515/// 1. **A preflight.** Beck's own, and small — a browser's defaults disagree with every utility
1516///    that sets a margin. It is *not* Tailwind's, which is the delivery mechanism's opinionated
1517///    global sheet rather than the design system §104.4 takes, and §104.4 says which rules it has.
1518/// 2. **The theme tokens the rules read**, and only those: a page using one colour defines one
1519///    colour. This is the half [`docs/08`](../../../../../docs/08-roadmap.md) §8.5.4's styling item
1520///    5 makes a Beck value; here it is Tailwind's defaults.
1521/// 3. **`@property` for the internals the rules read**, with the fallback Tailwind ships for
1522///    browsers that do not register custom properties.
1523/// 4. **One rule per class the program can carry**, in name order — which puts `p-4` before
1524///    `px-2` because `-` sorts before a letter, so a shorthand loses to the longhand that follows
1525///    it, which is the order a reader expects and the order Tailwind's own sheet has.
1526///
1527/// A class the program carries that is **not** a utility contributes nothing: it is the program's
1528/// own name, and the compiler has nothing to say about what it should look like.
1529pub fn stylesheet(styles: &Styles) -> String {
1530    let mut rules: Vec<(Arc<str>, Rule)> = styles
1531        .classes
1532        .iter()
1533        .filter_map(|c| rule(c).map(|r| (c.clone(), r)))
1534        .collect();
1535    // Base rules before conditional ones, so a media query overrides what it narrows.
1536    rules.sort_by(|(a, x), (b, y)| x.at.len().cmp(&y.at.len()).then_with(|| a.cmp(b)));
1537
1538    let mut tokens: BTreeSet<&'static str> = PREFLIGHT_TOKENS.iter().copied().collect();
1539    let mut internals: BTreeSet<&'static str> = BTreeSet::new();
1540    for (_, r) in &rules {
1541        r.tokens(&mut tokens);
1542        for (property, value) in &r.decls {
1543            for (name, _) in PROPERTIES {
1544                if property == name || value.contains(name) {
1545                    internals.insert(name);
1546                }
1547            }
1548        }
1549    }
1550
1551    let mut out = String::with_capacity(2048);
1552    out.push_str("/* Written by `beck build` — one rule per class this program's pages can\n");
1553    out.push_str("   carry, and nothing else. docs/104 §104.4. */\n");
1554    out.push_str(PREFLIGHT);
1555    if !tokens.is_empty() {
1556        out.push_str(":root{");
1557        for token in &tokens {
1558            let value = THEME
1559                .iter()
1560                .find(|(n, _)| n == token)
1561                .map(|(_, v)| *v)
1562                .unwrap_or("");
1563            let _ = write!(out, "{token}:{value};");
1564        }
1565        out.push_str("}\n");
1566    }
1567    for (name, body) in PROPERTIES.iter().filter(|(n, _)| internals.contains(n)) {
1568        let _ = writeln!(out, "@property {name}{{{body}}}");
1569    }
1570    // The fallback for a browser that does not register custom properties: the same initial values,
1571    // set where a declaration would have found them. Its condition is Tailwind's own, captured
1572    // rather than transcribed — a browser-detection expression is the kind of string nobody can
1573    // check by reading it.
1574    let initial: Vec<&(&str, &str)> = PROPERTIES
1575        .iter()
1576        .filter(|(n, body)| internals.contains(n) && body.contains("initial-value"))
1577        .collect();
1578    if !initial.is_empty() {
1579        let _ = write!(out, "@supports {SUPPORTS}{{*,::before,::after,::backdrop{{");
1580        for (name, body) in initial {
1581            let value = body
1582                .rsplit_once("initial-value:")
1583                .map_or("", |(_, v)| v.trim());
1584            let _ = write!(out, "{name}:{value};");
1585        }
1586        out.push_str("}}\n");
1587    }
1588    for (_, rule) in &rules {
1589        for at in &rule.at {
1590            let _ = write!(out, "{at}{{");
1591        }
1592        let _ = write!(out, "{}{{", rule.selector);
1593        for (property, value) in &rule.decls {
1594            let _ = write!(out, "{property}:{value};");
1595        }
1596        out.push('}');
1597        out.push_str(&"}".repeat(rule.at.len()));
1598        out.push('\n');
1599    }
1600    out
1601}
1602
1603/// Beck's preflight: what a browser has to be told before a utility means anything.
1604///
1605/// Nine rules, and each is here because a browser default fights a utility rather than because it
1606/// is a taste: `p-0` cannot win against a `ul`'s padding, `flex` cannot lay out an `li` carrying a
1607/// marker, and a `button` renders in the browser's font whatever `font-sans` says. Tailwind's own
1608/// preflight is four times this and is part of the *delivery mechanism* — an opinionated global
1609/// sheet that arrives with the tool — rather than the design system §104.4 takes.
1610const PREFLIGHT: &str = concat!(
1611    "*,::before,::after{box-sizing:border-box;margin:0;padding:0;border:0 solid}\n",
1612    "html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:var(--default-font-family)}\n",
1613    "ul,ol{list-style:none}\n",
1614    "a{color:inherit;text-decoration:inherit}\n",
1615    "button,input,select,textarea{font:inherit;color:inherit;background:transparent}\n",
1616    "button{cursor:pointer}\n",
1617    "img,svg,video,canvas{display:block;max-width:100%}\n",
1618    "h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}\n",
1619    "table{border-collapse:collapse}\n",
1620);
1621
1622/// The tokens [`PREFLIGHT`] reads, so a sheet defines them however few utilities a page uses.
1623const PREFLIGHT_TOKENS: &[&str] = &["--default-font-family", "--font-sans"];
1624
1625/// The registered custom properties the utilities use, and what each is.
1626const PROPERTIES: &[(&str, &str)] = &[
1627    (
1628        "--tw-border-style",
1629        "syntax:\"*\";inherits:false;initial-value:solid",
1630    ),
1631    ("--tw-font-weight", "syntax:\"*\";inherits:false"),
1632    (
1633        "--tw-space-x-reverse",
1634        "syntax:\"*\";inherits:false;initial-value:0",
1635    ),
1636    (
1637        "--tw-space-y-reverse",
1638        "syntax:\"*\";inherits:false;initial-value:0",
1639    ),
1640];