---
title: "Architecture: trees, handles and the layout entry"
url: https://recursiveai.org/onerender/docs/architecture/
description: "This page describes the front half of the pipeline as built: the styled-node tree, computed values, the box tree and the fragment tree. Layout algorithms…"
version: "1.0.0"
source: https://github.com/recursive-ai-oss/onerender-csharp/blob/1fc2990e8d013c56ca24cf327cf375b101bba1fa/docs/architecture.md
---

# Architecture: trees, handles and the layout entry

This page describes the front half of the pipeline as built: the styled-node tree, computed values, the box tree and
the fragment tree. Layout algorithms, paint and the display list build on it.

```
OneHtml Document + DocumentStyles ──► HtmlFrontEnd ─┐
                                                    ├─► StyledTree ─► BoxTree ─► ILayoutEngine ─► FragmentTree
C# builder (StyleDeclaration) ──────────────────────┘      (+ StyleTable)
```

## Shared rules

- **Handles.** `StyledNode`, `Box` and `Fragment` are `readonly struct { Tree, Index }`. They hold no data; every
  accessor reads a column of the owning tree. The default value is the null handle (`IsNull`). A handle is valid until
  its tree is cleared or rebuilt; indexes are dense, assigned in document order (parents before children).
- **Storage.** Each tree is struct-of-arrays: parallel `int`/`byte`/`double` columns and first-child / next-sibling
  links, grown by doubling and **retained** across `Clear()` / `Build()`. Rebuilding a tree of the same size allocates
  nothing (asserted by tests for a 1,000-node document built from HTML and from the builder).
- **No recursion on depth.** Every traversal (building, dumping, layout helpers) uses an explicit stack. Trees of
  100,000 levels or 100,000 siblings build and dump.
- **Limits.** `TreeLimits` bounds node count (2M), depth (1,048,576) and text length (32M UTF-16 units); the box tree
  allows twice the node limit. A limit is checked before anything is stored; the over-limit node is dropped and a
  `LimitExceeded` entry goes to `StyledTree.Diagnostics` (capped at 256 entries, the rest counted in `Dropped`).
  Declarations that survive the cascade but do not parse raise `InvalidValue` diagnostics.
- **Determinism.** Ordinal comparisons only, no culture-sensitive APIs, no clock, no file or network access. The host
  supplies natural sizes of replaced content through `IResourceResolver` and fonts through `IFontProvider`.

## Computed values (`OneRender.Style`)

- `PropertyId` lists the longhands the engine models (display, box model, position, sizing, borders, overflow, font,
  text, color and a few more). Shorthands are expanded before they reach the pipeline: by OneHtml's cascade for the
  HTML front end and by `StyleDeclaration.TrySet` for the builder, with `ShorthandFallbacks` covering `border-radius`,
  `overflow`, `flex`, `grid` and the others the cascade keeps whole. (`white-space`, `text-wrap` and `border-block`/`border-inline` are expanded by
  OneHtml 1.2 on both paths.)
- `ComputedValues` is one struct of typed fields (enums, `Length`, `ColorValue`, doubles). **Styles are interned**: a
  `StyleTable` stores one row per distinct computed style, and a node holds a `StyleId`. A document with thousands of
  nodes typically has tens of rows. Rows are found by value equality, so `StyleId` equality means identical style.
  The table holds at most 65,536 rows at once; rows nothing refers to any more are freed and reused (see Long-running stability). If a single flush needs more than the table can hold, a diagnostic is raised and the parent's style is reused.
- A computed style is cached by (declaration source, version, parent `StyleId`, root font size); a warm rebuild
  computes nothing and allocates nothing.
- `Length` keeps what layout needs: pixels for every absolute, font-relative and viewport unit, and an unresolved form for
  percentages and `calc()` containing percentages (`CalcValue` evaluates against a containing-block size at layout
  time). Keywords (`auto`, `min-content`, `max-content`, `fit-content`, `normal`, `none`) are `LengthKind` values.
- `inherit`, `initial` and `unset` are resolved when the style is computed. `revert` and `revert-layer` act as `unset` in
  builder trees, which have no earlier origin.
- **Invalid declarations are dropped in the cascade.** `StyleValidation.Validator` is the engine's grammar offered to
  OneHtml as `StyleSourceOptions.DeclarationValidator` (`HtmlPage` sets it; a host that builds its own `DocumentStyles`
  should too). OneHtml asks it once per declaration, when a rule's declarations are first collected, for author and user
  declarations of properties OneHtml does not type itself. A rejected declaration neither wins nor counts as
  `!important`, so an earlier valid one stands (CSS Syntax 3 section 5.4.6, CSS Cascade 4 section 4.3). For a
  longhand the validator runs the same parser that computes the value, against initial values, so the two cannot
  disagree; for `border-radius`, `overflow`, `flex`, `grid` and the other shorthands OneHtml passes
  through whole, it expands them with `ShorthandFallbacks` and checks every longhand. The check parses the value text
  again (OneHtml's token range cannot be turned back into a value list), which costs one small allocation per
  declaration per stylesheet and nothing per restyle. It holds one lock around a shared scratch computer.
- What still reaches the style computer as invalid behaves as `unset` and raises `InvalidValue` (CSS Variables 1
  section 3.2 for `var()`): values with `var()`, which are only known at computed-value time; properties OneHtml
  types itself (`display`, `position`, the offsets, `overflow-x/y`, `visibility`, `font-size`, `font-weight`,
  `font-style`, `font-family`), where OneHtml's own grammar decides; and builder trees, which have no cascade.
- What is deliberately lossy is listed, with the reason, in `tests/OneRender.Tests/testdata/wpt-style/expected-failures.tsv`
  (colors are stored as sRGB, `content` with counters is kept as raw text, and so on).

## Styled-node tree (`OneRender.Tree.StyledTree`)

Elements and text nodes in document order, each with a tag name (builder trees and diagnostics), a `StyleId` and, for
the HTML front end, the `Node.Index` of the OneHtml node it came from (`StyledTree.FindBySource`). Two front ends feed it:

- `HtmlFrontEnd.Build(DocumentStyles)` walks the OneHtml document with an explicit stack, visits elements and text
  (not comments), computes styles from the cascade, and adds `::before`, `::after` and `::marker` nodes from OneHtml's
  pseudo-element styles when they generate content. Elements the user-agent sheet hides (`head`, `script`, `style`)
  are `display: none` and get no box.
- The builder: `AddRoot` / `AddElement` / `AddText` with a `StyleDeclaration` of CSS text per node
  (`new StyleDeclaration().Set("width", "50%")`). It goes through the same value parsers as a style sheet.

Replaced elements (`img`, `video`, `canvas`, `iframe`, `embed`, `object`, `svg`) have no children in the styled tree.
Their natural size comes from `IResourceResolver` (or `StyledTree.SetNaturalSize` in a builder tree); with none, 300x150
for non-images and NaN for images.

## Box tree (`OneRender.Tree.BoxTree`)

`BoxTree.Build()` generates boxes from the styled tree following CSS Display 3 and CSS2 §9.2.

- A `Box` has a `Kind` (Container, Inline, Text, Replaced, LineBreak, Marker), an outer `Level` (Block or Inline) and,
  for containers, a `FormattingContext` (Block, FlowRoot, Flex, Grid, Table and its parts). Flex, grid and table boxes
  are marked only; table internals are not fixed up.
- `display: none` makes no box; `display: contents` makes the element's children boxes of its parent.
- Anonymous block boxes wrap inline runs when a container also has block-level in-flow children (§9.2.1.1), and anonymous
  inline (text) boxes wrap text (§9.2.2.1). Out-of-flow boxes and outside markers do not split or start runs.
- A block inside an inline splits the inline into pieces linked by `Box.NextPiece` (`Continuation` marks the later ones);
  nesting deeper than 64 turns the block into an inline-block and raises a diagnostic.
- Whitespace-only text that collapses is dropped at run edges, in flex and grid containers and inside table structure
  (CSS Text 3 §4.1.1). Children of flex and grid containers are blockified; their text runs get anonymous items.
- `list-item` boxes get a `Marker` box (a placeholder; none when `list-style-type` is `none`).
- `BoxTree.PrincipalBox(StyledNode)` maps a styled node to its (first) box.
- `BoxTree.Dump()` prints an indented text form, used by tests to compare the two front ends.

## Fragment tree (`OneRender.Layout`)

Layout writes a `FragmentTree` and paint, hit testing and the harnesses read it.

- Kinds: `Box` (a box fragment, one per box or per split piece), `Line` (a line box inside an inline formatting
  context), `Text` (a run of text in one line, referencing a glyph run) and `Column` (a fragmentainer, see
  "Fragmentation"; it has no box).
- **Coordinates.** Every fragment's `BorderBox` is in **CSS px, relative to the border-box origin of its parent
  fragment**; the root's is relative to the initial containing block. `AbsoluteBorderBox` and `AbsoluteOrigin` walk up the
  chain. `Border` and `Padding` are the resolved edge widths; the content box follows from them. `ScrollableOverflow`
  is in the fragment's own coordinates and covers the box's descendants' overflow; `Baseline` is measured from the
  border-box top.
- **Glyph runs.** `AddGlyphRun` stores a OneFont `FontFace`, the font size, glyph ids, x/y positions and cluster
  indexes (into the box's text) in shared columns; a `Text` fragment holds a `GlyphRunId`.
- **Fragmentation.** A box that splits (inline pieces, later fragmentainers) has several fragments chained by
  `Fragment.NextOfBox`; `FragmentOf(box)` returns the first. `Fragment.FragmentainerIndex` is the index of the column a
  fragment landed in (0 outside any fragmentation context).
- **Keyed by element.** `TryGetBorderBox(StyledNode, out Rect)` and `TryGetBorderBox(Node, out Rect)` (a OneHtml node)
  return the union of the element's fragments in document coordinates; this is what the geometry differential compares.
- `FragmentTree.Dump()` prints the tree as text.

## The layout entry

```csharp
public interface ILayoutEngine
{
    void Layout(BoxTree boxes, in LayoutInput input, FragmentTree output, CancellationToken cancellationToken = default);
}
```

`LayoutInput` carries the initial containing block (`AvailableWidth`, and `AvailableHeight` or NaN when the content
decides) and an `IFontProvider`, the text service layout calls: `Resolve(in ComputedValues)` matches a style to a font list with
per-character fallback, `Shape(text, in ComputedValues, rtl)` returns a cached `ShapedText`, and `GetMetrics(in ComputedValues)`
returns the primary face's metrics (see `text.md`; `TextFontProvider` is the engine's implementation). An implementation clears `output`, writes a fragment for the root and for every box that generates one in tree
order, reads nothing but its arguments, and checks the token every few hundred boxes. The result is a pure function of
the box tree, the fonts and the available size. Block and inline flow, floats, positioning, flex, grid and tables are separate implementations behind this one entry; fragmentation is a
stage the block engine runs at the end (see "Fragmentation").

## Block layout (`BlockLayoutEngine`)

`BlockLayoutEngine` implements `ILayoutEngine` for normal-flow block-level boxes: the block formatting context of CSS2
§9.4.1, widths (§10.3.3, replaced §10.3.2/§10.3.4 with the constraint table of §10.4), heights (§10.5 percentages that
behave as `auto`, §10.6.3, min/max §10.7), `box-sizing`, `aspect-ratio` (Sizing 4 §5), margin collapsing (§8.3.1) and
intrinsic sizes (Sizing 3 §5). Grid and table containers are laid out as plain block containers until their own
layout exists (flex containers: see "Flex layout"); floats and `clear` are described under "Floats" below; absolutely positioned boxes are described under "Positioning".

- **No recursion.** One explicit stack of frames (a struct array kept between runs) walks the box tree; a frame is a box
  whose children are being placed. Depth is capped at 262,144 levels (a diagnostic, and the deeper boxes get no fragment).
  Laying out an unchanged tree again allocates nothing.
- **Coordinates.** Every fragment is added when its box is opened, with its border-box rectangle fixed when it closes. Child
  rectangles are relative to the parent's border box, as the fragment tree requires. `Baseline` of a block is its first
  in-flow line or child baseline from the border-box top, NaN when there is none.
- **Margin collapsing.** One margin run (largest positive plus most negative) is kept for the block formatting context.
  A box whose top margin adjoins its first child's (no top border or padding, not a BFC root) is *pending*: the run keeps
  growing through the chain of pending boxes and, when the first thing with content (or a box with its own top border or
  padding, or a BFC root) is reached, it is resolved once and moves the outermost pending box; nested pending boxes sit at
  offset 0 in their parent. A pending box that closes without content collapses through: it sits where a box with a
  non-zero bottom border would (§8.3.1), and its bottom margin joins the run. At the end of a box the run passes through to
  the parent only if the height is auto, nothing is below the last child, the box is not a BFC root, and min/max/aspect-ratio
  do not change the height; otherwise it is resolved into the content height. Boxes that establish a BFC are the root
  element, anything but `display: block`/`list-item`/anonymous containers, floats, absolutely positioned boxes and
  scroll containers (`overflow` other than `visible`/`clip`).
- **Intrinsic sizes** (`GetIntrinsicSizes`) are content-box min-content and max-content widths, cached per box until the next
  `Layout`, computed with an explicit stack. A block's sizes are the largest child contribution (the child's definite width
  or intrinsic size, clamped by min/max width, plus fixed margins, border and padding). They back `min-content`,
  `max-content`, `fit-content` and shrink-to-fit widths. Percentages count as auto.
- **Re-entry.** `LayoutIndependent` lays out one box as its own BFC root, nested inside a running layout; inline layout
  uses it for inline-block and other atomic boxes (`shrinkToFit` selects CSS2 §10.3.5 widths).
- **List items.** An outside `::marker` box gets a fragment one `font-size` wide and one line high just outside the item's
  start edge, at the content top. This is a placeholder until real markers.
- **Not yet:** the block-axis `fit-content`.

### The inline seam (`IInlineLayout`)

A block container with `ChildLayout.Inline` hands its children to an `IInlineLayout`:

```csharp
InlineLayoutResult Layout(in InlineLayoutRequest request, FragmentTree output, Fragment parent, CancellationToken ct);
void Measure(Box container, IFontProvider fonts, out double minContent, out double maxContent);
```

- The request carries the container, the content-box width to fill, the content box's offset inside the container's border
  box (`ContentLeft`, `ContentTop`, where lines are added as children of `parent`), the content box's origin in the BFC
  root's content box (`BfcLeft`, `BfcTop`, where float exclusions are looked up), the font provider and the exclusion space
  (`Exclusions`, see "Floats"). A request built without one has the empty `NoExclusions` space.
- The result is the total line height, the line count and the first and last baselines from the content top. A container
  with lines counts as having content for margin collapsing; the block layout settles the margin run before the call.
- `Measure` returns the content-box min-content and max-content widths.
- The result also carries the extent of everything the lines occupy (`HasOverflow`, `Overflow*`, in the container's border-box
  coordinates), which the block layout adds to the scrollable overflow, and `QueuedAbsolutes`, true when the inline layout has
  already handed the absolute boxes of its content to the block layout (`IAtomicInlineHost.QueueAbsolute`).
- `InlineLayoutEngine` (see "Inline layout") is the default. `PlaceholderInlineLayout` is the stand-in for tests of the block
  layout that need no fonts: a container with any non-blank text, replaced or atomic inline child gets one empty line box as
  tall as `line-height` (`normal` is 1.2 × font size) and the intrinsic inline sizes are 0.

### Floats

Floats, `clear` and the boxes that avoid floats live in `Layout/Block/BlockLayoutEngine.Floats.cs` and `.Open.cs`.

- **Placement (CSS2 §9.5.1).** Each block formatting context keeps its floats as margin boxes in the coordinates of its root's
  content box, sorted by top (rule 5 makes the tops non-decreasing). A float is laid out first, as the root of a context of
  its own, so its size is known; its position is then the highest one (rule 8) that is as far left, or right, as the earlier
  floats allow (rules 2, 3, 7 and 9), not above the flow position it was met at, the top of its containing block or the line
  it sits on (rules 4 to 6). Rule 3 is measured against the other side's floats wherever they are; rule 7 applies whenever
  another float on the same side is in the way, even outside the containing block, and a float with none to its side may stick
  out of its containing block. `float: inline-start | inline-end` and the logical `clear` values are resolved to left and
  right by the computed style.
- **Width and height.** A float's `auto` width is shrink-to-fit (§10.3.5, from the intrinsic-size cache) and its auto margins
  are 0; a floated replaced box uses §10.3.6. The auto height of a block formatting context root reaches the bottom margin
  edge of its lowest float (§10.6.7); floats are also added to the root's scrollable overflow. Intrinsic sizes count floats
  (Sizing 3 §5.2): the widest float for min-content, and the sum of consecutive floats (broken by `clear`) for max-content.
- **Floats and margins.** A float is placed where a block would be, after the margins collected so far. While the top margin
  of the box that holds it is still pending (§8.3.1) the float is held until that position is settled, so that a larger
  margin collapsing through the container moves the float with it. A box that starts a formatting context is separated from
  adjoining floats: a margin that would pull them down does not.
- **Clearance (§9.5.2).** A box with `clear` gets clearance when its hypothetical position, with its own top margin and the
  margins of the first children that adjoin it collapsed into the run, is above the bottom edge of the floats it clears, or
  when a float adjoining it would otherwise be pulled down with it. The margins before the box end there; its border edge goes
  to the bottom edge of the floats whatever its own margin is. A cleared box that is still pending keeps the margin
  collapsing with its first child, and its position is raised to the floats' edge when the pending chain is settled.
- **Boxes beside floats (§9.5).** An in-flow block that is a block formatting context root, and block-level replaced boxes,
  are put beside the floats at their top edge. An `auto` width shrinks to the room between the floats (not below its
  min-content width); a definite width fits when its border box and start margin do (an over-constrained end margin is
  dropped). If the box does not fit it moves down to the next float edge, and with no float edge left it stays.
- **The exclusion space.** `IExclusionSpace`, reached through `InlineLayoutRequest.Exclusions`, is what inline layout asks:
  - `Query(bfcTop, height, bfcLeft, bfcRight, out start, out end, out nextEdge)` returns the usable inline range of a band
    (floats whose margin box overlaps the band; with height 0 the single line at `bfcTop`) and the smallest bottom edge
    among them, the next y where more room can appear (`+Infinity` when no float intersects). `start == end` means no room.
  - `PlaceFloat(request, container, floatBox, minBfcTop, out marginBox)` lays out and places a float met in inline content.
    `minBfcTop` is the top of the current line when the float fits on that line and the line's bottom when it does not; the
    inline layout decides, which is the hook for rule 6 and 7 of §9.5.1. The float's fragment is added to the container's
    fragment and the margin box comes back in block-formatting-context coordinates.
  - `PlaceholderInlineLayout` places the floats of its container at the top of the content box, then puts its one line in the
    room beside them, below the next float edge when there is none.
- **Fragments.** A float's fragment is a child of its containing block's fragment, in tree order; painting orders it
  (Appendix E step 5).
- **Bounds.** No recursion on tree depth: floats in the block flow are frames of the same explicit stack (nested floats
  need no call depth), and floats in inline content nest one engine call each, at most 32 deep (then a diagnostic and no
  fragment). A context stops searching after 50 million float visits or 262,144 floats: later floats are stacked below
  everything placed so far, lines ignore floats, and a diagnostic says so. Placement only scans the floats that can still
  matter (those ending below the lowest float top), so rows of thousands of floats cost about linear time. The margin-collapse
  look-ahead for clearance follows at most 64 first children. All storage is kept between layouts.
- **Chosen where the specs leave room.** Floats in a settled position use the margins collected so far (matching what
  browsers do); a cleared box's border edge ignores its own margin when clearance applies; the 'separated from adjoining
  floats' rule for formatting-context boxes uses the smaller of the two outcomes (pulled down or not).
- **Checked by.** `FloatLayoutTests` (spec examples by fragment border boxes, allocation and limits) and `WptFloatReftests`:
  58 CSS2 `floats` and `floats-clear` reftests whose page and reference render to the same pixels through `HtmlRender`,
  with the whole-pixel line height injected. The test with `ONERENDER_FLOAT_SWEEP` set sweeps the whole
  directories of a web-platform-tests checkout and writes one result per test.

## Inline layout (`Layout/Inline/`)

`InlineLayoutEngine` is the `IInlineLayout` behind `BlockLayoutEngine()`. It lays out one inline formatting context (CSS2 §9.4.2,
CSS Text 3, CSS Inline 3) per call, as one paragraph, and keeps one worker per nesting depth because atomic inlines
re-enter it through `IAtomicInlineHost` (at most 40 deep; deeper content is dropped). Everything is stored in arrays that grow and
are reused, so a relayout of unchanged content allocates nothing.

- **Collect.** An explicit-stack walk of the container's subtree produces the paragraph text and raw elements: text runs, inline
  box open and close, atomic inlines (U+FFFC in the text), tabs, forced breaks, floats and absolute boxes. White space is
  processed here (CSS Text 3 §4.1, every `white-space-collapse` and `text-wrap-mode` value): collapsing crosses inline box
  boundaries, segment breaks and `tab-size` follow the mode, and each character keeps a flag for "collapsible", "word separator"
  and the source offset it came from. `unicode-bidi` embeddings, isolates and overrides become the UAX #9 controls.
- **Prepare.** UAX #14 opportunities and grapheme starts come from OneFont; `AdjustOpportunities` turns them into CSS's:
  `white-space`, `word-break` (normal, break-all, keep-all, break-word), `line-break: anywhere`, soft hyphens (`hyphens: manual`
  only; `none` removes them), the `break-spaces` rule, and breaks around atomic inlines. Bidi levels come from UAX #9; a
  paragraph with a left-to-right base and no character at U+0590 or above skips the algorithm (all levels are 0). Text is split by
  level and into pieces of at most 1,024 characters (at a break opportunity or grapheme boundary) and shaped through
  `IFontProvider.Shape`, whose cache is keyed by text and style, so an unchanged paragraph hits it without allocating (a page with
  more than about 4,096 distinct runs thrashes that cache and allocates on a relayout).
- **Scan.** `ScanLine` is the greedy breaker: it accumulates clusters, remembers the last opportunity that fits, rolls back to
  it, hangs trailing collapsible and preserved spaces (CSS Text 3 §4.1.3), and breaks inside a word only when the line has no
  opportunity (`overflow-wrap: break-word | anywhere`). Min-content and max-content (`Measure`) run the same scan with an
  available width of 0 and infinity; a forced break splits max-content, atomic inlines count whole, `anywhere` counts in
  min-content and `break-word` does not.
- **Lines.** For each line the exclusion space is asked for the usable range at the line's top and height; a line whose first
  unbreakable item does not fit moves down to the next float edge. Floats met in the content are placed with
  `PlaceFloat`: at the line's top when the content before the float plus its shrink-to-fit width fits on the line, below the
  line otherwise (CSS2 §9.5.1 rule 6); the line is scanned again when a float joined it. The line's height needs the vertical
  metrics of its pieces, and the strut is always part of it (§10.8.1).
- **Vertical metrics.** Each inline box has ascent, descent and half-leading from its font and `line-height`; `vertical-align`
  shifts it (baseline, sub, super, text-top, text-bottom, middle, lengths and percentages of the line height; top and bottom
  align to the line box after the others are placed). An inline-block's baseline is the baseline of its last in-flow line, or
  its bottom margin edge when `overflow` is not `visible` or it has no line. The first baseline is stored on the container's
  fragment (`SetBaseline`); an inline-block's last baseline is found by walking its fragments.
- **Emit.** Alignment (`text-align`, `text-align-last`, `text-indent` with `hanging` and `each-line`, justification between
  words, `letter-spacing`, `word-spacing`), then bidi reordering of the line's pieces (UAX #9 L2), then fragments: a `Line`
  fragment per line; inside it one `Box` fragment per run of consecutive pieces of an inline box (so a box split across lines, or
  across a nested bidi run, has several pieces chained with `NextOfBox`, with `ContinuesBefore`/`ContinuesAfter`, and its
  border and padding only on the first and last piece, `box-decoration-break: slice`); inside those one `Text` fragment per
  font face of each piece with a glyph run (glyph ids, positions, source offsets). Atomic inlines are laid out through the host
  while preparing and moved into the right fragment with `FragmentTree.Reparent`. `TryGetBorderBox` of an inline element is the
  union of its pieces.
- **Bounds.** No recursion on tree depth (a 10,000-deep span nest is walked with the explicit stack). A paragraph holds at most
  4,194,304 characters (the rest is dropped); line scanning checks the cancellation token every 1,024 steps; a line that cannot
  place anything still consumes one cluster, so there is no endless loop. Costs on the development machine: a 1,000,000-character
  paragraph (12,171 lines) lays out in about 0.3 s including the document build, a 100,000-character unbreakable word in about
  25 ms (one line) or 30 ms with `overflow-wrap: anywhere` (4,000 lines), a 10,000-deep inline nest in about 30 ms. All are
  linear: shaping is per piece of at most 1,024 characters and a line is scanned once, plus once more for each float that
  joins it.
- **Choices where the specs leave room** (each checked against Chrome by the `inline-lab` pages, where 98.7% to 99.4% of the
  elements are within the geometry tolerance):
  - Ascent, descent and line gap are rounded to whole pixels (half away from zero) before half-leading is computed; the
    half-leading's top is the floor of half the leading and the bottom takes the rest. This put the lab from 9% to 90% within
    tolerance.
  - `sub` lowers by a fifth of the parent's font size plus 1 px and `super` raises by a third plus 1 px.
  - An inline box's open and close edges take the bidi level of the next/previous content, so the box's pieces stay contiguous.
    Known deviation: an RTL-direction box around left-to-right text puts its start edge on the wrong side.
  - `text-justify: inter-character` is treated as `inter-word`; `line-break: strict | loose | normal` are alike; letter-spacing
    does not disable ligatures; a percentage `word-spacing` is a fraction of the separator's advance.
  - An empty inline box alone on a line produces no fragment (Chrome gives a zero-size box).
  - Percentage `line-height`, and `em` lengths of `letter-spacing`, `word-spacing`, `text-indent` and `tab-size`, inherit as the
    ancestor's computed length (CSS Values 4 §7.1), not as text resolved against the descendant's font.
- **Not done:** `::first-line` and `::first-letter`, `text-overflow`, line clamping, `hyphens: auto`, `text-decoration` and text
  shadows, vertical writing modes, ruby, `box-decoration-break: clone`, inline-flex, inline-grid and inline-table as anything
  but blocks, markers inside the line, and `initial-letter`.
- **Checked by.** `InlineLayoutTests` (a section per topic, the 0 B relayout of a 1,000-box text page and the limits above);
  the `inline` fuzz target; the `inline-lab` differential pages in `testdata/inline-lab/`.

## Positioning (`BlockLayoutEngine.Positioned.cs`)

- **Containing blocks** (CSS2 §10.1, Position 3 §2). Each block frame carries `CbFrag`, the fragment of its nearest positioned
  ancestor (`position` other than `static`), or -1 for the initial containing block. Nested layouts started through
  `LayoutIndependent` inherit it from the frame that started them. A positioned inline box is a containing block too (§10.1 item 4): inline layout
  passes the fragment of the nearest positioned inline box around an absolute box as that box's containing block. When the
  inline box is split across lines this is the piece that holds the absolute box, not the union of the first and last pieces.
- **Relative** (§9.4.3). The shift is applied when a box closes, to the fragment rectangle only: the flow position that siblings
  and the parent's height see is unchanged. A shifted box adds its shifted rectangle to the parent's scrollable overflow.
  `left` wins over `right` in a left-to-right containing block, `top` over `bottom` always; percentage `top`/`bottom` against a
  height that depends on content are `auto`.
- **Absolute and fixed** (§10.3.7, §10.6.4; replaced §10.3.8, §10.6.5). While placing the in-flow children of a block, an
  absolute child is not laid out. It gets a placeholder fragment, appended as the next child of its *parent's* fragment so
  fragment order is document order (paint order needs that), and a queue entry holding its static position in the parent's
  border-box coordinates. The static top is the cursor plus the pending margin run (0 when the parent's own top margin is still
  pending). After the whole tree is done the queue is resolved in discovery order, so a box is resolved after the box that contains
  it:
  1. document-space origins of all fragments are computed in one pass (parents precede children), so a containing block's padding
     box and a parent's offset cost O(1) however deep they are;
  2. `SolveAbsoluteAxis` solves one axis (`start + margins + size + end + padding/border = containing size`) with `auto` as NaN:
     every case of §10.3.7, and §10.6.4 with the vertical differences (static top, equal margins even when negative, bottom
     ignored when over-constrained); the containing block's direction picks the ignored inset. Min/max apply as §10.4 says, by
     solving again with the violated size. An `auto` width is shrink-to-fit from the intrinsic sizes; an `auto` height is the
     content height (laid out once with the solved width forced into the box) or follows from `top`/`bottom` or `aspect-ratio`;
  3. the box is laid out through the same entry as every independent block (`LayoutIndependent`) with the solved sizes forced, into
     the placeholder, then moved to its place. Percentages of the box itself resolve against the containing block's padding box.
  Fixed boxes use the viewport (`LayoutInput`, at scroll position 0) as their containing block; painting counters the viewport
  scroll, so layout needs no scroll input. The root element can itself be `absolute`/`fixed`; it is then placed in the initial
  containing block.
- **With floats.** An absolute box inside a float (or in any nested formatting context) is queued like any other: the float lays
  its content out in its own context, and the box's static position is taken from the float's frame. Resolution runs after
  every float of the document is placed, and lays the box out in a formatting context of its own, so it is neither pushed by nor
  does it push floats. A relatively positioned float is placed as a float first and drawn shifted afterwards; the
  exclusion it leaves for lines and later floats stays where the float was placed, and its shifted rectangle adds to the scrollable
  overflow.
- **Absolute boxes inside inline content.** The inline layout reports each one with `IAtomicInlineHost.QueueAbsolute`: the static
  position is the inline position reached on the line when the box is met, at the top of the line box. A box with nothing
  but out-of-flow boxes before it on its line is at the start edge of the container's content box, however floats narrow the
  line. An inline layout that does not report (the placeholder) leaves the box at the start of the content box
  (`QueueInlineAbsolutes`).
- **Relatively positioned inline boxes** are drawn shifted by their offset; the line and the box's content move with the
  fragment and the line is unchanged.
- **Sticky** (Position 3 §3.4) takes no part in layout (it is in flow like `relative` with no offset). The shift is computed at
  paint time, see below.
- **Bounds.** The queue is capped at 4M boxes, nothing recurses, the resolution pass is linear in fragments plus queue entries, and
  the token is checked every 64 boxes. A 3,000-deep nest of positioned boxes with an absolute child at each level lays out in
  milliseconds (test).

## Overflow and scrolling

- **Overflow values** (Overflow 3 §3.1) are already computed with the visible/clip → auto/hidden rule when the other axis scrolls.
  A *scroll container* is a box with `overflow` other than `visible`/`clip` on an axis; `overflow: clip` clips like `hidden` but
  is not scrollable. `overflow-clip-margin` (Overflow 4 §3.2: `<visual-box> || <length>`, padding-box and 0 by default) outsets
  the clip edge on `clip` axes only.
- **Viewport propagation** (Overflow 3 §3.3). `ViewportOverflow.Resolve` picks the root's `overflow` or, when the root is `html`
  with `visible` and has a `body` child with something else, the body's; `visible` becomes `auto` and `clip` becomes `hidden` for the
  viewport. The element it came from neither clips, nor is a scroll container, nor establishes a block formatting context for that
  reason. Layout and the painter both ask the same function.
- **Scrollable overflow** (Overflow 3 §2.2) is recorded on every box fragment (`Fragment.ScrollableOverflow`, in the fragment's
  border-box coordinates). It starts from the border box and takes in-flow children's overflow (shifted by relative offsets). A
  box that clips an axis contributes only its border box on that axis. A scroll container's own area starts at its padding box,
  and content that reaches past the end of the content box also brings the end padding in. Absolute boxes add their overflow to
  the fragment of their containing block, not their parent, and from there to every ancestor that does not clip; fixed boxes add
  nothing. This is one pass over the fragments in reverse order after the queue is resolved.
- **Scroll positions** are host input in a `ScrollState`: one viewport offset and one offset per scroll container (keyed by box
  index, so call `Clear()` after rebuilding the box tree). They are stored as requested and clamped when used to what
  `ScrollState.GetRange` (a container) or `GetViewportRange` (the root's scrollable overflow beyond the viewport) allows:
  `0..scrollable overflow − padding box`, and for `direction: rtl` containers `overflow start − padding start .. 0`.
  `HtmlPage.Scroll` is the page's state. Layout never reads it.
- **Painting scrolls.** `PaintOptions.Scroll` turns it on. A scroll container that can scroll emits `PushClipRect` (fixed) and, inside
  it, a `PushTransform` that translates its content by the negative offset; the viewport gets one around everything but the
  canvas; fixed boxes get a counter-translation by the viewport offset; sticky boxes get a translation of their own. A box painted
  outside its containing block's scope (an absolute or relative box deferred to its stacking context) gets a copy of the scroll
  transform of each scroll container on its containing-block chain, so it scrolls with the container but not with ancestors it
  escapes. Items are only emitted when the container can scroll, which depends on layout, never on the offset. Their ids use parts 6 (scroll container), 7 (sticky), 8 (fixed counter-scroll) and 9 (copy of a scroll transform pushed for a deferred box), and `ItemId.Of(0, 6, 0)` for the viewport.
- **Changing a position never repaints.** `Painter.PatchScroll(retained)` recomputes the translation of every scroll, viewport,
  fixed and sticky item the last paint emitted and calls `PatchTransform` on those that changed; the damage is the old and new
  bounds of what moved, clipped by the container, and nothing allocates. The patched list equals a fresh paint (tested by comparing
  ids, bounds and both hashes). The sequence for a host is: `page.Scroll.SetOffset(box, x, y)`, `painter.PatchScroll(retained)`,
  draw the damage.
- **Sticky shifts** are `StickyPositioning.Offset(borderBox, margins, containingBlock, scrollport, scroll, insets)`, a pure
  function: on each axis the box is moved inward of the scrollport edge by its inset (the scrollport shifted by the scroll offset,
  in unscrolled coordinates) and then limited so its margin box stays in its containing block (the parent's content box); with
  both insets on an axis, the start inset wins. The painter records the geometry in document space when it paints, so a scroll
  change is arithmetic on a table. A sticky box inside another sticky box is shifted relative to the outer one's final position.
  The scrollport of a sticky box is its nearest scroll container's padding box, or the viewport.
- **Not done:** scrollbars (no gutter, nothing is drawn), scroll snapping, scroll events, `scroll-behavior`, `overscroll`, `contain`, and
  the overflow of the content of an atomic inline (the border box is used).
- **Overflow of inline content.** Line boxes, inline box edges, text that runs past the end of its line (an unbreakable word, a
  `nowrap` line) and atomic inlines add to the scrollable overflow of the container (`InlineLayoutResult.Overflow*`).

## Flex layout (`Layout/Flex/`, `BlockLayoutEngine.Flex.cs`)

Flexbox Level 1 §9 on the block engine's frame stack, with Box Alignment 3 for `justify-content`, `align-content`, `align-items`,
`align-self` (including `safe`/`unsafe`) and `gap`. Block-level and inline-level flex containers are both handled; the container
is a formatting-context root like a BFC root, so floats stay outside it and margins do not collapse through it.

- **Style.** `flex-direction`, `flex-wrap`, `flex-grow`, `flex-shrink`, `flex-basis`, `order`, the alignment properties and
  `row-gap`/`column-gap` are typed in `ComputedValues`. The cascade layer does not expand `flex`, `flex-flow` and `place-*`, so
  `ShorthandFallbacks` does, with the Flexbox §7 rules (`flex: 1` is `1 1 0`, `flex: auto` is `1 1 auto`, `none` is `0 0 auto`).
- **Phases per container.** Init (collect in-flow items in `order`, skipping `display: none`; absolutely positioned children are
  queued for the static position), Base (flex base size and hypothetical main size per item, §9.2), Lines (§9.3), Resolve flexible
  lengths (§9.7, with min/max violation freezing; capped at 256 rounds), Cross (§9.4 hypothetical cross sizes, line cross size,
  `align-content`, stretch), Final (§9.5 main-axis placement, §9.6 cross-axis alignment, §8.3 baseline alignment) and Close.
- **Trial layouts.** Measuring an item means laying it out at a forced border-box width and/or height (`ForceW`/`ForceH`), then
  truncating the fragments it produced (`FragmentTree.Mark`/`Truncate`). The measurement is memoized per box by the forced sizes,
  the containing block size, and the containing height only when the subtree has percentage block sizes. A trial-mode container
  skips its own final phase, so a chain of nested flex containers is measured in linear time. The exact item layout happens once.
  A budget of 1M + 64 per box trials guards hostile input; past it a diagnostic is recorded and sizes come from style only.
- **`min-width/height: auto`** (§4.5) uses the smaller of the content-size suggestion and the specified size suggestion, clamped by
  `max`; for aspect-ratio items the transferred suggestion applies. Column containers compute the content-size suggestion with a
  trial at the resolved width.
- **Intrinsic sizes** (§9.9) are in `FlexIntrinsic.cs` and feed the shared intrinsic cache: single-line containers sum item
  contributions along the main axis, multi-line take the largest for min-content, and gaps are added.
- **Absolutely positioned children** (§4.1) use the flex container's content box as the containing block and are aligned with
  `justify-content`/`align-self` when both insets on the axis are `auto`.
- **Choices where the specs leave room (to be checked against the Chrome differential once text lands):** overflow alignment
  defaults to `unsafe`; `space-around`/`space-evenly` with negative free space fall back to `safe center` and `space-between` to
  `flex-start`; `align-content: normal` behaves as `stretch`; `baseline` in a column container falls back to `flex-start`;
  `place-items`/`place-self` ignore the justify half (it has no effect on flex items); `float` and `clear` on items are ignored.
- **Bounds.** No recursion (a 50,000-deep nest of flex containers lays out), 100,000 items lay out in well under a second, the
  measurement count for a 2,000-level alternating nest is at most twice the depth, and relayout of a retained tree allocates
  nothing (the zero-byte assertion covers a page of more than a thousand flex items).
- **Not done:** `visibility: collapse` on items, `list-item` flex containers' outside marker, percentage and `min-content`
  `flex-basis` in column containers (treated as `content`), multi-line column containers size trial layouts from the container
  width rather than the line width, vertical writing modes. Text items take their min-content and
  max-content from the inline layout's intrinsic sizes and their baselines from its lines.

## Fragmentation (`Layout/Fragmentation/`)

CSS Fragmentation 3 over the fragment tree. The design is **lay out as one strip, then cut**: a fragmented flow is first laid out
as one tall continuous block at the fragmentainers' inline size, which the block and inline engines already do; the
`Fragmenter` then finds the break points in that strip, chooses breaks, and rewrites the fragment tree into one fragment per
fragmentainer. Breaking never feeds back into inline layout, because every fragmentainer of a flow has the same inline size.

- **Fragmentainers.** `FragmentationKind` says what is being filled. `Column` is used by multi-column layout. `Page` selects the page
  values of the break properties (`page`, `left`, `right`, `recto`, `verso` force; `avoid-page` discourages) and is what paged media
  uses with `Slice` over fragmentainer sizes that differ page by page (`Slice` takes the block size of the first fragmentainer, the second
  and so on, the last one repeating). A paged unit supplies the sizes, calls `Collect`, `Finish`, `Slice` and `Apply` (or `Plan`), and
  lays its page boxes and margin boxes around the fragmentainers; the core does not change. `left`/`right`/`recto`/`verso` are carried
  in `BreakCandidate.Bits`; inserting blank pages is the page layer's job.
- **Strip coordinates** are block offsets from the top of the unfragmented flow. A break point is a pair: where the content above ends
  (`Ye`) and where the content below resumes in the next fragmentainer (`Ys`). The distance between them is the truncated margin.
- **Break points** (§4.1), found by one explicit-stack walk of the strip (`Collect`): class A between in-flow block siblings and class B
  between the lines of a block container. Class C (between a container's content edge and its content) is used only for the empty
  room below the last child of a box, which behaves like an empty box. Floats are not break points and are never cut. Boxes that
  cannot fragment are *monolithic* (§4.1): replaced boxes, markers, scroll containers (`overflow` other than `visible`/`clip`),
  boxes with `contain: size`, and nested multi-column containers. Their insides contribute no break points and a forced break
  inside them has no effect. On pages a monolithic box or float taller than the page stays whole on the page it starts on and its
  background and borders carry on in the pages the overflow reaches, as pieces without content.
- **Flex, grid and tables.** A flex container fragments between its flex lines (Flexbox 1 §12) and a grid between its rows; items on
  one line sit side by side, so a break is offered only where no item spans it, and the strip cut then splits each item that crosses
  it. Reversed flex containers are walked in block order. Page names are ignored inside flex and grid containers. Table rows break
  between rows; the places inside a row (its cells' lines, and empty cells) are used only as a last resort, when the row is taller than
  the fragmentainer, so a row that fits the next page moves whole. An empty box with a height has no break points but may be cut
  anywhere when it is what does not fit (the "break anywhere" last resort of §4.4, applied to boxes that have nothing in them).
- **Break values** (§3). `break-before` and `break-after` on the first and last in-flow child propagate to the parent chain (§3.1.1),
  and the values at a break point combine. A forced value (`column` in columns, the page values in pages) forces the break and
  overrides avoid values; `break-inside` of every ancestor applies (rules 2 and 4); `always` and the `page-break-*` aliases are
  accepted (§3.4).
- **Choosing breaks** (§4.3, §4.4, `Slice`). For each fragmentainer: the first forced break that fits wins; if the rest of the flow fits, it is the
  last fragmentainer; otherwise the break is the latest candidate that fits (counting the border and padding that
  `box-decoration-break: clone` repeats at the break) among those no rule forbids. Candidates carry a *level*: 0 breaks no rule,
  1 breaks only orphans/widows (rule 3), 2 breaks an avoid rule (rules 1, 2, 4). The search takes the latest level-0 candidate,
  else the latest level-1 one, else the latest candidate of any level, so conflicting avoids end in the latest possible break. If
  nothing fits, the first break after the point where content stops fitting is used and the content overflows the fragmentainer
  (monolithic overflow). A break at the very top of a fragmentainer is never chosen unless forced (§4.4 progress), fragmentainers
  are at least 1px high (§4), and a break that would cut through a float is level 2. Orphans and widows: a break between lines `j` and
  `n - j` is level 1 unless `j >= orphans` and `n - j >= widows`; a block with fewer lines than either value is kept whole.
- **Margins at breaks** (§5.2). At an unforced break the margin between the two boxes is dropped (the next fragmentainer starts at the
  top of the later box); at a forced break the margin above is dropped and the later box keeps its own top margin (clamped to the
  collapsed gap). Cloned margins are always zero.
- **Splitting boxes** (§5.3, §5.4, `Apply`). A box that crosses breaks gets one fragment per fragmentainer it occupies. The first keeps
  the original fragment, so handles stay valid; the others are copies without children, chained after it with `NextOfBox`. A piece
  that continues extends to the end of its fragmentainer (the box's content box "fills the remaining extent"). With `slice` the pieces
  take the original border and padding only at the real start and end (the top and bottom edges of the other pieces are 0, so
  painting needs no special case); with `clone` every piece keeps all of them and the break leaves less room. Lines, text, floats,
  absolutely positioned boxes and monolithic boxes are not split; each goes to the fragmentainer where its top is.
  Every fragment records its `FragmentainerIndex`; pieces of a box are in index order.
- **Bounds.** No recursion (explicit stacks, 3,000-deep and 100,000-line flows are tested). `Slice` is O(fragmentainers × log candidates)
  with prefix tables, so a 100,000-line paragraph cuts in milliseconds. A flow makes at most 100,000 fragmentainers and 4,000,000
  extra box pieces; past either the rest stays whole in the last or current fragmentainer and a diagnostic is recorded. All storage is
  pooled: re-cutting an unchanged flow allocates nothing.
- **Choices where the specs leave room.** Class C points other than the end gap are not used. Floats and absolutely positioned
  boxes are never fragmented (Fragmentation 3 leaves their fragmentation to the layout mode). A box whose containing block is
  inside the strip is placed by the strip's coordinates, so an absolutely positioned box is assigned to the column its top lands
  in. Percentage heights inside a fragmented flow resolve against the container, not the column.

## Paged media (`Paged/`, `Layout/PageFragmentation.cs`, `Tree/ContentList.cs`, `Tree/GeneratedContent.cs`)

CSS Paged Media 3, CSS Generated Content 3 and GCPM 3 on top of the fragmenter. `PagedDocument.Create(html, PagedOptions)` is the entry; the PDF happy path goes through it.

- **Pipeline.** The document is cascaded with the `print` media type, laid out as one strip at the first page's content width, and cut by
  `PageFragmentation.Paginate` into page-sized fragmentainers placed side by side (page *i* at x = i · stride). Each page then gets a
  *page layer*: the page box's background and its margin boxes, laid out as a small HTML document of their own (a probe document per
  distinct page style) and painted in page coordinates. Content is painted once for all pages (`PaintContent`); a page is a region of that
  list plus its layer (`GetPage`, `GetPageLayer`).
- **`@page`** (`PageRules.cs`): `size` (keywords, `landscape`/`portrait`, one or two lengths), `margin` with percentages against the page box,
  the selectors `:first`, `:left`, `:right`, `:blank` and a page name, combined by specificity like selectors (names beat pseudo-classes by
  the specificity rule of §7.3, later rules win ties). Page *i* is a left or right page by its number and the document's direction; a
  right-to-left document starts on a left page. Blank pages that a `left`/`right`/`recto`/`verso` break inserts count in the numbering.
- **Named pages.** `page` is inherited through `auto`. A change of the used name between siblings forces a break (§7.3). Page sizes are
  per page, so the cut is planned again when a named page has a different height: at most 4 passes. Pages whose content width differs
  from the one the strip was laid out at keep the strip's width (content is cut or padded) and a diagnostic says so.
- **Margin boxes.** The 16 boxes are sized by the algorithm of §5.3 (`ComputeBoxRects`): corners are fixed by the margins; the three
  boxes of an edge share the edge's length by the resolved-width rules, with `auto` widths distributed by the content's max-content
  widths (`Distribute`), and a box that has no content takes no room. A margin box is a flex column with `overflow: hidden`, and the
  `vertical-align` of the box sets its `justify-content`. Their text properties inherit from the root element, not from the page.
- **Generated content.** `GeneratedContent` evaluates `content`, `counter-reset/-increment/-set` (including `reversed()`, whose
  initial value is the sum of the negated increments in its scope, found by a bounded walk), `counter()`, `counters()`, `attr()`,
  quotes, `string-set`/`string()`, `target-counter()`, `target-counters()`, `target-text()` and `leader()` while the front end builds the
  styled tree. Counter scopes follow the element tree with `display: contents` elements transparent. A counter name that is `none` or
  a CSS-wide keyword makes the value invalid and the generated box empty. `counter(page)` and `counter(pages)` are known only after
  a cut: when content reads them, or reads a `target-counter()` of a later page, the content is built again with the numbers of the
  previous pass, at most three times; if the numbers do not settle the last ones are used and a diagnostic is recorded.
  `string()` takes `first`, `start`, `last` and `first-except` over the `string-set` events on each page.
- **Leaders.** `leader()` evaluates to a pattern between two private marker characters. Inline layout turns that into a text run that
  takes no room while a line is broken and is given the room the other pieces of the line leave when the line is placed; it shows whole
  pattern clusters and the text after it ends at the line end. The glyph run's source text is the pattern. Leaders share the room equally.
  The pattern is not aligned across lines.
- **Table headers and footers.** A `thead`/`tfoot` is reserved on every page a table continues onto (`CloneTop`/`CloneBottom` of the
  candidates) and copied there (`FragmentTree.CloneSubtree`, traits `Repeated`). In 1.0.0 the copies don’t show in the PDF: the
  header copy is placed above the page’s content area, where it is clipped, and the footer copy is not written. The copies are artifacts in tagged output: they stay
  inside the table's structure element, so the table is one element across pages. Header and footer groups are not split.
- **Limits.** `MaxPages` (default and ceiling 100,000) is checked before every cut: forced breaks and blank pages past it are not taken,
  `Truncated` is set and the rest stays on the last page. A `content` value is bounded (16 KiB of source, 64 items, 64 KiB of text once evaluated), `@page` rules (1,024) and
  their declarations (256) by count, blocks by length (64 KiB) and nesting (16), counters by scope depth and number (4,096), string-set events by count, scan budgets for reversed counters by
  node count. Nothing recurses over the tree.
- **Not done.** `position: running()`/`element()` (the cascade does not keep `running()`), footnotes, leaders aligned across lines, `@counter-style` rules and the counter styles that `CounterStyles.cs` lacks (such as `georgian`), the implicit `list-item` counter for reversed lists.

## Multi-column layout (`Layout/Multicol/`)

CSS Multi-column Layout 1. A container with `column-count` or `column-width` other than `auto` (`ComputedValues.IsMulticol`) is a
block container that establishes a block formatting context (§2).

- **Used count and width** (§3.4, `BlockLayoutEngine.ResolveColumns`): with only `column-width`, `N = max(1, floor((W + gap) / (width + gap)))`; with both,
  `N` is capped by `column-count`; with only `column-count`, `N = count`. The used width is `(W - (N - 1) * gap) / N`. `column-gap: normal` is
  `1em`; percentages resolve against the content width. At most 10,000 columns per row.
- **Layout.** The container's children are laid out as a normal block flow at the column width (the frame's content width is the column
  width). When the container closes, `Fragmenter.Plan` picks each row's column height and cuts the strip; after the whole layout (and the
  resolution of absolutely positioned boxes) `Apply` rewrites the container: its children become `Column` fragments, each positioned at
  `contentLeft + k * (columnWidth + gap)` (mirrored for `direction: rtl`) with its content as children relative to the column.
  A trial layout that is discarded (flex measurement) drops its plans.
- **Rows and spanners** (§6). A direct in-flow block child with `column-span: all` is laid out at the container's full content width and
  splits the flow into rows: the content before it is balanced on its own, then the spanner, then the content after. The gaps in the
  strip around a spanner (margins) are kept. A row with no content creates no columns. Spanners deeper than a direct child have no
  effect yet.
- **Height.** Every row's columns share one height. Non-final rows always balance. The final row balances (`column-fill: balance`, the
  default) or fills (`auto`): balancing finds the smallest height, in steps of 1/64 px, at which the row fits in `N` columns without
  overflow and without breaking an avoid or orphans/widows rule (`Fragmenter.Balance`: a binary search of at most 40 trial slices, repeated
  once with the rules relaxed if that fails; each trial stops as soon as it needs more than `N` columns). With a definite `height` (or a
  `max-height`) the final row is limited to what is left; if the content still does not fit, the extra columns are added in the inline
  direction after the last one (§8) and become scrollable overflow. With `column-fill: auto` and an `auto` height all the content stays in
  one column unless a forced break splits it. The container's height is its `height`, or the rows and spanners, clamped by `min-height`/`max-height`.
- **Column rules** (§4): `Painter` draws one border item (part 10 of the container's box id, sub id the column's index) in the middle of the gap
  between two adjacent columns of the same row, as high as the columns, after the container's background and border and before its
  content. Rules separate columns that exist; a spanner ends the run. `column-rule` and `columns` expand to their longhands
  (`ShorthandFallbacks`).
- **Intrinsic sizes** (no section in level 1): min-content is `N * max(content min, column-width) + (N - 1) * gap` with `N = max(1, column-count)`;
  max-content is the same with the max-content width.
- **Scrollable overflow** of the container is its border box plus the columns, so overflow columns extend it sideways; overflow of the
  content inside columns is not propagated past the container.
- **Not done:** `column-span: all` on non-children, nested multi-column containers that break across the outer columns (a nested
  container stays whole in one outer column), fragmentation of flex, grid and table containers, `column-height`, `column-wrap`,
  and vertical writing modes.

## Grid layout (`Layout/Grid/`)

CSS Grid 2 as a formatting context of the block engine. `OpenChild` calls `GridOpen` for a container whose context is grid (flex has the same hook one line above); `GridIntrinsicSizes` answers min-content and max-content widths for shrink-to-fit and for items that are grids themselves (`display: inline-grid` is an atomic inline through `LayoutIndependent`). A grid nested more than 24 grid items deep is laid out as a block with a diagnostic.

- **Style.** `GridTypes.cs` holds `GridTrackList` (head, `repeat()` body, tail, line names, `subgrid`), `GridTrackSize`, `GridLine`, `GridAreas`; `StyleComputer.Grid.cs` parses the longhands and `ShorthandFallbacks.Grid.cs` the shorthands (`grid`, `grid-template` with the ASCII-art form, `grid-area`, `grid-row`, `grid-column`, `place-*`). Property ids are appended after `ColumnGap`.
- **Contexts.** A grid and every subgrid below it form one flat list of contexts built parents first; an axis of a context either owns its tracks or points to the owner context with an offset and a span. Everything is held in per-depth pooled arrays (`GridLevel`), so a relayout of an unchanged grid allocates nothing; named lines are the exception (the name index allocates per layout).
- **Placement** (`GridPlacement.cs`, §8): line resolution with names, spans, negative lines and implicit names from areas; sparse and dense auto-placement for row and column flow over an occupancy bitmap; `order` sorts the items first. Subgrid placement is clamped to the parent span.
- **Track sizing** (`GridTrackSizer.cs`, §12): initialize, resolve intrinsic sizes in span order (items spanning flexible tracks last, and only they raise flexible tracks that have an intrinsic minimum), maximize, expand flexible tracks (find the size of an fr; indefinite space uses the items' max-content contributions), stretch auto tracks. `auto-fit` collapses unused tracks and their gutters. Columns are sized parents first; rows children first, because a subgrid's height is an input of its parent's rows. Row percentages resolve in a second pass once the container height is known.
- **Items.** Each item is measured by laying it out at its used width with `_absForce` and truncating the fragments (`FragmentTree.Mark/Truncate`), then laid out again at the final size in its area. Replaced items get their used size through `_gridReplacedForce`. Baseline-aligned items in the same first row get a shim so their baselines meet; a box without a baseline uses its border-box bottom.
- **Subgrid** (§9): subgridded items contribute their own children to the parent's tracks; the subgrid's margin, border and padding at an edge, and half the gap difference elsewhere, become extra margin on the items there. Line names from the parent are visible inside, and a subgrid with an automatic span covers as many tracks as its name list.
- **Absolutely positioned children** (§10): with grid lines the containing block is the grid area (only when the grid container is the containing block; a missing line is the padding edge); without lines the static position is the box's own alignment inside the container's content box.
- **Limits.** 10,000 tracks per axis (a 1,000,000-track `repeat()` is clamped, line numbers clamp to the same bound as §5.4 allows), 1,048,576 items per container, 50 million cell visits in placement (then the remaining items stack one per row), 100 million units of sizing work, 32 levels of shared subgrid tracks (deeper ones become independent grids). All checks run before the work they guard; nothing recurses on tree depth.
- **Known deviations.** Inline-axis `baseline` falls back to start; last-baseline container baselines are not produced; `justify-items: legacy` computes to `normal`; masonry (`grid-lanes`), fragmentation across pages and non-horizontal writing modes are not implemented.
- **Checked by.** `GridLayoutTests` (placement, auto-repeat, sizing, alignment, baselines, subgrid, abspos, 0 B relayout of a 1,000-item grid with subgrids, hostile limits), the `grid` fuzz target, `testdata/grid-lab/` against Chrome (`results/grid/summary.md`) and WPT `css-grid`.

## Tables (`Layout/Table/`, `BoxTree.Table.cs`)

- **Box generation.** `display: table`, `table-row`, `table-cell` and the rest build the table box tree; missing parents and children are generated as anonymous table objects (CSS 2.1 §17.2.1) by an iterative pass over the box tree, so nesting depth never reaches the call stack. HTML tables get their display values from the user agent sheet; `colspan`, `rowspan` and `span` are read by the HTML front end and clamped to 1000 and 65534 (HTML §4.9.11).
- **Grid.** Rows and cells are placed into a slot grid (spans, `thead`/`tfoot` ordering, `col`/`colgroup`, `visibility: collapse` rows and columns). Limits: nesting 16 tables deep, 1000 columns, 4 million collapsed-border slots; a table nested deeper is laid out as blocks, cells beyond the column limit are dropped, and each case records a `LimitExceeded` diagnostic.
- **Columns.** Auto layout takes each column's min- and max-content width from its single-column cells, then distributes the excess of spanning cells by max-content weight, preferring columns that are not fixed. A table narrower than its columns' sum in the min case overflows; wider space goes first to auto columns, then fixed, then percent. Percent columns are served first when the available width allows. `table-layout: fixed` takes widths from the first row and `col` elements only (CSS 2.1 §17.5.2.1). A caption's min- and max-content widths widen the table.
- **Rows.** A row is as tall as its tallest cell (spanning cells share the excess among the rows they span) and at least its specified height. `vertical-align` in a cell is top, middle, bottom or baseline (§17.5.3); baseline cells align on the first baseline in the row. A cell is laid out once; percent heights inside cells do not resolve.
- **Borders.** With `border-collapse: separate`, `border-spacing` and the cell borders are ordinary box geometry and `empty-cells: hide` suppresses empty cells' decoration. With `collapse`, borders resolve per edge by the conflict rules of §17.6.2.1 (hidden wins, then width, then style, then cell over row over group over column over table); the before half of a shared border is `floor(width/2)` and the after half is the rest. The winners are stored per fragment (`FragmentTree.TableBorders`) and the painter draws them in place of the box's own border.
- **Captions.** A caption is laid out above or below the rows by `caption-side`. It lives inside the table fragment (the table wrapper and the table box are one fragment), so a table border and background also surround the caption; this differs from the specification, where the wrapper is the table's parent.
- **Backgrounds.** Table, column group, column, row group, row and cell backgrounds paint in that order (§17.5.1). Column and column group fragments exist only when they have a background.
- **Presentational hints** (HTML §15.3.8). `width`, `height`, `cellspacing`, `cellpadding`, `border` and `align` on `table`, `width`, `height` and `align` on `td` and `th`, and `align` on `tr`, apply where the computed value is still the user agent default, because the cascade has no hint layer. `align` on a cell centers a table child the way a centered block does in a browser. `bgcolor` and `valign` are not implemented.
- **Fragmentation.** In paged media the rows of a table are break points and a row's cells are never split; header and footer groups are reserved on each page the table continues onto (see Paged media above).
- **Cost.** A relayout of an unchanged table reuses the grid and allocates nothing (checked for 1,000 cells); a 10,000-row table lays out in time and memory linear in its rows.
- **Checked by.** `TableLayoutTests` (anonymous objects, spans, auto and fixed layout, collapsed borders, captions, alignment, hints, 0 B relayout, the 10,000-row shape, hostile spans and nesting, relayout equal to a fresh layout), the `table` fuzz target, WPT `css2-tables` and `css-tables`, and the `templates` set against Chrome.

## Paint (`Painting/Painter*.cs`)

The painter walks the fragment tree in CSS 2 Appendix E order and writes the display list (`display-list.md`). The second paint area added the pieces below; each is computed from typed values in `Style/PaintTypes.cs` and painted by one method.

- **Backgrounds** (Backgrounds 3 §3). One painted layer per `background-image` entry; the other lists cycle (§3.1). Layers paint last to first under `background-color`, which fills the clip box of the final layer. Origin, clip (`border-box`, `padding-box`, `content-box`; rounded by the box's radii) and size (`auto` with the natural ratio, `cover`, `contain`, lengths) follow §3.4 to §3.9. Repeat follows §3.5: `repeat`, `no-repeat`, `space` (the whole number of tiles that fit, spread with equal gaps; a single tile is placed at the start) and `round` (the tile is rescaled to fit a whole number, at least one). `background-position-x/-y` are the longhands (OneHtml 1.0 expands the shorthand into them). A `url()` layer that repeats is one tiled image item. A gradient layer is one rectangle per tile on snapped edges, so no seam shows. `fixed` attachment is relative to the viewport: the layer sits inside a translation that `PatchScroll` updates in place, the same entry the fixed-position patch uses. The canvas takes the root's (or the body's) background (§2.11.2). `background-clip: text` and `border-area` are not painted and record a diagnostic.
- **Gradients** (Images 3 §3). Linear, radial and conic, plain and repeating. Stops get the fixup of §3.4.1 (missing positions spread evenly, positions raised to the largest one before, transition hints expanded to a few intermediate stops). Interpolation is premultiplied sRGB, which is what the display list defines and what Chrome does for legacy colors; the `in <colorspace>` syntax is not parsed yet.
- **Borders.** Widths floor to whole device pixels (minimum one). `border-image` (§6) slices the source into nine parts, paints the corners, repeats, rounds, stretches or spaces the edges, and draws a gradient source through a transform and clip; `fill` paints the middle. Radius clipping of backgrounds is by clip box (§5.3).
- **Shadows** (§7). Box shadows paint last to first, outer before the box, inset after the background and before the border, with spread and blur.
- **Outlines** (UI 4 §4). Drawn after the content of the paint unit that owns them, outside the border edge, offset by `outline-offset`, with the border-style look. `outline-style: auto` is a 2 px ring in the outline color with a 1 px white ring inside it, a fixed deterministic look; it follows the border radius.
- **Text paint.** `text-shadow` paints each shadow as a blurred copy of the glyph run before the text, the first shadow on top. Decorations (Text Decoration 4) are drawn by the box that declares them across its inline text: underline and overline under the text, line-through over it. Thickness is the font's `post` underline thickness (minimum one device pixel) and the line's edges snap like any rectangle. The underline's center sits half the font's descent below the baseline, which is what Chrome 156 does with the packaged fonts rather than what the `post` table's underline position says: probing 10 to 48 px Liberation Sans, the rule puts the underline on the same rows as Chrome at 10 of 13 sizes and one row off at 18, 24 and 40 px (`UNDERLINE=1` mode of `tools/snap/probe.py`); other families are not probed. The strike-through comes from the `OS/2` strikeout metrics; `text-underline-offset` and `-position: under` override; `solid`, `double`, `dotted`, `dashed` and a zigzag `wavy`. Skip-ink is off. Markers: `disc`, `circle` and `square` are shapes beside the first line; `decimal`, alpha, roman and greek markers are text built from the item's index among its list siblings; `list-style-image` draws the image. `start`, `reversed` and `value` are not read, and `list-style-position: inside` markers are not painted.
- **Replaced content.** `img` paints through `object-fit`, `object-position` and `image-rendering` (Images 3 §5); the natural size comes from the host's `IImageSource`. A missing image paints nothing and records a diagnostic; alt text and the broken-image icon are not drawn.
- **Pixel snapping.** Every edge of a background, border, outline, clip and decoration rectangle is placed at `round(x · dpr) / dpr` (half up), and the far edge is snapped on its own. Evidence (Chrome for Testing 156.0.8078.47, headless, `tools/snap/probe.py`; columns shown as `.` white, `R` red, `B` blue):

  | Case | Chrome result |
  |---|---|
  | left 10.4, width 10.4 | red on columns 10 to 20 (edges 10 and 21) |
  | left 10.5, width 10 | columns 11 to 20 (10.5 rounds up to 11, 20.5 to 21) |
  | left 10.6, width 10 | columns 11 to 20 |
  | boxes at 0 and 10.5, both 10.5 wide | red 0 to 10, blue 11 to 20, no blended column |
  | boxes at 0 and 10.3, both 10.3 wide | red 0 to 9, blue 10 to 20 |
  | scale 2, left 10.25, width 10 | 20 device pixels from device column 21 (20.5 rounds up) |
  | scale 2, left 10.2, width 10 | 20 device pixels from column 20 |
  | scale 1.5, left 10.2, width 10 | 15 device pixels from column 15 |
  | top 10.5, height 10 | rows 11 to 20 |
  | border 0.4, 0.9, 1.0, 1.6 px at scale 1 | 1 px |
  | border 2.5, 2.99 px at scale 1 | 2 px |
  | border 0.4, 0.6, 1.3, 1.6 px at scale 2 | 1, 1, 2, 3 device pixels |

  Known difference: a box narrower than half a device pixel whose edges snap to the same pixel is painted one pixel wide by Chrome and not at all by us.
- **Resources.** The painter never loads anything: images come from `PaintOptions.Images`, fonts from `PaintOptions.Fonts`. `.Image` supplies `ImageStore`, which decodes through oneimage from the host's `Resources` lookup and answers all of layout (`IResourceResolver`), painting (`IImageSource`) and rasterizing (`IImageResolver`).
- **Ids.** Items of this area take ids from a second id space (`Id2`, keyed by the owner, a part number and an index) with a free-id search on collision, so ids stay stable across unchanged frames.
- **Allocation.** Repainting an unchanged page with layered backgrounds, border-image, shadows, outlines, decorations, text-shadow, markers and images, and patching fixed backgrounds on scroll, allocate 0 bytes once warm (`PaintAllocationTests`).
- **Checked by.** `PaintStyleTests` (parsing and computed values), `PaintPixelTests` (analytic pixels plus two golden scenes), `PaintAllocationTests`.

### Third paint area: transforms, effects and inline SVG (`Painter.Effects.cs`, `TransformGeometry.cs`)

- **Transforms** (Transforms 1 §6 to §13, Transforms 2). `transform`, the individual `translate`, `rotate` and `scale`, `transform-origin` (with z), `transform-box`, `perspective`, `perspective-origin`, `transform-style` and `backface-visibility` are typed values in `Style/EffectTypes.cs`. `TransformGeometry` composes `translate · rotate · scale · transform` around the origin into one 4×4 matrix in the containing space; the painter and hit testing both use it, so they agree. A parent's `perspective` multiplies the matrix of each child. A box whose back faces the viewer with `backface-visibility: hidden` gets an empty clip. `preserve-3d` is flattened (a diagnostic when a projection is used); nested projections are flattened to their affine part. A matrix that does not map the plane in front of the viewer paints nothing.
- **Layout effects of a transform** (Transforms 1 §2). A box with a transform or `perspective` is the containing block of its absolute and fixed descendants: `Frame.CbFrag` stops at it, and `Frame.TfFrag` is the nearest such ancestor for `position: fixed` (which then scrolls with it and no longer takes the viewport patch). The bounding box of the transformed border box and transformed overflow replaces the untransformed extent in the parent's scrollable overflow (`BlockLayoutEngine.Transform.cs`; block, flex and grid items; floats and table cells are not handled yet).
- **Scopes.** One element opens, outermost first: transform (and backdrop filter, a leaf before the group), group (opacity, `mix-blend-mode`, `isolation`), mask, clip-path, filter, then the element's own painting; `ExitFx` and `ExitTransform` close them in reverse. All of them count against the 256-level scope budget and stop opening before it with a `LimitExceeded` diagnostic.
- **Filters** (Filter Effects 1). The `filter` functions resolve to at most 32 `FilterOp`s (blur and drop-shadow radii capped at 4096 px). `url(#id)` filter references are not rendered (diagnostic).
- **Clip paths** (Masking 1 §5). `inset()` with rounding, `circle()`, `ellipse()`, `polygon()` (at most 4096 points), `path()` (SVG path data through `PathDataParser`, arcs become cubics, at most 8192 segments) and the geometry boxes (`border-box`, `padding-box`, `content-box`, `margin-box`; the SVG boxes act as the border box). `clip-path: url(#id)` names a `<clipPath>` of an inline svg in the same document: the front end flattens its `rect`, `circle`, `ellipse`, `line`, `polyline`, `polygon` and `path` children to one path (`SvgClipDefs`, at most 256 per document), in user space or, for `clipPathUnits="objectBoundingBox"`, scaled to the border box. A `transform` on a clip child, `use` inside a `clipPath` and a `clip-path` on the `clipPath` itself are not applied.
- **Masks.** `mask-image` layers (gradients and images) with the mask longhands and the `mask` shorthand, at most 16 layers, are painted through the background layer code into `PushMaskLayer` scopes. `mask-image: url(#id)` to an inline `<mask>` is not rendered (that layer masks everything, with a diagnostic).
- **Compositing.** `mix-blend-mode`, `isolation` and `background-blend-mode` (a group around the background layers of the box).
- **Inline SVG.** An `<svg>` element is replaced content. Its natural size is its `width` and `height` or the ratio of its `viewBox`; `HtmlFrontEnd` serializes the subtree (iteratively, at most 20,000 nodes and 1,048,576 characters) and the painter emits one `SvgImage` item; `.Image` renders it through OneImage's SVG renderer at the item's size. Text inside an inline svg is not drawn yet (the SVG renderer is not given the engine's fonts), and the page's CSS rules do not reach inside the svg.
- **Allocation.** Repainting a page with all of these allocates 0 bytes once warm, and patching a transform or an opacity needs neither layout nor allocation (`EffectsPixelTests`).
- **Checked by.** `EffectsPixelTests` (analytic pixels per family, a golden scene, allocation), `DisplayListEffectsTests` (limits, nesting, patching, damage), the `effects` fuzz target, WPT css-transforms, css-masking, filter-effects and compositing.

## Interaction (`Interaction/`)

`InteractivePage` wraps an `HtmlPage` with everything a host needs to feed input: hit testing, DOM events, focus, hover/active state, scrolling input and CSSOM View queries. Inputs are plain structs (`PointerInput`, `WheelInput`, `KeyInput`) carrying the host's timestamp; the engine has no clock, so double-click timing uses those timestamps. A host that draws the display list with its own GPU renderer finds the whole surface in `docs/game-hosts.md`: host scopes (display list 1.3, the `--host-scope` property), per-handle image invalidation, stable glyph-face and SVG keys, name lookup (`FindElement`, `StyledTree.FindByName`) and a synthetic `Click`.

- **Hit testing (`HitTester`).** The fragment tree is indexed once per page into stacking contexts and the CSS 2.1 Appendix E phases, as the painter walks them, then searched in reverse paint order with an explicit stack (no recursion on tree depth). A candidate is rejected when it is outside an ancestor clip (overflow, rounded corners), `visibility` is not visible, or `pointer-events: none`. Scroll offsets and transforms of the ancestors are applied to the point (at most `MaxEffectChain` = 1024 such ancestors per fragment; beyond that the outermost are ignored). A transform makes a stacking context. The root element receives hits on the empty canvas; points outside the viewport hit nothing. A hit on text carries the character offset under the point. Known limits: sticky offsets, `clip-path`, masks and filters are not applied. A point within about 0.02 px of a transformed box's edge may differ from Chrome.
- **Events (`EventDispatcher`).** The DOM Living Standard dispatch algorithm: the path from the target up through the document to the window; capture, target and bubble phases; `stopPropagation`, `stopImmediatePropagation`, `preventDefault` with `cancelable` and `passive`; `once`; listener lists snapshotted per node so additions and removals during dispatch behave as specified; `composedPath()`. `EventTarget` is a handle over a OneHtml node or the window; C# listeners and a later script runtime use the same registry. The path is capped at `MaxPathLength` = 2048 targets (document and window always kept). Event objects are pooled and valid only during the listener call.
- **Pointer, focus and keyboard order (UI Events).** Each pointer event is followed at once by its mouse compatibility event. Moving between elements fires `pointerout`, `mouseout`, `pointerleave`, `mouseleave`, `pointerover`, `mouseover`, `pointerenter`, `mouseenter` (leave/enter on each ancestor that changes), then `pointermove`/`mousemove`. A canceled `pointerdown` suppresses the mouse events until release but not focus. `click` goes to the nearest common ancestor of the down and up targets; the click count grows while presses are within 500 ms and 5 px. Focus changes fire `blur`, `focusout`, `focus`, `focusin`.
- **Focus (`FocusRules`).** Focusable areas and the sequential order of HTML §6.6: positive `tabindex` first in ascending order (ties in tree order), then `tabindex=0` and natively focusable elements in tree order; negative `tabindex` is focusable but not a tab stop. Tab wraps by default. `:focus-visible` follows the Selectors 4 heuristic: keyboard-caused focus matches, pointer focus on a non-text control does not. `:hover`, `:active`, `:focus` and `:focus-within` are applied by restyling with a new `StyleContext`.
- **Scrolling input.** Wheel (line = 40 px, page = 0.875 of the viewport, Shift turns vertical into horizontal) and keys (arrows, Page Up/Down, Home/End, space) go to the nearest scrollable ancestor and chain to its parent at the limit, ending at the viewport. They use the scroll patch of the Overflow section, so there is no relayout, and fire `scroll` events. `ScrollIntoView` scrolls the nearest way.
- **CSSOM View (`LayoutQueries`).** `GetBoundingClientRect` and `GetClientRects` (after scroll and transforms) and the layout-space `Offset*`, `Client*`, `Scroll*` metrics. `offsetLeft`/`offsetTop` are relative to the offset parent's padding edge, or to the page origin when the offset parent is the body or null. `scrollWidth`/`scrollHeight` are the padding-edge-based overflow extent, never below `clientWidth`/`clientHeight`. Values were compared with Chrome 156 on three probe pages (`testdata/interaction`, produced by `tools/interaction/probe.sh`).
- **State-change seam.** After an input that changes `:hover`, `:active` or `:focus` state, the page computes the list of `ElementStateChange` (the element and the `ElementStates` bits that flipped, for every element in the affected chains) and passes it to `StateChangeHandler` (`IStateChangeHandler.ElementStatesChanged(page, changes, context)`) with the new `StyleContext`. If the handler returns true it has restyled and relaid out itself; otherwise the page restyles from the new context and rebuilds the box, fragment and hit structures when any computed value changed. `HandlePointer`, `HandleWheel` and `HandleKey` return `InputOutcome` flags. An incremental restyle engine plugs in at this interface.
- **Allocation.** A hit test, a pointer move that changes no hovered element, event dispatch without state change, wheel scrolling that keeps the hovered element, and viewport scrolling allocate 0 bytes once warm (`InteractionAllocationTests`). A change of hover or focus state on an HTML page restyles in place (see Incremental updates); OneHtml's cascade still allocates about 24 KB for a hover move on the 1,000-node sample, while the same change through the styled-node tree allocates nothing.

## Forms (`Forms/`, `HtmlFrontEnd.Forms.cs`, `Painter.Controls.cs`, `InteractivePage.Forms.cs`)

Controls are ordinary boxes with Chrome's default metrics; `docs/forms.md` holds every rule and its probe evidence.

- **Style.** When the styled tree is built (and again on restyle), `AdjustControlStyle` classifies the element (`ControlKind`), writes the widget size into `auto` width and height, makes the text single-line where needed and records paint-only values (`Control`, `ControlState`, `ControlFraction`; they differ in paint, never in layout). `WidthIsIntrinsic`/`HeightIsIntrinsic` let stretching and both-inset positioning treat the widget size as `auto`. A `display: grid | flex` fieldset is split into a block and an anonymous content box that carries the grid or flex properties.
- **Layout.** Block layout centers the lines of buttons, text fields and drop-downs in a taller box (`CentersItsContent`, skipped on a memo hit because the copied children are already moved); intrinsic sizes of a control are its widget width.
- **Paint.** `Painter.Controls.cs` draws the widgets from the control state and the fragment box (accent color, focus ring, disabled colors); with `appearance: none` the normal box painting applies.
- **State.** `FormState` holds what the user changed (checked, selected options, range values, open select); `HtmlPage.Form` owns it, `RefreshContent` rebuilds the value and option text. On the builder path `StyledTree.SetControl` stamps the kind and state per node, and the incremental pipeline treats it as a style change (0 B once warm).
- **Interaction.** Activation, labels, radio groups, submit and reset with constraint validation, range drag and keys, and the select popup go through the normal DOM event dispatch (`input`, `change`, `submit`, `invalid`, `reset`); the popup is a synthetic box painted in a top layer above the root stacking context. Text editing (`Forms/TextEditor`, `TextEditSession`, `TextGeometry`, `InteractivePage.Editing.cs`, `LiveTextEditor`, `Painter.Editing.cs`) is specified in `docs/forms.md`; the painter reads a `TextEditView` for caret, selection and preedit.

## Incremental updates (`LivePage`, `PipelineUpdater`, `StyledTree.Changes.cs`, `BoxTree.Incremental.cs`, `BlockLayoutEngine.Reuse.cs`)

One change-set model serves both front ends. The result of any sequence of edits followed by a flush equals building the page from scratch from the final tree; the equality fuzz (`IncrementalEqualityTests`, the `incremental` fuzz target) checks styles, boxes, fragments and the display list with item ids after every edit.

- **Change set.** `StyledTree` logs each edited node once, with flags (`Style`, `Text`, `TextStructure`, `Children`, `Replaced`, `Added`, `Removed`), coalesced per node. The log holds `MaxPendingChanges` (4,096) nodes; beyond that `PendingOverflowed` is set and the next flush rebuilds everything, which is also the fallback for a huge edit. Edits: `SetDeclaration`, `SetProperty`, `SetStateStyle` (hover, active and focus layers, applied focus < hover < active), `SetStates`, `SetText`, `InsertElement`, `InsertText`, `Remove`, `Move`, `Invalidate`. A removed node keeps its index and handle until the flush has consumed the change set; then its slot is reused (see Long-running stability). HTML pages feed the same machinery from `IStateChangeHandler` state changes (`HtmlPage.TryRestyle`).
- **Restyle.** Dirty nodes are recomputed; a child is re-evaluated only while its parent's style id changed, so inheritance stops where computed values do not change. Styles are interned in `StyleTable`; an unchanged result keeps the same id. A viewport change resets the table and recomputes everything.
- **Boxes.** A style change that leaves `display`, out-of-flow-ness, `white-space-collapse` and `list-style-position` unchanged is patched into the existing boxes (anonymous boxes below follow their parent's style) and the box and its ancestors are marked dirty. A **structural** edit (a node inserted, removed or moved, text that becomes empty or white space or stops being, a style change to `display`, out-of-flow-ness, `white-space-collapse` or `list-style-position`) patches the box tree in place (`BoxTree.Structure.cs`). The nearest ancestor of the edit whose box is a block container has its children generated again from the styled children, by the same code a full build runs: inline boxes and generated-content boxes on the way are made again with everything below them, the container's anonymous boxes (CSS2 §9.2.1.1 block wrappers, §17.2.1 table fix-up, flex and grid item wrappers) are dissolved and made again, blockification and white-space trimming are decided over the whole child list, and an inline box split around a block is made again with its continuations. A direct child whose node did not change keeps its box and everything below it, so layout reuse (box dirty marks) and the paint journal see it as the same box. Boxes that go away return their slots to a free list that the next new box takes, so a UI that adds and removes nodes every frame keeps its columns the same size; new boxes and the container's chain are marked dirty and hazards are recomputed for the new boxes and the ancestors. Box numbers are therefore not in tree order after an edit; nothing reads them as an order (the percentage-height flag of flex memoization is computed over the tree, not over numbers), and display-list ids, which name a box by its number, stay the same for every box an edit did not remake. The equality tests compare ids by each box's place in the tree. Only these still generate every box again: a change to the root element's own `display`, out-of-flow-ness, `white-space-collapse` or `list-style-position`; more than `MaxPendingChanges` edited nodes in one flush; a viewport change or a font change (every style row is recomputed); a cancelled flush; the box limit reached while generating. The epoch (which drops every layout record) only moves on a full build. The painter paints the whole list after a structural edit (the fragment tree no longer has the old shape); a display-list patch is for edits that keep the shape. When the old and new styles differ only in values that paint reads (colors, backgrounds, shadows, outlines, text decoration, opacity) no layout runs. Transforms, filters, masks and `clip-path` take the layout path because they can make containing blocks or ink overflow.
- **Layout reuse.** Layout writes into a scratch fragment tree and swaps it into the published one, so handles stay valid. When a block, flow-root, flex or (non-subgrid) grid container that is not dirty and whose subtree has no floats, out-of-flow boxes, columns or tables is opened with the same frame as last time (width, content origin, bottom edge, content width, specified, minimum, maximum and ratio heights, masked flags, grid, table and nesting depth), its children's fragments are copied from the earlier tree and the values the children leave in the frame are restored (a flex container also restores the definite heights it sets while closing). There is one record per box, stamped with the layout that wrote it: a record written in the current layout is never read back, dirty boxes' records are dropped when a layout starts, and a visit that cannot be described by a record drops the box's record. The copy moves a contiguous (preorder) run of fragments column by column, shifting the child and sibling links by a constant with vector adds, and falls back to one fragment at a time when the run is not contiguous. Flex item measurements (the trial layouts) are kept between layouts for boxes whose subtree did not change; a box that was dirty in a layout loses its earlier measurements for good, so they cannot come back when it is clean again. Intrinsic-size caches are kept and cleared along dirty chains. A new box-tree epoch, font catalog version or output object invalidates everything. A trial layout that is undone (flex measurement, grid sizing) drops the records and column plans made inside it. The layout also leaves a `LayoutDelta`: whether the new fragment tree has the same shape as the published one (same count, kinds, boxes and parents) and which fragments it wrote anew with a different position, size, text run or overflow.
- **Paint.** The retained display list is repainted with stable ids; damage is computed from item content and context hashes (display list format 1.2). Transform and opacity patches are unchanged. After a full paint the painter keeps a journal: for each fragment the span of items it wrote, its paint-order position (phase list and z-index) and flags for what makes it unsafe to rewrite alone (scopes opened around it, clips of containing blocks, text decoration, outlines, tables, multi-column, sticky, effects). When the layout delta is aligned, `Painter.TryPatch` re-classifies only the fragments in the delta (and the subtrees below the ones that moved) and the boxes whose style or text was patched (`BoxTree` records them; text boxes below a restyled box are included), then paints just those over their old slots (`DisplayList.BeginRewrite`: items are built at the tail, moved into the old slot, payload of equal size is overwritten in place, damage collects the old and new bounds). It gives up and the whole list is painted when a fragment's paint-order position, owner or flags changed, an unsafe fragment is touched, the item sequence would differ, the payload grew past twice its size at the last full paint, the viewport scroll range changed whether the page scrolls, or the changed set exceeds half the fragments. The canvas item is rewritten when the root's extent changed. `InteractivePage.Repaint` does the same for pages built from HTML, and patches transform and opacity animations of elements that did not change otherwise (the animation controller reports them apart from paint and layout changes, so they are neither marked dirty nor relaid out).
- **Flush and queries.** `LivePage.Flush` runs restyle, box patching, layout and paint and returns what ran. `LayoutQueries` over a `LivePage` flushes pending edits before answering (CSSOM View's forced layout); over an `HtmlPage` the changes are already applied. The hit tester re-indexes when the fragment tree's version moves, not only when its count does.
- **Long-running stability.** A UI that adds and removes nodes every frame for hours must not grow, so nothing here is append-only.
  - *Slots.* `Remove` (and the marker removal a style change can cause) queues the root of the removed subtree. When the flush has consumed the change set (`StyledTree.ClearPending`), every node of every queued subtree gives its slot back: its columns are reset (declarations, replaced source, states, control info, text span), its generation moves on and its index goes on a free list; the next `NewNode` pops it. A tree no page tracks reclaims when an insert finds no free slot. `Count` is the number of slots ever needed at once (an index bound, not a node count); `LiveCount`, `FreeSlots` and `Capacity` say the rest. `MaxNodes` bounds the nodes in the tree, not the slots ever used. Every column indexed by node (`_chg`, `_oldStyle`, the declaration layers, ...) is sized by the slot count, so the arrays stop growing once the peak is reached. The box tree's node-to-box table is kept up to date by the structural patch (a freed box clears its node's entry; the tables grow with the slot count), so nothing keyed by node index outlives a flush. Per-node state that lives outside the tree (event listeners, hover, focus and animation state) is keyed by OneHtml node on pages built from HTML, whose document is immutable, so it never sees a slot reused; builder pages have none. A `ScrollState` is keyed by the scroll container's element (styled node index plus the slot's generation), so an offset stays with its element through inserts, removals and moves around it, and is forgotten when the node is removed (its slot's generation moves on, so a node that takes the slot starts at 0).
  - *Handles.* `StyledNode` carries the slot's generation next to the index. `IsValid` is false once the node's slot was reclaimed or the tree cleared. Editing methods reject an invalid handle with `ArgumentException`; reading members throw `InvalidOperationException`; `BoxTree.PrincipalBox`, `LayoutQueries` and `ComputedStyleSerializer` answer "no box" or null for one. Equality includes the generation, so a stale handle never equals the node that reuses its slot. Until the flush after a `Remove` the removed handles stay valid, because the change set still names them.
  - *Text.* The text arena compacts when it fills (including while texts are added), counting only live nodes, so it follows the live text and not the text ever stored; the text limit counts live text.
  - *Style caches.* Styles computed from a builder `StyleDeclaration` (and from the layer keys of hover, active and focus declarations) are remembered on the declaration through a `ConditionalWeakTable`, at most 32 entries per declaration, not in the table's global map. A host that makes a declaration per inserted node does not keep them alive; one that keeps and shares declarations keeps their entries, so reusing them computes nothing. The OneHtml front end keeps its map (its keys live as long as the document).
  - *Style rows.* `StyleTable` interns distinct computed styles by value (at most 65,536 rows at a time), so no row has one owner that could free it; the table sweeps instead (`StyleTable.Reclaim`, mark and sweep). Every object that keeps `StyleId`s registers with the table as an `IStyleHolder` (weak reference): `StyledTree` (the style of every live node, and the old style of the nodes with a pending change), `BoxTree` (every box, anonymous boxes included) and the `AnimationController` (the private rows of animated elements and of the descendants that follow them). A sweep clears a mark per row, asks each holder to mark what it still reads, then frees every unmarked row to a free list (lowest index first, and the high-water mark shrinks when the top rows are free). Per-row bookkeeping follows: the by-value index is rebuilt, entries of the strong caches (`_intern`, anonymous-box rows) that name a freed row as value or parent are removed, and a per-row generation moves on so entries of the weak per-declaration caches that name a freed row stop matching (checked at lookup, so the weak tables are never walked). The inline layout's font-metrics cache is keyed by row and drops its entries through `StyleTable.ResetGeneration`, which a sweep that freed rows also bumps.
    - *When.* `PipelineUpdater.UpdateBoxesAndLayout` calls `ReclaimIfNeeded` after it cleared the pending changes: the one point of a flush where no style id is held in a local, for `LivePage` and the HTML page alike. A sweep is due when the rows in use exceed the number the last sweep left by at least 1,024 (more for a big document: the larger of 1,024 and the rows the holders keep, up to 16,384), so a sweep costs a few microseconds per thousand frames of the HUD page and allocates nothing. `Reclaim()` is also public for a host that wants to sweep at a time of its own, with the same rule: no `StyleId` (from `StyledNode.Style`, `Box.Style`, `GetAnonymous`) may be kept across a flush.
    - *One interval of grace.* A row that no holder reads survives a sweep when it was looked up or made since the previous sweep (a per-row epoch stamp). A style that a host asks for again and again through a cached declaration (the variants of a damage-number row) is never computed twice, because its row is touched in every interval; a style that was used once and dropped (a position that took a new value) goes at the second sweep. The number of rows therefore settles at what the holders keep plus about two intervals of new values.
    - *Releasing private rows* (`ReleasePrivate`) keeps the values and moves the row's generation on; the row can be handed out again at once. The transition code still reads a released row as a node's "before" style until its restyle is done, so rows are never cleared.
    - *Proven by* sweeping after every flush in the edit-sequence tests whose edits mostly make values no row holds (`EditSession` fresh-values profile; incremental equals a fresh build, and the `incremental` fuzz target has the same profile), and by the soak below.
  - *Measured.* The HUD page with a moving element (its `left` is the frame number in px and a label's `width` cycles) on top of the churn, a new style row per frame: the table size, live rows and heap stay flat over 1,000,000 frames and the engine's share of a frame (the flush, leaving out the host's own work of parsing the new values) allocates 0 bytes in steady state (`ChurnSoakTests`; `ONERENDER_SOAK_FRAMES_MOVING` sets the length, default 30,000). At 1,000,000 frames in a release build: style rows 3,304 (2,347 live) after the first 100,000 frames and unchanged to the end, the heap 27,172 KiB after warm-up and 27,139 KiB at the end, 980 sweeps, and 125,048 bytes allocated by all the flushes together (a one-time cost, the same at 28,000 frames). Known limit: computing a style that declares `display`, `color` or `background` allocates (OneHtml's typed value parsers), so an element with such declarations whose parent's style changes every frame allocates ~100 bytes per such property on that frame; the soak's moving elements use geometry only.
  - *Measured, churn.* The game-UI page (about 1,070 nodes) with a floating layer of about 50 nodes added and the previous one removed every frame, plus a chat line in and the oldest out, then a flush: slot count, capacity, style rows and the heap after a forced collection are identical after warm-up and after the run (`ChurnSoakTests`, with a declaration made per node as well as with shared ones), and a steady-state frame allocates 0 bytes with shared declarations.
- **Bounds and cancellation.** The pending log, node count and depth are bounded; a cancelled flush leaves the edits pending and the next flush starts again from the styled tree.
- **Allocation and time.** On the 1,000-node game-UI sample (`testdata/game-ui/hud.html`, the same scene through the builder), hover restyle, a text change and a one-subtree relayout allocate 0 bytes once warm and take about 3 µs (hover), 50 µs (text) and 0.35 to 0.45 ms (one subtree, which moves most of the page and paints it in full) in a release build on a loaded 32-core server part (`IncrementalBenchmarks` for the gated run; `GameUiIncrementalTests.WorkPerChange` checks without a clock that none of the changes generates the boxes again). A nothing-in-common reuse is guarded by key tests (`ReuseKeyTests`); nothing inside a multi-column container is reused, because the column cut rewrites its fragments after the layout.

## Animations (`Animation/`)

CSS Transitions 1 and CSS Animations 1, timed by the host. The engine reads no clock: the host passes the time of the frame to `InteractivePage.Advance(nowMs)` (any monotonic source, the same one that stamps its inputs; the start is `InteractionOptions.AnimationStartTime`). `HtmlPage.Create` without `HtmlPageOptions.AnimationTime` is a static page: the animated properties show their cascaded values, so documents and reftests are unchanged. `docs/animations.md` lists the matching choices and deviations.

- **Where it sits.** `AnimationController.Recalc` runs on the styled tree after the front end has built it and before the box tree is made. It compares each element with the same element of the previous tree (the before-change style), starts, retargets, reverses and cancels transitions (Transitions 1 §3), matches `animation-name` against the `@keyframes` collected from the document's sheets (`KeyframesRegistry`; the cascade does not carry them), and gives every animated element a private style row that replaces its style in the tree. `Tick(now)` rewrites those rows in place and reports the most expensive kind of change it made (`MotionLevel`): none, compositor (only `transform`, the individual transform properties or `opacity`), paint (colors, shadows, filters) or layout.
- **Values.** `PropertyAnimator` has one animation type per property: numbers and integers, lengths and percentages (a px and % mix becomes a `calc()`), colors (premultiplied sRGB), border radii, positions, shadow lists, filter lists, `clip-path` shapes of the same kind, transform lists (matching functions per argument, otherwise through 4×4 decomposition in `TransformMath` with quaternion interpolation), `visibility`, `line-height`, `vertical-align` and `display`; everything else is discrete and flips at the midpoint (Web Animations 1 §5.3), or runs only with `transition-behavior: allow-discrete`. `EasingFunction` is CSS Easing 1: `cubic-bezier()` with tangent extrapolation, `steps()` with the jump terms, `linear()` with stop filling. `EffectTiming.Sample` is the Web Animations timing model (phase, iteration, direction, fill).
- **Through the incremental pipeline.** On an interactive page a state change restyles in place (see Incremental updates); `AnimationController.RecalcPending` then runs for the restyled elements only, with each one's before-change style taken from the change log, so hover and focus transitions start without a full recalculation. When any restyled element has or had effects, its style row differs in content from the logged old style (rows are rewritten under their ids), so the boxes are generated again before layout; the cascade and the walk over unaffected elements are still skipped. A tick that changes a paint-only or layout property marks only the animated elements and the descendants that follow an inherited value (the controller's `ChangedNodes`): `BoxTree.ApplyAnimatedRows` refreshes the anonymous boxes below them and, for layout properties, marks their boxes dirty, and `PipelineUpdater.LayoutAll` lays out only the dirty subtrees and ancestors with the page's reusing layout engine. Rows of finished effects and of replaced followers go back to the style table for reuse. On the HUD page of about 1,800 nodes, ticking width and color on a handful of elements and repainting allocates 0 B once warm (`MotionGameUiTests`), and ticks equal a fresh render at the same time (`HtmlPage.Create` with `HtmlPageOptions.AnimationTime`, which starts the animations at the clock's origin and samples them at that time).
- **Compositor path.** An element whose transform or opacity is animated always gets a transform scope or opacity group in the display list, recorded as a patch entry. `InteractivePage.Advance` returns `InputOutcome.Animated` alone for such a tick; `PatchAnimations(painter, list)` then replaces the matrices and group opacities in place and adds the damage. No relayout, no allocation (`MotionAllocationTests`: 100 animations on a page of about 1,500 nodes, 0 B per frame, including frames that deliver `animationiteration` events). If a transform moves between planar and perspective the list cannot be patched and `PatchAnimations` returns -1: paint again.
- **Inheritance.** The text and the descendants that inherit an animated inherited property (`color`, `visibility`, `font-size`, ...) get private rows too and receive its animated value at each tick. Descendants that set the property themselves, or run effects of their own, keep their values. Lengths in `em` of element descendants are not recomputed when `font-size` animates.
- **Events.** `transitionrun`, `transitionstart`, `transitionend`, `transitioncancel` and `animationstart`, `animationiteration`, `animationend`, `animationcancel` are queued by the tick, sorted by the time they were scheduled for, then composite order, kind and element (a total order), and dispatched through `InteractivePage.Events` (`TransitionEvent`, `AnimationEvent`; they bubble and cannot be cancelled). Events of the first frame wait for the first `Advance`, so listeners added after `Create` see them.
- **Reduced motion.** `HtmlPageOptions.PrefersReducedMotion` sets the `prefers-reduced-motion` media feature; the page's own rules decide what to do.
- **Bounds.** 64 items per comma list, 1,024 keyframe blocks per rule, 100,000 animated elements, 256 transitions and 64 animations per element, 65,536 queued events, 4,096 followers per element; times are clamped to ±1e12 ms and iteration counts to 1e9; easing outputs to ±1e9; `steps()` to 2^20 steps and `linear()` to its stop limit. Nothing recurses over tree depth.
- **Out of scope.** Scroll-driven animations (`animation-timeline`, `animation-range`), `animation-composition`, `@starting-style`, view transitions, registered-property animation, the Web Animations script API, `var()` inside `@keyframes`, pseudo-element animations.
- **Checked by.** `MotionEasingTimingTests`, `MotionInterpolationTests`, `MotionPageTests` (start, reverse and cancel rules, event order, pause, inheritance), `MotionAllocationTests`, the `motion` fuzz target, and the replay of the css-transitions and css-animations testharness helpers (`WptMotionReplayTests`, `testdata/wpt-motion`): the parsing and computed-value files through the style replay and the interpolation helpers through the engine, with expectations, exclusions and per-module floors beside the files.
