40 KiB
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.mdrevision 3 conforms to PLAN §1; 16 owned mockups re-rendered and viewed;controls-measure.py all35/35 (evidence M0-* below). - M2 (T1) — reached for C4a's elements. Phase 2
theme/10-primitives.csslive since 05:48 (draft kept identical intheme/_wip/10-primitives.css; workflow: edit the draft,brace_errorfromdevice/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 vscontrols-system.html: identical row heights and pitch, offset from C6a's layout (verdict recorded). - M3 (T2) — reached.
device/rt/10-controls.js(flagwp.c4a, off by default; draft indevice/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.jsPASS in pad (9/9) and laser (6/6) incl. removal; gates withwp.c4aon 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 forcontrols-states.htmlandcontrols-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-runcontrols-gates.py runfor 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,
controlsmodule 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-tintis#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: #0d0e12and note that--lgs-text-on-tintis 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.playwith a#0d0e12label and glyph, so every area's mockup draws Play the same (C4a'scontrols.cssoverrides it meanwhile). FYI, no action needed: on 40 px check circles C4a raises--lgs-white-glow-spreadto 12 px locally (P-16 band +17.2 → +27.4 L). P4 (2026-10-07): done: (a)--lgs-text-on-color: #0d0e12added,--lgs-text-on-tintdocumented as blue only; (b)--lgs-fill-danger: rgb(255 66 69 / .92); (c) kit.lgk-btn.playlabelvar(--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-coloron 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 usesgame-pages.css), and use the dark label intheme/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 updatesettings.md§4.10's Refresh Rate row.p2_controls_steamvr.pngshows the target. C6a (R2 fix 1, 2026-10-07): done:.st-vr .hzcircles are 86 px at a 121 px pitch (gap 35, label 29 px) inmockups/settings.css, and SET §4.10's Refresh Rate row says 86 / 121 (60 / 84 main px). C6b re-renderssettings-steamvr.html. -
REQ C4a->C6b: re-render
settings-steamvr.htmlonce 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 2pxwhite .30" measures +4 to +12 L in the P-16 band (C1c E8, C3a,controls-measure.py); P4 has set--lgs-white-glowto0 0 22px 8pxwhite .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).
- Coordinator (2026-10-07, round R2): amended: §1.4 now names
-
REQ C4a->C4b:
controls.mdrevision 3 changes §12 for PLAN §1: glyph badges in gamepad mode only, keys keyed on the input mode, the keyboard as athickcover intheme/layers/36-keyboard.jsonwith K-G6 through glassd'scoverDz, and field names and Enter labels from Steam's localization (English only foren*). No change to your mockups was needed;controls.csschanges this session do not touch the keyboard block (verified:controls-keyboard-padand-t1render identically with the old and newcontrols.css). Please confirm §12 or answer with corrections. -
REQ C4a->P10:
gatesOUTLINE edge probes flag recessed fills as glass.lab_gates.jsclassifies 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, andedge_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) inedge(), reportpass: truewith"edge": "none"when the brightest quarter is under 3 L (an outline must be visible to be an outline, VP P-42); (b) inlab_gates.js, do not treat an element whose background is a black tint (r = g = b = 0, alpha < .5) as glass. Seen on/zoo/buttonsin 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-edgehook.
- P10 (2026-10-07 06:35): done, both: (a) an edge with brightest quarter < 8 dL is
-
REQ C4a->P10:
lab/exemptions.json: please add PLAN §1.16's control exemptions for Steam's own primitives, which T1 alone cannot tag withdata-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"]. Todaygatesfails 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 withcontrols-probe.jsC2 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.
- 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
-
REQ C4a->P10: three
gatesheuristics 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: aG>Fieldrow (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 onwindowroutes, e.g./settings/system"24-hour clock" row and its switch at y 629–710,/zoo/miscslider at 663,/zoo/fieldlayoutsrows 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 asskipped: 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 (nolgs-name). Please exempt animations that are notlgs-*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 areskipped: obscured; (3) MOTION judges only our animations (keyframes from our sheets or transitions our rules declare); Steam's running ones are listed assteamRunning, not failed.
- P10 (2026-10-07 06:35): done: (1) P-83 skips
-
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
classModulesis far below normal): after SharedJSContext crashed at 06:49:56 (/tmp/lgs/lgs.logline 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 twoglass.py reloadat 06:51:10 and 06:51:29), so almost no theme rule applies. In the new contextwebpackChunksteamui.push([[Symbol()],{},r=>req=r])givesreq.mwith 2614 factories but an emptyreq.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 untilreq.cfills). C4a cannot tell what crashed the renderer; C4a's gate pass was on/settings/audioaround 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 readsreq.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 answerswaiting; a cached index is reused only while plausible and current (same factory count), else rebuilt. (3) The retry is automatic inside everylgs 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 nextlgs onis 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 byonand byoff+on); and a real SharedJSContext crash at 10:56:45 (journal: steamwebhelperV8 process OOM (Oilpan: Large allocation), heap ≈ 195 MB, core dumped; daemonTarget crashed10:57:12): the nextlgs 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).
- P1 (2026-10-07 11:25, maintenance session): done (P1's review R1 M1,
-
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/audioMicrophone 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.
- 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
-
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 RegularPageListItem_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: navgives 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/systemin 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 inC1c.md. - REQ P4->C4a (drop
10-primitives.csssection 10, the Phase 1 FocusRing; opt Steam's buttons into--lgs-ill): done in the M2 rewrite (06:33); answered inP4.md. - REQ C1c->C4a (delete the menu and modal sections;
22-presentations.csscarries them): done 06:33 (only the rich pop-up label colours inside menu rows and the in-page sub-header colour remain, §10); answered inC1c.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 |