vintage-frames 0.1.1 → 0.2.0

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.
Files changed (46) hide show
  1. package/custom-elements.json +290 -38
  2. package/dist/components/vf-button.d.ts +1 -1
  3. package/dist/components/vf-container.d.ts +2 -2
  4. package/dist/components/vf-desktop.d.ts +1 -1
  5. package/dist/components/vf-fieldset.d.ts +1 -1
  6. package/dist/components/vf-fieldset.js +6 -1
  7. package/dist/components/vf-grid.d.ts +1 -1
  8. package/dist/components/vf-icon.d.ts +1 -1
  9. package/dist/components/vf-img.d.ts +1 -1
  10. package/dist/components/vf-label.d.ts +1 -1
  11. package/dist/components/vf-list-item.d.ts +9 -1
  12. package/dist/components/vf-list-item.js +15 -14
  13. package/dist/components/vf-list.d.ts +1 -1
  14. package/dist/components/vf-menu-bar.d.ts +1 -1
  15. package/dist/components/vf-menu-item.d.ts +10 -1
  16. package/dist/components/vf-menu-item.js +35 -34
  17. package/dist/components/vf-menu.d.ts +11 -2
  18. package/dist/components/vf-menu.js +33 -32
  19. package/dist/components/vf-option.d.ts +11 -1
  20. package/dist/components/vf-option.js +16 -15
  21. package/dist/components/vf-paragraph.d.ts +3 -3
  22. package/dist/components/vf-progress-bar.d.ts +1 -1
  23. package/dist/components/vf-scroll-area.d.ts +1 -1
  24. package/dist/components/vf-select.d.ts +1 -1
  25. package/dist/components/vf-separator.d.ts +1 -1
  26. package/dist/components/vf-slider.d.ts +1 -1
  27. package/dist/components/vf-stack.d.ts +27 -9
  28. package/dist/components/vf-stack.js +30 -8
  29. package/dist/components/vf-swatch.d.ts +1 -1
  30. package/dist/components/vf-window.d.ts +1 -1
  31. package/dist/cross-center.d.ts +76 -0
  32. package/dist/cross-center.js +69 -0
  33. package/dist/define.d.ts +2 -2
  34. package/dist/define.js +1 -1
  35. package/dist/grid-snap.d.ts +2 -24
  36. package/dist/grid-snap.js +7 -33
  37. package/dist/index.d.ts +5 -5
  38. package/dist/index.js +50 -50
  39. package/dist/position.d.ts +11 -2
  40. package/dist/styles/recipes/host.js +14 -1
  41. package/dist/styles/recipes/toggle.js +16 -0
  42. package/dist/text-control.d.ts +1 -1
  43. package/docs/SPEC.md +10 -9
  44. package/editor/vscode.html-custom-data.json +48 -8
  45. package/editor/web-types.json +92 -12
  46. package/package.json +1 -3
package/dist/index.js CHANGED
@@ -19,53 +19,53 @@ import { vfToggle as te } from "./styles/recipes/toggle.js";
19
19
  import { vfField as ne } from "./styles/recipes/field.js";
20
20
  import { vfScrollRail as re } from "./styles/recipes/scroll-rail.js";
21
21
  import { TileRasterCache as ie, patternOverride as ae, tileGrid as oe, vfTileGrid as se } from "./tile-grid.js";
22
- import { GridSnapController as ce, applyGridSnap as le, requestGridSnap as ue } from "./grid-snap.js";
23
- import { DocumentListenersController as de } from "./document-listeners.js";
24
- import { BUTTON_FACE as fe, BUTTON_FRAME as pe, RING_FRAME as me, RING_HOLE as he, RING_INSET as ge, SCREEN_CORNER as _e, steppedCornerClip as ve, steppedRectClip as ye, steppedRingClip as be } from "./pixel-frame.js";
25
- import { VfDesktop as xe } from "./components/vf-desktop.js";
26
- import { VfSized as Se } from "./size.js";
27
- import { DragController as Ce } from "./drag.js";
28
- import { emit as we } from "./events.js";
29
- import { ScrollStateController as Te } from "./scroll-state.js";
30
- import { BLINK_FLIPS as Ee, BLINK_INTERVAL_MS as De, PRESS_HOLD_MS as Oe, RENAME_DELAY_MS as ke, prefersReducedMotion as Ae, runSelectionBlink as je } from "./motion.js";
31
- import { CARET_DOWN as Me, CHECKBOX_X as Ne, CHECKMARK as Pe, RADIO_DOT as Fe, RADIO_FACE as Ie, RADIO_RING as Le, RADIO_RING_PRESSED as Re, SLIDER_THUMB as ze, SLIDER_THUMB_FACE as Be, STEPPER as Ve, STEPPER_DOWN_FILL as He, STEPPER_UP_FILL as Ue, glyphSvg as We } from "./glyphs.js";
32
- import { ScrollRailController as Ge, renderScrollRail as Ke } from "./scroll-rail.js";
33
- import { VfScrollArea as qe } from "./components/vf-scroll-area.js";
34
- import { VfWindow as Je } from "./components/vf-window.js";
35
- import { VfModalDialog as Ye, modalDialogStyles as Xe } from "./modal-dialog.js";
36
- import { VfButtonGroup as Ze } from "./components/vf-button-group.js";
37
- import { VfDialog as Qe } from "./components/vf-dialog.js";
38
- import { VfSeparator as $e } from "./components/vf-separator.js";
39
- import { VfFormControl as et } from "./form-control.js";
40
- import { VfButton as tt } from "./components/vf-button.js";
41
- import { VfSwatch as nt } from "./components/vf-swatch.js";
42
- import { VfToggleControl as rt } from "./toggle-control.js";
43
- import { VfCheckbox as it } from "./components/vf-checkbox.js";
44
- import { VfRadio as at } from "./components/vf-radio.js";
45
- import { VfRadioGroup as ot } from "./components/vf-radio-group.js";
46
- import { FocusRuleController as st, focusModality as ct, trackFocusModality as lt } from "./focus-modality.js";
47
- import { VfTextControlBase as ut } from "./text-control.js";
48
- import { VfTextField as dt } from "./components/vf-text-field.js";
49
- import { VfTextArea as ft } from "./components/vf-text-area.js";
50
- import { VfNumberField as pt } from "./components/vf-number-field.js";
51
- import { VfOption as mt } from "./components/vf-option.js";
52
- import { VfSelect as ht } from "./components/vf-select.js";
53
- import { TrackWidthController as gt } from "./track-width.js";
54
- import { VfProgressBar as _t } from "./components/vf-progress-bar.js";
55
- import { VfSlider as vt } from "./components/vf-slider.js";
56
- import { VfLabel as yt } from "./components/vf-label.js";
57
- import { VfParagraph as bt } from "./components/vf-paragraph.js";
58
- import { VfImg as xt } from "./components/vf-img.js";
59
- import { VfIcon as St } from "./components/vf-icon.js";
60
- import { VfMenuBar as Ct } from "./components/vf-menu-bar.js";
61
- import { VfMenu as wt } from "./components/vf-menu.js";
62
- import { VfMenuItem as Tt } from "./components/vf-menu-item.js";
63
- import { VfList as Et } from "./components/vf-list.js";
64
- import { VfListItem as Dt } from "./components/vf-list-item.js";
65
- import { VfFieldset as Ot } from "./components/vf-fieldset.js";
66
- import { VfGrid as kt } from "./components/vf-grid.js";
67
- import { VfStack as At } from "./components/vf-stack.js";
68
- import { VfContainer as jt } from "./components/vf-container.js";
69
- import { CURSOR_ARROW as Mt, CURSOR_CROSSHAIR as Nt, CURSOR_I_BEAM as Pt, CURSOR_WAIT as Ft } from "./cursor-art.js";
70
- import { applyCursor as It } from "./cursor.js";
71
- export { Ee as BLINK_FLIPS, De as BLINK_INTERVAL_MS, fe as BUTTON_FACE, pe as BUTTON_FRAME, Me as CARET_DOWN, Ne as CHECKBOX_X, Pe as CHECKMARK, n as CLASSIC_DPI, r as CSS_REFERENCE_DPI, Mt as CURSOR_ARROW, Nt as CURSOR_CROSSHAIR, Pt as CURSOR_I_BEAM, Ft as CURSOR_WAIT, de as DocumentListenersController, Ce as DragController, st as FocusRuleController, ce as GridSnapController, E as PIXEL_GRID_METRICS, Oe as PRESS_HOLD_MS, w as PlacementController, Fe as RADIO_DOT, Ie as RADIO_FACE, Le as RADIO_RING, Re as RADIO_RING_PRESSED, ke as RENAME_DELAY_MS, me as RING_FRAME, he as RING_HOLE, ge as RING_INSET, _e as SCREEN_CORNER, ze as SLIDER_THUMB, Be as SLIDER_THUMB_FACE, Ve as STEPPER, He as STEPPER_DOWN_FILL, Ue as STEPPER_UP_FILL, i as SYSTEM_PX_IN_CSS_PX, u as ScaleController, Ge as ScrollRailController, Te as ScrollStateController, L as TILE_LATTICE, ie as TileRasterCache, gt as TrackWidthController, O as VF_BODY_FAMILY, j as VF_DISPLAY_FAMILY, tt as VfButton, Ze as VfButtonGroup, it as VfCheckbox, jt as VfContainer, xe as VfDesktop, Qe as VfDialog, Ot as VfFieldset, et as VfFormControl, kt as VfGrid, St as VfIcon, xt as VfImg, yt as VfLabel, Et as VfList, Dt as VfListItem, wt as VfMenu, Ct as VfMenuBar, Tt as VfMenuItem, Ye as VfModalDialog, pt as VfNumberField, mt as VfOption, bt as VfParagraph, T as VfPositioned, _t as VfProgressBar, at as VfRadio, ot as VfRadioGroup, qe as VfScrollArea, ht as VfSelect, $e as VfSeparator, Se as VfSized, vt as VfSlider, At as VfStack, nt as VfSwatch, ft as VfTextArea, ut as VfTextControlBase, dt as VfTextField, rt as VfToggleControl, Je as VfWindow, It as applyCursor, le as applyGridSnap, d as applyScale, e as defineElement, a as devicePxPerSystemPx, f as effectiveScale, we as emit, ct as focusModality, p as getScale, o as getZoom, We as glyphSvg, Xe as modalDialogStyles, m as onScaleChange, s as onZoomChange, ae as patternOverride, Ae as prefersReducedMotion, k as registerBodyFace, M as registerDisplayFace, D as registerEmbeddedFont, Ke as renderScrollRail, ue as requestGridSnap, c as resetZoomBaseline, je as runSelectionBlink, h as snapSys, g as snapToDevicePx, _ as snapToSystemPx, ve as steppedCornerClip, ye as steppedRectClip, be as steppedRingClip, v as sys, y as sysLength, b as sysLengths, x as systemPxQuantum, oe as tileGrid, R as tileImage, z as tileRaster, B as tileRects, V as tileSpan, S as toSys, C as toSysExact, lt as trackFocusModality, l as truePixelRatio, F as vfBase, A as vfBodyDecls, q as vfChromeFrame, N as vfDisplay, P as vfDisplayDecls, W as vfDots, t as vfElement, ne as vfField, Q as vfFocus, $ as vfFocusRing, ee as vfFocusUnderline, K as vfHardShadowDecls, J as vfModalFrame, Y as vfPanel, re as vfScrollRail, I as vfStaticText, G as vfStripes, se as vfTileGrid, H as vfTileMaskSize, U as vfTileSize, X as vfTitleBar, te as vfToggle, Z as vfWindowWidgets };
22
+ import { GridSnapController as ce, requestGridSnap as le } from "./grid-snap.js";
23
+ import { DocumentListenersController as ue } from "./document-listeners.js";
24
+ import { BUTTON_FACE as de, BUTTON_FRAME as fe, RING_FRAME as pe, RING_HOLE as me, RING_INSET as he, SCREEN_CORNER as ge, steppedCornerClip as _e, steppedRectClip as ve, steppedRingClip as ye } from "./pixel-frame.js";
25
+ import { VfDesktop as be } from "./components/vf-desktop.js";
26
+ import { VfSized as xe } from "./size.js";
27
+ import { DragController as Se } from "./drag.js";
28
+ import { emit as Ce } from "./events.js";
29
+ import { ScrollStateController as we } from "./scroll-state.js";
30
+ import { BLINK_FLIPS as Te, BLINK_INTERVAL_MS as Ee, PRESS_HOLD_MS as De, RENAME_DELAY_MS as Oe, prefersReducedMotion as ke, runSelectionBlink as Ae } from "./motion.js";
31
+ import { CARET_DOWN as je, CHECKBOX_X as Me, CHECKMARK as Ne, RADIO_DOT as Pe, RADIO_FACE as Fe, RADIO_RING as Ie, RADIO_RING_PRESSED as Le, SLIDER_THUMB as Re, SLIDER_THUMB_FACE as ze, STEPPER as Be, STEPPER_DOWN_FILL as Ve, STEPPER_UP_FILL as He, glyphSvg as Ue } from "./glyphs.js";
32
+ import { ScrollRailController as We, renderScrollRail as Ge } from "./scroll-rail.js";
33
+ import { VfScrollArea as Ke } from "./components/vf-scroll-area.js";
34
+ import { VfWindow as qe } from "./components/vf-window.js";
35
+ import { VfModalDialog as Je, modalDialogStyles as Ye } from "./modal-dialog.js";
36
+ import { VfButtonGroup as Xe } from "./components/vf-button-group.js";
37
+ import { VfDialog as Ze } from "./components/vf-dialog.js";
38
+ import { VfSeparator as Qe } from "./components/vf-separator.js";
39
+ import { VfFormControl as $e } from "./form-control.js";
40
+ import { VfButton as et } from "./components/vf-button.js";
41
+ import { VfSwatch as tt } from "./components/vf-swatch.js";
42
+ import { VfToggleControl as nt } from "./toggle-control.js";
43
+ import { VfCheckbox as rt } from "./components/vf-checkbox.js";
44
+ import { VfRadio as it } from "./components/vf-radio.js";
45
+ import { VfRadioGroup as at } from "./components/vf-radio-group.js";
46
+ import { FocusRuleController as ot, focusModality as st, trackFocusModality as ct } from "./focus-modality.js";
47
+ import { VfTextControlBase as lt } from "./text-control.js";
48
+ import { VfTextField as ut } from "./components/vf-text-field.js";
49
+ import { VfTextArea as dt } from "./components/vf-text-area.js";
50
+ import { VfNumberField as ft } from "./components/vf-number-field.js";
51
+ import { VfOption as pt } from "./components/vf-option.js";
52
+ import { VfSelect as mt } from "./components/vf-select.js";
53
+ import { TrackWidthController as ht } from "./track-width.js";
54
+ import { VfProgressBar as gt } from "./components/vf-progress-bar.js";
55
+ import { VfSlider as _t } from "./components/vf-slider.js";
56
+ import { VfLabel as vt } from "./components/vf-label.js";
57
+ import { VfParagraph as yt } from "./components/vf-paragraph.js";
58
+ import { VfImg as bt } from "./components/vf-img.js";
59
+ import { VfIcon as xt } from "./components/vf-icon.js";
60
+ import { VfMenuBar as St } from "./components/vf-menu-bar.js";
61
+ import { VfMenu as Ct } from "./components/vf-menu.js";
62
+ import { VfMenuItem as wt } from "./components/vf-menu-item.js";
63
+ import { VfList as Tt } from "./components/vf-list.js";
64
+ import { VfListItem as Et } from "./components/vf-list-item.js";
65
+ import { VfFieldset as Dt } from "./components/vf-fieldset.js";
66
+ import { VfGrid as Ot } from "./components/vf-grid.js";
67
+ import { VfStack as kt } from "./components/vf-stack.js";
68
+ import { VfContainer as At } from "./components/vf-container.js";
69
+ import { CURSOR_ARROW as jt, CURSOR_CROSSHAIR as Mt, CURSOR_I_BEAM as Nt, CURSOR_WAIT as Pt } from "./cursor-art.js";
70
+ import { applyCursor as Ft } from "./cursor.js";
71
+ export { Te as BLINK_FLIPS, Ee as BLINK_INTERVAL_MS, de as BUTTON_FACE, fe as BUTTON_FRAME, je as CARET_DOWN, Me as CHECKBOX_X, Ne as CHECKMARK, n as CLASSIC_DPI, r as CSS_REFERENCE_DPI, jt as CURSOR_ARROW, Mt as CURSOR_CROSSHAIR, Nt as CURSOR_I_BEAM, Pt as CURSOR_WAIT, ue as DocumentListenersController, Se as DragController, ot as FocusRuleController, ce as GridSnapController, E as PIXEL_GRID_METRICS, De as PRESS_HOLD_MS, w as PlacementController, Pe as RADIO_DOT, Fe as RADIO_FACE, Ie as RADIO_RING, Le as RADIO_RING_PRESSED, Oe as RENAME_DELAY_MS, pe as RING_FRAME, me as RING_HOLE, he as RING_INSET, ge as SCREEN_CORNER, Re as SLIDER_THUMB, ze as SLIDER_THUMB_FACE, Be as STEPPER, Ve as STEPPER_DOWN_FILL, He as STEPPER_UP_FILL, i as SYSTEM_PX_IN_CSS_PX, u as ScaleController, We as ScrollRailController, we as ScrollStateController, L as TILE_LATTICE, ie as TileRasterCache, ht as TrackWidthController, O as VF_BODY_FAMILY, j as VF_DISPLAY_FAMILY, et as VfButton, Xe as VfButtonGroup, rt as VfCheckbox, At as VfContainer, be as VfDesktop, Ze as VfDialog, Dt as VfFieldset, $e as VfFormControl, Ot as VfGrid, xt as VfIcon, bt as VfImg, vt as VfLabel, Tt as VfList, Et as VfListItem, Ct as VfMenu, St as VfMenuBar, wt as VfMenuItem, Je as VfModalDialog, ft as VfNumberField, pt as VfOption, yt as VfParagraph, T as VfPositioned, gt as VfProgressBar, it as VfRadio, at as VfRadioGroup, Ke as VfScrollArea, mt as VfSelect, Qe as VfSeparator, xe as VfSized, _t as VfSlider, kt as VfStack, tt as VfSwatch, dt as VfTextArea, lt as VfTextControlBase, ut as VfTextField, nt as VfToggleControl, qe as VfWindow, Ft as applyCursor, d as applyScale, e as defineElement, a as devicePxPerSystemPx, f as effectiveScale, Ce as emit, st as focusModality, p as getScale, o as getZoom, Ue as glyphSvg, Ye as modalDialogStyles, m as onScaleChange, s as onZoomChange, ae as patternOverride, ke as prefersReducedMotion, k as registerBodyFace, M as registerDisplayFace, D as registerEmbeddedFont, Ge as renderScrollRail, le as requestGridSnap, c as resetZoomBaseline, Ae as runSelectionBlink, h as snapSys, g as snapToDevicePx, _ as snapToSystemPx, _e as steppedCornerClip, ve as steppedRectClip, ye as steppedRingClip, v as sys, y as sysLength, b as sysLengths, x as systemPxQuantum, oe as tileGrid, R as tileImage, z as tileRaster, B as tileRects, V as tileSpan, S as toSys, C as toSysExact, ct as trackFocusModality, l as truePixelRatio, F as vfBase, A as vfBodyDecls, q as vfChromeFrame, N as vfDisplay, P as vfDisplayDecls, W as vfDots, t as vfElement, ne as vfField, Q as vfFocus, $ as vfFocusRing, ee as vfFocusUnderline, K as vfHardShadowDecls, J as vfModalFrame, Y as vfPanel, re as vfScrollRail, I as vfStaticText, G as vfStripes, se as vfTileGrid, H as vfTileMaskSize, U as vfTileSize, X as vfTitleBar, te as vfToggle, Z as vfWindowWidgets };
@@ -11,8 +11,17 @@ export declare abstract class VfPositionedInterface extends LitElement {
11
11
  left?: number | null;
12
12
  }
13
13
  /**
14
- * Explicit placement — `top`/`left` in whole system px, on (nearly) every
15
- * component.
14
+ * Explicit placement — `top`/`left` in whole system px, on **every** component.
15
+ *
16
+ * No exceptions, deliberately. These are web components, and a consumer may put
17
+ * one wherever they like; the kit does not get to decide that a `vf-option` is
18
+ * only ever a popup row. The rows a container normally owns (`vf-option`,
19
+ * `vf-menu-item`, `vf-list-item`) and a bar's `vf-menu` take the pair on the
20
+ * same terms as anything else — and stating an origin on one *inside* its
21
+ * managing parent does take it out of that parent's flow layout, which is the
22
+ * placement working, not failing. Each of those components documents what its
23
+ * container stops doing for it. `vf-dialog` takes the pair in viewport
24
+ * coordinates, the one difference the platform forces (see modal-dialog.ts).
16
25
  *
17
26
  * A DITL resource laid a dialog out as a list of items, each with a rectangle
18
27
  * in the window's own coordinates; arranging controls by stating where they go
@@ -51,7 +51,20 @@ var n = t`
51
51
  painted element(s). Absolutely positioned satellites that anchor to the
52
52
  host (vf-menu's panel, the default button's ring) compose the same
53
53
  variables into their own insets instead. Controller-owned — never set the
54
- variables by hand. */
54
+ variables by hand.
55
+
56
+ The reset below makes the two properties per-host: custom properties
57
+ inherit, so without it a host needing no correction of its own would
58
+ resolve an ancestor component's values here and its paint would leave the
59
+ grid by exactly that amount — invisibly, since the controller measures
60
+ the host box, which never moves. (Measured: a placed vf-button on a
61
+ corrected vf-desktop painted at the desktop's -0.39px offset.) The
62
+ controller's writes land on the host's inline style, which wins over
63
+ this rule. */
64
+ :host {
65
+ --vf-snap-dx: 0px;
66
+ --vf-snap-dy: 0px;
67
+ }
55
68
  .vf-snap {
56
69
  position: relative;
57
70
  left: var(--vf-snap-dx, 0px);
@@ -7,6 +7,22 @@ var t = e`
7
7
  gap: calc(var(--vf-scale, 1) * 6px);
8
8
  cursor: var(--vf-cursor, default);
9
9
  }
10
+ /* The well PAINTS 3 system px below the row top. Centering the 13px well
11
+ in the 20px row is the layout — kept, because the host's height and its
12
+ exported baseline are consumer-visible (moving them off the well's
13
+ centered bottom reflowed every line holding a toggle) — but centering
14
+ lands on 3.5, an exact tie, half a system px off the pixel grid at every
15
+ density where 3.5 system px isn't whole device px. So the paint steps
16
+ back half a pixel through the same relative "top" the grid-snap
17
+ correction rides (this rule overrides vfBase's .vf-snap, so it must
18
+ compose the variable), landing the ink on row 3: ties resolve toward the
19
+ start, as QuickDraw's "div 2" did — the title-bar and vf-stack
20
+ convention. The half-pixel gap between layout box and painted box is the
21
+ documented snap idiom, not a fault. */
22
+ .box,
23
+ .circle {
24
+ top: calc(var(--vf-snap-dy, 0px) - var(--vf-scale, 1) * 0.5px);
25
+ }
10
26
  /* The focus rule is drawn under the box/circle, not around the host. */
11
27
  :host(:focus-visible) {
12
28
  outline: none;
@@ -69,7 +69,7 @@ export declare class VfTextControlBase extends VfShadowRoleControl {
69
69
  protected forwardedAttr(name: string): string | typeof nothing;
70
70
  /** Default-on display scaling (true 72dpi size); see src/scale.ts. */
71
71
  protected readonly scale: ScaleController;
72
- /** Device-pixel grid snapping (opt in with applyGridSnap()); see src/grid-snap.ts. */
72
+ /** Device-pixel grid snapping; see src/grid-snap.ts. */
73
73
  protected readonly gridSnap: GridSnapController;
74
74
  /**
75
75
  * Whether the well wears the kit's dashed focus rule — keyboard focus only.
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 free-standing 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. Excluded: the owned rows/options of a managing container (`vf-option`, `vf-menu-item`, `vf-list-item`, `vf-menu`). 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; 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`.
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.
@@ -121,7 +121,7 @@ Every length in this doc is a **system pixel** value; components multiply it by
121
121
 
122
122
  **One size.** Both embedded faces are single 16-design-px masters and render at exactly that size — one design px = one system px, always. "Smaller" is a *family* switch: System 7's fine print was Geneva 9 — the collection's smallest strike, which **is** the body face — so a dialog's disk-space caption is `face="body"` (usually `dim`), the way `vf-icon size="small"` swaps to the 16×16 art. A genuinely different size is a different strike, registered like the embedded ones and themed in through the font family/size tokens with its own whole-pixel metrics.
123
123
 
124
- **Grid snapping.** Whole system pixels only put an edge on the device grid *relative to the component's own origin*; a page that lands that origin on a fractional device pixel smears the whole 1-bit interior. `applyGridSnap()` (`src/grid-snap.ts`) opts a page into having every component measure its own paint and cancel the fractional remainder, so the origin is the component's responsibility rather than the page's. The correction is a `left`/`top` offset on the component's own paint root — the shadow element carrying the `.vf-snap` class (`vfBase`), relatively positioned, driven by two reserved custom properties (`--vf-snap-dx`/`-dy`) the controller writes on the host. A *layout*-stage shift, because a compositing-stage `transform` leaves the subtree rastered at its old position and only removes ~80% of the fringe. Nothing outside the shadow root is written but those two properties, so the correction cannot collide with a consumer's positioning or with `vf-window`'s drag coordinates. Absolutely positioned satellites that anchor to the host (`vf-menu`'s panel, the default button's ring) compose the same properties into their insets; rows and options inside a corrected container (`vf-list-item`, `vf-menu-item`, `vf-option`) and layout-only hosts (`vf-button-group`, `vf-radio-group`) carry no target and ride their surroundings. It corrects the origin only: a fractional *size*, and a `--vf-scale × trueDpr` that isn't whole, are still the page's to get right.
124
+ **Grid snapping.** Whole system pixels only put an edge on the device grid *relative to the component's own origin*; a page that lands that origin on a fractional device pixel smears the whole 1-bit interior. Every component measures its own paint and cancels the fractional remainder, always (`src/grid-snap.ts`), so the origin is the component's responsibility rather than the page's. The correction is a `left`/`top` offset on the component's own paint root — the shadow element carrying the `.vf-snap` class (`vfBase`), relatively positioned, driven by two reserved custom properties (`--vf-snap-dx`/`-dy`) the controller writes on the host. A *layout*-stage shift, because a compositing-stage `transform` leaves the subtree rastered at its old position and only removes ~80% of the fringe. Nothing outside the shadow root is written but those two properties, so the correction cannot collide with a consumer's positioning or with `vf-window`'s drag coordinates. Absolutely positioned satellites that anchor to the host (`vf-menu`'s panel, the default button's ring) compose the same properties into their insets; rows and options inside a corrected container (`vf-list-item`, `vf-menu-item`, `vf-option`) and layout-only hosts (`vf-button-group`, `vf-radio-group`) carry no target and ride their surroundings. The two properties are reset to `0` per host (`vfBase`): custom properties inherit, and without the reset a host needing no correction of its own would resolve an ancestor component's values in its shadow root and paint off the grid by exactly that amount. It corrects the origin only: a fractional *size*, and a `--vf-scale × trueDpr` that isn't whole, are still the page's to get right.
125
125
 
126
126
  **Tiled fills.** A repeating `background-image` is the one place where "one system pixel is a whole number of device pixels" is not enough on its own. That contract holds — but the CSS *length* expressing it need not be one the engine can store: Chromium lays out in 1/64 CSS px (Gecko in 1/60), and at `--vf-scale` 4/3 a 2-system-px tile is 2.6667 CSS px, held as 2.65625. A single edge survives that, because paint snaps each box to the device grid on its own — which is why every border, stepped corner and magnified icon rasterizes exactly at 4/3. A tiled fill does not: it is ONE snapped box holding N *unsnapped* repeats, each placed at `k × tileSize`, so the error compounds until the tile boundary has walked a whole device pixel. Unfixed, 75% of the desktop dither rasterizes to mid-gray at dpr 1.5.
127
127
 
@@ -273,7 +273,7 @@ The color-swatch button: a well of solid color — a palette cell.
273
273
 
274
274
  #### `vf-checkbox` (`VfCheckbox`, vf-checkbox.ts)
275
275
  - **Attributes/props:** `checked`, `disabled`, `name`, `value` (default `'on'`).
276
- - **Visual:** 13×13 white box, `1px solid black`, no radius; checked = classic ✕: the pixel-exact corner-to-corner cross from the sprite sheet, rendered as the `CHECKBOX_X` inline-SVG fill path (`shape-rendering: crispEdges`, `fill: currentColor`) — no anti-aliased strokes. Label (slot) sits right with 6px gap, bold. Disabled: only the label dims to `var(--vf-disabled, #c0c0c0)`; the box border and ✕ glyph stay black. Pressed (`:active` on box): border thickens to 2px (classic press feedback).
276
+ - **Visual:** 13×13 white box, `1px solid black`, no radius; checked = classic ✕: the pixel-exact corner-to-corner cross from the sprite sheet, rendered as the `CHECKBOX_X` inline-SVG fill path (`shape-rendering: crispEdges`, `fill: currentColor`) — no anti-aliased strokes. The box PAINTS 3 system px below the row top: centering the 13px box in the 20px row stays the layout (host height and exported baseline unchanged), but its 3.5 tie is half a system px off the pixel grid, so the paint steps back half a pixel through the same relative `top` the grid-snap correction rides — ties resolve toward the start, QuickDraw `div 2`. Label (slot) sits right with 6px gap, bold. Disabled: only the label dims to `var(--vf-disabled, #c0c0c0)`; the box border and ✕ glyph stay black. Pressed (`:active` on box): border thickens to 2px (classic press feedback).
277
277
  - **Behavior:** form-associated; toggles on click and Space; `role="checkbox"`, `aria-checked`; focusable (tabindex 0 on the host).
278
278
  - **When the toggle runs.** At the *end* of the click's propagation, like a native checkbox's activation behavior — so `preventDefault()` on the control or anything above it stops the state change, in either phase. `stopPropagation()` cancels nothing (HTML doesn't), and the disabled gate sits at the far end of the deferral, so a control disabled *during* propagation never acts. Space synthesises a cancelable click and inherits all of it. Shared with `vf-radio` through `VfToggleControl`, and with `vf-button` and the menus through `deferActivation` (src/events.ts). `npm run verify:toggle`, CANCELLATION group. `:focus-visible` marks the **box**, not the label and not either with a ring: `vfFocusUnderline` (§4) at `--vf-focus-underline-offset: -3px`, a dashed rule spanning the well's full 13px, one blank row under its border. The −3 and the ±1px width growth both count that 1px border, which an absolutely positioned pseudo sizes inside of. `npm run verify:focus`.
279
279
  - **Slots:** default (label). **Parts:** `box`, `label`.
@@ -281,7 +281,7 @@ The color-swatch button: a well of solid color — a palette cell.
281
281
 
282
282
  #### `vf-radio` (`VfRadio`, vf-radio.ts)
283
283
  - **Attributes/props:** `checked`, `disabled`, `value: string`.
284
- - **Visual:** 13×13 pixel circle drawn as inline SVG — the hand-tuned 1-bit `RADIO_RING` outline over a white `RADIO_FACE` disc (not `border-radius`, which anti-aliases); checked = the centered `RADIO_DOT` pixel disc. Pressed (`:active`): the ring swaps to `RADIO_RING_PRESSED` (2px-thick). Label right, 6px gap. Disabled dims like checkbox (label only; ring + dot stay black).
284
+ - **Visual:** 13×13 pixel circle drawn as inline SVG — the hand-tuned 1-bit `RADIO_RING` outline over a white `RADIO_FACE` disc (not `border-radius`, which anti-aliases); checked = the centered `RADIO_DOT` pixel disc. Pressed (`:active`): the ring swaps to `RADIO_RING_PRESSED` (2px-thick). The circle paints at the same 3-system-px registration below the row top as the checkbox's box (same half-pixel paint step off the centering tie). Label right, 6px gap. Disabled dims like checkbox (label only; ring + dot stay black).
285
285
  - **Behavior:** `role="radio"`, `aria-checked`. Click → asks parent group to select it (dispatch internal event or parent listens). NOT itself form-associated — the group is. Selection defers to the end of the click's propagation and is cancellable exactly as `vf-checkbox`'s is (same `VfToggleControl` skeleton). `:focus-visible` marks the **circle** with `vfFocusUnderline` (§4) at `--vf-focus-underline-offset: -2px` — one blank row below the same 13px well the checkbox uses, so the two rules share a row in a mixed list (the −1 difference is only that this well has no border). The 12px sprite sits half a system px proud of the well, so the gap to the circle itself reads as one row or two depending on how that rounds; the well is the anchor. The rule is also narrowed to **9** of the well's 13px (2px inset each side, 5 dashes): full width reads wider than the round shape above it, and 9 is the closest to two thirds that keeps whole-px insets and ink at both ends. Focus: inside a `vf-radio-group` the group owns the roving tabindex and is the single source of truth for `checked`; standalone, the radio self-manages its own tabindex (otherwise it would be keyboard-dead) and self-checks on activation. A consumer-authored `tabindex` always wins, and that ownership is latched on first connect so a reconnect can't mistake our own write for the consumer's.
286
286
  - **Slots:** default (label). **Parts:** `circle`, `label`.
287
287
  - **Events:** `vf-change` detail `{ value }` (fired by user interaction only).
@@ -390,7 +390,7 @@ A container whose scrollbars look like System 7.
390
390
  #### `vf-fieldset` (`VfFieldset`, vf-fieldset.ts)
391
391
  The "Install Location" group box.
392
392
  - **Attributes/props:** `legend: string`.
393
- - **Visual:** `border: 1px solid var(--vf-black, #000)`, no radius, `padding: 14px 12px 10px`, `margin-top: 8px` (room for legend). Legend: bold, positioned overlapping the top border (absolute, `top: -0.7em; left: 8px;`), `padding: 0 5px`, `background: var(--vf-surface, var(--vf-white, #fff))` so it punches out the border to match its surface.
393
+ - **Visual:** `border: 1px solid var(--vf-black, #000)`, no radius, `padding: 14px 12px 10px`, `margin-top: 8px` (room for legend). Legend: bold, positioned overlapping the top border (absolute, `top: -11px; left: 8px;` — whole system px, so a slotted `vf-label` legend lands on the device-pixel grid), `padding: 0 5px`, `background: var(--vf-surface, var(--vf-white, #fff))` so it punches out the border to match its surface.
394
394
  - **Slots:** default, plus named slot `legend` (overrides attr).
395
395
  - **Parts:** `fieldset`, `legend`.
396
396
 
@@ -409,14 +409,15 @@ A lattice of equal cells with 1px rules between them: the Figure 5-6 tool palett
409
409
  The kit's layout primitive: a flexbox whose `gap`, `pad`, `width` and `height` are declared in whole system px and converted internally, so a window's insides are laid out in the art's own unit with no `calc(var(--vf-scale, 1) * Npx)` in the consumer's stylesheet.
410
410
  - **Why a component and not a documented snippet:** scaling is default-on and *per component* — `ScaleController` sets `--vf-scale` on the component's own host, never on the document — so `var(--vf-scale, 1)` in page CSS resolves only where the rule's element happens to sit inside a `vf-*` ancestor and inherit it. Inside a window body it does (which is the only reason the demo's own hand-written rules ever worked); for a plain `<div>` holding two buttons on an ordinary page it does not, and the fallback `1` silently renders an 8px gap around 3×-sized controls. A page that hasn't called `applyScale()` cannot express "8 system px" at all. A component always can, because it *is* the scope — which is also why the stack carries a `ScaleController` of its own.
411
411
  - **Attributes/props:** `direction: 'column' | 'row'` (default `'column'`, reflected), `gap: number` (whole system px, default `0`), `pad: string | number` (whole system px, one to four values in CSS shorthand order), `place: 'start' | 'center' | 'end'` (reflected; unset resolves per direction), `width` / `height`: number (whole system px, optional). Whole system px is the only expressible value — a fractional entry is truncated — so the gap half of the layout contract (docs/SIZING.md rule 2) holds by construction, and a declared `width` covers the size half of rule 3. On a **child**: `fill-width` and `fill-height`, bare attributes like `nosnap`.
412
- - **Visual:** none. The stack paints nothing — no border, background or shadow — and takes no role, no keyboard behavior and no selection; what it holds decides what it is, as with `vf-grid`. `:host` is a plain block shell (`width: fit-content`); the flex container is one shadow box coinciding with the host box (`.vf-snap`, `height: 100%` so a declared or filled host height reaches the flex layout) — the `vf-container` arrangement, with the placed-child anchor and the grid-snap target on the same element. The box carries no padding until `pad` writes some, so its padding box IS the host box and the placed-child anchor ignores `pad` exactly as before (`verify:position` pins this). `gap`/`pad` are written to that box's inline style as `calc(var(--vf-scale, 1) * Npx)` via `sysLength`/`sysLengths` (the var resolves against the host's own `--vf-scale` by inheritance); `width`/`height` stay on the host via `VfSized` — each stays live against the display rather than freezing at write time. **Carries a `GridSnapController`** — a reversal of the original no-controller decision, which accounted only for kit children: a stack is a positioned ancestor and a layout box for *anything*, and consumer content inside it (a div, an `<img>`, a run of text) has no controller of its own. Under `applyGridSnap()` the correction moves the stack's whole coordinate system; kit children then find nothing left to correct (the sweep runs outermost-first). It does not fix what it never could: a text-governed child width mid-row still shifts later siblings fractionally (their own controllers cover that), and centering still can't land on a whole pixel.
412
+ - **Visual:** none. The stack paints nothing — no border, background or shadow — and takes no role, no keyboard behavior and no selection; what it holds decides what it is, as with `vf-grid`. `:host` is a plain block shell (`width: fit-content`); the flex container is one shadow box coinciding with the host box (`.vf-snap`, `height: 100%` so a declared or filled host height reaches the flex layout) — the `vf-container` arrangement, with the placed-child anchor and the grid-snap target on the same element. The box carries no padding until `pad` writes some, so its padding box IS the host box and the placed-child anchor ignores `pad` exactly as before (`verify:position` pins this). `gap`/`pad` are written to that box's inline style as `calc(var(--vf-scale, 1) * Npx)` via `sysLength`/`sysLengths` (the var resolves against the host's own `--vf-scale` by inheritance); `width`/`height` stay on the host via `VfSized` — each stays live against the display rather than freezing at write time. **Carries a `GridSnapController`** — a reversal of the original no-controller decision, which accounted only for kit children: a stack is a positioned ancestor and a layout box for *anything*, and consumer content inside it (a div, an `<img>`, a run of text) has no controller of its own. The correction moves the stack's whole coordinate system; kit children then find nothing left to correct (the sweep runs outermost-first). It does not fix what it never could: a text-governed child width mid-row still shifts later siblings fractionally (their own controllers cover that).
413
413
  - **The content governs the box.** A column is as wide as its widest child and a row as tall as its tallest; children neither grow nor shrink (`::slotted(*) { flex: 0 0 auto }`). System 7 controls keep their natural sizes — a push button is as wide as its label, a popup menu hugs its widest option, a swatch is a fixed well — and a window is a fixed box whose overflow is clipped at the frame, not a layout that squeezes its controls. The stack distributes; it never resizes. `width: fit-content` is the same rule stated in the box model: a layout box that claimed its parent's whole width would be handing out a size nobody declared. It shrink-wraps while staying **block-level**, which is deliberate — `inline-flex` gives the same width but puts the box on a line box, and a line box can never be shorter than its parent's strut, so a stack shorter than the surrounding `line-height` silently gains the difference as leading (the showcase's swatch panel, an 18px row in a 20px line box, grew by exactly that). `width`/`height` override the content when an author says so, in system px, and beat `fit-content` from the host's inline style.
414
414
  - **`place` defaults per direction** — `start` down a column (a field starts at the panel edge), `center` across a row (a caption sits beside its control). The two directions call for opposite defaults. Both are stated as the direction's own rather than as an `auto` value, so an unrecognized `place` — a stale `stretch` from before this API — lands on the sane one instead of on flexbox's `normal`, which stretches. It is the only placement the stack owns: there is no `justify`, and a right-aligned action row is a filled column whose one child sits at the end of it. **It is `place` and not `align` because `align` is a legacy HTML presentation attribute** — see the transparency bullet below.
415
415
  - **`fill-width` / `fill-height` name the outcome, not an axis**, so each compiles to the main axis (`flex: 1 1 0` plus the `min-*: 0` that lets it shrink below its content) or the cross axis (`align-self: stretch`) depending on which way the stack runs — four static rules, no allowlist. What follows is one rule about geometry rather than vocabulary: **the cross axis always has a size, the main axis only has slack if one was declared.** `fill-width` always works in a column and needs a declared `width` in a row; `fill-height` is the other way round; a fill with nothing to take is inert rather than an error; and two children filling along the main axis come out equal, the zeroed flex basis being what lets them divide the slack instead of keeping their natural sizes. A child that declares its own size shouldn't also ask to fill it — in a column the declared size wins, in a row the fill does. The host reads both attributes about **itself** too (`width`/`height: 100%`), for the parents that aren't stacks — a window body, a fieldset, a scroll well, a grid cell — which is where a panel's width enters the tree. Three components have no width of their own and need a fill to take one: `vf-separator`, `vf-progress-bar` and `vf-slider` are drawn as a rule or a track that *is* the width. A light-DOM declaration beats a `::slotted` one, so `align-self: stretch` in page CSS remains the escape hatch for a cross-axis fill a direction doesn't offer.
416
416
  - **Typographically transparent.** `vfBase` dresses a host as chrome (body face, a 1.25 ratio line box, black, unselectable); the stack returns `font`, `-webkit-font-smoothing`, `color`, `user-select` and `text-align` to `inherit`, because wrapping content in a layout box must not change how that content reads. `text-align` is on that list because **`align` is a legacy HTML presentation attribute**: Blink maps the `align` content attribute on any HTML element to `text-align` — "left" / "right" / "center" by name, anything else verbatim — so `align="end"` on an action row right-aligned every run of copy inside it. Renaming the attribute to `place` is the fix; the reset stays because it costs nothing and keeps markup written against the old spelling harmless. The hint loses to a `:host` rule, and the page's own `text-align` still inherits through (`verify:stack` asserts all three). Of every attribute name the kit uses, `align` was the only one that carried a hint — `width`, `height`, `color`, `size`, `direction`, `label` and `value` are all inert on a custom element. Inside a window it goes on inheriting the window's face and the SPEC §1 chrome selection rule; on an ordinary page it leaves the page's typography — and its whole-pixel line boxes — alone. A ratio line-height landing on slotted prose is the exact rule-2 fault the kit warns pages about.
417
417
  - **The gap is the real CSS property, not a token.** Written as a custom property it would inherit, and a nested stack would silently pick up its parent's spacing (`--vf-button-group-gap` inherits on purpose; this must not).
418
418
  - **What it deliberately doesn't do:** equalize its children's widths. A row of buttons still belongs in a `vf-button-group`, which sizes them to the widest and aligns their *faces* rather than the `variant="default"` ring boxes a plain flex row lines up. It is also not a grid: `grid-template-columns: 1fr auto 1fr` stays page CSS.
419
- - **Two traps it does not fix.** Centering cannot land on a whole pixel by itself — a 16px caption centered against the 25-system-px `vf-number-field` sits at 4.5 system px, and no container can round that without reading each child's height (`applyGridSnap()` keeps the caption's own ink crisp anyway; `place="start"` is the deterministic escape; `hit-list.md` files the 25px height as the underlying fault). And a flex container does not collapse margins, so `vf-fieldset`'s legend rooman 8px `margin-top` on its inner box that escapes the host in block flow is genuinely reserved inside a stack instead of being donated by whatever precedes it.
419
+ - **Centering lands on whole system px** (`CrossCenterController`, src/cross-center.ts). Halving an odd count of free system px gives a half — a 16px caption centered against the 25-system-px `vf-number-field` sits at 4.5, a 71px button in a 200px stack at 64.5 — and that is half a *system* px, so the child's whole 1-bit interior misses the device grid rather than just its paint. No container can round it in CSS (percentages resolve against the container in `left`/`margin` and against the element itself only in a transform, and a fractional transform leaves the composite-time fringe grid-snap.ts measured), so the controller measures where flexbox put each child, relative to the box, and steps it back onto whole system px — the exact half going toward the start, as QuickDraw's `div 2` did. It measures rather than derives, because a stack holds anything, including a child whose own width isn't whole system px (`vf-select` is 88 at 1× and 87⅓ at 1.5×, where its metrics land on whole device px instead) deriving would move such a child *off* a grid position it already held. Its whole footprint on consumer DOM is `data-vf-tie` and `--vf-stack-dx` / `--vf-stack-dy` on a corrected child, applied by a `::slotted([data-vf-tie])` rule so that any inline style (a `vf-window` drag writes its own `left`) or page rule wins. Re-measured on every render and on any resize of the box or a child; `place="start"` is still the way to opt out of centering altogether. It does not touch a stack the page itself put on a fractional origin — that stays the page's fault (docs/SIZING.md rule 3) or grid snapping's to absorb.
420
+ - **One trap it does not fix.** A flex container does not collapse margins, so `vf-fieldset`'s legend room — an 8px `margin-top` on its inner box that escapes the host in block flow — is genuinely reserved inside a stack instead of being donated by whatever precedes it.
420
421
  - **Slots:** default (the children; `fill-width` / `fill-height` on any of them). **Parts:** none. **Events:** none.
421
422
 
422
423
  #### `vf-container` (`VfContainer`, vf-container.ts)
@@ -424,7 +425,7 @@ A plain sized box: `width`/`height` in whole system px around a bare slot — no
424
425
  - **Attributes/props:** `width` / `height`: number (whole system px, via `VfSized`); the `top` / `left` pair via `VfPositioned` like nearly every component. On a **child**: `fill-width` and `fill-height`, bare attributes as in `vf-stack`. That is the whole API.
425
426
  - **Not a `vf-stack`.** The stack is a flexbox with opinions — an axis, a cross-axis default, fills compiled into flex. The container has none: in-flow children get normal flow, placed children get a coordinate system. Reach for it when the stack's opinions are the thing in the way — a field of placed icons, a fixed stage for placed art, a composition that brings its own layout with it.
426
427
  - **Visual:** none — no border, background, role, keyboard behavior or selection; what it holds decides what it is. `:host` is a block with `width: fit-content`, so an undeclared axis shrink-wraps rather than claiming the parent's width (the stack's rule — a layout box must not hand out a size nobody declared; a declared dimension lands on the host's inline style and beats it). The slot sits in one shadow box coinciding with the host box (`.vf-snap`, `display: flow-root`, `height: 100%`): flow-root so a slotted margin cannot collapse through the top edge and push the coordinate origin off the host's corner, 100% so percentage fills resolve against a declared height. That box owns `position: relative` — the anchor for `top`/`left` children — the component's purpose. Content that outgrows the declared box overflows it rather than growing it.
427
- - **Carries a `GridSnapController`.** A container's box is itself the consumer's coordinate system, including for non-`vf` content that cannot correct itself. The shadow box owns the positioning anchor and the `vf-snap` class *together*, so under `applyGridSnap()` the correction moves the whole coordinate system and everything placed against it rides along, kit or not. A declared size and whole-px `top`/`left` are on the grid by construction; the controller covers the origin the page contributes. (This is the arrangement `vf-stack` has since adopted too — one shadow box owning anchor and correction together.)
428
+ - **Carries a `GridSnapController`.** A container's box is itself the consumer's coordinate system, including for non-`vf` content that cannot correct itself. The shadow box owns the positioning anchor and the `vf-snap` class *together*, so the correction moves the whole coordinate system and everything placed against it rides along, kit or not. A declared size and whole-px `top`/`left` are on the grid by construction; the controller covers the origin the page contributes. (This is the arrangement `vf-stack` has since adopted too — one shadow box owning anchor and correction together.)
428
429
  - **Typographically transparent**, exactly as `vf-stack` and for the same reason: `font`, `-webkit-font-smoothing`, `color`, `user-select` and `text-align` return to `inherit`, so wrapping content in a sized box changes nothing about how that content reads.
429
430
  - **`fill-width` / `fill-height`** are read about the host (`width`/`height: 100%`, for a parent that can give the box a size) and compiled for slotted children as the percentage form — normal flow has no flex axes to translate onto. `width: 100%` always binds against the box; `height: 100%` binds only against a declared `height` (percentage-against-auto computes to auto), so a fill with nothing to take is inert, not an error. A light-DOM declaration beats the `::slotted` rule, as everywhere.
430
431
  - **Carries a `ScaleController`** for the stack's reason: a lone container on a plain page must resolve its declared size against the true scale, not the `var(--vf-scale, 1)` fallback its slotted children each escape on their own.
@@ -480,7 +481,7 @@ The Finder icon — pixel art in a reserved cell with its name on a plate below,
480
481
 
481
482
  The full-viewport System 7 desktop that used to close this spec — menu bar, nine windows, the composed alert, the utility palette, the Finder icons — is no longer part of this repo. It lives in **[aportilla/system7web](https://github.com/aportilla/system7web)** and consumes `vintage-frames` from npm, so it exercises the same published API a consumer gets rather than reaching into `src/`. Its clause-by-clause spec moved with it, to that repo's `docs/SPEC.md`.
482
483
 
483
- What the kit keeps as its own demo surface is `index.html`, the component reference (every element, its API, a live specimen of each state), and `blog.html`, the integration example the verify suite covers. See [DEVELOPING.md](DEVELOPING.md).
484
+ What the kit keeps as its own demo surface is `index.html`, the component reference (every element, its API, a live specimen of each state). See [DEVELOPING.md](DEVELOPING.md).
484
485
 
485
486
  <details>
486
487
  <summary>The original §7 clauses, for the record</summary>
@@ -154,7 +154,7 @@
154
154
  },
155
155
  {
156
156
  "name": "vf-container",
157
- "description": "`<vf-container>` — a box that is nothing but its declared size.\n\nThe kit's positioned-placement story (src/position.ts) ends with one line of\nCSS it can't write for you: children placed with `top`/`left` need a\npositioned ancestor, and while every kit container is one — a window body, a\nstack, a fieldset — a region of *your own* needs `position: relative` in a\nstylesheet. This component is that region as an element: declare `width` and\n`height` in whole system px, slot anything into it, place children against\nits origin. A DITL's enclosing rectangle, with nothing drawn in it.\n\n```html\n<vf-container width=\"200\" height=\"120\">\n <vf-icon label=\"System\" width=\"64\" top=\"8\" left=\"12\">…</vf-icon>\n <vf-icon label=\"Finder\" width=\"64\" top=\"8\" left=\"104\">…</vf-icon>\n</vf-container>\n```\n\nThe rectangle is the whole API — `width`/`height` here, plus the `top`/`left`\npair nearly every component takes (VfPositioned), so a container is\nitself placeable: inside a window, a desktop, or another container, at whole\nsystem px that keep its box on the device-pixel grid by construction.\n\n**It is not a `vf-stack`.** The stack is a flexbox with opinions — it\ndistributes children along an axis, compiles `fill-width`/`fill-height` into\nflex, defaults a cross-axis alignment. This box has no layout opinion at\nall: in-flow children get normal flow, placed children get a coordinate\nsystem, and that is the whole API. Reach for it when the stack's opinions\nare the thing in the way — a field of placed icons, a fixed stage for\nabsolutely-positioned art, a consumer's own composition that brings its\nlayout with it.\n\n**The declared size is the layout.** `width`/`height` land on the host as a\nlive `calc(var(--vf-scale, 1) * Npx)` (VfSized), so the box scales\nwith the display and sits on the device-pixel grid by construction. Content\nthat outgrows the box overflows it rather than growing it — the number is\nthe layout, and content that doesn't fit is a number to raise. Leave a\ndimension off and that axis shrink-wraps: `fit-content`, not the parent's\nwidth, because a layout box that silently claimed a size nobody declared\nwould be inventing one (the `vf-stack` rule, held here too).\n\n**It paints nothing and means nothing.** No border, background, role,\nkeyboard behavior or selection — what it holds decides what it is.\n\n**It holds its box on the device-pixel grid** — with a `GridSnapController`.\nA container's box is itself the consumer's coordinate system, including for\nnon-`vf` content that cannot correct itself, so the box is the thing to\nhold on the grid. The shadow box below owns the `position: relative` anchor\nand the `vf-snap` class together, so under `applyGridSnap()` the correction\nmoves the whole coordinate system — everything placed against it rides\nalong instead of being re-corrected child by child. (`vf-stack` shipped\nwithout a controller on the theory that slotted `vf-*` children correct\ntheir own origins; this component is where that theory's gap — consumer\ncontent — became visible, and the stack has since adopted the same\narrangement.)\n\nLike the stack it is **typographically transparent**: `vfBase`'s chrome\ndress is returned to `inherit` on the host, so wrapping content in a sized\nbox changes nothing about how that content reads.\n\n`fill-width` / `fill-height` work here the way they do everywhere: read\nabout the host (be as big as *its* parent allows), and compiled for slotted\nchildren — `width: 100%` in normal flow, so a child filling the cross of a\ndeclared box needs no stylesheet. A height fill needs a declared `height`\nto resolve against; with none it is inert, not an error.\n---\n\n\n### **Slots:**\n - _default_ - The content. In flow by default; `top`/`left` on a kit child places it against this box's origin. `fill-width` / `fill-height` on a child fills it to the declared box.",
157
+ "description": "`<vf-container>` — a box that is nothing but its declared size.\n\nThe kit's positioned-placement story (src/position.ts) ends with one line of\nCSS it can't write for you: children placed with `top`/`left` need a\npositioned ancestor, and while every kit container is one — a window body, a\nstack, a fieldset — a region of *your own* needs `position: relative` in a\nstylesheet. This component is that region as an element: declare `width` and\n`height` in whole system px, slot anything into it, place children against\nits origin. A DITL's enclosing rectangle, with nothing drawn in it.\n\n```html\n<vf-container width=\"200\" height=\"120\">\n <vf-icon label=\"System\" width=\"64\" top=\"8\" left=\"12\">…</vf-icon>\n <vf-icon label=\"Finder\" width=\"64\" top=\"8\" left=\"104\">…</vf-icon>\n</vf-container>\n```\n\nThe rectangle is the whole API — `width`/`height` here, plus the `top`/`left`\npair nearly every component takes (VfPositioned), so a container is\nitself placeable: inside a window, a desktop, or another container, at whole\nsystem px that keep its box on the device-pixel grid by construction.\n\n**It is not a `vf-stack`.** The stack is a flexbox with opinions — it\ndistributes children along an axis, compiles `fill-width`/`fill-height` into\nflex, defaults a cross-axis alignment. This box has no layout opinion at\nall: in-flow children get normal flow, placed children get a coordinate\nsystem, and that is the whole API. Reach for it when the stack's opinions\nare the thing in the way — a field of placed icons, a fixed stage for\nabsolutely-positioned art, a consumer's own composition that brings its\nlayout with it.\n\n**The declared size is the layout.** `width`/`height` land on the host as a\nlive `calc(var(--vf-scale, 1) * Npx)` (VfSized), so the box scales\nwith the display and sits on the device-pixel grid by construction. Content\nthat outgrows the box overflows it rather than growing it — the number is\nthe layout, and content that doesn't fit is a number to raise. Leave a\ndimension off and that axis shrink-wraps: `fit-content`, not the parent's\nwidth, because a layout box that silently claimed a size nobody declared\nwould be inventing one (the `vf-stack` rule, held here too).\n\n**It paints nothing and means nothing.** No border, background, role,\nkeyboard behavior or selection — what it holds decides what it is.\n\n**It holds its box on the device-pixel grid** — with a `GridSnapController`.\nA container's box is itself the consumer's coordinate system, including for\nnon-`vf` content that cannot correct itself, so the box is the thing to\nhold on the grid. The shadow box below owns the `position: relative` anchor\nand the `vf-snap` class together, so the correction\nmoves the whole coordinate system — everything placed against it rides\nalong instead of being re-corrected child by child. (`vf-stack` shipped\nwithout a controller on the theory that slotted `vf-*` children correct\ntheir own origins; this component is where that theory's gap — consumer\ncontent — became visible, and the stack has since adopted the same\narrangement.)\n\nLike the stack it is **typographically transparent**: `vfBase`'s chrome\ndress is returned to `inherit` on the host, so wrapping content in a sized\nbox changes nothing about how that content reads.\n\n`fill-width` / `fill-height` work here the way they do everywhere: read\nabout the host (be as big as *its* parent allows), and compiled for slotted\nchildren — `width: 100%` in normal flow, so a child filling the cross of a\ndeclared box needs no stylesheet. A height fill needs a declared `height`\nto resolve against; with none it is inert, not an error.\n---\n\n\n### **Slots:**\n - _default_ - The content. In flow by default; `top`/`left` on a kit child places it against this box's origin. `fill-width` / `fill-height` on a child fills it to the declared box.",
158
158
  "attributes": [
159
159
  {
160
160
  "name": "width",
@@ -470,7 +470,7 @@
470
470
  },
471
471
  {
472
472
  "name": "vf-list-item",
473
- "description": "`<vf-list-item>` — a row inside a `<vf-list>` list box.\n\nA 20px-tall single-line row; when selected the entire row inverts\n(white-on-black), the classic System 7 selection style. Selection and\nkeyboard focus are managed by the parent `<vf-list>`.\n---\n\n\n### **Slots:**\n - _default_ - The row's text/content.\n- **icon** - A leading graphic — usually a `vf-img` holding a 16×16 System 7 small icon, but any element rides here. The row lays it out as the icon gutter: flex-centered vertically (keep the difference between the row height and the icon height even, so the centering offset stays a whole pixel — 16 in a 20px row is a whole 2px), with the reference art's 4px gap to the text. Contributes no text, so first-letter type-ahead still reads the row's words. On a selected row the graphic rides the inverted bar as-is — System 7 left color icons unfiltered on the highlight.\n\n### **CSS Parts:**\n - **text** - The text span beside the icon gutter (ellipsizes).",
473
+ "description": "`<vf-list-item>` — a row inside a `<vf-list>` list box.\n\nA 20px-tall single-line row; when selected the entire row inverts\n(white-on-black), the classic System 7 selection style. Selection and\nkeyboard focus are managed by the parent `<vf-list>`.\n---\n\n\n### **Slots:**\n - _default_ - The row's text/content.\n- **icon** - A leading graphic — usually a `vf-img` holding a 16×16 System 7 small icon, but any element rides here. The row lays it out as the icon gutter: flex-centered vertically (keep the difference between the row height and the icon height even, so the centering offset stays a whole pixel — 16 in a 20px row is a whole 2px), with the reference art's 4px gap to the text. Contributes no text, so first-letter type-ahead still reads the row's words. On a selected row the graphic rides the inverted bar as-is — System 7 left color icons unfiltered on the highlight.\n\n### **CSS Parts:**\n - **text** - The text span beside the icon gutter (ellipsizes). Takes `top`/`left` like every other element ({@link VfPositioned}), for the consumer who wants a row somewhere other than a list. Inside its parent `<vf-list>` the rows stack in flow, so stating an origin lifts that row out of the stack and the ones below it close the gap — the placement doing what it says, but not how a list box is laid out.",
474
474
  "attributes": [
475
475
  {
476
476
  "name": "value",
@@ -486,6 +486,16 @@
486
486
  "name": "disabled",
487
487
  "description": "Disables the row: dimmed text, not selectable or focusable.",
488
488
  "values": []
489
+ },
490
+ {
491
+ "name": "top",
492
+ "description": "Offset from the top of the positioning parent, in whole system px.\nSetting this (or `left`) absolutely positions the element within its\nparent; the coordinate left unset is 0. Remove both to return the\nelement to normal flow.",
493
+ "values": []
494
+ },
495
+ {
496
+ "name": "left",
497
+ "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
498
+ "values": []
489
499
  }
490
500
  ],
491
501
  "references": []
@@ -556,7 +566,7 @@
556
566
  },
557
567
  {
558
568
  "name": "vf-menu-item",
559
- "description": "`<vf-menu-item>` — a single command inside a `<vf-menu>` panel.\n\nRenders the classic System 7 menu row: optional ✓ check in the\n`--vf-select-gutter` left column (16px, shared with vf-select/vf-option),\nlabel, and the keyboard shortcut left-aligned in a right-anchored column,\nso every ⌘ lands at the same x. On activation the item performs the classic\n3-blink inversion (~250ms), then dispatches `vf-menu-select` and asks its\nancestors to close the menu.\n---\n\n\n### **Events:**\n - **vf-menu-select** - After the blink completes. `detail: { value, item }`. Named for the menu rather than plain `vf-select`, which would collide with the `<vf-select>` popup on any delegated ancestor listener (that component commits with `vf-change`).\n\n### **Slots:**\n - _default_ - The item label.\n\n### **CSS Properties:**\n - **--vf-menu-row-height** - `vf-menu-item` row pitch (`Menus.png`; kept separate from `--vf-popup-height` so re-theming the popup pill doesn't move pulldown rows) _(default: 16px)_\n- **--vf-menu-shortcut-column** - `vf-menu-item` shortcut slot, right-anchored with the text left-aligned in it so every ⌘ lands at the same x (`Menus.png`) — the MDEF reserve, ⌘'s 11px advance + the face's widest letter (M/W, 12px); widen it to line up longer shortcuts (\"⌘⇧S\") _(default: 23px)_\n- **--vf-select-gutter** - checkmark column: `vf-select` left inset / `vf-option` + `vf-menu-item` ✓ column (shared so the value doesn't shift on open) _(default: 16px)_\n\n### **CSS Parts:**\n - **item** - The row container.\n- **check** - The ✓ checkmark glyph (rendered when `checked`).\n- **label** - The label wrapper around the default slot.\n- **shortcut** - The shortcut text, left-aligned in the shared column.",
569
+ "description": "`<vf-menu-item>` — a single command inside a `<vf-menu>` panel.\n\nRenders the classic System 7 menu row: optional ✓ check in the\n`--vf-select-gutter` left column (16px, shared with vf-select/vf-option),\nlabel, and the keyboard shortcut left-aligned in a right-anchored column,\nso every ⌘ lands at the same x. On activation the item performs the classic\n3-blink inversion (~250ms), then dispatches `vf-menu-select` and asks its\nancestors to close the menu.\n\nTakes `top`/`left` like every other element (VfPositioned), for the\nconsumer who wants a row somewhere other than a pulldown. Inside its parent\n`<vf-menu>` the panel is as wide as its widest row and stacks them in flow,\nso stating an origin takes that row out of both: it no longer contributes to\nthe panel's width and the rows below close the gap. The placement doing what\nit says — but not how a pulldown is laid out.\n---\n\n\n### **Events:**\n - **vf-menu-select** - After the blink completes. `detail: { value, item }`. Named for the menu rather than plain `vf-select`, which would collide with the `<vf-select>` popup on any delegated ancestor listener (that component commits with `vf-change`).\n\n### **Slots:**\n - _default_ - The item label.\n\n### **CSS Properties:**\n - **--vf-menu-row-height** - `vf-menu-item` row pitch (`Menus.png`; kept separate from `--vf-popup-height` so re-theming the popup pill doesn't move pulldown rows) _(default: 16px)_\n- **--vf-menu-shortcut-column** - `vf-menu-item` shortcut slot, right-anchored with the text left-aligned in it so every ⌘ lands at the same x (`Menus.png`) — the MDEF reserve, ⌘'s 11px advance + the face's widest letter (M/W, 12px); widen it to line up longer shortcuts (\"⌘⇧S\") _(default: 23px)_\n- **--vf-select-gutter** - checkmark column: `vf-select` left inset / `vf-option` + `vf-menu-item` ✓ column (shared so the value doesn't shift on open) _(default: 16px)_\n\n### **CSS Parts:**\n - **item** - The row container.\n- **check** - The ✓ checkmark glyph (rendered when `checked`).\n- **label** - The label wrapper around the default slot.\n- **shortcut** - The shortcut text, left-aligned in the shared column.",
560
570
  "attributes": [
561
571
  {
562
572
  "name": "disabled",
@@ -587,13 +597,23 @@
587
597
  "name": "value",
588
598
  "description": "Value reported in the `vf-menu-select` event detail. Defaults to the item's\ntrimmed text content when unset.",
589
599
  "values": []
600
+ },
601
+ {
602
+ "name": "top",
603
+ "description": "Offset from the top of the positioning parent, in whole system px.\nSetting this (or `left`) absolutely positions the element within its\nparent; the coordinate left unset is 0. Remove both to return the\nelement to normal flow.",
604
+ "values": []
605
+ },
606
+ {
607
+ "name": "left",
608
+ "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
609
+ "values": []
590
610
  }
591
611
  ],
592
612
  "references": []
593
613
  },
594
614
  {
595
615
  "name": "vf-menu",
596
- "description": "`<vf-menu>` — a pull-down menu: a bar label plus a dropped panel of\n`<vf-menu-item>` / `<vf-separator>` children.\n\nInside a `<vf-menu-bar>` the bar coordinates open state (only one menu open,\nhover-switching, outside-click/Escape dismissal) and owns the pointer\ngesture, which may travel between its menus. Used standalone, the menu\ntoggles itself on label click and manages its own dismissal, its own press\ngesture and item keyboard navigation (ArrowUp/ArrowDown, Home/End, and the\nshared first-letter type-ahead — src/type-ahead.ts) while open.\n\nPointer — the two styles `vf-select` supports, on the same terms (see\nsrc/menu-press.ts): the System 7 press-drag-release (press the title, slide\nonto a command, release over it) and a modern quick tap that leaves the menu\ndropped for a second click.\n---\n\n\n### **Slots:**\n - _default_ - Menu contents: `vf-menu-item` and `vf-separator` elements.\n- **label** - Replaces the `label` text in the bar — e.g. a `vf-img` apple icon for the Apple menu. Keep the `label` attribute set too: it stays the menu's accessible name (the bar item's `aria-label` and the panel's) when the visible title is an image.\n\n### **CSS Properties:**\n - **--vf-menubar-height** - `vf-menu-bar` _(default: 20px)_\n- **--vf-menu-row-height** - `vf-menu-item` row pitch; the panel also spends one of these on every slotted `vf-separator`, the full row the MDEF gave a divider (rule 8px into it — see the panel CSS) _(default: 16px)_\n\n### **CSS Parts:**\n - **label** - The menu title in the bar (inverts while open).\n- **panel** - The dropped `.vf-panel` containing the items.",
616
+ "description": "`<vf-menu>` — a pull-down menu: a bar label plus a dropped panel of\n`<vf-menu-item>` / `<vf-separator>` children.\n\nInside a `<vf-menu-bar>` the bar coordinates open state (only one menu open,\nhover-switching, outside-click/Escape dismissal) and owns the pointer\ngesture, which may travel between its menus. Used standalone, the menu\ntoggles itself on label click and manages its own dismissal, its own press\ngesture and item keyboard navigation (ArrowUp/ArrowDown, Home/End, and the\nshared first-letter type-ahead — src/type-ahead.ts) while open.\n\nPointer — the two styles `vf-select` supports, on the same terms (see\nsrc/menu-press.ts): the System 7 press-drag-release (press the title, slide\nonto a command, release over it) and a modern quick tap that leaves the menu\ndropped for a second click.\n\nTakes `top`/`left` like every other element (VfPositioned) — the\nnatural fit being a standalone menu, which is a free-standing menu button and\nplaces like any other control. The panel is anchored to the host's own box\n(`top: 100%`), so it follows a placed menu down without any further work.\nInside a `<vf-menu-bar>`, the bar lays its titles out in flow — placing one\nlifts it off the bar and the titles beside it close the gap.\n---\n\n\n### **Slots:**\n - _default_ - Menu contents: `vf-menu-item` and `vf-separator` elements.\n- **label** - Replaces the `label` text in the bar — e.g. a `vf-img` apple icon for the Apple menu. Keep the `label` attribute set too: it stays the menu's accessible name (the bar item's `aria-label` and the panel's) when the visible title is an image.\n\n### **CSS Properties:**\n - **--vf-menubar-height** - `vf-menu-bar` _(default: 20px)_\n- **--vf-menu-row-height** - `vf-menu-item` row pitch; the panel also spends one of these on every slotted `vf-separator`, the full row the MDEF gave a divider (rule 8px into it — see the panel CSS) _(default: 16px)_\n\n### **CSS Parts:**\n - **label** - The menu title in the bar (inverts while open).\n- **panel** - The dropped `.vf-panel` containing the items.",
597
617
  "attributes": [
598
618
  {
599
619
  "name": "label",
@@ -604,6 +624,16 @@
604
624
  "name": "open",
605
625
  "description": "Whether the panel is dropped. Reflected. Managed by the parent\n`vf-menu-bar` when present, otherwise by the menu itself.",
606
626
  "values": []
627
+ },
628
+ {
629
+ "name": "top",
630
+ "description": "Offset from the top of the positioning parent, in whole system px.\nSetting this (or `left`) absolutely positions the element within its\nparent; the coordinate left unset is 0. Remove both to return the\nelement to normal flow.",
631
+ "values": []
632
+ },
633
+ {
634
+ "name": "left",
635
+ "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
636
+ "values": []
607
637
  }
608
638
  ],
609
639
  "references": []
@@ -682,7 +712,7 @@
682
712
  },
683
713
  {
684
714
  "name": "vf-option",
685
- "description": "`<vf-option>` — a single choice inside a `<vf-select>` popup menu.\n\nA light-DOM child of `<vf-select>` (slotted into the popup panel). Renders\nits slotted label at menu-item metrics (16px row — the pill's content height,\nso a selected option overlays the closed pill exactly; the left checkmark gutter is\n`--vf-select-gutter`, shared with the closed control's left inset so the value\ndoesn't shift on open). The parent select manages `selected` and the transient\n`active` highlight, and slots this element into its popup panel.\n\nThe host carries `role=\"option\"` with `aria-selected`/`aria-disabled`.\n---\n\n\n### **CSS Properties:**\n - **--vf-popup-height** - `vf-select` pill (border box; its 1px hard shadow makes the sheet's 157×19 ink box) _(default: 18px)_\n- **--vf-select-gutter** - checkmark column: `vf-select` left inset / `vf-option` + `vf-menu-item` ✓ column (shared so the value doesn't shift on open) _(default: 16px)_\n\n### **CSS Parts:**\n - **check** - The ✓ checkmark shown in the left gutter when selected.",
715
+ "description": "`<vf-option>` — a single choice inside a `<vf-select>` popup menu.\n\nA light-DOM child of `<vf-select>` (slotted into the popup panel). Renders\nits slotted label at menu-item metrics (16px row — the pill's content height,\nso a selected option overlays the closed pill exactly; the left checkmark gutter is\n`--vf-select-gutter`, shared with the closed control's left inset so the value\ndoesn't shift on open). The parent select manages `selected` and the transient\n`active` highlight, and slots this element into its popup panel.\n\nThe host carries `role=\"option\"` with `aria-selected`/`aria-disabled`.\n\nTakes `top`/`left` like every other element (VfPositioned), for the\nconsumer who wants one somewhere other than a popup row. Inside its parent\n`<vf-select>` it is a row the panel measures and scrolls in flow, so stating\nan origin there takes it out of that measurement: the panel stops counting it\ntoward its own width and its scroll clamp, and the rows below close the gap.\nThat is the placement doing exactly what it says — not a bug — but it is not\nhow a popup is laid out.\n---\n\n\n### **CSS Properties:**\n - **--vf-popup-height** - `vf-select` pill (border box; its 1px hard shadow makes the sheet's 157×19 ink box) _(default: 18px)_\n- **--vf-select-gutter** - checkmark column: `vf-select` left inset / `vf-option` + `vf-menu-item` ✓ column (shared so the value doesn't shift on open) _(default: 16px)_\n\n### **CSS Parts:**\n - **check** - The ✓ checkmark shown in the left gutter when selected.",
686
716
  "attributes": [
687
717
  {
688
718
  "name": "value",
@@ -703,13 +733,23 @@
703
733
  "name": "active",
704
734
  "description": "Transient highlight (hover / keyboard cursor) — full-row inversion.\nManaged by the parent `<vf-select>`; not part of the authoring API.",
705
735
  "values": []
736
+ },
737
+ {
738
+ "name": "top",
739
+ "description": "Offset from the top of the positioning parent, in whole system px.\nSetting this (or `left`) absolutely positions the element within its\nparent; the coordinate left unset is 0. Remove both to return the\nelement to normal flow.",
740
+ "values": []
741
+ },
742
+ {
743
+ "name": "left",
744
+ "description": "Offset from the left of the positioning parent, in whole system px.\nSee top.",
745
+ "values": []
706
746
  }
707
747
  ],
708
748
  "references": []
709
749
  },
710
750
  {
711
751
  "name": "vf-paragraph",
712
- "description": "`<vf-paragraph>` — a paragraph of copy on the kit's body face and grid.\n\nThe installer's welcome text, a dialog's explanation, an article's prose:\nanything a page would otherwise set with a `<p>` and hope the leading landed\nsomewhere sensible. What it adds over that `<p>`:\n\n- the **Geneva body face** by default (`face=\"display\"` switches to\n the Chicago-style chrome face);\n- a **whole-system-pixel line box at the face's native pitch** —\n `--vf-paragraph-line-height`: 12px for the body face (Geneva 9's own\n strike line) and 16px under `face=\"display\"` (Chicago 12's — ascent 12 +\n descent 3 + leading 1, the pitch a real dialog wrapped its copy on).\n Whole pixels are the point of the component: line boxes are the single\n biggest source of off-grid layout, because a ratio resolves to whatever\n it resolves to (`1.65 × 17px = 28.05px`) and every line of prose nudges\n everything after it further off the device-pixel grid, smearing 1-bit\n borders and bitmap glyph stems (docs/SIZING.md rule 2). A\n whole-pixel line box accumulates whole offsets;\n- its own GridSnapController, so it holds its own origin once the\n page opts in with `applyGridSnap()`;\n- a **declared box** when the layout wants one — `width`/`height` in whole\n system px (VfSized). In flow a paragraph takes its container's\n width, which is usually right; a *placed* one (`top`/`left`) shrink-wraps\n its longest line instead — a fractional glyph-run width, wrapped wherever\n the parent's edge happens to fall — so a DITL-style layout states the\n measure the copy wraps to, whole and on the grid.\n\nThe shadow root renders a real `<p>`, so the copy keeps paragraph semantics\nfor assistive tech, and — unlike the kit's chrome — the text is selectable.\nThere is no margin: per SPEC §2 a component adds nothing outside its own box,\nso paragraph spacing is the page's (a `gap` on the column, or a margin on the\nhost — kept a whole number of pixels, like everything else in the contract).\n---\n\n\n### **Slots:**\n - _default_ - The paragraph copy.\n\n### **CSS Properties:**\n - **--vf-paragraph-line-height** - This paragraph kind's own line box, in system px. Unset (the default) the box follows the face tokens below; set, it overrides both faces for paragraphs alone. Keep any override a whole *even* number — a ratio is what puts a page off the grid in the first place, and half the difference to the 16px em is half-leading, so an odd value lands the baseline on a half pixel. _(default: undefined)_\n- **--vf-line-height** - The body face's native line (default `12px`, Geneva 9's) — the face-level knob: retheming the body face to another strike states this alongside `--vf-font-family` / `--vf-font-size`. _(default: undefined)_\n- **--vf-line-height-display** - The display face's native line (default `16px`, Chicago 12's) — the display retheme's third number. _(default: undefined)_\n\n### **CSS Parts:**\n - **paragraph** - The inner `<p>`.",
752
+ "description": "`<vf-paragraph>` — a paragraph of copy on the kit's body face and grid.\n\nThe installer's welcome text, a dialog's explanation, an article's prose:\nanything a page would otherwise set with a `<p>` and hope the leading landed\nsomewhere sensible. What it adds over that `<p>`:\n\n- the **Geneva body face** by default (`face=\"display\"` switches to\n the Chicago-style chrome face);\n- a **whole-system-pixel line box at the face's native pitch** —\n `--vf-paragraph-line-height`: 12px for the body face (Geneva 9's own\n strike line) and 16px under `face=\"display\"` (Chicago 12's — ascent 12 +\n descent 3 + leading 1, the pitch a real dialog wrapped its copy on).\n Whole pixels are the point of the component: line boxes are the single\n biggest source of off-grid layout, because a ratio resolves to whatever\n it resolves to (`1.65 × 17px = 28.05px`) and every line of prose nudges\n everything after it further off the device-pixel grid, smearing 1-bit\n borders and bitmap glyph stems (docs/SIZING.md rule 2). A\n whole-pixel line box accumulates whole offsets;\n- its own GridSnapController, so it holds its own origin\n wherever the page puts it;\n- a **declared box** when the layout wants one — `width`/`height` in whole\n system px (VfSized). In flow a paragraph takes its container's\n width, which is usually right; a *placed* one (`top`/`left`) shrink-wraps\n its longest line instead — a fractional glyph-run width, wrapped wherever\n the parent's edge happens to fall — so a DITL-style layout states the\n measure the copy wraps to, whole and on the grid.\n\nThe shadow root renders a real `<p>`, so the copy keeps paragraph semantics\nfor assistive tech, and — unlike the kit's chrome — the text is selectable.\nThere is no margin: per SPEC §2 a component adds nothing outside its own box,\nso paragraph spacing is the page's (a `gap` on the column, or a margin on the\nhost — kept a whole number of pixels, like everything else in the contract).\n---\n\n\n### **Slots:**\n - _default_ - The paragraph copy.\n\n### **CSS Properties:**\n - **--vf-paragraph-line-height** - This paragraph kind's own line box, in system px. Unset (the default) the box follows the face tokens below; set, it overrides both faces for paragraphs alone. Keep any override a whole *even* number — a ratio is what puts a page off the grid in the first place, and half the difference to the 16px em is half-leading, so an odd value lands the baseline on a half pixel. _(default: undefined)_\n- **--vf-line-height** - The body face's native line (default `12px`, Geneva 9's) — the face-level knob: retheming the body face to another strike states this alongside `--vf-font-family` / `--vf-font-size`. _(default: undefined)_\n- **--vf-line-height-display** - The display face's native line (default `16px`, Chicago 12's) — the display retheme's third number. _(default: undefined)_\n\n### **CSS Parts:**\n - **paragraph** - The inner `<p>`.",
713
753
  "attributes": [
714
754
  {
715
755
  "name": "face",
@@ -1014,7 +1054,7 @@
1014
1054
  },
1015
1055
  {
1016
1056
  "name": "vf-stack",
1017
- "description": "`<vf-stack>` — arrange things inside a window, in system pixels.\n\nA window body is the one place the kit stopped short: every control inside it\nis authored in system px, but the *spaces between* them were the consumer's\nproblem, written by hand as `calc(var(--vf-scale, 1) * 12px)`. This is that\ncalculation, as a component — a flexbox whose `gap`, `pad`, `width` and\n`height` are declared in whole system px and converted internally:\n\n```html\n<vf-stack gap=\"12\"> <!-- a column -->\n <vf-stack fill-width direction=\"row\" gap=\"8\"> <!-- a labeled field -->\n <vf-label width=\"80\" for=\"name\">Name:</vf-label>\n <vf-text-field id=\"name\" fill-width></vf-text-field>\n </vf-stack>\n <vf-stack fill-width place=\"end\"> <!-- an action row -->\n <vf-button-group>\n <vf-button>Cancel</vf-button>\n <vf-button variant=\"default\">Save</vf-button>\n </vf-button-group>\n </vf-stack>\n</vf-stack>\n```\n\n**Why this can't just be page CSS.** Scaling is default-on and *per\ncomponent*: `ScaleController` sets `--vf-scale` on the component's own host,\nnot on the document. So `var(--vf-scale, 1)` in a consumer's stylesheet\nresolves only where the rule's element happens to sit inside a `vf-*`\nancestor and inherit it — true inside a window body, false for a plain\n`<div>` holding two buttons on an ordinary page, where the fallback `1` wins\nand the gap renders at 8px around 3×-sized buttons with no warning. A page\nthat hasn't called `applyScale()` has no way to write \"8 system px\" at all.\nA component always can, because it *is* the scope — which is also why this\none carries a ScaleController of its own: without it a lone stack\nwould resolve its own gap against that same fallback while its children each\nself-scaled around it.\n\n**Whole system px is the only expressible value**, so the gap half of the\nlayout contract (docs/SIZING.md rule 2) stops being a rule to remember. Declaring\n`width`/`height` covers the size half of rule 3 as well.\n\n**The geometry is governed by the content.** A column is as wide as its\nwidest child and a row as tall as its tallest; children keep the size they\ndrew themselves at (`flex: 0 0 auto` — no growing, no shrinking). System 7\nboxes are the size they are: a push button is as wide as its label, a popup\nmenu hugs its widest option, and a window is a fixed box whose overflow is\nclipped at the frame, not a layout that squeezes its controls to fit. The\nstack distributes; it does not resize. That is why the box shrink-wraps\n(`fit-content`) rather than claiming its parent's whole width — a layout box\nthat did would be inventing a size nobody declared.\n\n**`fill-width` / `fill-height` are how a child asks for more**, as bare\nattributes on consumer DOM the way `nosnap` opts an element out of snapping:\n\n```html\n<vf-text-field fill-width></vf-text-field>\n```\n\nEach names the *outcome*, not the axis, so the markup means the same thing\nwherever it lands; the stack does the flexbox translation, which is the whole\nreason to have a component. One rule to learn, about geometry rather than\nvocabulary: **the cross axis always has a size, the main axis only has slack\nif you declared one.** So `fill-width` always works in a column (the width is\nthe widest child's) and needs a declared `width` in a row; `fill-height` is\nexactly the other way round. A fill with nothing to take is inert, not an\nerror. Two children filling along the main axis end up *equal* — the zeroed\nflex basis is what lets them divide the slack rather than keep their natural\nsizes — and a child that declares its own size shouldn't also ask to fill it.\n\nA stack reads the same two attributes about *itself*, for the parents that\naren't stacks: a window body, a fieldset, a scroll well, a grid cell. That is\nwhere a panel's width enters the tree, and from there `fill-width` hands it\ndown a level at a time.\n\n**It paints nothing and means nothing.** No border, no background, no role,\nno keyboard behavior — what it holds decides what it is, as with\nVfGrid.\n\n**It holds its box on the device-pixel grid** — the `vf-container`\narrangement, adopted here after first shipping without it. The original\nreasoning (\"no ink of its own, and slotted `vf-*` children correct their own\norigins\") accounted only for kit children: a stack is a positioned ancestor\nand a layout box for *anything*, and consumer content inside it — a div, an\n`<img>`, a run of text — has no controller of its own. So the flex\ncontainer, the placed-child anchor and the `vf-snap` class live on one\nshadow box, and under `applyGridSnap()` the correction moves the stack's\nwhole coordinate system; children (kit or not) ride it, and the sweep's\noutermost-first order means the kit children then find nothing left to\ncorrect. What this deliberately does not fix: a text-governed child width\nmid-row still shifts later siblings fractionally (their own controllers\ncover that), and centering still can't land on a whole pixel by itself.\n\nIt is also the kit's one **typographically transparent** component: `vfBase`'s\nchrome face, ratio line box, color and non-selectability are all reset to\n`inherit` on the host. Wrapping content in a layout box must not change how\nthat content reads — inside a window it goes on inheriting the window's face\nexactly as before, and on a plain page it leaves the page's typography (and\nits whole-pixel line boxes) alone.\n\nWhat it deliberately does **not** do: equalize its children's widths. A row of\nbuttons still belongs in a `vf-button-group`, which sizes them all to the\nwidest and aligns their *faces* rather than the `variant=\"default\"` ring\nboxes a plain flex row would line up.\n---\n\n\n### **Slots:**\n - _default_ - The children to arrange. `fill-width` / `fill-height` on any of them.",
1057
+ "description": "`<vf-stack>` — arrange things inside a window, in system pixels.\n\nA window body is the one place the kit stopped short: every control inside it\nis authored in system px, but the *spaces between* them were the consumer's\nproblem, written by hand as `calc(var(--vf-scale, 1) * 12px)`. This is that\ncalculation, as a component — a flexbox whose `gap`, `pad`, `width` and\n`height` are declared in whole system px and converted internally:\n\n```html\n<vf-stack gap=\"12\"> <!-- a column -->\n <vf-stack fill-width direction=\"row\" gap=\"8\"> <!-- a labeled field -->\n <vf-label width=\"80\" for=\"name\">Name:</vf-label>\n <vf-text-field id=\"name\" fill-width></vf-text-field>\n </vf-stack>\n <vf-stack fill-width place=\"end\"> <!-- an action row -->\n <vf-button-group>\n <vf-button>Cancel</vf-button>\n <vf-button variant=\"default\">Save</vf-button>\n </vf-button-group>\n </vf-stack>\n</vf-stack>\n```\n\n**Why this can't just be page CSS.** Scaling is default-on and *per\ncomponent*: `ScaleController` sets `--vf-scale` on the component's own host,\nnot on the document. So `var(--vf-scale, 1)` in a consumer's stylesheet\nresolves only where the rule's element happens to sit inside a `vf-*`\nancestor and inherit it — true inside a window body, false for a plain\n`<div>` holding two buttons on an ordinary page, where the fallback `1` wins\nand the gap renders at 8px around 3×-sized buttons with no warning. A page\nthat hasn't called `applyScale()` has no way to write \"8 system px\" at all.\nA component always can, because it *is* the scope — which is also why this\none carries a ScaleController of its own: without it a lone stack\nwould resolve its own gap against that same fallback while its children each\nself-scaled around it.\n\n**Whole system px is the only expressible value**, so the gap half of the\nlayout contract (docs/SIZING.md rule 2) stops being a rule to remember. Declaring\n`width`/`height` covers the size half of rule 3 as well.\n\n**The geometry is governed by the content.** A column is as wide as its\nwidest child and a row as tall as its tallest; children keep the size they\ndrew themselves at (`flex: 0 0 auto` — no growing, no shrinking). System 7\nboxes are the size they are: a push button is as wide as its label, a popup\nmenu hugs its widest option, and a window is a fixed box whose overflow is\nclipped at the frame, not a layout that squeezes its controls to fit. The\nstack distributes; it does not resize. That is why the box shrink-wraps\n(`fit-content`) rather than claiming its parent's whole width — a layout box\nthat did would be inventing a size nobody declared.\n\n**`fill-width` / `fill-height` are how a child asks for more**, as bare\nattributes on consumer DOM the way `nosnap` opts an element out of snapping:\n\n```html\n<vf-text-field fill-width></vf-text-field>\n```\n\nEach names the *outcome*, not the axis, so the markup means the same thing\nwherever it lands; the stack does the flexbox translation, which is the whole\nreason to have a component. One rule to learn, about geometry rather than\nvocabulary: **the cross axis always has a size, the main axis only has slack\nif you declared one.** So `fill-width` always works in a column (the width is\nthe widest child's) and needs a declared `width` in a row; `fill-height` is\nexactly the other way round. A fill with nothing to take is inert, not an\nerror. Two children filling along the main axis end up *equal* — the zeroed\nflex basis is what lets them divide the slack rather than keep their natural\nsizes — and a child that declares its own size shouldn't also ask to fill it.\n\nA stack reads the same two attributes about *itself*, for the parents that\naren't stacks: a window body, a fieldset, a scroll well, a grid cell. That is\nwhere a panel's width enters the tree, and from there `fill-width` hands it\ndown a level at a time.\n\n**It paints nothing and means nothing.** No border, no background, no role,\nno keyboard behavior — what it holds decides what it is, as with\nVfGrid.\n\n**It holds its box on the device-pixel grid** — the `vf-container`\narrangement, adopted here after first shipping without it. The original\nreasoning (\"no ink of its own, and slotted `vf-*` children correct their own\norigins\") accounted only for kit children: a stack is a positioned ancestor\nand a layout box for *anything*, and consumer content inside it — a div, an\n`<img>`, a run of text — has no controller of its own. So the flex\ncontainer, the placed-child anchor and the `vf-snap` class live on one\nshadow box, and the correction moves the stack's\nwhole coordinate system; children (kit or not) ride it, and the sweep's\noutermost-first order means the kit children then find nothing left to\ncorrect. What this deliberately does not fix: a text-governed child width\nmid-row still shifts later siblings fractionally (their own controllers\ncover that).\n\n**Centering lands on whole system px as well** — halving an odd count of free\nsystem px gives a half, which no container can round in CSS. See\nCrossCenterController.\n\nIt is also the kit's one **typographically transparent** component: `vfBase`'s\nchrome face, ratio line box, color and non-selectability are all reset to\n`inherit` on the host. Wrapping content in a layout box must not change how\nthat content reads — inside a window it goes on inheriting the window's face\nexactly as before, and on a plain page it leaves the page's typography (and\nits whole-pixel line boxes) alone.\n\nWhat it deliberately does **not** do: equalize its children's widths. A row of\nbuttons still belongs in a `vf-button-group`, which sizes them all to the\nwidest and aligns their *faces* rather than the `variant=\"default\"` ring\nboxes a plain flex row would line up.\n---\n\n\n### **Slots:**\n - _default_ - The children to arrange. `fill-width` / `fill-height` on any of them.",
1018
1058
  "attributes": [
1019
1059
  {
1020
1060
  "name": "direction",
@@ -1033,7 +1073,7 @@
1033
1073
  },
1034
1074
  {
1035
1075
  "name": "place",
1036
- "description": "Where the children sit across the stack — `start`, `center` or `end`.\nUnset resolves per direction: `start` down a column, `center` across a row.\n\nNamed `place` rather than `align` for a reason worth keeping in the source:\n`align` is a legacy HTML presentation attribute, and Blink maps it to\n`text-align` on any element, so the cross-axis switch used to re-align every\nrun of copy inside the stack (see the `text-align` reset above).\n\nNote the one thing centering cannot do: land on a whole pixel by itself. A\n16px caption centered in a row set by the 25-system-px `vf-number-field`\nsits at 4.5 system px, and no container can round that it would have to\nread each child's height. `applyGridSnap()` keeps the caption's own ink\ncrisp regardless (it corrects the origin inside the child's shadow root);\n`place=\"start\"` is the deterministic escape.",
1076
+ "description": "Where the children sit across the stack — `start`, `center` or `end`.\nUnset resolves per direction: `start` down a column, `center` across a row.\n\nNamed `place` rather than `align` for a reason worth keeping in the source:\n`align` is a legacy HTML presentation attribute, and Blink maps it to\n`text-align` on any element, so the cross-axis switch used to re-align every\nrun of copy inside the stack (see the `text-align` reset above).\n\nCentering divides the free space in two, so an odd count of system px would\nland a child on a half a 16px caption centered in a row set by the\n25-system-px `vf-number-field` sits at 4.5. CrossCenterController\nsteps that back onto whole system px, the exact half going toward the start\nthe way QuickDraw's `div 2` did; `place=\"start\"` is still the way to ask\nfor no centering at all.",
1037
1077
  "values": [{ "name": "VfStackPlace" }]
1038
1078
  },
1039
1079
  {