--- enabled: true touches: [crates/layout/src/inline_atomic.rs, crates/layout/src/boxtree.rs, crates/layout/src/fragment.rs, crates/layout/src/lib.rs, crates/layout/src/generated/inline_atomic.rs, crates/layout/src/generated/inline_atomic.provenance.json, crates/layout/src/generated/mod.rs, crates/dom/src/computed.rs, crates/harness/src/inline_atomic_cases.rs, crates/harness/src/lib.rs, crates/generation/**, docs/model-first-cost.md, roadmap.md, corpus/inline-atomic-ground-truth/**, crates/provenance/src/*inline_atomic*.rs, crates/provenance/src/lib.rs, docs/generated-functions.md, crates/layout/src/height.rs] depends-on: [fixed-positioning, transforms2d] --- # `inline-atomic`: line boxes over atomic inlines, text-free The engine has no inline formatting context. `crates/layout/src/boxtree.rs`'s module doc says every computed `display` other than `none` "generates exactly one box", and `crates/dom/src/computed.rs` has no `display` accessor beyond `is_display_none` and `is_display_flow_root`, so a `display: inline-block` box is stacked in normal flow like a block. The Web Features map (`docs/web-features-map.json`) lists `vertical-align` (98 WPT test files, `css/CSS2/linebox`) and `line-height` (111) as `none`. Line boxes are the substrate every text-bearing area, tables' cell baselines and flexbox's baseline alignment sit on. This spec builds them for the one kind of content that needs no font: atomic inlines with explicit sizes. The provisional rule for features whose math an existing generated function already covers (lift the adjacent hand-written logic into a new generated function; the roster files it as an open decision) does not come into play: no existing generated function places anything on a line. **Scope.** In: a block container whose in-flow children are all `display: inline-block` with explicit `width` and `height` (lengths only), laid out in line boxes (CSS 2.1 §9.4.2, §10.8); line breaking between atomic inlines when the next one does not fit, and an atomic inline wider than the line placed alone on its own line; `vertical-align: baseline | top | bottom | middle | | ` (§10.8.1); `line-height` on the container as the strut's height; `text-align: left | right | center` on the container; margins, padding and borders on the atomic inlines; an inline-block's own baseline (§10.8.1: the baseline of its last in-flow line box, or its bottom margin edge when it has none or its `overflow` is not `visible`), which makes inline-blocks nested inside inline-blocks a category; the container's auto height as the sum of its line boxes (§10.6.3). Out: text and every font-dependent value (`sub`, `super`, `text-top`, `text-bottom`, `line-height: normal` on a non-zero font size); `width: auto` inline-blocks (shrink-to-fit belongs to `min-max-content`); a container mixing block-level and inline-level children (anonymous block boxes, §9.2.1.1); floats and absolutely positioned boxes inside the inline formatting context; `text-align: justify`; bidi and vertical writing modes; `inline-table`, `inline-flex`, replaced elements. Say so in the new module's doc. **Keeping the corpus text-free and font-free.** The case builder in `crates/harness/src/cases.rs` (`document()`, `write_body()`) already writes no whitespace between tags, so no text node can appear between atomic inlines; the new case file must assert that for every case it produces. The strut still reads the container's font (§10.8.1: its ascent and descent place the baseline inside the line-height), so every inline formatting context container in the corpus sets `font-size: 0` with an explicit `line-height` in px. With a zero-size font the strut's ascent and descent are zero, the baseline sits at the middle of the strut, and `middle` aligns on the baseline itself, so no case depends on a font's metrics. That choice holds until `text-metrics-oracle` has measured whether the three engines agree on real text; record the choice in the case file's module doc. Build the same set the absolute-positioning function has: - `crates/dom/src/computed.rs`: a `Display` enum and `display()` accessor with `Block`, `FlowRoot`, `InlineBlock`, `None` and an `Other` catch-all (keep `is_display_flow_root` working; `tables-fixed-layout` and `display-contents` extend this enum), plus `vertical_align()`, `line_height()` and `text_align()` accessors returning plain data. Add accessors only; do not change an existing one. - `boxtree.rs`: each `Box` records whether it is inline-level; a container whose in-flow children are all inline-level establishes an inline formatting context. Keep the hunk small: other specs edit this file. - `crates/layout/src/inline_atomic.rs` — `InlineAtomicInputs` (the container's content width, strut `line-height`, `text-align`; per atomic inline in document order its margin-box width and height, its baseline measured from its margin-box top, and its `vertical-align` with a percentage already resolved against its own `line-height`), `InlineAtomicOutputs` (each atomic inline's margin-box offset in the container's content box, each line box's top, height and baseline, the total height, and the last line box's baseline for an enclosing inline-block), `type InlineAtomicResolver`, and a deliberately wrong `resolve_baseline` that stacks the atomic inlines vertically the way the engine does today. - `fragment.rs`: an inline formatting context container lays out each inline-block as a block formatting context root at its explicit size, hands the results to the resolver, and places the fragments where it says. The container's `HeightInputs::content_height_from_children` (`crates/layout/src/height.rs`) is the resolver's total height. For margin collapsing the line boxes count as in-flow content with no adjoining child margins (§9.4.2, §8.3.1): an inline-block's margins never collapse. One more resolver argument on the three `layout*` entry points. - `crates/harness/src/inline_atomic_cases.rs` — own `Category` enum with `pub const ALL`, `Case`, `enumerated()`, `random(seed, count)`, built on `cases::{Elem, Source, px, RESET}` like `absolute_cases.rs`. Categories at least: one line of equal-height atomic inlines, baseline alignment of different heights, `top`, `bottom`, `middle`, a length and a percentage `vertical-align`, a line break when the row overflows, an atomic inline wider than the container, `text-align: right` and `center`, margins/padding/ borders on the atomic inlines, a strut taller than every atomic inline, an inline-block with `overflow: hidden` (baseline at its bottom margin edge), nested inline-blocks. Never add to `crates/harness/src/cases.rs` `RECIPES`. - `crates/generation/src/inline_atomic_runner.rs`, a `Seam::InlineAtomic` variant (`candidate_crate.rs`), `prompt::build_inline_atomic`, `admit::record_for_inline_atomic`, and `inline_atomic_ground_truth_cache`, `inline_atomic_generation_loop`, `inline_atomic_real_model_run` under `crates/generation/src/bin/`, plus one baseline and two seeded-wrong fixtures under `crates/generation/fixtures/inline-atomic-candidates/` (for example one that aligns every box's bottom edge on the baseline regardless of `vertical-align`, one that never breaks a line). **Shared files.** `crates/layout/src/fragment.rs`, `crates/layout/src/lib.rs` and `crates/generation/**` are edited by every spec that adds a generated function; such specs have landed concurrently before, so keep each hunk small and additive. `crates/dom/src/computed.rs`, `crates/harness/src/lib.rs`, `crates/layout/src/generated/mod.rs`, `roadmap.md` and `docs/model-first-cost.md` take additions only: new accessors, one `pub mod` line, appended records. `boxtree.rs` changes structurally, which is why this spec waits for `fixed-positioning` (it edits the same out-of-flow test there) and why `tables-fixed-layout` and `display-contents` wait for this one. The model endpoint and keys are read from the environment, and the binaries do not load `.env` themselves. Before every run: `set -a; source .env; set +a` in the worktree (`.env` is a symlink installed by `tracks.json`). Never print or commit any value from it. Run, in order, each as `cargo run --release -p generation --bin `: 1. `inline_atomic_ground_truth_cache` — fills `corpus/inline-atomic-ground-truth/` from the three browsers via `oracle/oracle.mjs`; record the case count and drops for engine disagreement. If more than a fifth of any category drops, record which engine disagrees and on what before going on: a font-size-zero strut that one engine treats differently is a finding, not a corpus bug to hide. 2. `inline_atomic_generation_loop` — the baseline seeds the regression set; both seeded-wrong fixtures must be rejected. If one is admitted, fix the loop or the corpus, not the fixture, and record what was wrong. 3. `inline_atomic_real_model_run` against the folknet model gateway, with `NEV_PROVIDER` unset and `NEV_MODELS=qwen3.8-flash-next:16:16,qwen3.8-27b-fast:32:32,qwen3.8-27b-fast-2:32:16,deepseek-v4.1-flash:2:2:1800` set after sourcing `.env`. Each entry is `model:count:concurrency[:timeout_secs]`, and the concurrencies match what each alias serves (`docs/infrastructure.md` → "Rig change log"). It stops on its own after two flat batches or six, whichever comes first. Score each model's share as it arrives and print per-model counts the way `overflow_real_model_run` does (`score_share`, `conclude_batch`, `crate::orchestrate` `print_per_model_summary` and `best_per_model`, and the `model_of` lookup at admission), so provenance records the winner's own model. Only if the gateway does not answer, fall back to `NEV_PROVIDER=claude-code`, and record that fallback and what the gateway returned as a Decision. Never set `NEV_PROVIDER=anthropic`. When a candidate is admitted, commit it as `crates/layout/src/generated/inline_atomic.rs` byte for byte with the header line `generated/width.rs` uses, `inline_atomic.provenance.json` beside it, `pub mod inline_atomic;` in `generated/mod.rs`, and the three `layout*` entry points defaulting to it. Record every batch in `docs/model-first-cost.md` → "Batch scores and token totals", with per-model counts, and append the outcome to `roadmap.md` → "Phase 1 — Engine bootstrap" step 4 in the form the other generated functions use. A hand-written candidate is allowed only after every batch has failed to admit one, and is recorded as an escalation with the batch count that preceded it. A floor for this function in `bench/seam-floor.sh` is a hand change after landing: the landing script refuses a diff under `bench/`, and a score line in `seam_floor_check` with no cache rebuild behind it would fail every later landing. Say so in the track's handoff. **Files beyond the function's own.** The spec text the function is generated against goes in a new provenance module whose file name contains `inline_atomic` (for example `crates/provenance/src/css21_inline_atomic.rs`), so the `touches` glob `crates/provenance/src/*inline_atomic*.rs` matches it, with its one `pub mod` line in `crates/provenance/src/lib.rs`, the way `css21_containing_block.rs` was added. The function's row in the table and its "Per function" entry go in `docs/generated-functions.md`. Do not update `docs/feature-map.json`, `docs/dashboard.json` or `README.md` here: `docs/dashboard.md` asks every spec that lands a generated function to, but those files belong to `specs/dashboard-history-graph.md`, and that instruction does not apply to an unattended track; the landing script refuses any file outside `touches`. In `docs/*.md` and `roadmap.md` prose, write "generated function", never the older word the root `AGENTS.md` forbids; `crates/harness/tests/no_seam_word.rs` fails `cargo test --workspace` otherwise (code identifiers and paths in backticks are exempt). The generated-function specs run one after another through `depends-on`, since each adds a resolver argument to the same `layout*` entry points and edits the same shared files. **Gates.** `cargo test --workspace` passes. `fragment_check --random` stays 227/227 with every category unchanged: no recipe in `cases.rs` uses `display: inline-block`. `bench/seam-floor.sh`'s floors hold (floats 109, stacking 105, relative positioning 104, block height 105).