//! The webview renderer for [`makeover_layout`].
//!
//!
//!
//! # The renderer that needs no palette
//!
//! `makeover-immediate` and `makeover-tui` both take a `Palette`, because egui
//! and a terminal need an actual colour before they can put anything on
//! screen. A webview does not: `var(--surface-raised)` *is* the late binding,
//! and the browser resolves it against whatever `themes.js` last wrote onto
//! `:root`.
//!
//! So this crate emits text naming intents, and never learns a colour. It is
//! the deferral rule with no adapter in the way, and it is why the webview was
//! always the wrong renderer to derive a vocabulary from: it can express
//! anything, so it never pushes back.
//!
//! # Phase A: the stylesheet
//!
//! This module emits component CSS and no markup, deliberately. GoingsOn has
//! 145 `innerHTML` sites and Balanced Breakfast 175 `createElement` sites, so
//! moving markup is a migration where adopting a generated stylesheet is not.
//! The apps keep every line of their markup and gain the classes.
//!
//! It is not a deletion either, which this header claimed until the measurement
//! came in. Adoption across goingson removed 49 declarations net and *added* 25
//! lines: a rule loses its depth declarations and gains a variant selector next
//! to it, so the file stays the same size. What phase A moves is where depth is
//! defined, not how much CSS exists. Numbers and method in the wiki note under
//! "The deletion test, run".
//!
//! The bevel properties are byte-identical to what both apps already
//! hand-write, which is asserted below.
//!
//! # Phase B: the markup, one description at a time
//!
//! [`form`] renders [`makeover_layout::Field`], which is the half of phase B
//! whose description is settled. It emits strings, because both apps
//! interpolate their fields into larger string-built forms and returning nodes
//! would rewrite those too. It owns its own escaping, on the reasoning in that
//! module: a Rust encoder can cover element text and attribute values with one
//! function, where the apps need four and have to choose correctly at every
//! call site.
//!
//! [`list`] is the other half: column tracks, the narrowing rules, and the cell
//! containers a row is made of. It stops at the cell boundary and does not
//! render what goes inside one, on the reasoning in that module. So phase B is
//! now the frame around content in both directions, and what an app still owns
//! is the content itself.
//!
//! # What phase A settled, and what it costs
//!
//! Decided 2026-07-29 against goingson's `styles.css` rather than against a
//! component list. The useful finding there was that `.btn` (line 644),
//! `.card` (768) and `.tag, .badge` (882) each hand-write the same
//! composition, so three quarters of phase A is one rule with several names.
//!
//! Two of the four decisions change how goingson looks, and adoption should
//! not be described as a pure deletion:
//!
//! - **Pressed carries its fill.** [`interactive_rules`] emits
//! [`Depth::pressed`] whole. goingson presses to `--surface-sunken` today and
//! will press to `--surface-well`, and hovers to `--surface-overlay` today
//! and will hover to `--hover-surface`. Since `surface-well` inverts by theme
//! where `surface-sunken` does not, a dark theme presses *lighter* than it
//! hovers. That falls out of `makeover`'s own derivation, which says outright
//! that `surface-sunken` cannot serve as a well, so if it reads wrong the
//! answer is there and not here.
//! - **Badges go flat.** See [`token_rules`].
//!
//! The other two: the progress trough is renderer-local and the scrollbar
//! track was dropped ([`component_rules`]), and no class prefix ships by
//! default, so adoption means deleting the app's hand-written rule in the same
//! commit that adds the generated one. `.card`, `.badge` and the tab classes
//! all already exist in goingson, and while both rules exist the cascade order
//! decides which wins. That is the one real risk in adopting this, and it is
//! why the migration lands per component rather than in one commit.
//!
//! # 0.10.0: the states this crate used to leave to its consumers
//!
//! [`interactive_rules`] emitted hover and pressed and stopped, because
//! `makeover-layout` modelled no interaction state. Focus and disabled were
//! therefore unsayable, and every app completed the primitive from outside the
//! only way that works: by out-specifying a rule it does not own. goingson
//! carries 19 such rules and the MNW server 21, and the three focus rings do
//! not match each other.
//!
//! That also blocked the cascade-layer work outright. An app that declares
//! `@layer` puts its own rules in a named layer, and unlayered declarations
//! outrank every named layer regardless of specificity, so all of those
//! overrides lose in the commit that adopts layers. They cannot simply be
//! deleted, because they are the only thing supplying the missing states.
//! Emitting the states here is what turns that adoption into a deletion.
//!
//! Four states now, in emission order, and the order is load-bearing: they are
//! all specificity (0,2,0), so disabled beats hover by coming last and by
//! nothing else. Nothing here reaches for `:not(:disabled)`, which would raise
//! a selector this crate will shortly be wrapping in its own layer.
//!
//! Hover additionally sits inside a capability query now. `makeover-touch`
//! answers whether a fingertip has hover and `makeover-geometry` spells the
//! condition; this crate asks and does not decide. goingson's section 60 exists
//! solely to take the hover state back on touch, which is a fight it should
//! never have been handed.
//!
//! # 0.11.0: the layer contract
//!
//! [`stylesheet`] emits into the `makeover` cascade layer ([`CSS_LAYER`], which
//! lives in `makeover-geometry` because that is the one crate every CSS emitter
//! in the family already depends on). `makeover-geometry` 0.6.0 does the same
//! for `geometry.css`.
//!
//! The cascade resolves origin and importance, then layer, then specificity,
//! then source order, and **unlayered normal declarations outrank every named
//! layer**. So before this, an app that declared `@layer base, components,
//! responsive` put every rule it owns into a named layer and lost all of them to
//! this unlayered file, regardless of specificity and regardless of loading
//! last. Nothing errors when that happens: the CSS is valid, the minifier is
//! happy, and buttons and badges look subtly wrong.
//!
//! That is why the layer belongs here rather than in each app. An app cannot fix
//! it from its own stylesheet, because the fix is to layer the file it does not
//! own.
//!
//! **What it flips**, and the reason each app wants a look when it bumps the
//! pin: a generated rule that currently beats an app rule by being more specific
//! stops beating it. The direction is always "the app wins", which is what the
//! apps already assume, but a hand-written rule an app thought was dead can come
//! back to life.
//!
//! An app should declare the order once, or the layer's position is decided by
//! whichever generated file the browser happens to see first:
//!
//! ```css
//! @layer makeover, base, components, responsive;
//! ```
//!
//! [`in_css_layer`] is re-exported for an app that assembles its own stylesheet
//! from this crate's pieces. goingson builds `tables.css` in its own `build.rs`
//! out of [`list::narrowing_css`] and [`list::grid_template_columns`], and those
//! rules are as generated as the ones here, so they belong in the same layer and
//! this crate cannot put them there on the app's behalf.
//!
//! # 0.12.0: the ring gets its own width
//!
//! [`focus_rule`] reused [`Emit::border_width`] and emitted a 1px ring. That was
//! an implementation convenience dressed as consistency with the invalid-field
//! ring: a bevel and a focus indicator answer different questions, and only one
//! of them has to be noticed from across a desk.
//!
//! Caught while adopting 0.11.0 into goingson, by the check the adoption tasks
//! ask for. Every consumer had already written its own ring and all three chose
//! at least 2px: the MNW server 2px across 10 rules, Balanced Breakfast 2px,
//! goingson 2px on three rules and 3px on the one covering twelve selectors. The
//! design system was the only thing in the tree saying 1px, so deleting the app
//! rules in favour of it would have thinned the focus indicator everywhere.
//!
//! [`Emit::focus_width`] now carries it, defaulting to `2px`, and the offset is
//! the same magnitude with its sign off the depth. Both values are the measured
//! consensus rather than a new opinion.
//!
//! # 0.47.0: a range, a chooser's ghost text, and an option that cannot be
//! picked yet
//!
//! `makeover-layout` 0.28.0's three form findings, all of them cheap here and
//! none of them cheap in the app that found them.
//!
//! - `FieldKind::Range` emits ``, and `Field::step` emits
//! `step`. The step is emitted only when the description carries one: the
//! browser's own default is `step="1"`, which is what a description means by
//! saying nothing, and is also what turns a 0-to-1 threshold into a
//! two-position control.
//! - A select with nothing chosen emits a disabled, selected, valueless first
//! option carrying `Field::placeholder`. HTML has no placeholder attribute on
//! `