Files

40 KiB
Raw Permalink Blame History

C4a Controls and primitives: evidence log

Package card: docs/phase2/PLAN.md §2.4 "C4a Controls and primitives". Concept: docs/phase2/concepts/controls.md (CTL, owned by C4a; its keyboard section §12 is built by C4b). Steam build at the time of writing: 11094443. No device step was needed for M0.

Status

Milestone reached: M3 (M2 and M3 for C4a's own elements; round R1, build session 1, 05:35–07:25; Status updated 07:25). Steam build 11094443.

  • M0 (2026-10-07 05:35): controls.md revision 3 conforms to PLAN §1; 16 owned mockups re-rendered and viewed; controls-measure.py all 35/35 (evidence M0-* below).
  • M2 (T1) — reached for C4a's elements. Phase 2 theme/10-primitives.css live since 05:48 (draft kept identical in theme/_wip/10-primitives.css; workflow: edit the draft, brace_error from device/lgs.py, copy, python glass.py check-theme, sync theme, reload). Buttons (60 capsules, 60 circles, tinted footer confirm, Play, destructive), rows/platters (80, separators, single- vs multi-control focus), switches (66.5 × 38.5, hit 87.5 × 80.5), check circles (40, 80 cells), sliders (64 capsule and knob, fill from origin, notch dots in the track, default tick, inert glyph, mute zone), pop-ups (60/76, rich two-line label), segmented controls, fields (64, the one ring), body text, markup classes (§11), High Contrast. Phase 1's FocusRing, menu and modal sections removed (REQ P4->C4a, REQ C1c->C4a done). Gates on all 10 C4a routes, CSS-only, pad and laser (M2-G3, G4): AUD, OUTLINE, MOTION PASS; SIZE and TYPE fail only on other packages' elements — the settings sidebar and the notification column headers (C6a, REQ open). One explained AUD item on /zoo/misc (Steam's marquee rests off-screen). C25 rule audit, G-FOCUS pad and laser, C2/C22/C23 probe, C7, C28, C14 reduce, C15 perf, C13 motion all PASS; G-MOCK vs controls-system.html: identical row heights and pitch, offset from C6a's layout (verdict recorded).
  • M3 (T2) — reached. device/rt/10-controls.js (flag wp.c4a, off by default; draft in device/rt/_wip/): row tags, switch spot host, knob-lift trigger, travelling pill (rt.use('controls').pill), icon-button tooltips, mute-zone tag, slider value text. docs/phase2/mockups/controls-m3test.js PASS in pad (9/9) and laser (6/6) incl. removal; gates with wp.c4a on PASS for C4a's elements (M3-G).
  • Test tools (durable, in C4a's mockup pattern): docs/phase2/mockups/controls-gates.py (run/summary), controls-c25.py, controls-m3test.js, controls-c7c28.js, controls-focus-pairs.json; docs/phase2/concepts/controls-probe.js (mode:'theme'); docs/phase2/wp/C4a-cmp.json.
  • Remaining (not blocking M3; next round or V2): M4 (native: C4a declares no pop of its own; check C1c's/areas' pops with C21 in a native session); C9 tooltip timing on C4a's registration (P3's IN-7 covers the timing); C26 glyph badges (no DialogButton on C4a's routes shows a Steam glyph today); C15 pointer-move perf on /zoo/sliders; G-MOCK for controls-states.html and controls-quickaccess.html (with C3b); the CTL crowding rule for long notch labels (T2) and the current-notch label colour; once C6a fixes the sidebar, re-run controls-gates.py run for fully green route gates; answer new REQs (grep -n "REQ [A-Za-z0-9]*->C4a:" docs/phase2/wp/*.md: none open at 07:24).
  • Incident 06:49–06:53 (not C4a's): SharedJSContext crashed (06:49:56) and the class index rebuilt too early (3 modules) until a reload at 06:53:45; gate runs in that window were discarded. REQ C4a->P1 (auto-retry); coordinator told.
  • Device left: theme on, CSS only, controls module off (flag off), native off, route /library/home, pointer and flags restored by the lab.

Interface announced to every area (names final; code lands at M2 / M3)

What Name and values Set by Notes for areas
Steam's primitives, everywhere DialogButton (+ .Primary, .Destructive, .GreenPlay), G>Toggle, .DialogCheckbox, the Slider* family, %{DropDownControlButton}, %{Group} radios, SteamVR SegmentedControlGroup, DialogInput / BasicTextInput, G>Field rows, S>EditableValue C4a T1 (theme/10-primitives.css), on every route and every popup Areas never restyle a primitive. They place them. A size or colour change is a request to C4a
Opt-out for a subtree that draws its own control forms [data-lgs-noprim] on the subtree root (T2/T3 markup) the area e.g. Control Center tiles' circular toggles, Home discs. C4a's rules skip it
Our own markup (T2/T3 elements) .lgs-btn + .lgs-btn-circle / .lgs-btn-capsule; sizes .lgs-btn-mini (38), -sm (44), default 60, -lg (70), -xl (86); roles .lgs-btn-primary, -play, -danger (red label), -danger-fill (red confirm), -plain (borderless in ornaments and menus). .lgs-switch[aria-checked], .lgs-check[aria-checked], .lgs-seg (children [aria-pressed]), .lgs-field, .lgs-platter, .lgs-row (+ .lgs-row-nav), .lgs-sep C4a T1 The same looks as Steam's primitives; state numbers from P4. Prefer Steam's own components (P2 re-exports DialogButton, ToggleField, SliderField, DropdownField) where they fit
Row tags G>Field[data-lgs-ctl="1"] (single-control row: the row carries the focus) or "n" (multi-control: the row warms) C4a T2 T1 fallback without the tag: rows with %{SettingTogglesCtn} / %{Toggles} are multi-control
Light-spot targets Switches are added to P3's spot hosts (rt.states.addSpotHost); rows, pop-ups, slider groups, check boxes and input wrappers are .Panels or buttons, which P3's default spot selector already covers. data-lgs-spot stays available to areas C4a T2 (on P3) the switch light reads the spot through the rail's ::after
Slider data-lgs-mute="on" on the icon column of a slider row with Steam's clickable mute icon ("muted" reserved: Steam's muted state is not readable on this build); value text for full-width sliders without Steam's own value: [data-lgs-val] on the slider group with --lgs-c4a-val (a CSS string) drawn by the group's ::after (no node inserted, pointer-events: none) C4a T2 areas need nothing; a slider that already shows Steam's DescriptionValue is left alone
Segmented pill .lgs-pill (decorative, aria-hidden) inside a strip marked [data-lgs-pill]; API rt.use('controls').pill(stripEl, {active: '<selector of the selected item>', inset: 2}) returns off(). %{Group} radios get it automatically when the laser or the focus first reaches them C4a T2 C2c (library tabs) and C5a (game-page tabs) may register their strips; the selected item's own fill must then be transparent in the area's CSS while [data-lgs-pill] is set
Switch knob lift (gamepad) the lgs-knob-lift trigger for 500 ms on an aria-checked change (P5's keyframe) C4a T2 the laser uses :active
Tooltips C4a registers icon-only DialogButtons and converted Close circles with P3's rt.tooltip.register (text @aria-label) C4a T2 on P3 areas register their own icon-only circles
Runtime module and flag device/rt/10-controls.js, define({name: 'controls', deps: ['input', 'states'], flag: 'wp.c4a'}) C4a off until V2 accepts C4a

Decisions taken at M0 (PLAN §1 applied; §1.17 rule where §1 is silent)

# Topic Decision Source Where
C4a-D1 Checked value in a value menu A check in the row's leading 28 px slot, which every row reserves; no fill. Single-choice lists in a platter keep the trailing blue check (Settings convention) C1c's D2; PLAN §1.12; VP P-64 CTL §8.2; pickers, timezone, steamvr
C4a-D2 Focus on a white or coloured fill P4's --lgs-white-glow (0 0 22px 8px white .55). On a 40 px check circle the spread is 12 px: the token gave +17.2 L in the P-16 band, 12 px gives +27.4 L PLAN §1.4 (its 18px 2px .30 measured +4..12 L), tokens.md §1.6, VP P-16 CTL C-D22, §6.2; controls.css
C4a-D3 Text on coloured fills #0d0e12 on green, orange, yellow and red (Play, a focused destructive row, a red confirm); white only on blue CC §7 (contrast 9.5:1 and 5.6:1 vs 2.0:1 and 3.4:1); PLAN §1.12; VP P-39 CTL C-D21, §5; buttons
C4a-D4 Red whole fill under a dark label rgb(255 66 69 / .92) (C1c's value) until P4 names a token (REQ 1); --lgs-tint-danger (.84) is darker and lowers the dark label's contrast C1c; CC §7 controls.css
C4a-D5 Mute zone Sound on and level > 0: the zone sits inside the white fill (dark glyph); level 0: no white; muted: the zone white with the slashed glyph, the level at .24. Reconciles CTL rev 2 (clear zone) with VP P-69 (glyph inside the fill) and CC's always-filled zone VP P-69; critique 4; PLAN §1.3 ("mute is its own zone") CTL C-D13, R12, §7.2; quickaccess
C4a-D6 Segment labels One line; a longer label widens its segment (never wraps); more than 4 options or long labels wrap the row of segments E-SEG; legibility CTL §8.3; controls.css, tooltips
C4a-D7 Navigation rows Rows that can carry the navigation selection (sidebars, selection lists, tab bars). Drill-down rows in a platter are not navigation rows and take the normal + .08 hover PLAN §1.4 (SM-D13, SET T-SEL) CTL §10; lists, states
C4a-D8 One lit thing in a single-control row The control's own .gpfocus look is replaced by the lifted look while its row carries the focus VP P-13 CTL §4.3
C4a-D9 Field focus ring in laser mode Keyed on the field's DOM focus (the caret's field), not on hover VP P-17, P-02 CTL §9
C4a-D10 Strings C4a draws no new words. Slider value text is a number formatted with Intl.NumberFormat for the UI language; menu headers copy the row's label; tooltips use aria-label PLAN §1.15 CTL C-D24
C4a-D11 (M2) Disabled capsule label White .55, not PLAN §1.4's .40: Steam's own disabled dropdown label measures 3.8:1 and G-AUD CONTRAST forbids less; .40 measured 2.9:1 over the bright room (/zoo/dropdowns). Rows keep .40 (they pass). Still clearly dimmer than the .96 enabled label PLAN §1.17 rule (G-AUD outranks a look value) 10-primitives.css §1
C4a-D12 (M2) Inert slider glyph colour T1 has no access to the value, so the glyph is white with mix-blend-mode: difference: dark on the white fill, light on the bare track, which is CTL C-D13's rule without T2 CTL C-D13 §5
C4a-D13 (M2) Notch row Steam's notch row (a focus scope with no click handler, probed) rises 36 px into the track so the dots sit on its centre line above the fill and under the knob (handle container z 2), labels below; the row is ≥ 60 tall (P-80) and a press on it still reaches the slider group CTL §7.3, P-C8 §5
C4a-D14 (M2) Row label column At least 30 % of the row next to a long value (otherwise 24 px labels are squeezed to their text, G-AUD SHRUNK) G-AUD §2
C4a-D15 (M2) Pill (T2) The travelling pill is created lazily, when the laser or the focus first reaches a segmented group; until then Steam's selected segment carries the white fill (identical at rest) CTL §8.3; cost 10-controls.js
C4a-D16 (M2) Laser hover inside rows When the laser is on a control inside a row, the row's hover light is off and the control is the one lit thing; on a capsule the pointer spot is .10 (P4's .12 elsewhere). Measured: the row light under a translucent button doubled hover to +42.3 L; with the row off +26.3; with the .10 spot +24.9 (VP P-03 +10..+25) VP P-03, P-13 §1, §2

New strings (PLAN §1.15) for V2's list: none from C4a. C4b's keyboard field names and Enter labels are listed by C4b (CTL §12.3, §12.5).

Requests

  • REQ C4a->P4: tokens for text and fills on colour. (a) --lgs-text-on-tint is #fff ("label on a tinted capsule"), but PLAN §1.12 and CC §7 make text on green, orange, yellow and red fills #0d0e12 (white on green .88 measures about 2.0:1, VP P-39); please add --lgs-text-on-color: #0d0e12 and note that --lgs-text-on-tint is for blue only. (b) A red whole fill for dark labels, --lgs-fill-danger: rgb(255 66 69 / .92) (C1c's and C4a's value; --lgs-tint-danger .84 is darker). (c) Kit parity: kit.css .lgk-btn.play with a #0d0e12 label and glyph, so every area's mockup draws Play the same (C4a's controls.css overrides it meanwhile). FYI, no action needed: on 40 px check circles C4a raises --lgs-white-glow-spread to 12 px locally (P-16 band +17.2 → +27.4 L). P4 (2026-10-07): done: (a) --lgs-text-on-color: #0d0e12 added, --lgs-text-on-tint documented as blue only; (b) --lgs-fill-danger: rgb(255 66 69 / .92); (c) kit .lgk-btn.play label var(--lg-text-on-color); kit twins --lg-text-on-color, --lg-fill-danger (parity PASS). The local 12 px glow spread on check circles is fine (it uses the part token).

  • REQ C4a->C5a: game-pages.css / game-pages.js: the Play capsule (.gp-ctl[data-tint="play"]) draws a white label and glyph on the green fill. Please make them #0d0e12 (CTL C-D21, CC §7, PLAN §1.12: white on green .88 is about 2.0:1, VP P-39) and re-render the title, details and running mockups. — C5a: live T1 draws --lgs-text-on-color on green, white only on blue (C5a-D2); mockups not re-rendered (expedited).

  • REQ C4a->C5b: after C5a's change, re-render game-pages-nowplaying*.html ("Resume Game" on the green fill uses game-pages.css), and use the dark label in theme/vr/20-nowplaying.css.

  • REQ C4a->C6a: settings.css .st-vr .hz: SteamVR's refresh-rate circles are 104 px at a 116 px pitch (72 main px with an 8 px gap, which fails the 21 px clear rule, VP P-08). Please make them 86 SteamVR px at a 121 px pitch (60 main px at 84, CTL R10, §7.4) and update settings.md §4.10's Refresh Rate row. p2_controls_steamvr.png shows the target. C6a (R2 fix 1, 2026-10-07): done: .st-vr .hz circles are 86 px at a 121 px pitch (gap 35, label 29 px) in mockups/settings.css, and SET §4.10's Refresh Rate row says 86 / 121 (60 / 84 main px). C6b re-renders settings-steamvr.html.

  • REQ C4a->C6b: re-render settings-steamvr.html once C6a has changed .st-vr .hz (REQ above), and measure 86 / 121 in SET T-VR. SteamVR's popovers: the current value's check in the leading slot (CTL §19.2 V4).

  • REQ C4a->C3b: control-center.md §4.4 (glyph zone "always filled") and the CC slider mockups: please adopt CTL C-D13 as reconciled in revision 3: the zone sits inside the white fill while sound is on and the level is above 0 (VP P-69 holds); at level 0 there is no white at all; muted = the zone white with the slashed glyph and the level fill at .24. Why: always-white makes 0 and muted read as "some volume" (CTL critique 4). If C3b keeps the always-filled zone at 0, please record it as a deviation from CTL R12.

  • REQ C4a->Coordinator: PLAN §1.4's "Focus on a white or coloured fill: an outer glow 0 0 18px 2px white .30" measures +4 to +12 L in the P-16 band (C1c E8, C3a, controls-measure.py); P4 has set --lgs-white-glow to 0 0 22px 8px white .55. Please amend §1.4's value (and A5) to name the token, so G-FOCUS and the text agree.

    • Coordinator (2026-10-07, round R2): amended: §1.4 now names --lgs-white-glow (and the final --lgs-focus-add .32); DESIGN2 A5 already names the glow token (line 685), so P4 has nothing to change for it (PLAN §1.4; §1.19 R2-7).
  • REQ C4a->C4b: controls.md revision 3 changes §12 for PLAN §1: glyph badges in gamepad mode only, keys keyed on the input mode, the keyboard as a thick cover in theme/layers/36-keyboard.json with K-G6 through glassd's coverDz, and field names and Enter labels from Steam's localization (English only for en*). No change to your mockups was needed; controls.css changes this session do not touch the keyboard block (verified: controls-keyboard-pad and -t1 render identically with the old and new controls.css). Please confirm §12 or answer with corrections.

  • REQ C4a->P10: gates OUTLINE edge probes flag recessed fills as glass. lab_gates.js classifies any element with a translucent background and radius ≥ 16 as glass, so the first row of a settings platter (G>Field + Background, black .14, radius 30: a recessed fill, not glass) gets an edge probe, and edge_profile.edge() returns ratio 1.0 for a uniform 1.1 L difference (segments all 1.1: no visible edge at all). Two possible fixes, either is fine: (a) in edge(), report pass: true with "edge": "none" when the brightest quarter is under 3 L (an outline must be visible to be an outline, VP P-42); (b) in lab_gates.js, do not treat an element whose background is a black tint (r = g = b = 0, alpha < .5) as glass. Seen on /zoo/buttons in both modes (<scratch>/c4a/gates/zoo_buttons_*.json, 2026-10-07 06:05).

    • P10 (2026-10-07 06:35): done, both: (a) an edge with brightest quarter < 8 dL is edge: "none", PASS; (b) a black tint (r = g = b = 0, alpha < .5) is not glass unless it has a backdrop filter or P4's --lgs-edge hook.
  • REQ C4a->P10: lab/exemptions.json: please add PLAN §1.16's control exemptions for Steam's own primitives, which T1 alone cannot tag with data-lgs-exempt: "E-SWITCH": ["%{*GamepadDialogContent>Toggle}", ".lgs-switch"], "E-CHECK": [".DialogCheckbox", ".lgs-check"], "E-SEG": ["%{Group>Button}", ".lgs-seg > [aria-pressed]"], "E-MINI": [".lgs-btn-mini.lgs-btn-circle", ".lgs-btn-sm.lgs-btn-circle"]. Today gates fails every 66.5 × 38.5 switch on P-80 ("visible short side 39 < 60", /zoo/toggles, both modes, 2026-10-07 06:00), although its hit is 87.5 × 80.5 (E-SWITCH; C4a measures each criterion itself with controls-probe.js C2 and records it).

    • P10 (2026-10-07 06:35): done: E-SWITCH, E-CHECK, E-SEG, E-MINI added as given; criteria checked live (E-SWITCH hit ≥ 95 % own over 86 × 80, E-CHECK and E-MINI over 80 × 80 (E-MINI: the field it clears allowed), E-SEG ≥ 60 × 120 and contiguous) and reported under SIZE exempt.
  • REQ C4a->P10: three gates heuristics that fail C4a's routes for reasons outside the control, all seen 2026-10-07 06:10 (<scratch>/c4a/gates/*.json): (1) P-83 on rows: a G>Field row (a focusable Panel with text and the platter fill) that is alone in its platter (/settings/system "Select preferred language", /zoo/misc "Show Chord Hint") fails "text control radius 30 < 0.45 × height 80"; rows are not buttons (CTL §10: a platter is a rounded rectangle, radius 30, VP P-83 is for buttons and circles), and the same row passes when it has stacked neighbours. Please skip %{*GamepadDialogContent>Field} (and [role="option"], [role="tab"] rows) in P-83. (2) Obscured elements: elements whose 80 px hit box lies under the bottom ornament band (y ≥ 628 on window routes, e.g. /settings/system "24-hour clock" row and its switch at y 629–710, /zoo/misc slider at 663, /zoo/fieldlayouts rows at 606–739) are reported "hit 0 % own, 0 % other"; the user scrolls them up before using them (C1a's scroll guard). Please judge only elements whose centre is in the unobscured scrollport, or list them as skipped: obscured. (3) MOTION at rest: Steam's own indeterminate progress sweep (%{Indeterminate>Percent}, infinite, also running with the theme off) fails "at rest" on /zoo/misc; it is a functional loading indicator, not ours (no lgs- name). Please exempt animations that are not lgs-* and run identically with the theme off (or that sit inside %{ProgressBarIndeterminate>ProgressBar}).

    • P10 (2026-10-07 06:35): done: (1) P-83 skips %{*GamepadDialogContent>Field} and option/tab/row/listitem rows; (2) controls whose centre is clipped or under chrome outside their scroller are skipped: obscured; (3) MOTION judges only our animations (keyframes from our sheets or transitions our rules declare); Steam's running ones are listed as steamRunning, not failed.
  • REQ C4a->P1 (was urgent; cleared by itself at 06:53:45: a later reload rebuilt the index with 550 modules, so the harvest had run before the restarted context finished loading; the request is now: retry the harvest automatically, e.g. on the next reload tick, while classModules is far below normal): after SharedJSContext crashed at 06:49:56 (/tmp/lgs/lgs.log line 2860: Runtime.evaluate … 'Target crashed', preceded at 06:49:46 by "an evaluation timed out (Steam busy?)") Steam restarted the context, and since then the theme's class index has 3 class modules instead of 550 (status: classModules: 3, 735 unresolved tokens, unchanged after two glass.py reload at 06:51:10 and 06:51:29), so almost no theme rule applies. In the new context webpackChunksteamui.push([[Symbol()],{},r=>req=r]) gives req.m with 2614 factories but an empty req.c (module cache), so a harvest that reads the cache finds nothing. Please make the index harvest work in a fresh context (e.g. read CSS-module exports by evaluating the factories that look like class maps, or wait/retry until req.c fills). C4a cannot tell what crashed the renderer; C4a's gate pass was on /settings/audio around then, and another agent's step had turned Reduce Motion emulation on at 06:49:31.

    • P1 (2026-10-07 11:25, maintenance session): done (P1's review R1 M1, wp/P1.md; runtime.md §5). (1) The harvest no longer reads req.c: it runs only the "pure" CSS-module factories (e=>{e.exports={…}}, side-effect free), so it works in a fresh context. (2) A short index is never used or cached: the core installs only with ≥ 200 CSS modules and at most half the theme's tokens unresolved, otherwise it changes nothing and answers waiting; a cached index is reused only while plausible and current (same factory count), else rebuilt. (3) The retry is automatic inside every lgs on / reload: backoff 2, 4, 8, then every 15 s, up to 120 s, each attempt in /tmp/lgs/lgs.log. There is no reload tick after a renderer crash on purpose: the theme dies with the old context (nothing persists, PLAN §0), so the next lgs on is the trigger, and it now waits for a good index. Evidence: RT-I PASS (10:58: forced short harvest gives up after logged backoff and changes nothing; a planted 3-module cache is rebuilt by on and by off + on); and a real SharedJSContext crash at 10:56:45 (journal: steamwebhelper V8 process OOM (Oilpan: Large allocation), heap ≈ 195 MB, core dumped; daemon Target crashed 10:57:12): the next lgs on (10:57:17) built the index in the fresh context (2614 factories) with 544 modules at the first attempt and installed. RT-1 now also checks that an on/off cycle retains no JS heap (−0.28 MB per cycle at 11:17).
  • REQ C4a->P10 (refinement of (2) above): an element whose centre is 3 px above the bottom ornament band but whose 80 px hit box lies 45 % under it (/settings/audio Microphone slider, centre y 625, "hit 55 % own, 0 % other") is still judged; please also count it as obscured when ≥ 25 % of its hit box is under chrome outside its scroller.

    • P10 (2026-10-07 session 4): done, with one honesty condition: a control in a scroller now counts as obscured when its centre is clipped or under chrome or ≥ 25 % of its hit box is under chrome outside its outermost scroller or outside the fixed clip (window edge, glass cut), but it is skipped only when its scrollers have the room to bring the whole box clear (scroll left at the box's obscured edge); otherwise it is judged in place and its P-08 failure says "… cannot bring it clear: needs down N (room M) px" (a last row that can never leave the ornament is a real defect). Offline fixture tools/p2/test_gates_page.py (30 % under, room → skipped; centre under, room → skipped; no room → judged, P-08 FAIL with the note); live /settings/audio --mode laser (11:33): SIZE PASS, the Keyboard sidebar row skipped as obscured (35 % under, room to scroll); the Microphone slider is now wholly under the glass cut there (clipped), so your 625 px case did not recur on today's layout. contracts/lab.md §4 sweep scope.
  • REQ C4a->C6a: the settings sidebar (PagedSettingsDialog_PageListItem, theme/60-settings.css) fails G-SIZE (42 px rows, P-80; hit overlap, P-08) and G-TYPE (16 px Regular PageListItem_Title) on every /zoo/* and /settings/* route; C4a's gates on those routes stay red until it is at 72 + 8 (CTL §10, free-standing rows) with 24 px Medium labels and the navigation-selection look (PLAN §1.4; --lgs-ill: nav gives the spot-only hover). Also on /settings/notifications: the "Show Toast" / "Play Sound" column headers (%{NotificationSectionHeader} %{Toggles}, 10 px uppercase bold in your file) fail G-TYPE, and they no longer sit over their switches, because C4a's switches are now 66 px wide with 14 px side margins (10-primitives.css §3): please centre each header over its switch column (switch centres measured live: 1039 and 1185 main px on 2026-10-07). C6a (R2 fix 1, 2026-10-07): done in session 1: rows 72 at an 80 pitch (4 px hit extender each side), 24 px Medium labels, the navigation look (--lgs-ill: nav); the notification headers are 18 px Semibold, title case, in 94 px cells centred on your switch columns. G-SIZE / G-TYPE pass on /settings/system in both modes (review R2).

Requests to C4a, handled

  • REQ C1c->C4a (value-menu depth +10 mm, leading check, Cancel 56): done in CTL rev 3 §8.2, C-D16, §14 and in controls-pickers.html, -timezone.html, -steamvr.html; answered in C1c.md.
  • REQ P4->C4a (drop 10-primitives.css section 10, the Phase 1 FocusRing; opt Steam's buttons into --lgs-ill): done in the M2 rewrite (06:33); answered in P4.md.
  • REQ C1c->C4a (delete the menu and modal sections; 22-presentations.css carries them): done 06:33 (only the rich pop-up label colours inside menu rows and the in-page sub-header colour remain, §10); answered in C1c.md.

grep -n "REQ [A-Za-z0-9]*->C4a:" docs/phase2/wp/*.md: M0 start (05:00) these two; R1 session 1 start (05:35) only P4's open; 06:33 a new one from C1c, done.

Acceptance tests at M0 (the full list is CTL §18)

Id State
C1 mockups render and measure PASS (M0-R1, M0-G2)
C20 state boards comparable PASS (M0-G2: 2.6 / 4.1 / 3.8 / 2.7 / 2.9 / 5.4 L)
C2, C22, C23 (geometry probe) passed in probe mode on 2026-10-07 (CTL §17, P-C2, P-C8); rerun in theme mode at M2
C3–C9, C13–C15, C19, C21, C25–C28, PLAN-4a-1, PLAN-4a-2 not run: they need the M2/M3 build
C10–C12, C15b–C17, C24 C4b's
C18 C6b's

Evidence

Test Command Result Files Date Steam build
M0-G1 G-FOCUS on the session 1 mockups python docs/phase2/mockups/controls-measure.py all 35/35 PASS (P-14, P-15, P-16, SEL, DIS, P-03, C20) controls-states.html, -system*.html, -notifications.html 2026-10-07 05:05 n/a (mockup)
M0-R1 renders python tools/mockshot.py docs/phase2/mockups/controls-<name>.html shots/p2_controls_<name>.png for buttons, fields, lists, motion, notifications, pickers, quickaccess, states, steamvr, switches, switches-t1, system, system-disabled, system-popup, timezone, tooltips 16 PNGs, each viewed. Fixed after viewing: overlapping notes (pickers), two-line segment labels (tooltips), trailing checks (pickers, timezone, steamvr), white Play label (buttons), the Volume glyph outside the fill (quickaccess), a second hovered control while the laser was elsewhere (switches, -t1) shots/p2_controls_*.png (16; local, gitignored) 2026-10-07 05:10–05:32 n/a
M0-G2 G-FOCUS after the changes python docs/phase2/mockups/controls-measure.py all 35/35 PASS. P-14: capsule +43.8, circle +48.0, switch track +53.2, check +62.9, pop-up +47.4, row +57.5; system rows +68.5 (switch) and +60.8 (pop-up). P-16 with P4's token: capsule +22.7, circle +28.6, switch +23.5, check +27.4 (12 px spread; +17.2 with 8 px: FAIL, hence C4a-D2), Notifications +36.1. P-15 +23.4; SEL +16.9; DIS +10.3 to +23.3 (system row +15.3); P-03 +16.0 to +21.1; C20 2.6–5.4 L same 2026-10-07 05:24 n/a
M0-K1 C4b's renders untouched each controls-keyboard*.html rendered to the scratch folder with the HEAD and the new controls.css (temporary copies controls-zztmp*, deleted after) and pixel-diffed -pad, -t1, -states, -vr: identical. keyboard, -dim: 6 066 / 5 998 px differ only in the dashboard bar, and two renders of the same file differ by the same 5 998 px (load timing), so not the CSS. The committed keyboard shots differ from today's renders because kit.css and window-nav-shared.* changed (P4, C1a) scratch only 2026-10-07 05:30 n/a
M0-T1 concept tables a scratch script counting the cells of every markdown table row, pipes in code spans included 0 rows with a wrong cell count in controls.md (two old bare |v − o| pipes and one new one escaped or reworded) concepts/controls.md 2026-10-07 05:35 n/a
M2-I1 install edit theme/_wip/10-primitives.css → device/lgs.py brace_error → copy to theme/ → python glass.py check-theme → sync theme → reload → status check-theme PASS each time; unresolved: [], ambiguous: []; cssBytes 482 481 → 637 162 with the rewrite (the %{*…} tokens expand to 4 hashes each), 06:33 after the menu/modal removal theme/10-primitives.css 2026-10-07 05:48–06:33 11094443
M2-V1 live views python glass.py shot main p2_c4a_<route> --route /<route> for zoo/buttons, toggles, sliders, dropdowns, fieldlayouts, misc, input, settings/system, notifications; pad focus shots p2_c4a_zoo_toggles_pad, _buttons_pad, _toggles_onfocus, p2_c4a_settings_system_pad; p2_c4a_zoo_sliders_notch Each viewed. Fixed after viewing: switches sat high in their rows (control column now centred), stepped-slider knob missing (Steam's handle is absolutely positioned at its container's end; my position: relative moved it off-screen), notch ticks under the track (now dots inside it), on-switch knob 6.6 px from the end (an independent scale also scaled Steam's translateX; the inset is now a gradient), bare zoo textarea white. Next to p2_controls_system.png: same platter, 80 px rows, lit focused row with the lifted switch; differences: the rich Timezone pop-up shows Steam's three lines (upper description, title, description) instead of the mockup's joined second line, and the window glass behind is the live one shots/p2_c4a_*.png (local) 2026-10-07 05:40–06:28 11094443
M2-G1 gates, pass 1 <scratch>/c4a/run_gates.py gates (= glass.py gates main --route R --mode pad|laser --flags wp.p3 --json for the 10 C4a routes), gate_summary.py gates C4a's own elements: AUD 0 on buttons, toggles, input, system, audio-pad; SIZE/TYPE only gate heuristics (switch P-80 without E-SWITCH, rows alone in a platter on P-83, elements under the ornament band) and others' elements (sidebar 32–34 per route: C6a); fixed afterwards: disabled pop-up label contrast (C4a-D11), below-label hit margins, notch row (SHRUNK 250×12 → 256×8 and P-80), label column (SHRUNK "Inline Text"), plain value text (contrast 4.8 → 3.3), free-standing field and bare textarea <scratch>/c4a/gates/*.json 2026-10-07 06:00–06:15 11094443
M2-C25 rule audit python <scratch>/c4a/c25_audit.py theme/_wip/10-primitives.css (nesting flattened, %{} kept as tokens) PASS: 208 rules, 230 selectors; 0 .gpfocus/.gpfocuswithin look outside :not(.lgs-input-laser); 0 :hover look outside :not(.lgs-input-pad); 0 .Focusable; 12 rest resets (Steam's white focus/hover fills put back to rest in every mode); C5 0 0 0 Npx layers only on the check-circle glyph ring (allowed case a) — 2026-10-07 06:33 n/a
M2-F1 G-FOCUS live (= C6 / PLAN-4a-1 part) python glass.py focus main --route /zoo/<p> --pairs <scratch>/c4a/focus/<p>.json --mode pad --flags wp.p3 --keep for buttons, toggles, dropdowns, sliders PASS all: single-control row focus vs rest: button row +167.0 L, switch row +88.0, pop-up row +90.8, slider row +79.3 (P-14 ≥ 40); disabled + focus vs disabled: button row +98.7, switch row +28.7 (≥ 10); P-16 band 8–16 px around a focused on switch +100.7 (≥ 20). Caveat: another agent's native session was on part of the time, so the rest backdrop was dark (rest L 20–38): the margins are large but are not CSS-only window-glass values; rerun planned shots/p2_focus_*.png (overwritten per route) 2026-10-07 06:17–06:18 11094443
M2-C2 geometry, theme mode (C2, C22, C23) python glass.py js "$(cat docs/phase2/concepts/controls-probe.js)({parts:['hit','slider'],mode:'theme'}).then(o=>JSON.stringify(o))" PASS 10/10 after the knob fix (06:28; first run 9/10, switch_on.knobOk false at 6.6 px). Switch 66.5 × 38.5, extender hits 19 px above/below and 8 px left/right (E-SWITCH 87.5 × 80.5); slider track 64, group 80 with its top hit, handle 64 with centre = value (0.400), mid mapping unchanged (−0.038 … 1.038 both), low values 0/0.05/0.1 hit the group (C22), default tick at the same value 0.5 (C23), notch ticks 0 px error at all four notches (C23), 18 px labels in 270 px no overlap. The probe's knob tolerance is 4 px in theme mode (the knob is drawn 3 px inside the track, CTL §6.1) <scratch>/c4a/c2_theme.json, c2_hit.json 2026-10-07 06:22, 06:28 11094443
M2-C7 press look, C28 colours, C14 reduce part python glass.py js "$(cat <scratch>/c4a/c7c28.js)" and with --media reduce PASS: lgs-pressed on a 160 px capsule → scale 1.0375 (= 1 + 6/160), on its row → none, back to 1 after 650 ms; Reduce Motion → 1.0 and the knob transition 180 ms. Test buttons with Steam's classes (own container, removed): footer Primary white on blue .86, footer Destructive #0d0e12 on red .92, GreenPlay #0d0e12 on green .88, a Destructive row action red label rgb(255 130 125); no 0 0 0 Npx ring on any of them <scratch>/c4a/c7_none.json, c7_reduce.json 2026-10-07 06:30 11094443
M3-T1 module tests python glass.py js "$(cat <scratch>/c4a/m3test.js)" --flags wp.p3,wp.c4a --mode pad and --mode laser PASS both modes. Module installed; pad: /zoo/toggles row data-lgs-ctl="1", row --lgs-focus 1, switch rail lifted .3125; knob lift: class on, P5's lgs-knob-lift running 488 ms, off after 650 ms (the zoo demo switch flipped and was flipped back); /settings/notifications check row "n", row focus 0 (focus only, nothing pressed); pill on a test strip travels 304 → 701 px in 488 ms with the 180 ms lift, aria-hidden, removed by off(); tooltip: "Show Keyboard" shown for the 60 px circular ShowKeyboardButton (/settings/keyboard, look only); removal (C19 part): flag off → state off, 0 data-lgs-ctl / .lgs-pill / [data-lgs-pill] / .lgs-knob-lift in any window, reinstalled after <scratch>/c4a/m3_pad.json, m3_laser.json 2026-10-07 06:25 11094443
M2-F2 laser (C6b: P-03, P-02) python glass.py focus main --route /zoo/buttons --pairs <laser pairs> --mode laser --flags wp.p3 with P10's real CDP hover (@hover SEL,400 / @unhover) PASS after C4a-D16: hover on the second "Click me" vs rest +24.9 L (first +42.3, then +26.3); P-02: row 1 holding Steam's stale .gpfocus under the laser vs rest +0.0 L (no focus look in laser mode) docs/phase2/mockups/controls-focus-pairs.json (pad pairs); laser pairs in this row 2026-10-07 06:36–06:42 11094443
M2-G3 gates, pass 3 (native mode off) python docs/phase2/mockups/controls-gates.py run <dir> then summary <dir> /zoo/buttons, /zoo/toggles, /zoo/sliders, /zoo/dropdowns, /zoo/fieldlayouts, both modes: AUD PASS, OUTLINE PASS, MOTION PASS; SIZE and TYPE fail only on the settings sidebar (PagedSettingsDialog_PageListItem, 26 SIZE / 14 TYPE per route: C6a, REQ). C4a's own elements: 0 failures (switches and the notch row now judged by P10's E-SWITCH/E-SEG criteria and scope rules). /zoo/misc: AUD 1 (Steam's marquee demo, below); Steam's indeterminate progress listed as steamRunning (not ours, pass) <scratch>/c4a/gates3/*.json 2026-10-07 06:35–06:43 11094443
M2-G4 gates, remaining routes (native off, after the 06:53 recovery) controls-gates.py run <dir> /zoo/misc /zoo/input /settings/system /settings/audio /settings/notifications (gates4, gates5) /zoo/input, /settings/system, /settings/audio, /settings/notifications, both modes: AUD, OUTLINE, MOTION PASS; SIZE/TYPE only C6a's sidebar and notification column headers. Fixed on the way: SettingsDialogBodyText box shrank 23 → 19 px with P4's font (now Subhead 20, text-2; also SettingsDialogDescriptionText → Footnote 18). /zoo/misc: one AUD "GONE" = the second copy of Steam's marquee text. Explained, not a regression: Steam's Marquee (module 18970) scrolls only while its IntersectionObserver reports it visible; C4a's taller rows put the demo row below the fold (y 863), so it rests with one copy; scrolled into view it plays (Marquee class, 2 copies; probed 07:05). Steam's indeterminate progress is steamRunning (not ours) <scratch>/c4a/gates4, gates5 2026-10-07 06:55–07:10 11094443
M3-T2 module tests incl. value text python glass.py js "$(cat docs/phase2/mockups/controls-m3test.js)" --flags wp.p3,wp.c4a --mode pad and laser PASS both modes (pad 9/9 checks, laser 6/6): as M3-T1 plus slider value text on /settings/audio ("25%", "100%" drawn by the group's ::after, pointer-events: none; a 0..1 level without a unit reads as a percentage with Intl.NumberFormat), and removal also clears data-lgs-val / data-lgs-mute <scratch>/c4a/m3b_pad.json, m3b_laser.json 2026-10-07 07:20 11094443
M3-G gates with T2 on glass.py gates main --route R --mode pad|laser --flags wp.p3,wp.c4a --json for /settings/audio, /zoo/sliders, /settings/notifications AUD, OUTLINE, MOTION PASS in both modes (the 84 px value column does not shrink any control below 85 %); SIZE: C6a's sidebar, plus the Microphone slider on /settings/audio whose 80 px box straddles the ornament band (centre y 625, 3 px above it; 55 % own, 0 % other) — obscured by C1a's ornament, not a C4a size; REQ to P10 below <scratch>/c4a/gates_t2/*.json 2026-10-07 07:22 11094443
C15 performance python glass.py perf main --route /settings/system PASS: theme off 90 fps (median 11.1 ms, 0 long frames); theme on 88 fps (median 11.1 ms, p95 11.5, worst 23.5, 0 frames > 34 ms); within 5 % — 2026-10-07 07:23 11094443
C13 motion (switch) python glass.py motion main --route /zoo/toggles --pre "<L.click on the zoo demo switch>" --name c4a_switch --flags wp.p3 C4a's part PASS: knob and fill travel 488 ms on --lgs-ease-b15, fill cross-fade 210 ms + 210 ms delay linear, light 294/441 ms on b0, lgs-focus-in 294 ms; 1 s later 0 running, 0 lgs-* left, 0 infinite; geometry 0 issues. The one non-token duration is the settings sidebar's 140 ms box-shadow (C6a). Strip viewed: knob travels, no text scales, no outline. The zoo demo pair reset itself on the next mount (states read back true,false,false) shots/p2_motion_c4a_switch_*.png, _strip.png 2026-10-07 07:24 11094443
PLAN-4a-2 G-MOCK (system) python glass.py cmp docs/phase2/mockups/controls-system.html --id C4a --mode pad --flags wp.p3 with docs/phase2/wp/C4a-cmp.json (pre: pad focus on the "Default to Desktop Mode on startup" switch, row scrolled to y 284) Rect deltas for the three mapped rows are one uniform offset: dh 0.0 for every row (focused 80, Timezone 96, rest 80 — identical heights and pitch), dx −168 / dw +192 / dy −70 all from the page layout around them (the live settings column is 952 wide at x 292 with C6a's current sidebar; the mockup's is 760 at x 460 under a 108 px header), so the ±8 rule fails on position only. Verdict (both images viewed): same platter, 80/96 px rows, separators hidden around the lit row, the lit row with the lifted switch and its glow, the rich pop-up as two lines ("UTC −04:00 · …"), green on-switch. Differences: Steam's down caret instead of the mockup's up/down chevron (CTL §8.1 keeps Steam's glyph), and the window tone and sidebar (C1a, C6a) shots/p2_cmp_c4a_controls-system.png, _live.png, .rects.json, .live.json 2026-10-07 07:23 11094443