--- enabled: true touches: [crates/layout/src/containing_block.rs, crates/layout/src/fragment.rs, crates/layout/src/boxtree.rs, crates/layout/src/lib.rs, crates/layout/src/stacking.rs, crates/layout/src/bin/fragment_check.rs, crates/layout/src/bin/held_out_check.rs, crates/layout/src/bin/paint.rs, crates/layout/src/generated/**, crates/dom/src/computed.rs, crates/harness/src/containing_block_cases.rs, crates/harness/src/lib.rs, crates/generation/**, docs/model-first-cost.md, roadmap.md, corpus/**, crates/provenance/src/css21_containing_block.rs, crates/provenance/src/lib.rs, docs/generated-functions.md] depends-on: [] --- # `fixed-positioning`: `position: fixed` and a containing-block seam `position: fixed` does not exist in the engine. `crates/dom/src/computed.rs` `position()` maps `Fixed` (and `Sticky`) to `Position::Static`, so a fixed box is laid out in normal flow, and `crates/layout/src/fragment.rs` `layout()` takes only the viewport's width, so nothing could place a box against the viewport's height. The Web Features map (`docs/web-features-map.json`, feature `fixed-positioning`) lists it as `unknown` with 17 WPT test files in `css/CSS2/positioning` and `css/css-position`. A fixed box's size and offsets follow the same CSS 2.1 §10.3.7/§10.6.4 equations an absolutely positioned box does, so the existing absolute seam (`crates/layout/src/absolute.rs` `AbsoluteResolver`, generated in `crates/layout/src/generated/absolute.rs`) solves them unchanged. What differs is which box is the containing block (§10.1). Today that choice is hand-written into `fragment.rs` (the `is_containing_block` test beside `layout_absolute_box`, where pending absolutes resolve against the nearest positioned ancestor or the tree root). This spec lifts that choice into a generated seam covering all four §10.1 rules and adds `fixed` as the one new input it has to get right. That keeps the model-generated part of the work real: a seam with its own corpus, drawn on the model pool, not only wiring. **Scope.** In: `position: fixed` on block-level, non-replaced boxes with no text; the containing block of every box by CSS 2.1 §10.1 rules 1–4 (root → initial containing block; `static`/`relative` → the content edge of the nearest block-container ancestor; `fixed` → the viewport; `absolute` → the padding edge of the nearest ancestor whose `position` is not `static`, else the initial containing block); a fixed box escaping positioned ancestors, `relative` and `absolute` alike. The oracle never scrolls, so a fixed box and an absolute box against the initial containing block land in the same place at scroll 0. The corpus has to separate them by nesting fixed boxes inside positioned ancestors and by using `bottom`/percentage heights against the 600 px viewport height. Out: scrolling and scroll position, `transform`/`filter`/`contain` making an ancestor the containing block for fixed descendants (CSS Transforms, not CSS 2.1), `position: sticky` (stays mapped to `Static`), paged media. Say so in the seam's module doc. **Viewport height.** Pass the viewport as `(width, height)` to `layout()`, `layout_with_resolver` and `layout_with_resolvers`, the same shape `crates/layout/src/lib.rs` `scrollbars()` already takes. That keeps it one argument changed at each call site rather than a new one appended. Callers pass 800×600, the oracle's `VIEWPORT` (`oracle/oracle.mjs`). Every caller listed in `touches` updates together, and the change is mechanical: no score may move because of it alone. Check this before building the seam: `fragment_check --random` stays 227/227 and `bench/seam-floor.sh`'s four floors hold. Build the same set the absolute seam has: - `crates/dom/src/computed.rs`: `Position::Fixed`, mapped from `StyloPositionProperty::Fixed`. Every existing `== Position::Absolute` test in `boxtree.rs` and `fragment.rs` that means "out of flow" becomes true for `Fixed` too. Keep those hunks small, since other seams edit these files. - `crates/layout/src/containing_block.rs` — `ContainingBlockInputs` (the box's own `Position`; its ancestors nearest-first, each with `Position`, whether it is a block container, and its content-box and padding-box rects in viewport coordinates; the viewport rect), `ContainingBlockOutputs` (the chosen rect, plus which rule chose it, for reporting), `type ContainingBlockResolver`, and a deliberately wrong `resolve_baseline` that reproduces today's behaviour (a fixed box treated as static). - `fragment.rs`: call the resolver where the containing block is chosen now. Pending fixed boxes bubble past every positioned ancestor to the root and resolve there against the viewport rect, not the root box's padding box. One more resolver argument on the three `layout*` entry points. - `crates/harness/src/containing_block_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: fixed at top level, fixed inside a `relative` ancestor, fixed inside an `absolute` ancestor, fixed with `bottom`/`right` only, fixed with a percentage `height`, absolute with no positioned ancestor, absolute inside nested positioned ancestors (nearest wins), static inside a padded block container. Never add to `crates/harness/src/cases.rs` `RECIPES`. - `crates/generation/src/containing_block_runner.rs`, a `Seam::ContainingBlock` variant (`candidate_crate.rs`), `prompt::build_containing_block`, `admit::record_for_containing_block`, and `containing_block_ground_truth_cache`, `containing_block_generation_loop`, `containing_block_real_model_run` under `crates/generation/src/bin/`, plus one baseline and two seeded-wrong fixtures under `crates/generation/fixtures/containing-block-candidates/` (for example one that gives fixed boxes the nearest positioned ancestor, one that uses the content edge instead of the padding edge for absolutes). 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. `containing_block_ground_truth_cache` — fills `corpus/containing-block-ground-truth/` from the three browsers via `oracle/oracle.mjs`; record the case count and drops for engine disagreement. 2. `containing_block_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. `containing_block_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 `absolute_real_model_run` does (`crate::orchestrate` `print_per_model_summary`, `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/containing_block.rs` byte for byte with the header line `generated/width.rs` uses, `containing_block.provenance.json` beside it, `pub mod containing_block;` 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 seams 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. **Gates.** `cargo test --workspace` passes. `fragment_check --random` stays 227/227 with every category unchanged: no recipe in `cases.rs` uses `position: fixed`, and the generated seam must reproduce today's containing block for every non-fixed box. `bench/seam-floor.sh`'s floors hold (floats 109, stacking 105, relative positioning 104, block height 105).