vintage-frames 0.5.6 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/SPEC.md CHANGED
@@ -46,7 +46,7 @@ Modern requirements that we deliberately keep (accessibility over purity):
46
46
  - **The name/description bridge** (`VfShadowRoleControl`, src/form-control.ts). On the controls whose role lives on a shadow-internal node (the three fields, `vf-select`, `vf-swatch`, `vf-button`), a host-level `aria-labelledby`, `aria-label` or associated `<label for>` resolves — in html-aam precedence — to the inner focusable element's `aria-label` whenever the `label` property is empty (`hostLabel`). Those six extend `VfShadowRoleControl`; the host-role controls (`vf-checkbox`, `vf-radio-group`, `vf-slider`) extend the plain `VfFormControl` and so never *carry* the bridge's API at all — an inherited `description` that renders nothing is the advertised-but-inert shape this split removes, and `verify:manifest` enforces it (a tag whose manifest lists `description` must call `renderDescription()`). `vf-button` takes the ARIA half alone (`hostAriaLabel`): a `<button>` is not a labelable element, so no caption names a native one and none names this one either — its slotted content is the name a bridge doesn't override. A `description` property (or, when it's empty, a host-level `aria-describedby`) renders as a hidden span in the control's own shadow root with the inner control's `aria-describedby` pointing at it — the shadow-internal IDREF idiom `vf-dialog`'s title patch uses — and a failing constraint's `validationMessage` joins the same node. Referenced text is flattened at render time, so an edit to a referenced element's *text* lands on the next render rather than instantly — the one divergence from native. Controls whose role sits on the host (the toggles, slider, radio group, bars) need none of this: the platform reads their host attributes directly.
47
47
  - **Constraint validation** (`VfFormControl`): a reflected `required` fails validation with `valueMissing` while the control is empty by its own definition (fields/select `value === ''`, checkbox unchecked, radio group unselected — each with its native counterpart's message), through one `syncValidity()` funnel run before each render. The native surface is complete — `checkValidity`/`reportValidity`/`validity`/`validationMessage`/ `willValidate`/`setCustomValidity` — `:invalid` matches on the host, `form.reportValidity()` blocks, and `disabled`/`readonly` bar validation per HTML's own rules. One divergence the platform imposes: `willValidate` is `true` on a `vf-button type="button"`, where a native button is barred from constraint validation. A form-associated custom element cannot opt out of the candidate set, so this is a platform limit. AT wiring is `aria-required`/`aria-invalid` on the inner control (plus internals mirrors for host-role controls) — never a forwarded native `required`, which would put UA `:user-invalid` styling on the artwork. Enter's implicit submission routes through the browser's validation, so it cannot submit past a failing constraint.
48
48
  - Components must render nothing surprising outside their box: no margins on `:host` by default.
49
- - **Explicit placement** (`VfPositioned`, src/position.ts): every component takes `top`/`left` in whole system px — set either and the host is absolutely positioned within its parent (`position: absolute` plus both offsets as live `calc(var(--vf-scale, 1) * Npx)` inline styles; the unset coordinate is 0; `right`/`bottom` released to `auto` and `margin` zeroed). Set neither and the element renders in flow; removing both unwinds every inline declaration. The DITL model: a dialog can be laid out by positioning its items just as validly as by stacking them. **No component is excluded** — the rows a container normally owns (`vf-option`, `vf-menu-item`, `vf-list-item`) and a bar's `vf-menu` take the pair on the same terms, because a consumer may stand one of them up on its own and the kit does not get to rule that out. Placing such a row *inside* its managing parent takes it out of that parent's flow, so the parent stops measuring it (a `vf-select`/`vf-menu` panel is as wide as its widest **flowed** row, and `popup-overflow`'s clamp counts the same set) and the rows after it close the gap — the documented consequence of the placement, not a failure of it; each component's own doc comment states what its container stops doing. Containers are deliberate anchors — the desktop raster, a window's *content region* (the frame's inner edge below the title bar; the 12px body inset governs flow content only), a dialog's content area, a stack's box, a fieldset's border interior, a scroll area's scrolled plane. The style writing rides a ReactiveController (`hostUpdated`), not an `updated()` override — component subclasses routinely skip `super.updated()` — and re-applies only when the property values changed, so an unrelated update never re-asserts a coordinate. `npm run verify:position`.
49
+ - **Explicit placement** (`VfPositioned`, src/position.ts): every component takes `top`/`left` in whole system px — set either and the host is absolutely positioned within its parent (`position: absolute` plus both offsets as live `calc(var(--vf-scale, 1) * Npx)` inline styles; the unset coordinate is 0; `right`/`bottom` released to `auto` and `margin` zeroed). Set neither and the element renders in flow; removing both unwinds every inline declaration. The DITL model: a dialog can be laid out by positioning its items just as validly as by stacking them. **No component is excluded** — the rows a container normally owns (`vf-option`, `vf-menu-item`, `vf-list-item`) and a bar's `vf-menu` take the pair on the same terms, because a consumer may stand one of them up on its own and the kit does not get to rule that out. Placing such a row *inside* its managing parent takes it out of that parent's flow, so the parent stops measuring it (a `vf-select`/`vf-menu` panel is as wide as its widest **flowed** row, and `popup-overflow`'s clamp counts the same set) and the rows after it close the gap — the documented consequence of the placement, not a failure of it; each component's own doc comment states what its container stops doing. Containers are deliberate anchors — the desktop raster, a window's *content region* (the frame's inner edge below the title bar, with no inset of its own — flow content starts there too), a dialog's content area, a stack's box, a fieldset's border interior, a scroll area's scrolled plane. The style writing rides a ReactiveController (`hostUpdated`), not an `updated()` override — component subclasses routinely skip `super.updated()` — and re-applies only when the property values changed, so an unrelated update never re-asserts a coordinate. **`fixed`** (bare attribute, reflected) holds the placement against the *visible* region of the nearest scrolling ancestor instead of its scrolled plane: `position: sticky` in place of `absolute`, the same live offsets, the flag alone at (0,0). Sticky boxes stay in flow, so the controller erases the footprint — the host is blockified (the outer display change `absolute` makes on its own: `inline-block` → `block`, `inline-flex` → `flex`, …), `max-width: fit-content`, `z-index: 1` (over the plane's placed children), and a negative right/bottom margin equal to its own border box, kept exact by a ResizeObserver — a 0×0 margin box that flow content lays out as if it weren't there. Two engine facts stated as rules: sticky only ever pushes a box *down* from where the flow put it, so a fixed child precedes the flow content in its parent; and a scroll container that never scrolls (a plain window body is `overflow: hidden`) holds it exactly where placement would. Removing the flag returns the host to ordinary placement with the recipe unwound. `npm run verify:position`.
50
50
  - `vf-dialog` takes the pair in **viewport** coordinates: `showModal()` puts the box in the top layer, whose containing block is the viewport rather than the nearest positioned ancestor. Unset means centered — recomputed on open and on every box/viewport/scale change, so a modal whose content upgrades after opening is never stranded; `position` is left to the UA and only the four inset/margin declarations are written.
51
51
  - **Gestures write through the same properties** (`PlacementController`): a `vf-window`/`vf-dialog` title-bar drag and a `vf-icon` drag or arrow nudge state `top`/`left`, and `vf-window`'s grow box states `width`/`height` (`VfSized`) — all in whole system px, so a moved element is placed the way an authored one is and holds its spot through a zoom. Writing resolved CSS px instead was the bug: `--vf-scale` moved under the constant and every zoom step re-read it as a different number of system px (by the ratio the scale itself moved — visible even at zoom levels where the target does not change, and nothing else on the page does). Values are snapped to `snapSys` at gesture time and never re-snapped afterwards: re-rounding onto each new lattice compounds (62 → 63 → 64), and whole system px is whole device px at every rung regardless. `npm run verify:zoom` group (e).
52
52
  - **The movable contract.** A host that moves under a gesture states its own rectangle, and its positioning parent is a box with a size. Both halves are the consumer's — a component can supply neither for itself — and both used to fail quietly, so each is a one-time `console.warn` (`warnMovableContract`, latched per element like `vf-window`'s size warning). `npm run verify:position` group CONTRACT.
@@ -156,7 +156,7 @@ The span construction stays load-bearing for the CSS-repeated underlays beneath
156
156
  - The title's clearance for anything else in the bar is `--vf-title-inset` (default 16px); `vf-window` sets 60px so an ellipsized title can't run under its close/zoom widgets.
157
157
  - `touch-action` is deliberately NOT in the recipe: `vf-dialog`'s bar is always a drag handle, `vf-window`'s only when `[movable]`, and suppressing touch scrolling on a bar that can't be dragged would be a behavior change.
158
158
  - The markup and the four DragController pointer bindings that go with it are `chromeTitleBar()` in `src/chrome.ts` (internal — it bakes in the kit's own `part` names, so it is not part of the public toolkit). Its third argument picks the texture layer class: `'vf-stripes'` (default) or `'vf-dots'` (`vf-window variant="utility"`).
159
- - `vfScrollRail` — the System 7 scroll rail, drawn by the kit as ordinary shadow DOM (`src/scroll-rail.ts` renders the subtree and drives it; this is its skin). The native scrollbar is hidden — `scrollbar-width: none` plus a `::-webkit-scrollbar { display: none }` for older WebKit, the last scrollbar pseudo in the kit — never the native scrolling: wheel, trackpad momentum, keyboard, touch and AT scrolling stay the platform's, and `ScrollRailController` syncs the rail to `scrollTop`/`scrollLeft` while driving the classic interactions (thumb drag; trough paging by a viewport minus one line, repeating until the thumb reaches the pointer; arrow lines with auto-repeat and the hollow→solid pressed glyph). Geometry: the classic 16px cell whose outermost line is the component's own 1px frame border — the rail element is the 15 inside it, a 1px divider on the content side plus the 14px channel; 15px arrow cells whose glyphs are the 16-unit sprites windowed to their 14×14 interior; the **fixed** 16px thumb (System 7's box, never proportional; whole-system-px travel); the 25% dot-dither trough rendered as a whole-surface `tileRaster` (1-bit at every scale, zoom included) on `var(--vf-scrollbar-track)`. States key off the attributes `ScrollStateController` writes — an idle axis keeps its arrows (drawn but inert) and drops the dither and thumb, an inactive window blanks both axes to the bare white channel, arrows included, a degenerate track drops the thumb, then everything (the Control Manager's decision table). The rail subtree is `aria-hidden` and pointer-only; scrolling's keyboard/AT contract stays on the viewport. Every engine renders the same rail — no Firefox fallback — and it is pixel-asserted headless (`npm run verify:scrollbars`), which the `::-webkit-scrollbar` skin it replaced never could be.
159
+ - `vfScrollRail` — the System 7 scroll rail, drawn by the kit as ordinary shadow DOM (`src/scroll-rail.ts` renders the subtree and drives it; this is its skin). The native scrollbar is hidden — `scrollbar-width: none` plus a `::-webkit-scrollbar { display: none }` for older WebKit, the last scrollbar pseudo in the kit — never the native scrolling: wheel, trackpad momentum, keyboard, touch and AT scrolling stay the platform's — minus the rubber-band: the scroller sets `overscroll-behavior: none` (a bounce moves content the rail cannot express; System 7 stopped hard), which also cuts scroll chaining, so a document that scrolls restores it where it wants it through the scroller's part (`viewport`, `textarea`, `list`, `content`; the reference page does, dialogs excepted) — and `ScrollRailController` syncs the rail to `scrollTop`/`scrollLeft` while driving the classic interactions (thumb drag; trough paging by a viewport minus one line, repeating until the thumb reaches the pointer; arrow lines with auto-repeat and the hollow→solid pressed glyph). Geometry: the classic 16px cell whose outermost line is the component's own 1px frame border — the rail element is the 15 inside it, a 1px divider on the content side plus the 14px channel; 15px arrow cells whose glyphs are the 16-unit sprites windowed to their 14×14 interior; the **fixed** 16px thumb (System 7's box, never proportional; whole-system-px travel); the 25% dot-dither trough rendered as a whole-surface `tileRaster` (1-bit at every scale, zoom included) on `var(--vf-scrollbar-track)`. States key off the attributes `ScrollStateController` writes — an idle axis keeps its arrows (drawn but inert) and drops the dither and thumb, an inactive window blanks both axes to the bare white channel, arrows included, a degenerate track drops the thumb, then everything (the Control Manager's decision table). The rail subtree is `aria-hidden` and pointer-only; scrolling's keyboard/AT contract stays on the viewport. Every engine renders the same rail — no Firefox fallback — and it is pixel-asserted headless (`npm run verify:scrollbars`), which the `::-webkit-scrollbar` skin it replaced never could be.
160
160
  - `vfFocusRing` / `vfFocus` / `vfFocusUnderline` — the two focus indicators. Neither is a System 7 reproduction: the machine drew no keyboard focus at all (see §1), so both are additions rendered in its vocabulary.
161
161
  - `vfFocusRing` is the dotted-outline declaration pair — `outline: var(--vf-focus-outline, 1px dotted currentColor)` plus a scaled `--vf-focus-offset` (default +2px) — interpolated into whatever selector a component focuses on; `vfFocus` wraps it as a `.vf-focus:focus-visible` class for the controls where focus and ring share one element. `currentColor`, not black, for the same reason the underline uses it: the ring must invert with the ink it sits on, or the multi-select keyboard cursor disappears riding a selected list row's black bar.
162
162
  - `vfFocusUnderline` is the alternative for a control that can carry the mark on its own face: an `::after` on that element, 1 system px tall, spanning its box, dashed 1px on / 1px off via a `repeating-linear-gradient` in `currentColor` — so it inverts to white with the label on a pressed face. The component suppresses the UA outline in the same rule set, and the underlined element needs `position: relative`. Two placements:
@@ -208,21 +208,22 @@ Full-bleed classic desktop container.
208
208
 
209
209
  #### `vf-window` (`VfWindow`, vf-window.ts)
210
210
  The desktop-window shell: the classic document window (see DragThing screenshot), parameterized down to the windoid (see the Group A recipe table).
211
- - **Attributes/props:** `heading: string` (title text), `width: number` / `height: number` (**declare them both** — whole system px, so the window keeps its proportion to the chrome inside it at every density. A window is a fixed box in both axes, the way a WIND resource was: one that grows with its body is one the user can neither predict nor control via the grow box. Each missing dimension falls back to something different — width to block layout, height to the content — and the window names whichever are missing, once, in the console), `active: boolean` (default **true**; reflect), `closable: boolean` (default true), `zoomable: boolean` (default false), `movable: boolean` (default false), `resizable: boolean` (default false), `min-width` / `min-height: number` (default 80 / 54) and `max-width` / `max-height: number` (unbounded) — the grow box's sizeRect, whole system px (see Behavior), `flush: boolean` (default false — removes the body padding; under `scrollbars`, the built-in viewport's 8px inset instead), `variant?: 'utility'` (the slim windoid chrome), `scrollbars?: 'vertical' | 'horizontal' | 'both'` (edge scroll rails).
212
- - **Visual:** `vfChromeFrame` + `vfTitleBar` (§4), plus a full-size flex-column layout on the frame. `display: block`. Sets `--vf-surface: var(--vf-white, #fff)` on itself.
211
+ - **Attributes/props:** `heading: string` (title text), `width: number` / `height: number` (**declare them both** — whole system px, so the window keeps its proportion to the chrome inside it at every density. A window is a fixed box in both axes, the way a WIND resource was: one that grows with its body is one the user can neither predict nor control via the grow box. Each missing dimension falls back to something different — width to block layout, height to the content — and the window names whichever are missing, once, in the console), `active: boolean` (default **true**; reflect), `closable: boolean` (default true), `zoomable: boolean` (default false), `movable: boolean` (default false), `resizable: boolean` (default false), `min-width` / `min-height: number` (default 80 / 54) and `max-width` / `max-height: number` (unbounded) — the grow box's sizeRect, whole system px (see Behavior), `variant?: 'utility'` (the slim windoid chrome), `scrollbars?: 'vertical' | 'horizontal' | 'both'` (edge scroll rails), `header-height: number` (`headerHeight` — the header's height in whole system px, rule included; unset, the header is as tall as its content plus the rule).
212
+ - **Visual:** `vfChromeFrame` + `vfTitleBar` (§4), plus a full-size flex-column layout on the frame — title bar, header, body, status strip. `display: block`. Sets `--vf-surface: var(--vf-white, #fff)` on itself.
213
213
  - Title bar: from `vfTitleBar` — height `var(--vf-titlebar-height, 18px)`, white bg, bottom `1px solid black`, contains `.vf-stripes` layer (only when `active`). `touch-action: none` only when `[movable]`.
214
214
  - Title: centered, bold, on a white patch (`padding: 0 6px`; cap band on interior rows 4..12, 7px of white between ink and stripes — see §4 `vfTitleBar` for the traced geometry and the lattice hold) above the stripes, with `--vf-title-inset: 60px` of clearance so it ellipsizes before reaching the widgets. Inactive: no stripes, widgets undrawn (transparent ink — they keep their tab stops; see A11y below), the grow box's nested squares hidden, and every managed scroll rail inside the window blanked (see "always-a-rail" §5 vf-scroll-area) — but the title text stays black (System 7 never grayed the window title).
215
215
  - Close box: LEFT side, 11×11px, 8px from the inner-left edge, with 3px of clear white above and below it, `1px solid black`, white bg, no bevel, surrounded by a 1px white patch interrupting the stripes. `:active` (pressed) → the interior fills with the classic radiating "go-away" sunburst: black 1-bit spokes on the white face (four orthogonal 3px spokes plus four diagonal 2px ones around an empty center), traced pixel-for-pixel from the UI kit's close-button-active-state sprite.
216
216
  - Zoom box: RIGHT side, same box, plus a small box nested in its top-left corner (sharing the widget's own top/left border; only the right and bottom edges are drawn). `:active` (pressed) → shows the identical sunburst as the close box; the nested box gives way to it.
217
- - Body: `padding: 12px` (0 if `flush` or `scrollbars`), and `overflow: hidden` — the window being a fixed box, content taller than it is clipped at the frame the way the classic content region was, rather than painting out over the desktop; `scrollbars` is how the user reaches the rest. Two deliberate exemptions: `[scrollbars]` restores `overflow: visible`, because the edge-rail composition below pulls the scroll area one system px *outside* the body on every side and clipping would shave exactly that overhang off (the scroll area does its own clipping); and a control's drop-open panel is not clipped — `vf-select`'s list is `position: fixed` off the control's rect precisely to escape clipping ancestors (§5 vf-select), and it still escapes, because nothing between it and the viewport establishes a containing block for fixed descendants (the grid-snap correction is a `position: relative` left/top offset, never a transform — see §7). A `vf-menu` panel is anchored `position: absolute` and *would* clip, but a menu bar belongs to the desktop, not inside a window body.
217
+ - Body: no inset of its own — content starts at the content region's corner, the frame's inner edge below the title bar, where a placed child's (0,0) is; an inset is the content's (a `vf-stack pad`) and `overflow: hidden` — the window being a fixed box, content taller than it is clipped at the frame the way the classic content region was, rather than painting out over the desktop; `scrollbars` is how the user reaches the rest. Two deliberate exemptions: `[scrollbars]` restores `overflow: visible`, because the edge-rail composition below pulls the scroll area one system px *outside* the body on every side and clipping would shave exactly that overhang off (the scroll area does its own clipping); and a control's drop-open panel is not clipped — `vf-select`'s list is `position: fixed` off the control's rect precisely to escape clipping ancestors (§5 vf-select), and it still escapes, because nothing between it and the viewport establishes a containing block for fixed descendants (the grid-snap correction is a `position: relative` left/top offset, never a transform — see §7). A `vf-menu` panel is anchored `position: absolute` and *would* clip, but a menu bar belongs to the desktop, not inside a window body.
218
218
  - Grow box (if `resizable`): 15×15 at bottom-right corner, white bg, 1px black top/left borders, containing two overlapping small square outlines. Inactive: the cell and its borders stay, the nested squares go — System 7 drew a deactivated window's size box hollow, with its blanked scroll rails.
219
+ - Header (`header` slot): a band between the title bar and the body, the full width of the window — the Finder window's header line — a white interior over a 1px black rule (`vfRule`'s `.vf-rule-bottom`), part `header` (a `<div>`, never a `<header>` element, for the banner-landmark reason the title bar is a `<div>`). Like the body it carries no inset and is a positioning anchor: (0,0) is the header's own corner, flow content starts there, an inset is the content's (a `vf-stack pad`). As tall as its content unless `header-height` states it (rule included, the way every kit bar counts its rule). Clipped like the body; a drop-open panel still escapes. Takes **no space** until the slot is populated, and collapses again when it empties. Composes with `scrollbars`: the edge rails sit in the body below it, the area's top overhang landing its frame line exactly on the header's rule, so the header spans the vertical rail's column and the rail's top arrow begins under it.
219
220
  - Status bar (`status` slot): the classic bottom readout strip ("40px x 40px") — a 1px black rule (`vfRule`'s `.vf-rule-top`) over a white interior, `--vf-status-bar-height` (15px) in all, the grow box's own height, so a `resizable` window's grow box sits flush in the strip's right end (its top/left borders take over the rule there; the strip reserves 21px of right padding under `resizable` so text clears the cell). Slotted content rides the body face's native 12px line (`--vf-line-height`), whole-pixel centered in the 14px interior, `padding-inline: 6px`, nowrap + clipped. Takes **no space** until the slot is populated (the dialog-footer pattern), and collapses again when it empties. Composes with `scrollbars`: the edge rails' bottom overhang lands its frame line exactly on the strip's rule, so the two never double up — though the grow box then sits in the strip rather than the rail corner cell.
220
- - Edge scroll rails (if `scrollbars`): the body slot renders inside a shadow `vf-scroll-area` (its `axis` = the attribute's value, `label` = the heading, `viewport` part re-exported) carrying the TeachText composition internally — `calc(100% + 2px·scale)` with `margin: -1px·scale`, one system px under the frame on every side, so the area's own frame border repaints the window's border lines and a `resizable` window's grow box (z-index 1) lands exactly over the rail-corner cell. `flush` passes through to the area (its viewport drops the 8px inset, keeping the border-floor term, so content and the (0,0) of placed children sit at the content region's corner); a `resizable` window sets the area's `corner`, so a single-axis rail stops at the corner cell too — unless the status strip is populated, when the grow box sits in the strip and the rail runs edge to edge onto the strip's rule. Same caveats as the slotted composition (see vf-scroll-area §5), which remains supported for inset wells.
221
+ - Edge scroll rails (if `scrollbars`): the body slot renders inside a shadow `vf-scroll-area` (its `axis` = the attribute's value, `label` = the heading, `viewport` part re-exported) carrying the TeachText composition internally — `calc(100% + 2px·scale)` with `margin: -1px·scale`, one system px under the frame on every side, so the area's own frame border repaints the window's border lines and a `resizable` window's grow box (z-index 1) lands exactly over the rail-corner cell. the area's viewport carries no inset (the border-floor term alone), so content and the (0,0) of placed children sit at the content region's corner; a `resizable` window sets the area's `corner`, so a single-axis rail stops at the corner cell too — unless the status strip is populated, when the grow box sits in the strip and the rail runs edge to edge onto the strip's rule. Same caveats as the slotted composition (see vf-scroll-area §5).
221
222
  - Utility variant (`variant="utility"`): the slim windoid bar traced from `Windows/utility-window.png` — `--vf-titlebar-height-utility` (12px = 11px interior + 1px rule), the `vfDots` dither instead of stripes (flush to the side borders — see §4 vfDots), 7×7 widgets (`top: 2px`; close `left: 7px`, zoom `right: 8px` — the art is asymmetric by that pixel) with a 2px patch ring where the striped bar's is 1px (`--vf-widget-ring`, internal geometry: the windoid sheet clears two px of dither beside its widgets), and the nested zoom square shrunk so its edges land at sprite col/row 3. No title patch: the display face's 16px line box can't sit in an 11px interior, so `.vf-title` is `display: none` under the variant (a retheming consumer can re-show it via `::part(title)`) and the heading names the widgets. A pressed windoid widget inverts whole — black interior under a white (invisible) borderline — rather than flashing the big bar's 9×9 sunburst, which can't land on a 5×5 interior.
222
223
  - **A11y:** the close/zoom `aria-label`s are qualified by the title when there is one (`Close ${heading}` / `Zoom ${heading}`, falling back to bare `Close` / `Zoom`) — several windows are open at once by design, so a bare repeated "Close" gives an AT user no way to tell which window a widget belongs to. The frame is `role="group"`, named by the title patch via `aria-labelledby` when there is a heading (the utility variant's hidden patch still names it — AccName resolves hidden labelledby targets) — `group` rather than `region` deliberately, so a desktop of windows doesn't pollute landmark navigation; the title bar is a `<div>`, never a `<header>`, which would map to an unnamed `banner` landmark even inside the shadow root. An inactive window's widgets stay in the tree and the tab order but paint no ink (transparent border/background/patch ring — the bare System 7 bar): a background window whose body holds nothing focusable is still reachable, activated by `vf-desktop`'s `focusin` raise the moment Tab lands on a widget, and never drops focus to `<body>` when it deactivates. `npm run verify:window-a11y`.
223
224
  - **Behavior:** close box click → `vf-close` (does NOT remove itself; consumer decides). Zoom box click → `vf-zoom`. If `movable`: dragging the title bar moves the window — the drag seeds its origin from the current offset position (once, converting to system px) and then states `left`/`top` via pointer capture, in whole system px like any authored placement. If `resizable`: dragging the grow box states `width`/`height` the same way, each axis clamped into the sizeRect (`min-width`/`max-width`, `min-height`/`max-height` — GrowWindow's: the app stated the rectangle of sizes and the Window Manager clamped the drag to it; the mins default to the 80×54 floor a window can still be worked at, the maxes are unbounded, and a min equal to its max locks the axis — the strip that scrolls sideways and never grows taller). The clamp runs after the lattice snap, so a bound lands exactly the way an authored size does even off the lattice (an odd height at 2×); where a min and max cross the max wins, so a window authored under the floor and held there never jumps to it. The rect bounds the gesture only: a size authored outside it renders as declared, and the first grow-box move brings it inside. The drag fires `vf-resize` (detail `{ width, height, commit }`, sizes in whole system px) — one event per size the drag actually writes (`commit: false`), fired *after* the new box is applied so a handler that measures reads the resized layout, then a final `commit: true` as the gesture settles (release or cancel), only when it changed the size. Window content can follow the grow box from this event alone, no ResizeObserver. Fired by the gesture only: a programmatic `width`/`height` write fires nothing, the way a value set fires no `vf-change`.
224
- - **Slots:** default (body content), `status` (the bottom status strip — see Visual).
225
- - **Parts:** `frame`, `title-bar`, `title`, `close-box`, `zoom-box`, `body`, `status-bar`, `grow-box`, plus `viewport` re-exported from the built-in scroll area when `scrollbars` is set.
225
+ - **Slots:** default (body content), `header` (the header strip between the title bar and the body), `status` (the bottom status strip — see Visual).
226
+ - **Parts:** `frame`, `title-bar`, `title`, `close-box`, `zoom-box`, `header`, `body`, `status-bar`, `grow-box`, plus `viewport` re-exported from the built-in scroll area when `scrollbars` is set.
226
227
  - **Events:** `vf-close`, `vf-zoom` (detail `{}`), `vf-resize` (detail `{ width, height, commit }`).
227
228
 
228
229
  #### `vf-dialog` (`VfDialog`, vf-dialog.ts)
@@ -389,11 +390,12 @@ Classic list box.
389
390
 
390
391
  #### `vf-scroll-area` (`VfScrollArea`, vf-scroll-area.ts)
391
392
  A container whose scrollbars look like System 7.
392
- - **Attributes/props:** `axis: 'vertical' | 'horizontal' | 'both'` (default `'vertical'`, reflected) — which scroll rails to reserve as permanent placeholders (see "always-a-rail" below); `flush: boolean` (default false, reflected) — drops the viewport's 8px inset, keeping the border-floor term, so content and the (0,0) of placed children sit at the frame's inner edge; `corner: boolean` (default false, reflected) — reserves the bottom-right corner cell on a single-axis rail (the rail stops 15px short of the frame, the viewport spans the rest), for a grow box to land in; inert on `both`, which always has the cell; `label: string` — accessible name for the viewport (`aria-label` on the viewport, since an `aria-label` on the host cannot reach into the shadow DOM). The viewport is a **Tab stop only while its content actually overflows** — the same state `ScrollStateController` measures for the rails; a fitting scroll area used to be a focusable stop with `role: generic` and no name, a dead Tab press. Whenever it is a stop it carries a role: `role="region"` when `label` names it (a named landmark), `role="group"` when not (an unnamed region is inert, so that role is reserved for the labelled case).
393
- - **Visual:** `display: block`; the snapped wrapper carries a real 1px black frame and a grid reserving each rail as its own edge column/row — `[viewport | vertical rail]` over `[horizontal rail | corner]` — with the white inner viewport padded `8px` plus the border-floor compensation (a `mod()` term restoring what engines floor off the fractional border-width, so slotted content and the (0,0) of placed children sit exactly 9 system px from the frame box at every scale; `flush` keeps only the compensation, and they sit exactly 1). Consumer sets width/height on host. The rails are the drawn `vfScrollRail` subtree (§4 — the classic 16px cell counting the frame line: divider, 14px channel, 15px arrow cells, the fixed 16px thumb, the `tileRaster` dot-dither trough), rendered as later siblings of the viewport and synced to its native scrolling by `ScrollRailController`; the viewport carries the `vf-scroll` class, which hides the native bar without touching the native scrolling. When both rails are reserved, the corner cell joins them, supplying the interior dividers the adjacent arrow cells leave to it; `corner` reserves the same 15×15 cell beside a single rail, its other divider continuing the rail's line across the cell — DrawGrowIcon's form, for the grow box. Every engine renders the same rail; the old `::-webkit-scrollbar` skin, its `.vf-scroll-frame` overlay contortion (WebKit quantized native scrollbar rects to whole CSS px) and the Firefox `scrollbar-color` fallback are all retired.
393
+ - **Attributes/props:** `axis: 'vertical' | 'horizontal' | 'both'` (default `'vertical'`, reflected) — which scroll rails to reserve as permanent placeholders (see "always-a-rail" below); `corner: boolean` (default false, reflected) — reserves the bottom-right corner cell on a single-axis rail (the rail stops 15px short of the frame, the viewport spans the rest), for a grow box to land in; inert on `both`, which always has the cell; `label: string` — accessible name for the viewport (`aria-label` on the viewport, since an `aria-label` on the host cannot reach into the shadow DOM). The viewport is a **Tab stop only while its content actually overflows** — the same state `ScrollStateController` measures for the rails; a fitting scroll area used to be a focusable stop with `role: generic` and no name, a dead Tab press. Whenever it is a stop it carries a role: `role="region"` when `label` names it (a named landmark), `role="group"` when not (an unnamed region is inert, so that role is reserved for the labelled case).
394
+ - **Visual:** `display: block`; the snapped wrapper carries a real 1px black frame and a grid reserving each rail as its own edge column/row — `[viewport | vertical rail]` over `[horizontal rail | corner]` — with the white inner viewport carrying no inset of its own — only the border-floor compensation (a `mod()` term restoring what engines floor off the fractional border-width), so slotted content and the (0,0) of placed children sit exactly 1 system px from the frame box at every scale; an inset is the content's (a `vf-stack pad`). Consumer sets width/height on host. The scrolled plane — the wrapper the slot renders into, and the positioned anchor for placed children — is `width: fit-content; min-width: 100%`: never narrower than the viewport, as wide as content that cannot wrap, so the controllers' ResizeObserver on it sees a row growing sideways the way it sees copy growing down (a block wrapper's auto width was the viewport's whatever the row did, and a row gaining a cell left the thumb and the overflow state stale until something else re-measured); copy wraps as before, and a `position: sticky; left: 0` child holds across the scroll, the plane being its containing block. `min-height: 100%` likewise: a `fixed` child (§2 Explicit placement) is a sticky box the plane contains, and a plane shorter than the child's stated `top` would clamp it to the plane's bottom edge; content that fits still fits. In the overflowing case a percentage-width or centered block child resolves against the grown plane, not the viewport. A `slotchange` re-measures; `measure()` covers a scroll range that changes with no box changing. The rails are the drawn `vfScrollRail` subtree (§4 — the classic 16px cell counting the frame line: divider, 14px channel, 15px arrow cells, the fixed 16px thumb, the `tileRaster` dot-dither trough), rendered as later siblings of the viewport and synced to its native scrolling by `ScrollRailController`; the viewport carries the `vf-scroll` class, which hides the native bar without touching the native scrolling. When both rails are reserved, the corner cell joins them, supplying the interior dividers the adjacent arrow cells leave to it; `corner` reserves the same 15×15 cell beside a single rail, its other divider continuing the rail's line across the cell — DrawGrowIcon's form, for the grow box. Every engine renders the same rail; the old `::-webkit-scrollbar` skin, its `.vf-scroll-frame` overlay contortion (WebKit quantized native scrollbar rects to whole CSS px) and the Firefox `scrollbar-color` fallback are all retired.
394
395
  - **Always-a-rail behavior:** each *reserved* axis (per `axis`) renders its rail element as a permanent placeholder — arrow buttons on an empty white channel (dither off, no thumb; System 7 drew the arrows on any bar in an active window, and a scroller outside a window always counts as active) — until the content overflows that axis, when the dither and thumb fill in and the arrows go live (an idle axis's arrows are drawn but inert — the press guard in `ScrollRailController` skips them). `ScrollStateController` (`src/scroll-state.ts`) measures both axes and writes `data-overflow-x` / `data-overflow-y` (`"true"` / `"false"`) on the scroll element; the recipe keys the dither and thumb off those attributes. The unreserved axis still scrolls natively (wheel, keyboard) but draws no rail. Shared by vf-list and vf-text-area; a future `@container scroll-state(scrollable)` query could replace the JS for slotted-content components.
395
396
  - **Inactive-window blanking:** the HIG's non-frontmost window must not display interactive scroll UX, so the controller also finds the nearest `vf-window` up the composed tree (light-DOM ancestor for a slotted scroller, shadow ancestor for `vf-window[scrollbars]`'s own edge rails), watches its reflected `active` attribute, and toggles a presence-only `data-window-inactive` on the scroll element. While present, the recipe empties dither/thumb/arrows on BOTH axes regardless of overflow — the bare channel, arrows included (unlike the idle rail, which keeps them), exactly as System 7 blanked a deactivated window's bars (its List Manager/TextEdit deactivated in-window scrollbars too). No `vf-window` ancestor → the attribute never appears: dialogs have no inactive state and a bare scroll component always draws live. Like the overflow half, this signal could one day go declarative — a custom property cascaded under `vf-window:not([active])` gating the recipe via an `@container style()` query; the rails being ordinary DOM, that migration is a plain selector swap (see the FUTURE note in scroll-state.ts).
396
- - **Document-window (TeachText) composition:** to put the rails on a window's edge rather than inset in its body, slot the scroll area into a `vf-window[flush]` sized `calc(100% + 2px·scale)` with `margin: -1px·scale` — one system pixel under the window frame on every side. Its own frame border then repaints the window's border lines exactly (no doubled frame), and a resizable window's grow box lands exactly over the rail's corner cell, giving the classic System 7 document window. `vf-window[scrollbars]` renders exactly this composition from its own shadow tree (forwarding its `flush`, and `corner` for a resizable window without a status strip), so the one-liner and the slotted form are geometrically identical; slot it yourself when the well should sit *inset* in the body instead (the installer's read-me well).
397
+ - **Document-window (TeachText) composition:** to put the rails on a window's edge rather than inset in its body, slot the scroll area into a `vf-window` sized `calc(100% + 2px·scale)` with `margin: -1px·scale` — one system pixel under the window frame on every side. Its own frame border then repaints the window's border lines exactly (no doubled frame), and a resizable window's grow box lands exactly over the rail's corner cell, giving the classic System 7 document window. `vf-window[scrollbars]` renders exactly this composition from its own shadow tree (forwarding `corner` for a resizable window without a status strip), so the one-liner and the slotted form are geometrically identical; slot it yourself when the well should sit inside the body instead of on its edge (the installer's read-me well, placed with `top`/`left`).
398
+ - **Methods:** `measure()` → re-measure overflow and re-sync the rails. The area tracks its content's box and its slot by itself; this is for a scroll range that changes with no box changing anywhere — a placed child moved through `top`/`left`, a transform — the way the kit's own fields re-measure on input.
397
399
  - **Slots:** default. **Parts:** `viewport`.
398
400
 
399
401
  #### `vf-fieldset` (`VfFieldset`, vf-fieldset.ts)
@@ -502,7 +504,7 @@ What the kit keeps as its own demo surface is `index.html`, the component refere
502
504
  5. An alert: menu item Special → "Erase Disk…" opens a composed alert box — `vf-dialog frame="plain"` with the demo's own 32×32 alert icon (`demo/icons/alert.png`) in a row `vf-stack` via `vf-img` — "Completely erase the disk named 'Macintosh HD'?" with Cancel / Erase buttons (Erase = default variant, closes the dialog). The kit ships no alert component; the showcase demonstrates the recipe.
503
505
  6. All windows `movable`; desktop stacking/active management demonstrably works. Every window starts put away and opens from its own desktop launcher icon — a `vf-icon` per demonstration window and dialog, clustered around the top-left so the set stays on-canvas at any viewport size; `vf-open` centers the window on the raster as it currently stands and raises it (no window carries an authored position). Closing a window hides it (listen for `vf-close`, set `hidden`); the close-box-less modal lookalikes dismiss via their own OK/Cancel buttons instead; Special → Show All Windows un-hides everything, cascaded around the center.
504
506
  7. **"Page Setup" modal dialog box** — File → Page Setup… opens a `vf-dialog frame="plain"` (dBoxProc double frame, heading drawn in content): Paper radio group in a fieldset, Reduce or Enlarge `vf-number-field`, Cancel / OK (default) buttons.
505
- 8. **"Desk Accessories" utility palette** — `vf-window variant="utility" movable flush`: a `vf-grid` of 3×3 26px cells holding 16×16 `vf-img` DA icons (one selected, inverted), frameless so the window's own border is the palette's, floating above the document windows on the desktop's utility tier and untouched by their active-state churn — the Group A archetype table's fifth recipe, live.
507
+ 8. **"Desk Accessories" utility palette** — `vf-window variant="utility" movable`: a `vf-grid` of 3×3 26px cells holding 16×16 `vf-img` DA icons (one selected, inverted), frameless so the window's own border is the palette's, floating above the document windows on the desktop's utility tier and untouched by their active-state churn — the Group A archetype table's fifth recipe, live.
506
508
  9. The **"DragThing Read Me"** window (item 2's copy points at it) carries the document-window archetype at full anatomy: `movable resizable scrollbars="both"`, the rails in the frame and the grow box in the corner cell.
507
509
  10. **Finder icons on the desktop** — "Macintosh HD" and "Trash" as `vf-icon`, each slotting its art at both resource sizes, `selectable movable editable`. They sit under the windows the way desktop icons do, taking the column to the right of the launcher cluster — with `left`/`top` in system px, never a `right`/`bottom` anchor, which lands somewhere different at each density since the desktop is the viewport. View → "by Small Icon" drives them (launchers included).
508
510
 
@@ -25,6 +25,11 @@
25
25
  "name": "left",
26
26
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
27
27
  "values": []
28
+ },
29
+ {
30
+ "name": "fixed",
31
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
32
+ "values": []
28
33
  }
29
34
  ],
30
35
  "references": []
@@ -92,6 +97,11 @@
92
97
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
93
98
  "values": []
94
99
  },
100
+ {
101
+ "name": "fixed",
102
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
103
+ "values": []
104
+ },
95
105
  {
96
106
  "name": "description",
97
107
  "description": "Description for the control — hint text, a format, a unit. A host-level\n`aria-describedby` cannot reach a focusable element inside a shadow root,\nso there was structurally no way to describe a field; this property is\nthat channel. It renders as a hidden span in the control's own shadow root\nwith the inner control's `aria-describedby` pointing at it — the\nshadow-internal IDREF idiom `vf-dialog`'s title patch already uses. A\nhost-level `aria-describedby` is bridged into the same span when this\nproperty is empty, and a failing constraint's validationMessage\njoins it too, so AT hears the error where it hears the hint.\n\nHost-role controls get neither half: their `aria-describedby` already\nworks, and their validation message reaches AT the way a native control's\ndoes — `aria-invalid` plus the browser's own validation UI, not AccName.",
@@ -139,6 +149,11 @@
139
149
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
140
150
  "values": []
141
151
  },
152
+ {
153
+ "name": "fixed",
154
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
155
+ "values": []
156
+ },
142
157
  {
143
158
  "name": "disabled",
144
159
  "description": "Disables the control: it stops responding and submits no value. Each\ncontrol dims per SPEC §1 (typically the label greys while the 1-bit chrome\nstays black).",
@@ -185,6 +200,11 @@
185
200
  "name": "left",
186
201
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
187
202
  "values": []
203
+ },
204
+ {
205
+ "name": "fixed",
206
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
207
+ "values": []
188
208
  }
189
209
  ],
190
210
  "references": []
@@ -222,6 +242,11 @@
222
242
  "name": "left",
223
243
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
224
244
  "values": []
245
+ },
246
+ {
247
+ "name": "fixed",
248
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
249
+ "values": []
225
250
  }
226
251
  ],
227
252
  "references": []
@@ -301,6 +326,11 @@
301
326
  "name": "left",
302
327
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
303
328
  "values": []
329
+ },
330
+ {
331
+ "name": "fixed",
332
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
333
+ "values": []
304
334
  }
305
335
  ],
306
336
  "references": []
@@ -353,6 +383,11 @@
353
383
  "name": "left",
354
384
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
355
385
  "values": []
386
+ },
387
+ {
388
+ "name": "fixed",
389
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
390
+ "values": []
356
391
  }
357
392
  ],
358
393
  "references": []
@@ -420,6 +455,11 @@
420
455
  "name": "left",
421
456
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
422
457
  "values": []
458
+ },
459
+ {
460
+ "name": "fixed",
461
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
462
+ "values": []
423
463
  }
424
464
  ],
425
465
  "references": []
@@ -447,6 +487,11 @@
447
487
  "name": "left",
448
488
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
449
489
  "values": []
490
+ },
491
+ {
492
+ "name": "fixed",
493
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
494
+ "values": []
450
495
  }
451
496
  ],
452
497
  "references": []
@@ -489,6 +534,11 @@
489
534
  "name": "left",
490
535
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
491
536
  "values": []
537
+ },
538
+ {
539
+ "name": "fixed",
540
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
541
+ "values": []
492
542
  }
493
543
  ],
494
544
  "references": []
@@ -521,6 +571,11 @@
521
571
  "name": "left",
522
572
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
523
573
  "values": []
574
+ },
575
+ {
576
+ "name": "fixed",
577
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
578
+ "values": []
524
579
  }
525
580
  ],
526
581
  "references": []
@@ -558,6 +613,11 @@
558
613
  "name": "left",
559
614
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
560
615
  "values": []
616
+ },
617
+ {
618
+ "name": "fixed",
619
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
620
+ "values": []
561
621
  }
562
622
  ],
563
623
  "references": []
@@ -590,6 +650,11 @@
590
650
  "name": "left",
591
651
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
592
652
  "values": []
653
+ },
654
+ {
655
+ "name": "fixed",
656
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
657
+ "values": []
593
658
  }
594
659
  ],
595
660
  "references": []
@@ -637,6 +702,11 @@
637
702
  "name": "left",
638
703
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
639
704
  "values": []
705
+ },
706
+ {
707
+ "name": "fixed",
708
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
709
+ "values": []
640
710
  }
641
711
  ],
642
712
  "references": []
@@ -669,6 +739,11 @@
669
739
  "name": "left",
670
740
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
671
741
  "values": []
742
+ },
743
+ {
744
+ "name": "fixed",
745
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
746
+ "values": []
672
747
  }
673
748
  ],
674
749
  "references": []
@@ -702,6 +777,11 @@
702
777
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
703
778
  "values": []
704
779
  },
780
+ {
781
+ "name": "fixed",
782
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
783
+ "values": []
784
+ },
705
785
  {
706
786
  "name": "value",
707
787
  "description": "Current value. Synced on every keystroke and submitted with forms.",
@@ -778,6 +858,11 @@
778
858
  "name": "left",
779
859
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
780
860
  "values": []
861
+ },
862
+ {
863
+ "name": "fixed",
864
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
865
+ "values": []
781
866
  }
782
867
  ],
783
868
  "references": []
@@ -815,6 +900,11 @@
815
900
  "name": "left",
816
901
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
817
902
  "values": []
903
+ },
904
+ {
905
+ "name": "fixed",
906
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
907
+ "values": []
818
908
  }
819
909
  ],
820
910
  "references": []
@@ -852,6 +942,11 @@
852
942
  "name": "left",
853
943
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
854
944
  "values": []
945
+ },
946
+ {
947
+ "name": "fixed",
948
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
949
+ "values": []
855
950
  }
856
951
  ],
857
952
  "references": []
@@ -885,6 +980,11 @@
885
980
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
886
981
  "values": []
887
982
  },
983
+ {
984
+ "name": "fixed",
985
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
986
+ "values": []
987
+ },
888
988
  {
889
989
  "name": "disabled",
890
990
  "description": "Disables the control: it stops responding and submits no value. Each\ncontrol dims per SPEC §1 (typically the label greys while the 1-bit chrome\nstays black).",
@@ -926,13 +1026,18 @@
926
1026
  "name": "left",
927
1027
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
928
1028
  "values": []
1029
+ },
1030
+ {
1031
+ "name": "fixed",
1032
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
1033
+ "values": []
929
1034
  }
930
1035
  ],
931
1036
  "references": []
932
1037
  },
933
1038
  {
934
1039
  "name": "vf-scroll-area",
935
- "description": "`<vf-scroll-area>` — a container whose scrollbars look like System 7.\n\nWhite box with a 1px black frame, an inner scrolling viewport, and scroll\nrails the kit draws itself as shadow DOM (the shared `vfScrollRail` recipe):\nboxed arrow buttons at each end, a loose 1-bit dot-dither trough, and the\nclassic fixed 16px thumb. The native scrollbar is hidden, never the native\nscrolling — wheel, trackpad momentum, keyboard, touch and assistive-tech\nscrolling stay the platform's, and ScrollRailController keeps the\nrail in sync while driving the classic interactions (thumb drag, trough\npaging, arrow stepping with auto-repeat). Every engine renders the same\nrail — there is no Firefox fallback skin anymore.\n\nEach reserved scroll rail is a permanent placeholder: arrow buttons on an\nempty white channel sit in the gutter even when the content fits, the\ndither and thumb filling in only once that axis overflows (System 7 drew\nan active window's no-overflow bar as arrows on a bare channel; driven\nby ScrollStateController). Which rails are reserved is set by\naxis; when both are reserved the bottom-right corner joins them,\nand corner reserves that cell on a single-axis rail too — the\nrail stops 15px short of the frame, for a grow box to land in.\n\nSize the host (width/height) from the outside; the viewport fills it. The\nviewport insets its content 8px; flush drops that inset so content\nruns to the frame and the rails.\n---\n\n\n### **Slots:**\n - _default_ - Scrollable content.\n\n### **CSS Properties:**\n - **--vf-scrollbar-thumb** - scrollbar thumb/elevator (white) _(default: undefined)_\n- **--vf-scrollbar-track** - the scroll trough's base color under the dot-dither (white) _(default: undefined)_\n\n### **CSS Parts:**\n - **viewport** - The inner scrolling container.",
1040
+ "description": "`<vf-scroll-area>` — a container whose scrollbars look like System 7.\n\nWhite box with a 1px black frame, an inner scrolling viewport, and scroll\nrails the kit draws itself as shadow DOM (the shared `vfScrollRail` recipe):\nboxed arrow buttons at each end, a loose 1-bit dot-dither trough, and the\nclassic fixed 16px thumb. The native scrollbar is hidden, never the native\nscrolling — wheel, trackpad momentum, keyboard, touch and assistive-tech\nscrolling stay the platform's, and ScrollRailController keeps the\nrail in sync while driving the classic interactions (thumb drag, trough\npaging, arrow stepping with auto-repeat). Every engine renders the same\nrail — there is no Firefox fallback skin anymore.\n\nEach reserved scroll rail is a permanent placeholder: arrow buttons on an\nempty white channel sit in the gutter even when the content fits, the\ndither and thumb filling in only once that axis overflows (System 7 drew\nan active window's no-overflow bar as arrows on a bare channel; driven\nby ScrollStateController). Which rails are reserved is set by\naxis; when both are reserved the bottom-right corner joins them,\nand corner reserves that cell on a single-axis rail too — the\nrail stops 15px short of the frame, for a grow box to land in.\n\nSize the host (width/height) from the outside; the viewport fills it.\nContent runs to the frame and the rails — the area adds no inset of its\nown; an inset is the content's (a `vf-stack pad`).\n\nThe scrolled plane sizes to its content never narrower than the\nviewport, as wide as content that cannot wrap — so the rails follow a row\nthat grows sideways the way they follow copy that grows down, and a\n`slotchange` re-measures. measure covers a scroll range that\nchanges with no box changing (a placed child moved through `top`/`left`).\n---\n\n\n### **Slots:**\n - _default_ - Scrollable content.\n\n### **CSS Properties:**\n - **--vf-scrollbar-thumb** - scrollbar thumb/elevator (white) _(default: undefined)_\n- **--vf-scrollbar-track** - the scroll trough's base color under the dot-dither (white) _(default: undefined)_\n\n### **CSS Parts:**\n - **viewport** - The inner scrolling container.",
936
1041
  "attributes": [
937
1042
  {
938
1043
  "name": "axis",
@@ -943,11 +1048,6 @@
943
1048
  { "name": "both" }
944
1049
  ]
945
1050
  },
946
- {
947
- "name": "flush",
948
- "description": "Drop the viewport's 8px inset: slotted content and the (0,0) of placed\nchildren sit at the frame's inner edge, one system px from the frame\nbox (the border-floor compensation stays — see the viewport rule).\n`vf-window[scrollbars]` forwards its own `flush` here.",
949
- "values": []
950
- },
951
1051
  {
952
1052
  "name": "corner",
953
1053
  "description": "Reserve the bottom-right corner cell on a single-axis rail: the rail\nstops 15px short of the frame and the viewport spans the rest, leaving\nthe 15×15 cell — with the two dividers a `both` rail's corner carries —\nfor a grow box to land in. A `both` rail always has the cell, so the\nflag changes nothing there. `vf-window[scrollbars resizable]` sets it.",
@@ -967,6 +1067,11 @@
967
1067
  "name": "left",
968
1068
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
969
1069
  "values": []
1070
+ },
1071
+ {
1072
+ "name": "fixed",
1073
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
1074
+ "values": []
970
1075
  }
971
1076
  ],
972
1077
  "references": []
@@ -1000,6 +1105,11 @@
1000
1105
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
1001
1106
  "values": []
1002
1107
  },
1108
+ {
1109
+ "name": "fixed",
1110
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
1111
+ "values": []
1112
+ },
1003
1113
  {
1004
1114
  "name": "description",
1005
1115
  "description": "Description for the control — hint text, a format, a unit. A host-level\n`aria-describedby` cannot reach a focusable element inside a shadow root,\nso there was structurally no way to describe a field; this property is\nthat channel. It renders as a hidden span in the control's own shadow root\nwith the inner control's `aria-describedby` pointing at it — the\nshadow-internal IDREF idiom `vf-dialog`'s title patch already uses. A\nhost-level `aria-describedby` is bridged into the same span when this\nproperty is empty, and a failing constraint's validationMessage\njoins it too, so AT hears the error where it hears the hint.\n\nHost-role controls get neither half: their `aria-describedby` already\nworks, and their validation message reaches AT the way a native control's\ndoes — `aria-invalid` plus the browser's own validation UI, not AccName.",
@@ -1036,6 +1146,11 @@
1036
1146
  "name": "left",
1037
1147
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
1038
1148
  "values": []
1149
+ },
1150
+ {
1151
+ "name": "fixed",
1152
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
1153
+ "values": []
1039
1154
  }
1040
1155
  ],
1041
1156
  "references": []
@@ -1084,6 +1199,11 @@
1084
1199
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
1085
1200
  "values": []
1086
1201
  },
1202
+ {
1203
+ "name": "fixed",
1204
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
1205
+ "values": []
1206
+ },
1087
1207
  {
1088
1208
  "name": "disabled",
1089
1209
  "description": "Disables the control: it stops responding and submits no value. Each\ncontrol dims per SPEC §1 (typically the label greys while the 1-bit chrome\nstays black).",
@@ -1140,6 +1260,11 @@
1140
1260
  "name": "left",
1141
1261
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
1142
1262
  "values": []
1263
+ },
1264
+ {
1265
+ "name": "fixed",
1266
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
1267
+ "values": []
1143
1268
  }
1144
1269
  ],
1145
1270
  "references": []
@@ -1188,6 +1313,11 @@
1188
1313
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
1189
1314
  "values": []
1190
1315
  },
1316
+ {
1317
+ "name": "fixed",
1318
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
1319
+ "values": []
1320
+ },
1191
1321
  {
1192
1322
  "name": "description",
1193
1323
  "description": "Description for the control — hint text, a format, a unit. A host-level\n`aria-describedby` cannot reach a focusable element inside a shadow root,\nso there was structurally no way to describe a field; this property is\nthat channel. It renders as a hidden span in the control's own shadow root\nwith the inner control's `aria-describedby` pointing at it — the\nshadow-internal IDREF idiom `vf-dialog`'s title patch already uses. A\nhost-level `aria-describedby` is bridged into the same span when this\nproperty is empty, and a failing constraint's validationMessage\njoins it too, so AT hears the error where it hears the hint.\n\nHost-role controls get neither half: their `aria-describedby` already\nworks, and their validation message reaches AT the way a native control's\ndoes — `aria-invalid` plus the browser's own validation UI, not AccName.",
@@ -1220,6 +1350,11 @@
1220
1350
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
1221
1351
  "values": []
1222
1352
  },
1353
+ {
1354
+ "name": "fixed",
1355
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
1356
+ "values": []
1357
+ },
1223
1358
  {
1224
1359
  "name": "value",
1225
1360
  "description": "Current value. Synced on every keystroke and submitted with forms.",
@@ -1282,6 +1417,11 @@
1282
1417
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
1283
1418
  "values": []
1284
1419
  },
1420
+ {
1421
+ "name": "fixed",
1422
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
1423
+ "values": []
1424
+ },
1285
1425
  {
1286
1426
  "name": "value",
1287
1427
  "description": "Current value. Synced on every keystroke and submitted with forms.",
@@ -1327,7 +1467,7 @@
1327
1467
  },
1328
1468
  {
1329
1469
  "name": "vf-window",
1330
- "description": "`<vf-window>` — the System 7 desktop-window shell.\n\nStriped title bar with optional close box (left) and zoom box (right), a\nsolid-white frame with a hard offset shadow, an optional grow box for\nresizing, optional edge scroll rails (`scrollbars`), and the slim windoid\nchrome (`variant=\"utility\"`). The HIG's window archetypes are parameter\nrecipes over this shell rather than fixed anatomies — the component enables\nHIG compliance, it doesn't enforce it (see docs/LAYOUT.md \"Window archetypes\"):\nthe full document window is `closable zoomable movable resizable\nscrollbars=\"both\"`, a modeless dialog box is `closable movable`, a utility\nwindow is `variant=\"utility\" movable`. Place inside `<vf-desktop>` to get\nclick-to-front stacking and automatic `active` management (utility windows\nfloat above the document tier).\n\nEvery recipe also declares `width` AND `height` (VfSized), in whole\nsystem px — the art's own unit, so the window keeps its proportions to the\nchrome inside it at every display density (a CSS-px size stays put while\nthe components in it triple). A window is a fixed box in both axes, the way\nthe WIND resource carried it: left to layout it takes whatever its\ncontainer or content hands it, which is how a title bar ends up wider than\nthe screen or a dialog reflows as it moves — and a window that grows with\nits body is one the user can neither predict nor (via the grow box) own.\nContent taller than the declared box is clipped at the frame the way the\nclassic content region was; `scrollbars` lets the user reach the rest.\nUnset, the window still renders — normal block layout, as before — and\nsays so once in the console.\n---\n\n\n### **Events:**\n - **vf-close** - Close box clicked. Detail `{ reason: 'close' }` (shape- compatible with vf-dialog's `vf-close`). The window does NOT remove itself; the consumer decides what closing means.\n- **vf-zoom** - Zoom box clicked. Detail `{}`.\n- **vf-resize** - The grow box resized the window. Detail `{ width, height, commit }`, sizes in whole system px: one event per size the drag writes (`commit: false`), fired after the new box is applied so a handler that measures reads the resized layout, then a final `commit: true` as the gesture settles — only when it changed the size. Fired by the gesture alone: a programmatic `width`/`height` write fires nothing, the way a value set fires no `vf-change`.\n\n### **Slots:**\n - _default_ - Default slot: window body content.\n- **status** - Optional status-bar content — the classic bottom readout strip (\"40px x 40px\"): a 1px rule over a 15px white band under the body, body-face text on its native line. Takes no space until populated; a `resizable` window's grow box sits flush in its right end.\n\n### **CSS Properties:**\n - **--vf-dots-pattern** - the windoid bar's dot-grid dither — a 2×2 motif, one black pixel at its origin, on a 30-system-px tile (`vfDots`; override the whole tile like `--vf-desktop-pattern` — consumer art renders as a placed tile grid at that same geometry) _(default: undefined)_\n- **--vf-titlebar-height** - window/dialog title bars _(default: undefined)_\n- **--vf-titlebar-height-utility** - the slim `vf-window[variant=\"utility\"]` (windoid) bar — 11px interior + 1px bottom rule, traced from `Windows/utility-window.png` _(default: 12px)_\n- **--vf-status-bar-height** - the status strip: 1px rule + 14px interior — the grow box's own height, so the two compose flush _(default: 15px)_\n- **--vf-line-height** - the body face's native line, which the status strip's text rides (whole-pixel centered in the 14px interior) _(default: 12px)_\n\n### **CSS Parts:**\n - **frame** - The outer chrome frame.\n- **title-bar** - The striped (or dithered) title bar.\n- **title** - The centered title patch (hidden on the utility bar).\n- **close-box** - The close widget (left).\n- **zoom-box** - The zoom widget (right).\n- **body** - The content area.\n- **status-bar** - The bottom status strip (when the `status` slot is populated).\n- **grow-box** - The resize widget (bottom-right, when `resizable`).\n- **viewport** - The built-in scroll area's viewport (when `scrollbars`; re-exported from vf-scroll-area).",
1470
+ "description": "`<vf-window>` — the System 7 desktop-window shell.\n\nStriped title bar with optional close box (left) and zoom box (right), a\nsolid-white frame with a hard offset shadow, an optional grow box for\nresizing, optional edge scroll rails (`scrollbars`), and the slim windoid\nchrome (`variant=\"utility\"`). The HIG's window archetypes are parameter\nrecipes over this shell rather than fixed anatomies — the component enables\nHIG compliance, it doesn't enforce it (see docs/LAYOUT.md \"Window archetypes\"):\nthe full document window is `closable zoomable movable resizable\nscrollbars=\"both\"`, a modeless dialog box is `closable movable`, a utility\nwindow is `variant=\"utility\" movable`. Place inside `<vf-desktop>` to get\nclick-to-front stacking and automatic `active` management (utility windows\nfloat above the document tier).\n\nEvery recipe also declares `width` AND `height` (VfSized), in whole\nsystem px — the art's own unit, so the window keeps its proportions to the\nchrome inside it at every display density (a CSS-px size stays put while\nthe components in it triple). A window is a fixed box in both axes, the way\nthe WIND resource carried it: left to layout it takes whatever its\ncontainer or content hands it, which is how a title bar ends up wider than\nthe screen or a dialog reflows as it moves — and a window that grows with\nits body is one the user can neither predict nor (via the grow box) own.\nContent taller than the declared box is clipped at the frame the way the\nclassic content region was; `scrollbars` lets the user reach the rest.\nUnset, the window still renders — normal block layout, as before — and\nsays so once in the console.\n---\n\n\n### **Events:**\n - **vf-close** - Close box clicked. Detail `{ reason: 'close' }` (shape- compatible with vf-dialog's `vf-close`). The window does NOT remove itself; the consumer decides what closing means.\n- **vf-zoom** - Zoom box clicked. Detail `{}`.\n- **vf-resize** - The grow box resized the window. Detail `{ width, height, commit }`, sizes in whole system px: one event per size the drag writes (`commit: false`), fired after the new box is applied so a handler that measures reads the resized layout, then a final `commit: true` as the gesture settles — only when it changed the size. Fired by the gesture alone: a programmatic `width`/`height` write fires nothing, the way a value set fires no `vf-change`.\n\n### **Slots:**\n - _default_ - Default slot: window body content.\n- **header** - Optional header content — a band between the title bar and the body, the full width of the window (the Finder window's header line): a white interior over a 1px rule, no inset of its own, a positioning anchor for placed children. As tall as its content unless `header-height` states it. Under `scrollbars` the vertical rail begins below it, so the header spans the rail's column. Takes no space until populated.\n- **status** - Optional status-bar content — the classic bottom readout strip (\"40px x 40px\"): a 1px rule over a 15px white band under the body, body-face text on its native line. Takes no space until populated; a `resizable` window's grow box sits flush in its right end.\n\n### **CSS Properties:**\n - **--vf-dots-pattern** - the windoid bar's dot-grid dither — a 2×2 motif, one black pixel at its origin, on a 30-system-px tile (`vfDots`; override the whole tile like `--vf-desktop-pattern` — consumer art renders as a placed tile grid at that same geometry) _(default: undefined)_\n- **--vf-titlebar-height** - window/dialog title bars _(default: undefined)_\n- **--vf-titlebar-height-utility** - the slim `vf-window[variant=\"utility\"]` (windoid) bar — 11px interior + 1px bottom rule, traced from `Windows/utility-window.png` _(default: 12px)_\n- **--vf-status-bar-height** - the status strip: 1px rule + 14px interior — the grow box's own height, so the two compose flush _(default: 15px)_\n- **--vf-line-height** - the body face's native line, which the status strip's text rides (whole-pixel centered in the 14px interior) _(default: 12px)_\n\n### **CSS Parts:**\n - **frame** - The outer chrome frame.\n- **title-bar** - The striped (or dithered) title bar.\n- **title** - The centered title patch (hidden on the utility bar).\n- **close-box** - The close widget (left).\n- **zoom-box** - The zoom widget (right).\n- **body** - The content area.\n- **header** - The header strip between the title bar and the body (when the `header` slot is populated).\n- **status-bar** - The bottom status strip (when the `status` slot is populated).\n- **grow-box** - The resize widget (bottom-right, when `resizable`).\n- **viewport** - The built-in scroll area's viewport (when `scrollbars`; re-exported from vf-scroll-area).",
1331
1471
  "attributes": [
1332
1472
  {
1333
1473
  "name": "variant",
@@ -1384,20 +1524,20 @@
1384
1524
  "description": "See minWidth. Unbounded by default.",
1385
1525
  "values": []
1386
1526
  },
1387
- {
1388
- "name": "flush",
1389
- "description": "Remove the default 12px body padding. Under `scrollbars` the body has\nnone to remove, so the flag reaches the built-in scroll area instead\nand drops its viewport's 8px inset: content runs to the frame and the\nrails, and the (0,0) of placed children is the content region's corner.",
1390
- "values": []
1391
- },
1392
1527
  {
1393
1528
  "name": "scrollbars",
1394
- "description": "Put System 7 scroll rails on the window edge — the classic document\nwindow. The body slot renders inside a built-in `vf-scroll-area` pulled\none system pixel under the frame on every side, so the rails repaint the\nborder lines and a `resizable` window's grow box lands in the corner\ncell — reserved on a single-axis rail too (the area's `corner`), unless\na populated status strip holds the grow box, when the rail runs edge to\nedge onto the strip's rule. Values mirror `vf-scroll-area`'s `axis`; the\n`heading` names the scroll region; the viewport part is re-exported.\nThe body's own padding goes; the viewport keeps its 8px inset unless\n`flush`, which passes through. The slotted composition (SPEC §5\nvf-scroll-area) still works for windows that want an inset well instead.",
1529
+ "description": "Put System 7 scroll rails on the window edge — the classic document\nwindow. The body slot renders inside a built-in `vf-scroll-area` pulled\none system pixel under the frame on every side, so the rails repaint the\nborder lines and a `resizable` window's grow box lands in the corner\ncell — reserved on a single-axis rail too (the area's `corner`), unless\na populated status strip holds the grow box, when the rail runs edge to\nedge onto the strip's rule. Values mirror `vf-scroll-area`'s `axis`; the\n`heading` names the scroll region; the viewport part is re-exported.\nContent runs to the frame and the rails; an inset is the content's own.\nThe slotted composition (SPEC §5 vf-scroll-area) still works for a well\nplaced inside the body.",
1395
1530
  "values": [
1396
1531
  { "name": "vertical" },
1397
1532
  { "name": "horizontal" },
1398
1533
  { "name": "both" }
1399
1534
  ]
1400
1535
  },
1536
+ {
1537
+ "name": "header-height",
1538
+ "description": "The header's height in whole system px, rule included — the way every\nkit bar counts its rule (an 18px title bar is 17 + 1). Unset, the header\nis as tall as what is slotted into it, plus the rule. Only matters while\nthe `header` slot is populated.",
1539
+ "values": [{ "name": "" }]
1540
+ },
1401
1541
  {
1402
1542
  "name": "width",
1403
1543
  "description": "Width in whole system px. What the number means is the component's own\naffair — the measure a paragraph wraps to, a caption column, a window's\nbox — but the mechanics are shared: the length lands on the host as a\nlive `calc(var(--vf-scale, 1) * Npx)`, so the box scales with the\ndisplay and sits on the device-pixel grid by construction. Remove it\nand the width returns to layout.",
@@ -1417,6 +1557,11 @@
1417
1557
  "name": "left",
1418
1558
  "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
1419
1559
  "values": []
1560
+ },
1561
+ {
1562
+ "name": "fixed",
1563
+ "description": "Hold the placement against the visible region of the nearest scrolling\nancestor: the element keeps its stated `top`/`left` while the content\nscrolls under it, and the flag alone places it at (0,0). It comes\nbefore the flow content in its parent. Where nothing scrolls it renders\nexactly as placed.",
1564
+ "values": []
1420
1565
  }
1421
1566
  ],
1422
1567
  "references": []