//! 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.
//!
//! [`facet`] renders `makeover_layout::Facet`: a dimension a set is narrowed by,
//! and the one phase-B emitter whose markup an app is not keeping, because the
//! markup it replaces was two mechanisms rather than one. A tag's selection and
//! a tag's browse position were separate state on MNW's discover page, which is
//! why every filter row there carries a tick box *and* a chevron; one gesture
//! doing both is what lets the second one go.
//!
//! [`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.57.0: a suggestion's second line, in place of a reason it cannot be picked
//!
//! `1fcf2e9b`, ruled by Max 2026-08-21. `Outcome::Suggestions` now carries
//! `Candidate` rather than `Choice`, and a candidate has no `unavailable`: a
//! suggestion that cannot be picked is a row a route should not have offered.
//! What it has instead is a `detail`, the line that tells it from a row reading
//! the same -- "Format" under audio, software, writing and video.
//!
//! So `.form-suggestion-why` is gone and `.form-suggestion-detail` is here, and
//! the difference is not only the name. `why` was drawn in the disabled token,
//! which said the row was refusing. A detail orients rather than refuses, so it
//! is `--content-muted`, which is what every other secondary line in this crate
//! reads as.
//!
//! No consumer is affected: the only emitter is `quasi-webview`, which moves in
//! the same cascade.
//!
//! # 0.56.0: an interval, as the group MNW already wrote by hand
//!
//! [`makeover_layout::FieldKind::Interval`] emits a `role="group"` named by the
//! field's label, holding one `` per end. That is not a
//! shape invented here: MNW's discover sidebar has a `role="group"` with
//! `aria-labelledby` over `min_price` and `max_price` today, written by hand
//! because nothing in the description could say the two boxes were one
//! question. The markup is what the measurement found, and this emits it.
//!
//! - **The group carries the error and the descriptions**, on the split
//! [`makeover_layout::FieldKind::Radio`] already uses here: what is wrong is
//! the answer, and a crossed interval is not the fault of either end.
//! - **Both boxes take the whole extent.** `min`, `max` and `step` describe the
//! axis, so they are written twice. The crossing rule is not emitted, because
//! HTML has no attribute for it and the description does not carry it: it
//! comes back as an error on the group, like every other refusal.
//! - **Which end is which is `aria-label` and nothing more.** The description
//! states direction structurally, by which member holds which name, and never
//! in words. Visible Min and Max captions are a page's own and reach the
//! group through [`form::Filling::trailing`].
//!
//! [`form::Value::Between`] is the second value. A separator inside one string
//! would have made this crate the owner of a delimiter that either end could
//! contain.
//!
//! # 0.55.0: a number's unit, as adjacent text
//!
//! `makeover-layout` 0.33.0's `Field::unit`. HTML has no unit attribute and
//! inventing one would be markup nothing reads, so it is a `` after the
//! control -- and it is named in `aria-describedby` rather than left as
//! decoration, because a number and what it is measured in are one fact and
//! reading the first without the second is reading it wrong.
//!
//! This is the renderer the member costs something. egui already had somewhere
//! better than the label to put a unit; here the two placements are equally
//! adjacent, and what is bought is that the unit is a value a consumer can read
//! back rather than a suffix on a label it would have to parse.
//!
//! # 0.54.0: a curve this renderer can carry, and one it declines
//!
//! `makeover-layout` 0.32.0's `Curve`. A range takes its granularity from the
//! curve now (`Field::curve.step()`), every other kind keeps `Field::step`, and
//! `Curve::Linear` emits exactly what it emitted before.
//!
//! **A constant-ratio curve emits a linear track, and that is the answer, not a
//! debt.** HTML has no logarithmic range input, so a described screen asking
//! for one is asking the browser for something it does not have, the same class
//! of request as [`makeover_layout::FieldKind::Date`] on a host with no
//! calendar. The renderer answers with the nearest control the host really
//! offers and keeps every fact that survives the translation: the extent, the
//! granularity, and the value's own units. What does not survive is resolution
//! at the small end. The value submitted is still a value in the field's own
//! units, which is what every handler on this path reads.
//!
//! The alternatives are worse in the specific way this stack exists to avoid.
//! Shipping JS that maps thumb position to value puts app code back in the
//! renderer. Changing what the control submits from a value to a fraction moves
//! the mapping to whoever reads the form, and the MNW server reads these forms
//! with its own handlers, so a fraction arriving where a value is expected
//! would be silent.
//!
//! When this reopens: the day a described screen on the webview path asks for a
//! non-linear range. The answer then is mapping in `quasi-router`, where one
//! implementation serves every host, not JS here. Nothing asks today. No
//! described screen in the tree uses a non-linear curve; the one real consumer
//! is audiofiles' ADSR (`instrument_panel.rs:643`), which is egui and stays
//! egui because audiofiles takes only `quasi-immediate` and
//! `makeover-immediate`; and the two webview-served consumers, goingson and the
//! MNW server, use no [`makeover_layout::FieldKind::Range`] at all.
//!
//! # 0.52.0: a markdown field gets the preview it was permitted
//!
//! 0.50.0 marked a [`makeover_layout::FieldKind::Rich`] field with
//! `data-format="markdown"` and stopped there, and nothing read the mark: no
//! rule, no markup, no selector anywhere in the tree. So a described editor was
//! a plain textarea carrying an attribute, and MNW converting its four
//! hand-written section editors onto the member would have *lost* the
//! Write/Preview pair one of them already has. A permission taken and not spent
//! turns every conversion into a regression.
//!
//! [`form::editor_rules`] and the chrome beside it are the spending. The pair is
//! a segmented control, so it takes the depth, the focus ring and the chosen
//! state from rules that already existed; the preview pane is a well, because it
//! stands where the control stood. Both are gated on the attribute rather than
//! on a class, which is what the attribute was for.
//!
//! **This crate still renders no markdown.** The pane arrives empty and is
//! filled by whatever binds the editor, which is where the host's sanitiser
//! already is. A converter here would have moved that guarantee into a crate
//! with no view of the host's content-security posture.
//!
//! # 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
//! `