112 KiB
Glass Shell Phase 2 design system (DESIGN2)
This is the design system for Phase 2: the rules that make the Steam Frame's Steam and SteamVR interface look and behave like a native visionOS app with Liquid Glass. Phase 2 also redesigns layouts, not only skins them. Every per-surface redesign (library, game page, settings, Quick Access, friends, bar, launcher) builds on this file.
It replaces docs/DESIGN.md (Phase 1) wherever the two disagree. Phase 1 functionality guardrails (DESIGN.md §8, LAB.md) still apply unchanged.
What this file is built from
| Source | Used for |
|---|---|
research/visionos.md (VR) |
Units, window anatomy, ornaments, components, type, colour, depth, accessibility |
research/liquid-glass-motion.md (MO) |
All motion, verbatim in substance, and the material and edge spec |
research/references.md (REF) |
Measurements from the 12 reference shots: luminance band, frost, edge anatomy, fill polarity |
capabilities/spatial.md (SP) |
What the scene graph and glassd can do; live geometry; scene units |
capabilities/steam-react.md (SR) |
What new Steam React views can do |
audit/*.md |
Per-surface problems and proposals: shell-nav (SN), library-apps (LA), game-pages (GP), system (SY), social-media (SM) |
Design bible (../bible/) and its rulebook design-language.md (DL) |
The seven material behaviours, layer rule, concentric shape, review checklist |
refs/visionos/1–12 |
The look the user asked for |
Companion files
| File | What it is |
|---|---|
docs/phase2/mockups/kit.css, kit.js |
The mockup kit: this system as static HTML at true size (§15) |
docs/phase2/mockups/_example.html → shots/p2_example.png |
Worked example: the Library window with an open Sort menu |
docs/phase2/fontkit.py |
Builds the UI font subset and its @font-face stylesheet (§5) |
theme/fonts/ |
Inter 4.1 (SIL OFL 1.1) and its licence |
Evidence tags. Numbers carry the tag of their source: [official] Apple HIG or WWDC, [measured] from the reference shots or the live device, [community], [inferred], [ours] (chosen here, with the reason given). Where the sources disagree, §0.2 records which one this file follows and why.
0. Read this first
0.1 The twelve rules
- Size by angle. 1 visionOS point = 4/3 CSS px = the CSS
ptunit on the main window. Minimum target 80 px (60 pt), regular button 60 px (44 pt), body text 24 px. Nothing interactive is smaller than 60 px visible with an 80 px hit region. (§2, §4) - The Steam window is a small visionOS window. 1280 × 720 px is 960 × 540 pt. Show fewer, larger things; page or group the rest. Never shrink visionOS sizes to keep Steam's density. (§2.4)
- Chrome is outside the content. Main navigation is a vertical tab-bar ornament beside the window (Steam's frame menu). Toolbars are capsule ornaments that straddle the window's bottom edge by 28 px. No full-width header or footer bands. (§3)
- Circles and capsules. Icon-only controls are circles; text controls are capsules. No square buttons, no text "Back" or "Close". (§7.9)
- No outlines. Glass edges come from a tone step, a lens band, specular arcs lit from above, a darkened inner edge and a depth shadow. No
border, nooutline, no 1 px ring, anywhere on glass. (§6.2) - Glass only on the functional layer, never glass on glass. Window, ornaments, menus, sheets, toasts are glass. Everything inside glass is a fill. Content (art, posters, web pages) is never glass. (§6.1, §6.5)
- Recessed containers, raised controls. Platters, sidebars, fields and tracks are darker than the glass; buttons, selected rows and bubbles are lighter. (§6.5)
- White means "on". White fill + dark label = a toggled or selected button, a selected segment, or the button whose menu is open. Navigation selection (tab, sidebar row) is a lighter translucent pill. (§8.2)
- Light, not rings, for state. Hover is a soft light at the pointer; gamepad focus is illumination from within; press is a glow that spreads. Rows and toolbar buttons never scale; content cards lift. The only ring is the text-field focus ring. (§10)
- Depth carries hierarchy, a few centimetres at most. Default profile: tab bar and bar popups +25 mm, focused cards and Home cells +15 mm, menus, alerts and sheets +10 mm, everything in the window at 0; at most four depths at rest. The larger interactive depths (menus +30, sheets +50 mm) wait for a wearer (§3.8) [A3]. Text never gets depth of its own. (§3.8)
- Motion is springs, from MO only. Glass materializes, menus grow out of their source, nothing moves at rest, Reduce Motion keeps fades and removes movement. (§11)
- Title case, Inter, white. Inter Variable (Medium body, Bold titles); no uppercase chrome, no letter-spaced labels, no italics; white text at three vibrancy levels. (§5, §8)
0.2 Conflicts between the sources, and the decision taken
| # | Topic | Sources disagree | Decision in this file | Why |
|---|---|---|---|---|
| D1 | Viewing distance | Brief and VR: window at 1.43 m (0.0307°/px). SP measured: eye to window ≈ 1.15 m at summon (0.038°/px at r = 1), and the user's resize factor r = 0.863 today | Size everything for 0.0307°/px (1.43 m, r = 1). 1 pt = 4/3 px | It is the smallest angle a CSS px realistically gets. At the summon distance the same px are ≈ 24 % larger, which the Frame's lower pixel density welcomes. 80 px still meets 60 pt down to r ≈ 0.82 at 1.15 m (SP §1.2) |
| D2 | Press feedback | Phase 1 and VR §12: scale: .97 (shrink) |
Swell, +6 px on the longest side, ≤ ×1.06 (MO §4.3) | Liquid Glass grows under the finger; the task makes MO binding for motion |
| D3 | Motion tokens | VR §18 has its own easing table; GP C.11 proposes bible k/c springs; Phase 1 uses cubic-bezier(.2,.9,.25,1.15) |
MO §3.2–3.3 only | Task instruction; MO's curves are exact SwiftUI springs |
| D4 | Hover-in timing | VR §12: 120 ms in, 250 ms out | MO: 294 ms in (hover-in), 441 ms out (fade) |
As D3 |
| D5 | Reduce Motion | Phase 1 and GP C.11: every duration to 1 ms | Keep fades (150–200 ms), remove movement, bounce 0 (MO C8) | Apple keeps fades under Reduce Motion |
| D6 | Navigation selection | Phase 1 DESIGN §3 and SN C.2: white fill | Lighter pill, white .18. White only for on/selected buttons, segments and open-menu sources | Refs 5, 8, 11, 12 and WWDC23-10076 (REF H.2, VR §14) |
| D7 | Window corner radius | Phase 1: 32 px | 54 px | A 60 px circle inset 24 px is then concentric (30 + 24); refs measure 6.4–7.5 % of H |
| D8 | Font | REF H.5: keep Motiva Sans with heavier weights | Inter 4.1 Variable, Motiva Sans and Steam's fallbacks after it | Task instruction; Inter is the free face closest to SF, with an optical-size axis |
| D9 | Body text | Audits: 18–22 px | 24 px Medium; 22 px Callout for dense secondary text; 18 px floor | VR §13; the Frame's 19.6 ppd needs the larger angle |
| D10 | Focused content card depth | MO §4.4 and layers.json: +8 mm |
+15 mm static depth, reached on the depth spring |
8 mm is ≈ 0.3 display px of disparity, below what people see (VR §1.6). +15 mm is inside MO's 20 mm cap for animated depth |
| D11 | Menu depth | VR: +35 mm | +30 mm, growing from the source's depth (MO §4.6) | Keeps the animated delta (15 → 30 mm) under the 20 mm cap |
| D12 | Sheet depth | VR §16: sheet stays, parent pushes back −60 mm | MO §4.8: sheet +30 → +50 mm, parent stays and dims (scrim 35 %) | MO binds motion and its depth deltas. The 50 mm separation matches visionOS's push-back in relative depth; on fixed-focus optics only vergence changes. The SP §5 recede (proven) stays an option needing sign-off (§3.8) |
| D13 | Scrim | VR: .40 | .35 | Apple's dimming number (MO, DL §2) |
| D14 | dz units |
NATIVE.md and layers.json call them metres |
They are scene units: metres = units × S × r, S = 0.369 (SP §1.1) | Measured live. This file gives depths in mm and in units |
| D15 | Bar and popup scale | SN: popups 0.034°/px; SY: bar and its popups 0.037°/px | Both, per surface (§2.5) | The bar sits 13 cm nearer than the window |
0.3 Amendments A1–A14 (PLAN §1.18, applied by P4)
The build plan (docs/phase2/PLAN.md §1) decides conflicts between this file and the seven area concepts. Its §1.18 lists the changes to this file; they are applied in place and tagged [A] where the text changed. Where this file and PLAN §1 still disagree, PLAN §1 wins.
| # | Sections changed | Change | Source |
|---|---|---|---|
| A1 | §3.1, §3.2, §3.3, §7.4, §7.5 | Glass 656 with a 64 px ornament margin; ornament y 628–712, ≤ 960 wide; title at x 100; search 520 / 640 / circle; no microphone | WN D-1 to D-4, D-8; PLAN §1.2, §1.9, §1.10 |
| A2 | §3.5 | Windowless routes drawn with plates; LB/RB = sections; one-line labels; folder pages | HA-1, HA-3, HA-5, HA-14; PLAN §1.2, §1.14 |
| A3 | §0.1 rule 10, §3.3, §3.7, §3.8 | Two depth profiles (default, wearer), the seven admission rules, the values table; the dimming recipe | PLAN §1.7, §1.8; WN D-7, D-12 |
| A4 | §6.1, §6.2, §6.6 | θ from the shorter side; E3 radial lobe instead of the linear top layer; plates and hole treatment in the tier map | GM §1.7, WN D-15; PLAN §1.6 |
| A5 | §6.5, §8.2, §10 | Focus add .28 → .32 (the tuning range .28–.32, set from G-FOCUS); navigation-row hover = the spot only; navigation selection with arc and Semibold; input-mode keying; laser dwell; glyph badges in gamepad mode only; white-fill focus glow | PLAN §1.4; VP D-1, D-2, D-5, D-7 |
| A6 | §3.7 | Menu layouts by item count; exemption E-MENU; destructive rule by count; Steam's Cancel as a quiet capsule | WN D-5, D-16; PLAN §1.12, §1.16 |
| A7 | §12 | The friends list is not virtualized; settings drill-down may hide a section under E-DRILL | SM-D9, SET §4.3; PLAN §1.16 |
| A8 | §2.5, §3.3 | Bar pitch 64 bar px; tab-bar live pitch with a floor of 52 | CC D-CC8, WN D-2; PLAN §1.3 |
| A9 | new §10.5 | Feedback: Steam's sounds through its bus; haptics off | VP D-4, IM §6; PLAN §1.13, S17 |
| A10 | §11.5 | Route transition overrides adopted within Steam's timeouts | PLAN §1.5, §1.17 S3 |
| A11 | §17 | The verification checklist points to PLAN §4 | PLAN §4 |
| A12 | §5.2, §18 | theme/01-font.nowrap.css exists (generated) and is live; measured results |
P4 evidence (docs/phase2/wp/P4.md, FD-2) |
| A13 | §14 | Tier status from the capability studies: T3 proven; T4 click unproven; plates and holes planned | SR, SP, E2E; PLAN §1.6, §1.7 |
| A14 | §3.2, §3.7, §3.8 | Round R2 of the build: the quiet legend sits on a dim band on window routes; the library ornament hugs its members and Steam's laser Sort & Filter is the toolbar's trailing group; menu layout by count with 7–14 in the grid, E-MENU widths and Cancel at 60; alerts and sheets placed on the glass; the source card at 0 with a glow; the measured t1 dimming |
PLAN §1.19 R2-1 to R2-6 (docs/phase2/wp/coordinator.md) |
Token values are in theme/00-tokens.nowrap.css; their contract (names, meaning, how areas use the hooks) is docs/phase2/contracts/tokens.md. §16 below is the original proposal, annotated where the live values differ.
1. What Phase 2 is aiming for
The user's brief, condensed: a radical overhaul so that the Frame's UI feels like a native visionOS experience; Steam integrated into SteamVR rather than "a window into a different app"; element sizes and each context's experience re-examined and redesigned where needed; glass slabs whose outline comes from the shader; Liquid Glass motion; full glass optics, not transparency; subtle stereo pop-out.
Translated into what an observer should see in a headset capture:
| Trait | Target | Section |
|---|---|---|
| Frosted, adaptive glass | Glass luminance inside L 55–110 (of 255) whatever the room; room texture contrast down ≥ 80 %; hue follows the room | §6 |
| No outlines | Pixel profile across any glass edge: a peak on the light-facing edge, flat sides, no closed ring | §6.2 |
| Chrome as ornaments | No element wider than 50 % of the window and shorter than 60 px at its top or bottom edge | §3 |
| Size by angle | 0 controls under 60 px visible; 0 hit regions under 80 px (72 px floor where a quad is fixed) | §4 |
| visionOS type | Inter; no text under 18 px; no uppercase, tracking or italics in chrome | §5 |
| Depth hierarchy | Ornaments, menus, sheets and the focused card in front of the window by the §3.8 amounts | §3.8 |
| Liquid Glass motion | Every animation one of the MO tokens; nothing running at rest | §11 |
2. Units and viewing geometry
2.1 The conversion
- visionOS defines a point as an angle: 60 pt ≈ 2.5°, so 1 pt ≈ 0.0417° [official, VR §1.1].
- On the main window 1 CSS px = 0.766 mm = 0.0307° at 1.43 m with r = 1 [NATIVE.md, VR §1.2].
- So 1 visionOS pt = 1.36 CSS px. Working rule: 1 pt = 4/3 CSS px = the CSS
ptunit (0.0409°, within 2 %) [ours].
| visionOS | pt | CSS px | ° at 0.0307°/px | ° at 1.15 m (0.038°/px) |
|---|---|---|---|---|
| Minimum hit region and centre spacing | 60 | 80 | 2.45 | 3.04 |
| Regular button | 44 | 60 | 1.84 | 2.28 |
| Large button (primary action) | 52 | 70 | 2.15 | 2.66 |
| Extra-large button | 64 | 86 | 2.64 | 3.27 |
| Small / mini button (alone only) | 32 / 28 | 44 / 38 | 1.35 / 1.17 | 1.67 / 1.44 |
| Margin around an interactive item | 16 | 24 (21 exact) | 0.74 | 0.91 |
| Space between list or menu items | 4 | 6 | 0.18 | 0.23 |
| Bottom-ornament overlap | 20 | 28 | 0.86 | 1.06 |
| Body text | 17 | 24 (23 exact) | 0.74 | 0.91 |
| Minimum text | 12 | 18 (16 exact) | 0.55 | 0.68 |
2.2 Why distance 1.43 m (decision D1)
SP §1.1 measured the eye-to-window distance at summon as about 1.15 m and the window's resize factor as r = 0.863 today; users can also push or pull the dashboard. Designing at 0.0307°/px is the conservative case: at 1.15 m and r = 1 everything is 24 % larger in angle, and at today's r = 0.863 it is still 7 % larger. Hit regions never drop below 72 px in any quad whose size Steam fixes (SP §1.2: 72 px keeps 60 pt at r ≈ 0.86).
2.3 Display density
- Steam Frame ≈ 19.6 pixels per degree; Vision Pro ≈ 34 [press, VR §1.5]. One main-window CSS px is 0.6–0.74 display px.
- Consequences: no 1 px lines (they shimmer); glyph strokes ≥ 2 CSS px; text floor 18 px; Medium and Bold weights only.
2.4 The window-size consequence
1280 × 720 px = 960 × 540 pt, 56 % of the area of a default 1280 × 720 pt visionOS window. Every surface is laid out as a compact visionOS window: drop secondary items behind a "More" control, page them, or move them to an ornament. Expect fewer items per row and more paging. Never scale visionOS numbers down.
2.5 Every surface has its own scale
Steam and SteamVR render several quads at different distances and metres-per-pixel. To give an element the same angle as N main-window px, write N × m of that surface's own CSS px.
| Surface | °/CSS px | m (multiplier) | 80-px target becomes | 24-px body becomes | Source |
|---|---|---|---|---|---|
| Main window (all Steam routes, menus, sheets) | 0.0307 | 1.00 | 80 | 24 | NATIVE, SN §0.2 |
Steam popups at the window plane (frame.menu, tooltip, toasts, floating footer) |
0.034 | 0.90 | 72 | 22 | SN §0.2 |
Bar and its child popups (bar, barpopup: + menu, Quick Access) |
0.037 | 0.83 | 67 (use 72 where it fits); bar discs 56 at a 64 bar px pitch in 64 × 72 slots (E-BAR) [A8] | 20 | SY §0.3, CC D-CC8 |
| Steam VR keyboard (across) | 0.0416 | 0.74 | 59 | 18 | SM §0.2 |
| Volume HUD [inferred] | ≈ 0.059 | 0.52 | 42 | 12 | SY §0.3 |
| SteamVR frame controls, their tooltips, More Options | 0.023 | 1.33 | 107 | 32 | SN §0.2 |
| SteamVR Settings, Now Playing (1858 px panel) | 0.0213 | 1.44 | 115 | 35 | GP §0.2 |
| SteamVR controller binding UI (2400 px page) | 0.0164 | 1.87 | 150 | 45 | GP §0.2 |
Unless a section says otherwise, sizes in this file are main-window px. Multiply by m for other surfaces.
Tab-bar items [A8, WN D-2]. The frame menu holds ten destinations, so a fixed 72 popup px pitch would need 764 popup px against 653 available. The tab bar uses a live pitch of 52–66 frame-menu px (58 at r = 1 with Console on), circle = pitch − 8, with a floor of 52 (exemption E-TAB, PLAN §1.16).
2.6 Depth units
- Stereo disparity at 1.43 m: 1 cm ≈ 0.35–0.43 display px; 2.5 cm ≈ 0.9–1.1; 5 cm ≈ 1.8–2.2; 8 cm ≈ 2.9–3.6 [ours, VR §1.6]. Below about 1 cm stereo alone is barely visible, so every lift is paired with a shadow.
- Scene units.
vsg-transformtranslations, popup offsets andtheme/layers.jsondzare in the main panel's local units: metres = units × S × r, S =DashboardStore.dashboardScale= 0.369 [SP §1.1]. At r = 1: 1 cm = 0.0271 units. Read S and r live (SP §1.2 snippet); never hard-code.
3. Spatial layout
3.1 The scene
frame.menu popup Steam main overlay 1280 x 720 (transparent outside the glass)
= tab-bar ornament +--------------------------------------------------------------------+
+----+ | ( < ) Title ( Search capsule 64 ) ( .. ) | <- in-window toolbar row
| () | gap 16-24 | | 108 px
| () | | content (cards, lists, platters) |
| () | | |
| () | | window glass, radius 54 |
+----+ | |
+----+ +-----------+------------------------------------------+-------------+ <- glass bottom (656) [A1]
| () | | bottom ornament 84 px, overlaps by 28 | <- Steam footer, restyled
+----+ +------------------------------------------+ (720)
( frame controls ) SteamVR systemui, window plane, idle-dim
( Steam apps + ) ( clock battery bell QAM ) dashboard bar, 13 cm nearer
==== grab pill ==== SteamVR grab handle
Rendered: shots/p2_example.png.
3.2 The window (main overlay)
| Item | Value | Notes |
|---|---|---|
| Overlay | 1280 × 720 CSS px, fixed by Steam | Never resize the Steam window (SP §7) |
| Glass | Set by the route's glass mode (PLAN §1.2), never by focus: 1280 × 656 with a 64 px ornament margin below (window: library, search, Downloads, Settings, …), 1280 × 720 (window-full: What's New, /account, routes without a footer), none (windowless: Home, folders, /invites, §3.5) or art-filled (hero) [A1] |
The ornament margin technique (VR §3.3): chrome outside the glass that is still Steam DOM, so laser and gamepad work unchanged. 656 rather than 664 because SteamVR's window-bar row sits 19 px below the panel and needs ≥ 24 px clearance (WN D-1) |
| Corner radius | 54 px | Concentric with 60 px corner circles at a 24 px inset |
In-window toolbar row (Steam's #header) |
108 px tall: 24 + 60 + 24 | Needs the taller-header capability (SN C.0.1, CQ1): --basicui-header-height and HeaderStore.m_flCurrentHeaderHeight |
| Back / close | 60 px circle at (24, 24) | Always top-left. After 0.6 s of hover or focus it grows rightwards into a capsule with the previous page's title (§10.3) |
| Title | Large Title 46 px Bold, left, at x 100 (right of the Back circle, which is on every route), vertically centred on the row; nested pages: Title 2 30 px Bold centred [A1, WN D-3] | No title strip, no app name |
| Search | Centred at x 640: a 64 px capsule 520 wide on section roots (Library, Photos, Downloads), 640 on nested routes and the search route, or a 60 px circle in an 80 × 80 box (Home, folders, hero routes, Steam Settings, /account, …; PLAN §1.9). No microphone [A1, WN D-4, D-8] |
§7.5 |
| Trailing actions | 60 px circles, right inset 24, 20 px apart. On library routes in laser mode Steam's Sort & Filter pill is the trailing group: one 60 px capsule holding Steam's two buttons (80 px hits), visible y 24–84, its right edge at x 1256, ≥ 20 px clear of the search field (the account-alert circle, when shown, stays outermost) [A14, PLAN R2-2] | Account, More. In gamepad mode the same functions stay Steam's X and Y legends in the ornament |
| Content inset | 40 px left/right for text columns and grids; ≥ 24 px for anything else | |
| Bottom ornament | 84 px capsule, y 628–712, ≤ 960 wide, centred, straddling the glass bottom by 28 px. When the legend holds only A and B it is a quiet legend (no capsule material, 60 px items, Medium white .70). On window routes, where it sits in the margin over the room, the quiet members sit on one dim band: black .62, 60 px tall, radius 30, edges blurred 7 px, spanning the members + 10 px on each side, no rim, sheen or backdrop-filter, and no plate or slab in native mode (a shadow on the room, not glass); labels there 22 px Medium white .82 with the on-room shadow (flag quietBacking, default "window"; window-full routes keep the bare look inside the glass) [A14, PLAN R2-1]. The ornament hugs its members on every route, library routes included (HA's five fixed slots stay behind libFixedSlots, off) [A14, PLAN R2-2]. Members switch to a compact style rather than exceed 960; nothing is dropped or hidden (PLAN §1.10) [A1] |
Steam's #Footer restyled into it; its measured height moves page bottoms automatically (SN C.0.1) |
| Curvature | SteamVR already curves the window | Describe, don't set |
| Shape tolerance | Scroll content fades under the ornament (scroll edge, §6.7), never a hard clip |
3.3 Ornaments
General rules [official, VR §4]: ornaments sit in a parallel plane slightly in front of the window, move with it, never scroll with content, overlap the bottom edge by 20 pt (28 px), use borderless buttons inside, stay visible, and hold controls, not content. At most one extra ornament per edge.
| Ornament | Steam node | Shape and size | Depth | Tier |
|---|---|---|---|---|
| Tab bar (main navigation) | Steam's VR main menu, the frame.menu.<id> popup beside the window's leading edge (SN C.2) |
Vertical liquid capsule: circular items at the live pitch of §2.5 (52–66 frame-menu px, circle = pitch − 8) [A8]. Primary capsule: Home, Library, Store, Friends, Media, Downloads (6). A second capsule 16 px below: Settings, VR Settings, Power. Centred vertically on the glass, 16–24 px outside its edge | +25 mm (the frame-menu popup's own transform [PLAUSIBLE]; fallback Steam's +10) | T1 (Steam's own menu variables --menu-item-height, --menu-icon-size), T3/T4 (always visible: clear only_visible_with_laser, SP §6.3 [PLAUSIBLE]), T5 slab |
| Bottom toolbar | Steam's #Footer legend and per-route action buttons |
84 px capsule, 60 px controls, 12 px padding, centred, y 628–712, overlaps the glass by 28 px [A1] | 0, with an inset slab behind it (ghost rule, E2E §1) [A3, WN D-7] | T1 layout, T4 pop, T5 slab |
| Top ornament (optional, Safari-style) | Search or browser navigation for web views | 84 px capsule above the glass, 12 px gap | +25 mm | Only on routes that give up 96 px of glass |
- The tab bar expands on hover after 0.4 s (gamepad: at once on entry) to about 280 px, labels in Headline 24 px Semibold, overlaying the content; collapses 0.3 s after leaving (VR §5). Steam already expands it after 500 ms and collapses after 800 ms; keep Steam's timers if they are JS constants (SN C.2).
- Selected tab: lighter platter, white .18 (D6). Badges only for critical counts.
- Never put a menu that opens downward on a bottom ornament; open upward or centred.
3.4 Popups and their own quads
- Bar popups, the frame menu, tooltips, toasts and the volume HUD are separate SteamVR quads, placed by
ShowDashboardPopuprequests with offset (x, y, z), rotation andscale(SP §3.1). Their sizes follow §2.5. - Popups can be asked for more depth and scale (×1.1–1.25) by wrapping
SendPendingInstanceParamsToSteamVR[PLAUSIBLE, SP §3.3]. Use it for depth (+25–30 mm) before using it for size. - A barpopup host is 300 × 1024 popup px. A launcher or Control Center wider than 300 px needs several hosts (SY C.1) or a T3 route in the main window (LA C.1).
- First paint of a new popup takes ≈ 2.5 s (SP §3.2): open ornaments early, hidden, and reveal them with CSS.
3.5 Windowless views (Home and pickers)
visionOS Home and the Environments picker have no window: circular icons float in the room (refs 1, 9, 10).
| Property | Value |
|---|---|
| Route | The windowless glass mode (PLAN §1.2): /library/home, the folder routes /library/lgs/folder/*, /invites. A T3 view in the main window with no window glass; every glass element is a plate (PLAN §1.6): glassd's plate primitive in native mode, the CSS plate (black .58 under a white .16 → .04 gradient, plus edges) otherwise. Only the attended item pops [A2, HA-1] |
| Icon | 120 px circle (3.7°). Diameter / column pitch 0.54 (refs 0.52–0.55) |
| Lattice | 13 per page as 4-5-4, rows offset by half a pitch. Column pitch 224 px, row pitch 188 px (0.84 × column, refs 0.84) |
| Label | Subheadline 20 px Medium, white .92, one line ending in an ellipsis, centred, 14 px below the icon, with the on-room text shadow (§8.3); the full name shows on attention (card or name plate) [A2, HA-5] |
| Focusable | The whole cell (icon + label), ≥ 200 × 180 px |
| Sections and paging | LB/RB switch sections (Recent · Collections · Apps), as on every Steam tabbed page; D-pad Left/Right past a row end turns the page; LT/RT also turn pages when delivered. Every section and page also has a D-pad path. Page dots 12 px at 24 px pitch in a capsule; neighbour pages' edge icons at 35 % opacity [A2, HA-3] |
| Hover / focus | Scale 1.10, shadow deepens; the attended cell pops +15 mm as a non-interactive crop over its plate (plates stay at 0, PLAN §1.7) [A3]. After 0.8 s of attention (P3's attention state, gamepad focus or laser dwell) the disc morphs into a 320 × 240 card holding Play and More (HA-6). A opens the game page, X is Play (HA-4) |
| Hexagonal D-pad | Needs explicit neighbour maps (LA LQ3) so Down then Up returns to the same icon. Fallback: a 5 × 3 square lattice of the same circles |
| Folders | A collection opened from Home is a folder route (/library/lgs/folder/<id>, HA-14): pages of the same honeycomb (1 + 6 per page, VP D-13), Steam's Back and B work natively [A2] |
Use it for "+ > Launch Program" (all programs, A–Z), Library home (recent games, collections as folders) and pickers. The library catalogue stays a window of rectangular posters (LA D.1: circles are for launching, posters for browsing 350 items).
3.6 Control Center (Quick Access)
visionOS 27 Control Center is three separate portrait tiles (ref 3). SY C.1 Proposal A maps them to three bar-popup quads: the centre tile is Steam's own Quick Access, the side tiles are T3 views in other popup hosts.
| Property | Value (bar popup px; m = 0.83 vs main) |
|---|---|
| Tiles | 300 × 440 popup px each (11 × 16°), 24 px gaps, radius 44 (≈ 40 pt), panel glass, room visible between tiles |
| Toggles | 56–60 px circles at 72 px pitch; on = whole-fill colour + white glyph; off = black .30 fill |
| Rows | 64 px, glyph + label, inside a recessed platter (radius concentric) |
| Sliders | 64 px capsule track (black .30); value = white .86 fill from the leading edge with a dark glyph inside; knob only on hover or focus |
| Close | 48 px circle centred 16 px below the centre tile (B still closes) |
| Fallback (CS5 fails) | One tile in Steam's popup; its five tabs as a vertical capsule tab ornament on the tile's leading edge |
3.7 Sheets, menus, alerts, toasts
| Surface | Spec (main px) | Depth |
|---|---|---|
| Menu / context menu / dropdown | Thick glass, radius 32, padding 8, rows radius 24, min width 320, max 592, symbols left of labels (the current value's check in a leading 28 px slot), title as a 40 px header row inside the slab (22 px Bold, text-2), groups separated by 8 px of space (no separator lines). Layout by item count (table below) [A6]. The invoking button turns white while open. Grows from its source (§11.6). The slab stays inside the menu box, y 108–616 (12 px clear of the ornament's top). Steam's Cancel stays, as a 60 px quiet capsule: 60 visible on its 60 px element (no clear border), ≥ 192 wide, white .08 fill, 22 px Medium white .70, centred under the rows (across both columns in the grid), last in Steam's order, ≥ 4 px clear of the last row's fill; it has its own E-MENU line [A14, PLAN R2-5] | +10 mm (default profile), appearing with the materialize; +30 in the wearer profile [A3] |
| Popover | As a menu, sized to content, no arrow. Over 40 % of the window → treat as a sheet | +10 mm (wearer +30) [A3] |
| Sheet / modal | Centred on the glass: centre x 640, top y 108 (under the toolbar row), at most 488 tall on window routes and 552 on window-full, so its centre stays within 24 px of the glass centre (VP P-35); content scrolls inside. Width ≤ 960 (75 % of the glass), radius 44, thick glass. Close or Back as a 60 px circle top-left. Parent: scrim black .35. An outside click cancels exactly as stock (Steam's ModalClickToDismiss, never a confirm): a recorded deviation from P-65 [A14, PLAN R2-6] |
+10 mm (wearer +30 → +50); parent dims (§3.8, Dimming) [A3] |
| Alert | Centred on the route's glass: centre (640, 328) on window routes, (640, 360) on window-full and windowless (± 24, VP P-35) [A14, PLAN R2-6]. 640 px wide, radius 44, title Title 3 28 px Bold left-aligned, body left-aligned, buttons 60 px capsules side by side (keep Steam's row order: DialogTwoColLayout is a nav row; Steam's default focus untouched, S6, a recorded deviation from P-67), primary tinted, destructive red with a #0d0e12 label |
+10 mm; 0 if it contains a destructive button (wearer +30, still 0 if destructive); parent dims [A3] |
| Toast | Uses the whole 340 × 80 popup quad: panel glass card, radius 30, 48 px circular app icon, title 20 px Semibold, body 18 px, one line each. Materializes in place (no slide) | Placement is SteamVR's |
| Tooltip | Thick-glass capsule 48 px tall, 20 px Semibold, after 0.8 s of hover or focus (0.2 s out), below its button (PLAN §1.13) | CSS shadow only (wearer: owner + 5 mm) [A3] |
Menu layout by count [A6, WN D-5; A14, PLAN §1.12 R2-5]. No menu of up to 10 items scrolls; the menu box is y 108–616.
| Actionable items | Layout |
|---|---|
| ≤ 5 | One column of 72 px rows, 6 px apart, a 40 px header row |
| 6 | Compact: 60 px visible rows on a contiguous 64 px pitch, inline 26 px label |
| 7–10 | Two columns of 72 px rows, ≤ 592 px wide (columns ≥ 280): seven compact rows with a title and Cancel need 564 px, more than the 508 px box |
| 11–14 | Two columns, ≤ 592 px wide; the row area scrolls inside the slab: scroll-edge fade, no scrollbar at rest (P-72), the focused row kept fully inside the slab |
Value menus (dropdowns) follow the same count table; "up to 8" is their placement [A14, PLAN R2-5]: up to 8 options a slab anchored to its capsule; more than 8 on settings routes a list page over the detail pane; elsewhere 9–14 options the two-column grid; 15 or more one scrolling column of two-line rows. Menu rows are exempt from the 80 px size gate under E-MENU: ≥ 60 visible on a contiguous pitch of ≥ 64 (compact) or 78 (regular), rows ≥ 320 wide in one column and ≥ 280 in the two-column grid; Steam's Cancel has its own line (60 visible, ≥ 192 wide, ≥ 4 px clear of the last row, inside the slab, last in DOM order) (PLAN §1.16) [A14].
Destructive items [A6, WN D-16 = CC D-CC1]. With ≤ 2 destructive rows: red label at rest. With more (Power): red glyph and white label at rest, because a wall of red carries no signal. Always a red whole fill with a #0d0e12 label on focus. Never reordered (SM-D8); Steam's default focus untouched (S6).
3.8 Depth plan
Two profiles [A3, PLAN §1.7]. The default profile ships: every pop is a non-interactive crop, so the laser passes through it to Steam's panel at the same x/y (only proven mechanisms by default, WN D-12). The wearer profile is behind the flag interactivePops (off, sign-off S2); it restores the larger interactive depths of the table at the end of this section and turns on only after the SP §12 check by a person wearing the headset.
Admission rules (default profile). A pop must satisfy all of these; the reporter (P6) enforces them and gate G-DEPTH checks them:
- Covered (ghost rule, E2E §1): inside a cover or a plate; never over the bottom ornament, the store's navigation ornament, the tab bar, the window-bar row or the
/invitesheader. - Click-safe: dz_mm ≤ 0.25 × s × 0.769, s = the shorter side (CSS px) of the smallest visible focusable intersecting the crop; in units dz ≤ 0.000521 × s. +10 mm needs s ≥ 52, +15 mm needs s ≥ 78. A pop that fails drops to the next lower allowed value.
- Not over media or opaque art, unless the hole treatment is live and an off-axis
hvgrabpasses. - Never a destructive confirmation: an alert or sheet holding a destructive button stays flat.
- Containers only: ≥ 60 × 60 px, never text or a glyph alone.
- Still: never while its scroller moves; while a modal is open only the modal pops.
- Few depths: at most 4 distinct dz values at rest per route, from {0, +10, +15, +25} mm.
Values (default / wearer).
| Element | Default | Wearer | Mechanism |
|---|---|---|---|
| Window, content, rows, in-window controls, fields, platters, bottom ornament, store ornament | 0 | 0 | Cover; ornaments get an inset slab behind them |
| Tab-bar ornament, bar popups (+, Quick Access, …) | +25 | +25 | The popup's own transform / request z [PLAUSIBLE]; fallback Steam's |
| Home / folder plates, labels, page dots | 0 | 0 | Plates + mosaic bands |
| Focused Home cell, open card, name plate | +15 | +15, interactive | Crop over its plate |
| Focused or hovered content card ≥ 150 px | +15 | +15, interactive | Crop over the cover |
| Primary capsule ≥ 70 px; the Pause circle on the Now Downloading card | +10 | +15, interactive | Crop + tinted slab |
| Game-page cluster, Play, tab row at rest (over art) | 0; +10 once the hole treatment and the off-axis check pass | Play +15, others +10 | Crop + hole treatment |
| Game-page tabs, pinned | +10 | +20 | Crop + slab |
| Menus, popovers, dropdown slabs | +10 | +30, growing from the source's depth | Crop + thick slab |
| Alerts | +10; 0 if destructive | +30 (0 if destructive) | Crop + thick slab, or a flat thick plate |
| Sheets, the search sheet | +10 | +30 → +50 | Crop + thick slab |
| Source card while its menu is open | 0 with a CSS glow (rule 6: only the modal pops) [A14, PLAN R2-3] | 0 with a CSS glow | — (the control that opened the menu is white .94) |
| Control Center tiles (CC-M) | 0 | 0 | Plates |
| Tooltips | CSS shadow only | Owner + 5 mm | — |
Dimming [A3, PLAN §1.8]. Menus: no scrim. Alerts and sheets: Steam's .ModalOverlayBackground restyled to black .35 (--lgs-scrim) in both modes; under a pop the hole treatment's fill follows the scrim. The t1 tint is not used for in-window modals: measured in native mode (SG-5, contracts/sg.md §5), a t1 tint dims everything reparented to Steam's panel, glassd's cover, the base mosaic, the pops and their slabs, so it would dim the modal's own crop too (and in CSS-only mode it dims the modal as well) [A14, PLAN R2-4]. A window that must dim behind a bright popped element uses the surface dim (cover and base wrappers only). t1 is used only for CC-A (window.dim 0.6 alone, never with a surface dim, which would compound to 0.36) and the sheet-recede variant (S7, off). Control Center (CC-M) hides the window's content with CSS opacity while its tile plates materialize.
The original depth classes (now the wearer profile's reference) with their disparity:
| Element class | Depth (mm) | Units at r = 1 | Disparity (display px) | Paired cue |
|---|---|---|---|---|
| Window glass, content, rows, in-window buttons, fields, platters | 0 | 0 | 0 | Shadows only |
| Home icons at rest | +10 | 0.027 | 0.35–0.43 | Contact shadow (6 px y, 18 px blur, black .35) |
| Primary action capsule (Play, Install, Resume) | +15 | 0.041 | 0.5–0.6 | Tint + shadow |
| Focused or hovered content card (poster, capsule, event card) | +15 (lift) | 0.041 | 0.5–0.6 | scale: 1.05, deeper shadow, sheen |
| Tab bar, bottom toolbar, top ornament, Home icon focused | +25 | 0.068 | 0.9–1.1 | Ornament shadow on the window: 10 px y, 30 px blur, black .28 |
| Tooltips | owner + 5 | — | — | — |
| Menus, popovers, alerts | +30 | 0.081 | 1.1–1.3 | Thick glass, 12 px y / 36 px blur shadow |
| Sheets | +30 → +50 | 0.081 → 0.136 | 1.1 → 2.0 | Scrim .35 on the parent (t1 tint node, SP §5 [PROVEN]) |
| Dashboard bar | SteamVR's own, ≈ 130 mm nearer | — | — | No extra depth |
Rules:
- Pop containers, never labels or glyphs on their own. Text rides its glass.
- Crops stay at their original x/y (input lands on Steam's real panel). In the default profile crops are non-interactive, which is why depths stay click-safe (rule 2) [A3]. The wearer profile makes popped crops interactive (
interactive: true,steam-input-appid: 769,can-take-keyboard-focus: true) so the laser hits the crop and not Steam's panel behind it; this removes a 4–14 px parallax error on popped menus (SP §2.4). Registration is [PROVEN]; the click is [PLAUSIBLE]. - Animated depth deltas ≤ 20 mm, on the
depthspring (MO M4). Static depths may be larger. Disparity cap: 80 mm (≈ 3 display px). - Shadow sized by depth: y-offset 0.4 px per mm, blur 1.2 px per mm, black .25–.35, only where something of the same page is behind (CSS); glassd draws the contact shadow over the room.
- Sheet push-back variant (sign-off S7: off, flag
sheetRecede). SP §5 proved that the transform above Steam's frame page (t1) can recede the window (−0.3 units ≈ 11 cm) while it stays a laser target, and that glassd covers and crops reparented to main recede with it. The visionOS geometry (sheet at the original distance, parent −50 mm) is therefore available: recedet1by 50 mm and add +50 mm to the sheet's crop. It moves the window, which MO §4.8 lists under "must not animate" for CSS only; decide after anhvgrabcomparison of both variants. - Units:
dz_units = dz_m / (S × r).theme/layers.jsontoday uses 0.010–0.03 units (3.7–11 mm), which this plan supersedes.
4. Size scale
All values are main-window px (multiply by m from §2.5 for other surfaces).
| Element | Visible size | Hit region | Spacing | Notes |
|---|---|---|---|---|
| Icon button (circle) | 60 | 80 | centres ≥ 80 apart | Symbol 26–28 px |
| Text button (capsule) | 60 tall, 24 px side padding | 80 tall (transparent padding) | 20 px gap in a row, 24 px when stacked | Label Headline 24 px Semibold |
| Primary action | 70 tall capsule (Play: 80 tall, 280 wide on the game page, GP C.1) | ≥ 80 | — | One per screen, tinted |
| Hero action | 86 tall capsule | 86 | — | Editorial pages only |
| Mini circle (disclosure, clear-field) | 38–44 | 80 clear region | alone only | Never in rows |
| Row (list, sidebar, menu, settings) | 72 tall | 80 pitch (8 px gap) or contiguous 80 inside a platter | hover pill inset 6 px | Free-standing rows 72 + 8 |
| Text / search field | 64 tall capsule | 80 | — | Never below 60 |
| Segmented control | 64 tall track, segments ≥ 140 wide | 80 (padding inside the tab element) | 4 px between segments | Inside an 84 px ornament: 60 tall |
| Toggle | ≈ 66 × 40 (Steam's toggle at scale: 1.75, SY C.0) |
Whole row where Steam's row handles the click | — | |
| Slider | 64–72 tall capsule | 80 | — | Hit area by padding; track geometry untouched (SY CS2) |
| Checkbox | 40 × 40 rounded square | 80 column pitch | — | |
| Tab-bar item | 62 (56 popup px) circle | 79 pitch (72 popup px) | — | |
| Ornament | 84 tall | — | ≥ 24 px clearance above SteamVR's frame controls | |
| Content grid gap | ≥ 24 | — | — | Posters, tiles |
| Page dot | 12 | in a ≥ 64 px capsule with ‹ › hit areas | 24 pitch | |
| Badge | ≥ 28 tall red capsule, 18 px Semibold | — | top-right, overlapping by a third |
Audit gates (§17): no focusable element under 80 × 80 px hit box unless its centre is ≥ 80 px from every other target with ≥ 21 px clear gap; glass.py audit SHRUNK = 0.
5. Typography
5.1 The face: Inter 4.1 Variable
- Why Inter. SF Pro is licensed only for Apple platforms. Inter is the free face closest to SF's proportions, and its
opszaxis (14–32) switches to a display cut for large titles automatically, as SF switches from Text to Display [VR §13]. - Licence. SIL Open Font License 1.1, "Copyright (c) 2016 The Inter Project Authors"; no Reserved Font Name, so subsetting and embedding are allowed. The licence travels with the files:
theme/fonts/LICENSE.txt. - Files (downloaded 2026-10-07 from the Inter 4.1 release,
Inter-4.1.zip→web/InterVariable.woff2):
| File | Bytes | Content |
|---|---|---|
theme/fonts/InterVariable.woff2 |
352,240 (sha256 693b77d4…a8e3) |
The full variable font: wght 100–900, opsz 14–32 |
theme/fonts/InterVariable-lgs.woff2 |
121,460 | The Phase 2 subset built by docs/phase2/fontkit.py: 939 glyphs, 638 codepoints, both axes, features kern calt ccmp locl mark mkmk case tnum |
theme/fonts/LICENSE.txt |
4,380 | OFL 1.1 |
- Subset coverage. Basic Latin, Latin-1, Latin Extended-A, Romanian comma letters, Greek, Russian/Ukrainian Cyrillic, general punctuation, € ₹ ₽ ™, arrows, a few math signs. Measured alternatives: adding Vietnamese, IPA and Latin Extended-B/Additional doubles the file (229 KB); pinning
opszsaves 42 KB but loses the display cut. The subset keeps the OFL copyright name record.
5.2 How it is embedded
The theme is injected as one <style> per Steam and SteamVR window and nothing is written into Steam's files, so the font travels inside the CSS as a data: URI.
- Generate:
python docs/phase2/fontkit.py --css theme/01-font.nowrap.css. P4 generated it on 2026-10-07 and it is live [A12]: 163,504 bytes (the subset as 161,948 bytes of base64, the face rule and the sweep); measured 163,530 bytes inside each window's bundle. Never edit it by hand. - It must be a
*.nowrap.cssfile:device/lgs.pynests every other stylesheet underhtml.lgs-on {}and warns on@font-facethere;device/lgs_vr.pyalso sends everytheme/*.nowrap.cssto SteamVR's pages, so the same face reaches systemui. - The generated rule (base64 elided):
@font-face {
font-family: "LGS Inter"; /* private name: never collides with a page's own Inter */
src: url(data:font/woff2;base64,…) format("woff2");
font-weight: 100 900;
font-style: normal;
font-display: block; /* in-memory data: no swap flash */
unicode-range: U+0000-00FF, U+0100-017F, U+0192, U+0218-021B, U+02C6-02C7, U+02D8-02DD,
U+0300-0308, U+030A-030C, U+0326-0328, U+0370-03FF, U+0400-045F, U+0490-0491,
U+2000-206F, U+20AC, U+20B9, U+20BD, U+2122, U+2190-2193, U+2212, U+2215,
U+2248, U+2260, U+2264-2265, U+FEFF, U+FFFD;
}
- Use it through one token, with Steam's own families after it, so every codepoint outside the range (CJK, Thai, Arabic, Hebrew, Vietnamese) falls through to Steam's fonts:
html.lgs-on { --lgs-font: "LGS Inter", "Motiva Sans", Arial, sans-serif; }
html.lgs-on body {
font-family: var(--lgs-font); font-synthesis: none; font-optical-sizing: auto;
-webkit-font-smoothing: antialiased;
}
/* the sweep as generated [A12]: (0,3,2) beats Steam's strongest family rule (".BasicUI .A .B",
0,3,0) and reaches form controls, which do not inherit font-family in Chromium */
html.lgs-on.lgs-on.lgs-on body :where(:not(code, pre, kbd, samp, tt, code *, pre *, kbd *, samp *)) {
font-family: var(--lgs-font);
}
- Then sweep every surface for elements whose computed
font-familysets Motiva Sans explicitly (python glass.py styles SURFover the text runs) and point them atvar(--lgs-font). Leave monospace, the keyboard's glyph labels and any Steam icon font alone. - Languages that would mix faces in one word (Vietnamese, and any language whose letters Inter covers only partly) keep Steam's stack:
html.lgs-on:lang(vi) { --lgs-font: "Motiva Sans", Arial, sans-serif; }. Check what Steam puts indocument.documentElement.lang; if it is not the UI language, key the rule on Steam's language class instead.
- Cost: +163 KB of CSS per window that receives the bundle. Measure with
python glass.py perfand a memory read before and after; abandonopsz(−42 KB) before abandoning the face. - Verify on each surface:
document.fonts.check('500 24px "LGS Inter"')is true, a CJK friend name still renders, and no CSP error appears forfont-src data:(the risk is listed in §18). Measured 2026-10-07 [A12]: every Steam window (main,bar,barpopup,keyboard,notifications,tooltip,frame.menu,floatingfooter,volumelevel) loads the face, labels and inputs compute"LGS Inter", …, CJK falls through to Steam's font at the same width as stock, and scrollingmainstays within 5 % (88.8 vs 87.7 fps). SteamVR's pages sendContent-Security-Policy: default-src 'self' 'unsafe-eval', which blocksfont-src data:; there the face is added from script as a binaryFontFace(no fetch, so the CSP does not apply), by the SteamVR theme core (P8). Until then those pages keep Steam's stack.
5.3 Type scale
iOS default sizes × 4/3, rounded up for the Frame's density; visionOS weights (Medium body, Bold titles) [VR §13].
| Style | Size (px) | Weight | Line height | Tracking | Use |
|---|---|---|---|---|---|
| Extra Large Title 1 | 64 | 700 | 1.10 | −0.02 em | Game hero title |
| Extra Large Title 2 | 52 | 700 | 1.10 | −0.02 em | Section heroes |
| Large Title | 46 | 700 | 1.18 | −0.015 em | Window / sidebar title |
| Title 1 | 38 | 700 | 1.20 | −0.015 em | Detail-pane page title |
| Title 2 | 30 | 700 | 1.25 | −0.01 em | Section headers, nested-page centred title |
| Title 3 | 28 | 600 | 1.25 | −0.01 em | Alert titles, card titles |
| Headline | 24 | 600 | 1.30 | −0.005 em | Button labels, row titles |
| Body | 24 | 500 | 1.30 | −0.005 em | Default text |
| Callout | 22 | 500 | 1.30 | 0 | Descriptions, dense secondary text |
| Subheadline | 20 | 500 | 1.30 | 0 | Secondary row text, Home labels |
| Footnote | 18 | 500 | 1.35 | +0.005 em | Metadata |
| Caption | 18 (floor) | 600 | 1.35 | +0.01 em | Badges, legends |
Rules:
- Title case everywhere in chrome (section headers, tabs, legends, buttons). Undo Steam's
text-transform: uppercaseandletter-spacingin CSS; strings that are uppercase in Steam's localisation stay as they are. - No italics (Steam's italic search placeholder becomes upright).
- No Light, Thin or Ultralight weights. Text that sits directly on the room (Home labels) is Medium or heavier with the on-room shadow (§8.3), never thin.
- Tabular figures (
font-variant-numeric: tabular-nums) for clocks, battery, download progress, prices and sizes. - Never scale text in motion (§11).
- Inter sets wider than Motiva Sans; with the 1.5× size increase, expect truncation. Every route needs
glass.py auditand a visual pass for ellipsis on important labels.
6. Materials
6.1 Two glasses, five materials
- visionOS glass is the window itself: frosted, adaptive, no light/dark flip, chrome steady [official, DL §0].
- Liquid Glass is the functional layer floating over content: ornaments, menus, sheets, toasts, controls that float, with lensing and the iOS 27 tuning (darkened edge, brighter specular) [official, MO §1.3].
| Material | Use | glassd preset (T5, native/glassd/README.md, dial 0.5) |
Thickness θ (MO §2.3) |
|---|---|---|---|
window |
The Steam main window; SteamVR's settings and Now Playing pages | tint .55, frost mip 3.6, bezel 20 mm, lens 6 mm, dispersion .25, soft rim .6 / 12 mm | 1 |
panel |
Standalone popup quads over the room: bar popups, Control Center tiles, frame-menu host, toasts, HUDs | tint .50, mip 3.4, bezel 12 mm, lens 4 mm, rim .7 | 0.5 |
liquid |
Ornaments: tab bar, toolbars, bar segments, floating controls, Home icon discs | tint .18, mip 1.4, bezel 16 mm, lens 16 mm, dispersion .35, bright rim 1.2 | 0 |
thick |
Menus, sheets, alerts, the keyboard platter | tint .45, mip 4.2, bezel 14 mm, rim .7 | 0.5–1 |
clear |
Only over media (video controls, the Play group over hero art), always with a 35 % black dimming layer under it | liquid with low tint and low frost, 1.6× refraction | 0 |
Thickness θ = clamp(log2(shorterSide / 44 px) / 4, 0, 1) interpolates frost, lens strength, tint, specular, darkened edge and shadow between presets, including during a morph (MO §2.3). θ is taken from the shorter side: the longer side made a 600 × 64 px tab bar "thick", against its own liquid preset, and the shorter side is what bounds a slab's bezel [A4, GM §1.7]. The CSS presets (T1) do not vary with size; glassd does.
6.2 Edges without outlines
The user's requirement: menus and panels have no clear outlines; the outline comes from the glass. Five cues build every edge, none of them a uniform line [MO §2.2, REF D.3]:
| Cue | Look | T1 CSS | T5 glassd |
|---|---|---|---|
| E1 Tone step | The glass is frosted and pulled toward a mid luminance, so its silhouette shows as a change of brightness and detail | Window and panel tint (--lgs-window-bg); over the room CSS cannot frost |
Frost + adaptive tint |
| E2 Lens band | What is behind bends inward over a 6–16 px band at the bezel; the centre stays clear | SVG feDisplacementMap in backdrop-filter on the 1–3 most important small controls in a page (Chromium only); otherwise omitted |
uBezel, uRefr, uDisp |
| E3 Specular arcs | A bright arc where the edge faces the key light (top, top-left), fading to nothing down the sides; a weaker arc (25–40 %) on the opposite edge; gaps on both sides, so never a closed loop | Masked conic-gradient ring 1.5–2 px (≥ 1.2 display px) on a free pseudo-element, key alpha .45–.62, plus a radial lobe at 26 % of the width (no linear top layer: it read as a full-width line on wide slabs) [A4, WN D-15] |
uRim, uSpec, world-fixed light (the GM v2 crescent) |
| E4 Darkened edge | A 2–14 px dark band just inside the arcs, 8–20 % black; more on larger glass | Blurred inset shadow: inset 0 0 Wpx -0.15W rgb(0 0 0 / A) |
uDarkEdge |
| E5 Thickness and depth shadow | The lower edge reads slightly occluded; a shadow sized by depth grounds the slab | inset 0 -2px 4px -1px rgb(0 0 0 / .12); outer shadow per §3.8 |
Inner shadow, contact shadow |
Rules:
- R1 No
border, nooutline, no uniforminset 0 0 0 1pxring on any glass. The Phase 1 rim tokens--lgs-window-rim,--lgs-panel-rim,--lgs-glass-rimlose their closing ring term. - R2 One light for the whole scene: from above, 20° left of vertical (
conic-gradient(from 340deg …)), matching glassd's light. Every arc peaks at the same angle. - R3 Arcs scale with size: on a 40 px capsule the key arc spans the top half; on a window it is a band near the top-left corner plus the lobe at 26 % of the width [A4].
- R4 Selection and focus never add an outline (§10).
- R5 The only stroke allowed is under Increase Contrast / Steam high contrast: 2 px solid white .70, glass opaque (§13).
- No 1 px lines inside glass either: separate panes by a material step, rows by space, and use a ≥ 2 px soft separator (white .08, inset 72 px) only where grouping is otherwise lost.
The edge CSS (T1), on a pseudo-element Steam leaves free, with the host already positioned by Steam. As built [A4]: theme/03-material.css draws it on ::before of any element that sets --lgs-edge: window|panel|liquid|thick|clear|control (a CSS style query; contract docs/phase2/contracts/tokens.md §2), with the lobe in place of the linear layer below and the rim ×1.5 under gamepad focus. The original recipe:
.lgs-edge::before {
content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
padding: var(--lgs-rim-w, 1.5px);
background:
conic-gradient(from 340deg,
rgb(255 255 255 / calc(.62 * var(--lgs-rim, 1))) 0deg, rgb(255 255 255 / calc(.20 * var(--lgs-rim, 1))) 38deg,
transparent 72deg, transparent 148deg,
rgb(255 255 255 / calc(.20 * var(--lgs-rim, 1))) 180deg,
transparent 214deg, transparent 288deg,
rgb(255 255 255 / calc(.22 * var(--lgs-rim, 1))) 322deg, rgb(255 255 255 / calc(.62 * var(--lgs-rim, 1))) 360deg),
linear-gradient(180deg, rgb(255 255 255 / calc(.30 * var(--lgs-rim, 1))) 0, transparent 26%);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor; mask-composite: exclude;
}
.lgs-edge { box-shadow: inset 0 0 var(--lgs-de-w, 7px) calc(var(--lgs-de-w, 7px) * -.15) rgb(0 0 0 / var(--lgs-de-a, .14)),
inset 0 -2px 4px -1px rgb(0 0 0 / .12); }
| Material | Rim width | Rim strength | Dark edge width / alpha |
|---|---|---|---|
| window | 2 px | .78 | 16 px / .20 |
| panel | 1.5 px | .90 | 10 px / .16 |
| liquid | 1.5 px | 1.10 | 6 px / .12 |
| thick | 1.5 px | .85 | 12 px / .20 |
| clear | 1.5 px | 1.20 | 5 px / .10 |
6.3 Tone and adaptation
- Target band. Glass luminance converges to L 55–110 of 255 whatever the room [REF D.1]: bright rooms darken by 10–50 %, dark rooms lift by 20–50 %. Text-bearing glass (window, menus) should sit at L 70–90, so secondary text keeps 4.5:1 (§8.1).
- Hue follows the room. No fixed blue-grey tint: the frosted room carries its own hue; the tint layer is neutral grey.
- T5: glassd's adaptive tint (darker and stronger over bright rooms, lighter over dark ones) does this per pixel at a coarse mip.
- T1 fallback over the room:
backdrop-filtercannot see the room (DESIGN.md §1), so the window is a smoky tintrgb(20 22 30 / .60–.84)(the dial) plus the edge cues. That keeps contrast but shows the room unblurred at reduced strength; it is the accepted fallback when glassd is not running. In-page Liquid Glass (menus, ornaments over the window) still frosts what is inside the same page. - Measured in the mockup kit (which stands in for T5 by frosting a room photo): window glass L 84 over the bright lounge (room L 134), L 84 over the dim room (room L 51), L 117 under a bright window in the studio photo (above the band; glassd's local adaptation is expected to pull it down). T1 tint: L 57.
6.4 Variants in Steam
| Variant | Steam use | Rule |
|---|---|---|
| Regular | Default for everything | Adaptive; text-safe |
| Clear | Media controls over trailers and streams, the Play group over hero art, the screenshot viewer | Only with a 35 % black dimming layer under it and bold, bright content on top; never mixed with Regular in one group |
| Tinted | One primary per screen: Play / Resume (green), Install / Update / confirm (blue), destructive confirm (red) | Colour in the glass or the whole fill, never in the label |
| Identity (off) | Anything already inside glass; everything under Increase Contrast | — |
6.5 Fills on glass (no glass on glass)
| Fill | Value | Use | Polarity |
|---|---|---|---|
| Thin | white .10 | Buttons at rest, chips | Raised |
| Navigation selected | white .18 | Selected tab, sidebar row, list selection | Raised |
| Hover add | + white .08 | Hover | — |
| Focus add | + white .32 (the final value of the .28–.32 tuning range, set from G-FOCUS: .28 failed P-14 and P-15 on the tab bar over glass at L ~95; docs/phase2/wp/P4.md P4-D8) [A5, VP D-2] |
Gamepad focus | — |
| Regular | black .14 | Sidebar, grouped platters, Control Center inner platter | Recessed |
| Thick | black .30 + inset 0 2px 5px rgb(0 0 0 / .26) |
Text and search fields, slider tracks, segmented tracks | Recessed |
| Selected | white .94, label #0d0e12 |
Toggled / selected button, selected segment, open-menu source | Raised, reserved |
| Bubble (others) | white .14–.18 | Chat bubbles | Raised |
Never make a grouped section lighter than its glass (Phase 1's white .06 platters are inverted).
6.6 Tier map for materials
| Element class | T5 (glassd running) | T1 fallback (CSS only) |
|---|---|---|
| Main window | window cover over the room (656 or 720 by glass mode), shapes exclude the ornament margin |
Smoky tint rgb(20 22 30 / .60–.84) (dial) + E3–E5 |
Windowless routes (Home, folders, /invites), Control Center tiles, menus and alerts kept flat |
Plate [A4]: an opaque glass shape with its own material, phase and tint at the cover's depth; Steam's real panel hidden under it (PLAN §1.6, P9 G1) | CSS plate: black .58 under a white .16 → .04 gradient + edges (--lgs-mat-plate-bg) |
| Bottom ornament | liquid inset slab behind it at 0 mm [A3] |
In-page blur 12 + saturate 1.7 + liquid edges |
| Tab bar | liquid cover of the frame-menu popup at +25 mm |
Panel tint + edges |
| Menus, sheets, alerts | thick slab under the pop at +10 mm (wearer +30 / +50), or a flat thick plate [A3, A4] |
backdrop-filter blur 30 px + tint .30–.40 + edges + shadow |
| Under every pop | Hole treatment [A4]: the popped element's contact shadow and its container's tone drawn into the cover, so the hole reads as its shadow, not a bright sliver (PLAN §1.6, P9 G2) | — (nothing pops) |
| Bar segments, popup quads | liquid / panel cover |
Tint + edges, no blur |
| Primary capsule | Tinted liquid slab (needs a per-slab tint, GP GQ8) |
Tinted fill + edges |
| Content (art, posters, web pages) | Never glass | Never glass |
6.7 Scroll edges
Content fades under floating bars instead of hitting a hard clip or an opaque band [official, MO §4.13]: a band of the bar's height + 16 px with a progressive blur and a 30 % → 0 dimming gradient (dimming dominates over Steam's dark content), opacity scroll-linked over the first 24 px of scroll. Where Steam's scroller can take a mask, a mask-image gradient over its last 120 px under a bottom ornament is enough (as in the example), but only if nothing inside the scroller uses backdrop-filter: a mask makes the scroller a backdrop root (MO R1, R11). One edge effect per view, never stacked.
7. Spatial components
Each component lists shape and size, material, states, depth and tier. Sizes are main px.
7.1 Window anatomy
See §3.2 (window) and §3.1 (the scene).
7.2 Tab-bar ornament
See §3.3.
7.3 Bottom toolbar ornament
- Holds Steam's footer actions as real buttons: a mixed-case label 22–24 px with the controller glyph as a small leading or trailing badge (30 px circle, white .20, 16 px Bold letter) that teaches the gamepad mapping (SN C.4). The legend stays Steam's node; its click dispatch is unchanged.
- Shows current state: the current sort ("Recently Played"), the active filter count (a badge), the VR segment.
- Library example: Sort capsule, Filter capsule with count,
All · VR · Non-VRsegmented control, More circle; 896 px wide (shots/p2_example.png). - Its width follows its members on every route, the library included (no fixed slots); in laser mode Steam's Sort & Filter pill is not a member but the toolbar row's trailing group (§3.2), and in gamepad mode the same functions are Steam's X and Y legends here, Y carrying the current sort [A14, PLAN R2-2].
7.4 Corner buttons, back and close
60 px circles concentric with the window corner (inset 24), with 80 px hit boxes at (14, 14). Back top-left on every route, actions top-right. The Large Title therefore starts at x 100 [A1, WN D-3]. Back grows into a capsule with the previous page's title after 0.6 s of hover or focus (Subheadline 20 px Medium, max 240 px, the circle stays in place). B keeps working. Steam's "Back" text shown as the chevron circle is exemption E-BACK (PLAN §1.16).
7.5 Search field (the user's named example)
| Property | Value |
|---|---|
| Where | Centred in the window's toolbar row on every route (Steam's %{SearchBox} element restyled, SN C.1); the same capsule on the search route; optionally a Safari-style top ornament for web views |
| Shape | 64 px capsule (never below 60): 520 wide on section roots (Library, Photos, Downloads), 640 on nested routes and the search route; or a 60 px circle in an 80 × 80 box on Home, folders, hero routes, Steam Settings, the /chat sidebar row, the photo viewer, /account, the /invites card and achievements (PLAN §1.9) [A1, WN D-4] |
| Fill | Thick (recessed): black .30 + inner shadow |
| Content | Magnifier 26 px at 22 px inset, placeholder Body 24 px Medium at white .55 (upright), clear as a 44 px circle inside with an 80 px hit area. No microphone: Steam has no dictation [A1, WN D-8] |
| Focus | The one ring in the system: 3 px soft white .55 + 16 px glow |
| Keyboard | Opens on activation only. Typed text is echoed on the keyboard's own slab (SM C.2, SQ4), so eyes stay on the keys |
| Search sheet (T3) | Search is window-nav §4: a sheet over a snapshot of the page you were on, with Steam's search route underneath; the whole search stays in the sheet (WN D-17, PLAN §1.9). Other areas add results through C1b's provider API |
7.6 Sidebar and split view
- Sidebar runs the full height on the leading side, ≈ 30 % of the glass (340–400 px), one material step darker (black .14) with no line, no inset box and no rim (refs 2, 4, 8, 12).
- Rows 72 px, 8 px apart, Body 24 px Medium; selected row = white .18 capsule pill inset 8–12 px; hover is the light spot only (no fill) and gamepad focus adds white .32 + the spot + the arc as the E3 lobe (the
navkind, §10) [A5]. No scale, no lift. - Settings-style rows have a 40 px coloured circle with a white glyph (T2 decoration keyed by a
data-lgs-pageattribute, SY C.2), trailing value in text-2, chevron. - Header: Large Title 46 px Bold at 32 px from the top; search capsule under it when it filters the sidebar.
7.7 Lists and grouped rows
Platter: black .14, radius 30 (54 − 24), 24 px from the glass edge, rows 80 px contiguous with a hover pill inset 6 px (radius 24). Trailing value + chevron in text-2. No separators, or 2 px white .08 inset 72 px. Platter max width ≈ 760 px centred on wide detail panes, which cuts the label-to-control sweep from 25–28° to 18–20° (SY C.2).
7.8 Content cards and posters
Content is opaque and rounded, never tinted or blurred. Posters radius 20 (concentric); focus or hover = lift: scale: 1.05, +15 mm, shadow 0 18px 44px /.48, a soft white glow 30 px, and a diagonal specular sheen (tvOS-style; as built with normal compositing, not plus-lighter, like every illumination layer, §10) [A5]. No ring. Title appears under the focused poster only (Steam's shelf behaviour), Headline 24 px.
7.9 Buttons
Circle (icon only) or capsule (text, or text + icon) [official, VR §11]. Rest fill thin; inside ornaments borderless (transparent at rest). One tinted primary per screen. Disabled: content at 40 % opacity, no hover. Destructive: red label at rest, red whole fill on focus.
7.10 Segmented control
Recessed capsule track (black .30, 64 px), segments as capsules with 22 px Semibold labels, selected segment = white pill with a dark label (this is where white is right). The pill travels on snappy (§11). Steam's tab strips with explicit onMoveLeft/onMoveRight handlers must stay horizontal (GP §0.5).
7.11 Toggle and slider
- Toggle: visionOS proportions ≈ 66 × 40, green whole-fill track when on, white knob; enlarged with the independent
scaleproperty so Steam's knob translation keeps working (SY C.0, CS1). - Slider (Control Center style): capsule 64–72 px, value = white .86 fill from the leading edge with a dark glyph inside it, track recessed, knob only on hover or focus. Value follows the pointer directly.
7.12 Menus, sheets, alerts, toasts
See §3.7 for sizes and depths, §11.5 for their motion.
7.13 Window bar and frame controls (SteamVR systemui)
- Grab handle → the visionOS window-bar pill: 180 × 16 visible (white .52, soft shadow), hit area ≥ 80 px tall, under the dashboard bar where SteamVR places it. Brighter on hover.
- Frame controls (keyboard, float, theater, more) → glass circles in a quiet panel capsule; can be enlarged with CSS because their quad follows the DOM (SP §6.2 [PROVEN]): 64 systemui px circles (= 48 main px; target in systemui px is 107, so prefer 80 systemui px circles where the atlas allows). Keep Steam's idle-dim opacity rule; compensate with brighter fills.
7.14 Dashboard bar
Each %{BarSurface} becomes one liquid capsule (radius = half its height). Buttons 60–64 popup px circles at ≥ 72 popup px pitch; white only when toggled or when their popup is open; clock 24–28 px tabular; battery as a capsule with level fill and number (only when Steam's Battery Percentage setting is on); unread notifications as a red count badge; running apps as circular art (portrait crop) with a lighter platter and a small white dot when selected (SY C.4, LA C.7).
7.15 Keyboard
Keys stay Steam's (geometry fixed); keys become raised fills with radius 10–12, Enter the one blue key, the touched key white with a dark label. A glassd thick platter 1 cm behind the keyboard quad (SM SQ14) and a display-only echo row at the top of the slab with the field's label.
8. Vibrancy and colour
8.1 Text on glass
| Level | Value | Use |
|---|---|---|
| Primary | white .96 | Titles, labels, body |
| Secondary | white .70 | Values, metadata, descriptions |
| Tertiary | white .50 | Non-essential hints only, never required information |
| Quaternary | white .32 | Disabled glyphs |
Contrast check: on glass at L 85, secondary white .70 composites to ≈ L 204 → 4.6:1; at L 110 it drops to 3.4:1, which is why text-bearing glass targets L 70–90 (§6.3). Primary white (.96) keeps ≥ 4.8:1 across the whole band (4.85:1 at L 110).
8.2 Selection vocabulary
| State | Look |
|---|---|
| Toggled or selected button, selected segment, button whose menu is open | White .94 fill, dark label |
| Current navigation location (tab, sidebar row, list selection) | White .18 translucent pill with a specular top arc, white Semibold label [A5] |
| Hover (laser) | + white .08 and a light spot (.12) at the pointer, under the label; on navigation rows (sidebar, list, tab bar) the spot only, no fill [A5, SM-D13, SET T-SEL] |
| Gamepad focus | + white .32 uniform, a .16 spot in the upper third, the control's own specular arc ×1.5; the first frame shows 60 % [A5, VP D-2] |
| Focus on a white or coloured fill | A blurred outer glow (--lgs-white-glow, 22 px blur, 8 px spread, white .55), never a ring (VP P-16) [A5] |
| Disabled + focus | Fill .10, arc at 60 %, no spot (CTL C-D15) [A5] |
| Text field focus | 3 px soft ring white .55 (the only ring) |
Criteria (gate G-FOCUS, PLAN §1.4): focus ≥ +40 L over rest; focus ≥ selected + 15 L; selected ≥ hovered + 12 L; glow band ≥ +20 L on white and coloured fills; disabled + focus ≥ +10 L; first frame ≥ 60 % of the final contrast. Focus and selection also differ in shape where they share a control (WN D-10: the selected tab is a circle behind the glyph, focus a pill).
8.3 Colour
- visionOS uses the dark-appearance system colours [official]: blue rgb(0 145 255), green rgb(48 209 88), red rgb(255 66 69), orange rgb(255 146 48), yellow rgb(255 214 0), teal rgb(0 210 224), indigo rgb(109 124 255), purple rgb(219 52 242). Increased-contrast variants in VR §14.
- Colour only in whole fills: on-toggles, the tinted primary, badges, Steam's semantic online/in-game colours as dots or rings. Never thin coloured text on glass (exception: a destructive row's red label, which is an action label at Headline weight).
- Play keeps Steam green (
rgb(48 209 88 / .88)), Install/confirm blue (rgb(0 145 255 / .86)), destructive red (rgb(255 66 69 / .84)). - Text directly on the room (Home labels): Medium or heavier, white .92, shadow
0 1px 3px rgb(0 0 0 / .55), 0 0 12px rgb(0 0 0 / .35). - Large white areas glare in a dark room: keep white fills small (selected states, slider values).
- Test colour in both passthrough modes: mono removes room colour, the colour camera brings it back.
9. Iconography
9.1 Symbols
- SF Symbols cannot be used off Apple platforms. Use Steam's own SVG icons where Steam provides them, recoloured white; for new T2/T3 elements, use symbols drawn to SF proportions on a 24-unit grid with ≥ 2 CSS px round strokes (the kit's sprite in
kit.jsis the reference: home, library, store, friends, media, download, gear, VR, power, search, mic, sort, filter, play, keyboard, float, theater, check, xmark, speaker, wifi, bluetooth, bell, controller, grid, recenter, person, star, trash, share, clock, pencil, window, terminal, sparkle, moon, sun, airplane, room). A third-party set must be OFL, MIT or ISC and keep its licence file. - Sizes: 26–28 px in a 60 px circle (≈ 45 % of the diameter), 24 px in rows and menus, 28 px in tab-bar items, 22 px inside fields and chips.
- Icon-only buttons get an
aria-label(T2) and a tooltip after 0.8 s.
9.2 Circular icons for games (Home and launchers)
Composition for a Steam game (T3 view; LA D.2):
- A circle mask with the game's hero image,
object-fit: cover, centred (by Steam's asset rules the hero carries no logo). - The logo centred at 70 % of the diameter with a soft shadow.
- A specular arc from above and a contact shadow (6 px y, 18 px blur, black .35); no frost, no tint: art is content.
- Fallbacks in order: portrait capsule cropped at
object-position: 50% 30%, the user's custom art, then a monogram (initials in Bold on a gradient taken from the icon's average colour).
- Sources: Steam's own
appStore/appDetailsStoremethods (GetCachedVerticalCapsuleURL,GetHeroImages,GetLogoImages); always the first URL Steam returns, from the local/assets/cache. Hero composites only for the visible page (a hero decodes to ≈ 4.8 MB; LA LQ6). - Badges: red capsule top-right (updates, new); a "Running" badge from
MainRunningAppID.
9.3 Circular icons for programs (Launch Program)
A glass disc (T5 liquid slab, or a thin fill + specular arc in CSS) holding the program's icon at ≤ 1.5× its source size: 48 px for Steam's 64 px strIconDataBase64, up to 72 px when the daemon supplies a ≥ 128 px icon-theme file (LA D.3, LQ4). Missing icons: a category glyph (terminal, settings, display) chosen from the name, else the initial. Full-bleed only with a ≥ 192 px source.
9.4 Other circles
- Folders / collections: a circle holding a 3 × 3 grid of portrait crops; empty collections dimmed, sorted last, labelled "Empty".
- Avatars: circles; presence as a 3 px ring in the state colour drawn on the avatar image (Steam's animated status element untouched).
- Running apps in the bar: 56 popup px circles of portrait crops.
- Controller glyph badges: 30 px circles, white .20, 16 px Bold letter (A, B, X, Y, ≡), on visible buttons that have a gamepad shortcut.
10. Hover, focus and press
The laser plays the role of the eyes; Steam's gamepad focus (.gpfocus) plays the role of the controller focus system. Every focusable element shows a state in both models, and nothing is reachable by hover alone [official, VR §12].
Input-mode keying [A5, VP D-1]. Steam sets .gpfocus for the laser too, so every look is keyed on P3's input mode (html.lgs-input-laser / html.lgs-input-pad, from FocusNavController.NavigationSource): laser looks key on :hover only, never on .gpfocus (a stale .gpfocus can survive under the laser); the focus look keys on .gpfocus only in gamepad mode. theme/04-states.css publishes the states as numbers (--lgs-hover, --lgs-focus, --lgs-press, --lgs-lift, --lgs-dis) so areas never write input-mode logic (contract tokens.md §3).
Laser dwell [A5, VP D-7]. A laser sweeps through every card in its path, so under the laser brightness changes at once but lift, scale and depth start only after 80 ms of dwell (.lgs-dwell, sign-off S25). Gamepad focus lifts at once.
10.1 The illumination layer
States are drawn on a pseudo-element Steam leaves free (::after usually; Steam's FocusRing uses ::after on some nodes), pointer-events: none, never an outline (MO §6.4 recipe "Illumination layer"). As built: the hook --lgs-ill: raised|recessed|row|nav|white|card on an element draws the layer on its ::after (theme/04-states.css), with normal alpha compositing, not plus-lighter: VP D-2's "+48 L for .28" is normal compositing, and a blend mode on every hooked control would force isolated groups. Steam's floating FocusRing becomes a light plate (white .12 + the .16 spot + a soft glow, no outline, no pulse) where Steam draws its only focus cue (CTL §4.5), and paints nothing where the focused control has its own illumination layer (R1). The hook rules list their host elements instead of a universal *::after subject (a universal pseudo-element rule inside a style query cost the library grid about 20 % of its scroll frame rate; contracts/tokens.md §2).
| State | Laser hover | Gamepad focus |
|---|---|---|
| Fill | + white .08 (navigation rows: none) | + white .32, held [A5] |
| Light spot | Radial gradient at the hit point (T2 writes --hx/--hy from pointermove, throttled to frames), radius 1.5 × the shorter side, peak + .08–.12 additive (MO §4.2) |
Static, centred in the upper third, peak + .16 |
| Specular | — | The control's own arc ×1.5 (it "lifts into glass") |
| First frame | — | ≥ 60 % of final contrast immediately (P5's lgs-focus-in starts at --lgs-focus-first = .60), so focus is never invisible during D-pad auto-repeat |
| Content cards | Lift (§7.8) after 80 ms of dwell [A5] | Lift at once |
| Rows, toolbar buttons, tabs | No scale, no lift | Same |
| Text fields | Fill only | Fill + the 3 px ring |
10.2 Press
- Glow from the hit point (laser) or the centre (gamepad A), radius 0.5 → 1.5 × the shorter side, + white .15 additive, + .06 uniform.
- Glass controls swell by +6 px on the longest side, capped at ×1.06 (
scale: min(1.06, 1 + 6 / maxSide)); rows and cards brighten only. - Release: glow off in 90 ms linear, scale back on
snappy. The action fires immediately; nothing waits for an animation. - Gamepad A has no
:active; T2 must observe Steam's gamepad button events on the focused element to set algs-pressedclass (MO risk 4: event names to verify).
10.3 Delayed reveals
| Reveal | Delay in / out |
|---|---|
| Tooltips | 0.8 s / 0.2 s |
| Back button title | 0.6 s |
| Tab bar labels | 0.4 s (gamepad: immediate on entry) |
| Home icon actions (ramp: 2 % scale at once, pop at 0.8 s) | 0.8 s |
| Secondary actions on a card (like, comment) | Revealed by focus as Steam does today, materializing in 250 ms |
10.4 Parity rules
- Every hover reveal also appears under gamepad focus.
- Every footer-legend-only action gets a visible button (bottom toolbar, a "⋯" on the hovered or focused row) that dispatches the same Steam handler (SM §A.10, LA C.4).
- Unavailable items give no hover feedback.
- Controller glyph badges show only in gamepad mode (keyed on
html[data-lgs-vr-mode="gamepad"]); in laser mode they would teach buttons the user is not holding. Steam's own laser-mode pill replaces the legend there [A5, VP D-5].
10.5 Feedback: sound and haptics [A9]
- Sound (VP D-4, IM §6.4): our T2/T3 controls call Steam's sound bus with the same
ENavSoundSteam plays for the same event (navigation move, activate, toggle, back), so new controls do not feel dead next to Steam's. No new sound files, no sound on hover, and Steam's UI-sounds setting is honoured. - Haptics: off (sign-off S17, flag
haptics). No wearer can judge them.
11. Motion
Everything in this section comes from research/liquid-glass-motion.md (MO). Section numbers in brackets point there. Apple publishes no glass-specific durations; the official numbers are SwiftUI's springs, and glass timings are community 120 fps measurements of iOS 26, subdued for pointer and gamepad as the HIG prescribes ("a more subdued effect when a person interacts using a trackpad") [MO §0, §1].
11.1 The spring model [MO §3.1]
Springs are defined by perceptual duration d and bounce b (SwiftUI Spring), mass 1:
stiffness k = (2π / d)² damping c = 4π (1 − b) / d dampingFraction ζ = 1 − b
settle = last time |1 − x(t)| > 0.001 (Apple's epsilon)
Official presets [MO §3.1]: Animation.default = spring(response 0.55, dampingFraction 1.0): settles in 808 ms. smooth / snappy / bouncy = d 0.5 with b 0 / 0.15 / 0.3. interactiveSpring = response 0.15, ζ 0.86.
11.2 Tokens [MO §3.2]
The CSS duration of a token is the spring's settling time; the easing depends only on the bounce.
| Token | d (s) | b | t90 | CSS duration | Overshoot | Easing | Use |
|---|---|---|---|---|---|---|---|
interactive |
0.15 | 0.14 | 78 ms | 210 ms | 0.5 % | b15 | Press-in, drag-follow smoothing, gamepad slider steps |
hover-in |
0.20 | 0 | 124 ms | 294 ms | 0 | b0 | Hover light in, focus illumination in |
fade |
0.30 | 0 | 186 ms | 441 ms | 0 | b0 | Hover and focus out, content cross-fades, list insert |
snappy |
0.35 | 0.15 | 178 ms | 488 ms | 0.6 % | b15 | Selection pill travel, toggle knob, toast in, release |
release-touch |
0.40 | 0.25 | 178 ms | 510 ms | 2.8 % | b25 | Direct hand poke only (not used with laser or gamepad) |
morph-open |
0.45 | 0.20 | 214 ms | 607 ms | 1.5 % | b20 | Menu, popover, dropdown grows out of its source |
morph-close |
0.30 | 0 | 186 ms | 441 ms | 0 | b0 | Menu returns into its source |
sheet-in |
0.50 | 0 | 310 ms | 735 ms | 0 | b0 | Sheet or modal present; parent dims |
sheet-out |
0.35 | 0 | 217 ms | 514 ms | 0 | b0 | Sheet dismiss |
page |
0.45 | 0 | 279 ms | 662 ms | 0 | b0 | Route and tab content transitions |
depth |
0.30 | 0 | 186 ms | 441 ms | 0 | b0 | z of popped crops and slabs |
materialize-in (small glass) |
linear channels | — | — | 250 ms | — | linear | Toasts, tooltips, chips, ornaments appearing |
materialize-out (small glass) |
linear channels | — | — | 350 ms | — | linear | Small glass leaving |
materialize (large glass) |
rides the surface's spring | — | — | sheet-in / sheet-out |
— | — | Sheets, windows |
11.3 Easing strings [MO §3.3]
Chromium 126 (Steam's CEF) supports linear(). One curve per bounce value:
/* bounce 0 (settle = 1.470 × d) */
--lgs-ease-b0: linear(0, .005 1.2%, .020 2.3%, .084 5.2%, .159 7.7%, .461 16.9%, .556 20.2%, .639 23.5%, .709 26.9%, .767 30.2%, .817 33.7%, .861 37.6%, .900 42.1%, .930 46.9%, .954 52.4%, .971 58.6%, .991 73.8%, 1);
/* bounce 0.15 (settle = 1.393 × d, overshoot 0.6 %); also interactive (b .14) */
--lgs-ease-b15: linear(0, .006 1.3%, .024 2.7%, .054 4.2%, .093 5.7%, .188 8.7%, .518 18.2%, .613 21.4%, .696 24.5%, .767 27.7%, .825 30.9%, .872 34.1%, .913 37.6%, .945 41.4%, .970 45.6%, .988 50.3%, 1.000 55.8%, 1.006 67.9%, 1);
/* bounce 0.20 (settle = 1.349 × d, overshoot 1.5 %) */
--lgs-ease-b20: linear(0, .006 1.3%, .025 2.8%, .056 4.3%, .099 6.0%, .199 9.2%, .536 18.9%, .633 22.0%, .717 25.2%, .785 28.2%, .844 31.4%, .894 34.7%, .933 38.1%, .964 41.7%, .987 45.7%, 1.003 50.3%, 1.012 55.3%, 1.015 64.8%, 1);
/* bounce 0.25 (settle = 1.274 × d, overshoot 2.8 %) */
--lgs-ease-b25: linear(0, .007 1.5%, .026 3.0%, .058 4.7%, .104 6.5%, .207 9.8%, .556 20.0%, .656 23.4%, .739 26.5%, .806 29.5%, .865 32.7%, .913 35.9%, .952 39.2%, .983 42.9%, 1.005 46.7%, 1.019 50.9%, 1.027 55.8%, 1.027 63.8%, 1);
cubic-bezier() fallbacks for the rare place linear() cannot go: b0 (.327, .692, .114, 1), b15 (.311, .589, .077, 1.118), b20 (.330, .632, .076, 1.138), b25 (.344, .630, .084, 1.171) (1.1–1.4 % RMS error). They replace Phase 1's --lgs-spring and its 140/220/320 ms durations. docs/phase2/research/springs.py --css reproduces every curve.
11.4 Choreography rules [MO §3.4–3.5]
- C1 Two channels. The glass channel (lensing, frost, tint, rim, shadow, shape) and the content channel (Steam's pixels: opacity, a small blur, a small offset). Small-glass entrance: glass 0–92 % of the time, content 35–100 % with blur σ 8 → 0. Exit: content gone by 55 %, then the glass dissolves to the end.
- C2 Scale rides the glass channel (
s = s0 + (1 − s0) · g); no separate curve. - C3 Scale depends on size:
s0 = 1 + clamp(12 px / maxSide, 0.01, 0.15)(1.15 at ≤ 80 px, 1.04 at 300 px, 1.01 for windows). - C4 Pointer and gamepad are subdued: press swell +6 px (≤ ×1.06), bounce ≤ 0.2, gel stretch ≤ 5 %, no gel for gamepad.
- C5 Retarget, never queue: D-pad auto-repeat looks like one moving highlight.
- C6 Never block: actions fire on release (laser) or A-down (gamepad); no control is disabled during an animation; no transition layer intercepts input.
- C7 Nothing moves at rest: no shimmer, pulse or breathing; Steam's
Blinkerfocus pulse stays neutralised;document.getAnimations().length === 0at rest. - C8 Reduce Motion: bounce 0; scale, translate and depth become fades; no blur ramps; drags track input directly; materialize becomes a 150–200 ms cross-dissolve. Fades stay (Phase 1's 1 ms rule is withdrawn).
- M1 No lateral slide over 24 px of any surface wider than 600 px; use fade + ≤ 16 px parallax.
- M2 Nothing enters from the periphery; toasts appear in place.
- M3 No lasting oscillation: b ≤ 0.25 settles within one visible cycle; nothing repeats near 0.2 Hz.
- M4 Animated depth changes ≤ 20 mm, on
depth(b 0). Text never gets depth of its own. - M5 World-anchored: animations never move or rotate the window.
11.5 Interaction matrix [MO §4.1]
| Interaction | In | Out | Animates | Must not animate |
|---|---|---|---|---|
| Hover (laser) | 294 ms hover-in [b0] |
441 ms fade [b0] |
Light spot (follows the pointer), fill alpha | Scale, size, outline, blur radius, text |
| Press | 210 ms interactive [b15] |
— | Inner glow from the hit point; glass swell (glass controls only, ≤ ×1.06); fill brightness | Position, layout, row and card scale, backdrop blur |
| Release | Glow off 90 ms linear | Swell back 488 ms snappy [b15] |
Glow, scale | Delaying the action |
| Focus move (gamepad) | 294 ms hover-in, first frame ≥ 60 % |
441 ms fade |
Illumination, glass "lift" look, content-card z (§3.8) | A travelling indicator between items, bounce, delay, row/toolbar scale |
| Selection change (segmented, tabs) | Pill travel 488 ms snappy; lift 210 ms interactive |
Settle 488 ms snappy |
Pill position and width; pill lifted into clear glass while moving; stretch ≤ 5 % (pointer only); label colours swap at t90 | Tab labels, sizes, layout, bounce > 0.15 |
| Menu open | 607 ms morph-open [b20] |
— | Shape from the source rect and radius to the menu's; θ; glass channel; content 15–50 %; z from the source's depth to the menu's | Content scale, the source's position, item layout |
| Menu close | — | 441 ms morph-close [b0] |
Content out by 40 %, shape back into the source, ×1.03 catch on the source | Input capture by the closing menu |
| Popover | As menu | As menu | As menu | As menu |
| Sheet present | 735 ms sheet-in [b0] |
— | Glass on the sheet spring, scale 0.97 → 1 (or morph from source), content 25–70 %, parent scrim 0 → 35 % on fade, sheet z +30 → +50 mm |
Slide from an edge, parent position or scale in CSS, blur-radius ramps on large areas |
| Sheet dismiss | — | 514 ms sheet-out [b0] |
Content out first (0–40 %), glass dissolves, scale → 0.98, scrim → 0 | Swell of large surfaces toward the viewer |
| Alert | Morph from source if known; else materialize 250 ms + swell 1.02 → 1 | Dematerialize 250 ms | Glass channel, scrim 0 → 35 %, small swell | Shake, bounce > 0.15, delayed focusability |
| Toast in | Materialize 250 ms + translate −8 → 0 px snappy |
— | Glass channel, content 35–100 %, swell 1 + 12/maxSide |
Slide-in from off-screen (Steam's 300 px), pulses |
| Toast out | — | Dematerialize 350 ms | Content out by 55 %, glass dissolves | Sliding away |
| Window open / close | Glass ramp 735 ms sheet-in (glassd, geometry fixed) |
Glass dissolve 300 ms after content | Lensing, frost, tint, rim | Window position, rotation, scale > 1 % |
| Page / route | Content fade + 16 px parallax 662 ms page, 0–60 ms delay |
Old content fade 150 ms linear | Opacity, ≤ 16 px translate in the navigation direction | Full-width slides, zoom > 1.5 %, 3D, delays > 60 ms, re-animating persistent chrome |
| Scroll edge | Scroll-linked over 0–24 px | Scroll-linked | Edge-effect opacity | Blur radius, height, stacked effects |
| List insertion | 441 ms fade + translate 8 → 0 px, stagger 30 ms, ≤ 5 rows |
Removal instant | Opacity, small translate of really new rows | Height, margins, mount animations on virtualised rows |
| Toggle | Knob lift 210 ms interactive, travel and settle 488 ms snappy |
— | Knob scale ×1.3/×1.2, knob fill white → glass → white, track colour at t50 | Track size, bounce > 0.15 |
| Slider drag | Thumb lift 210 ms interactive |
Settle 488 ms snappy |
Thumb ×1.25 and glass look; value = pointer, no easing | Easing on the value, track size, glass thumb at rest |
| Tab bar minimize | Not adopted | — | — | — |
| Tooltip / reveal | After 0.8 s, materialize 250 ms | 0.2 s, dematerialize 350 ms | As toast | Instant reveals |
Steam-specific notes [MO §4.10, §4.12, §5]:
- Steam's toast children run
animation: toastEnter, toastExit; add our entrance in place oftoastEnterand keeptoastExitwith Steam's timing, or the toast never leaves. Read the real keyframe names from the live stylesheet first. - Route overrides (
TopLevelTransitionSwitchEnter / EnterActive / ExitActive) keep each phase at or below Steam's own totals (enter ≤ 800 ms with delay, exit ≤ 200 ms) so React's transition timeouts never cut an animation. Direction (±16 px) needs T2; T1 uses 0 px. These rules lift the Phase 1 ban on touchingTopLevelTransition. Adopted (sign-off S3: the brief asks for Liquid Glass motion): route, card and menu entrance overrides are allowed within Steam's timeouts; toasts keep Steam'stoastExit*[A10, PLAN §1.5]. - Steam's library tab slide (
translateX(±40 %), 320 ms, 80 ms delay) becomes ±16 px + fade onpage.
11.6 Menu morph and materialize recipes [MO §6.4]
- Materialize (small glass): two keyframe channels on the glass element and its children,
backwardsfill so nothing stays on Steam's nodes at rest:
.lgs-mat { --s0: calc(1 + min(.15, 12 / var(--lgs-maxside, 80)));
animation: lgs-mat-glass-in var(--lgs-d-mat-in) linear backwards; }
.lgs-mat > * { animation: lgs-mat-content-in var(--lgs-d-mat-in) linear backwards; }
@keyframes lgs-mat-glass-in {
0% { scale: var(--s0); background-color: transparent; backdrop-filter: blur(0px) saturate(1); }
92%, 100% { scale: 1; background-color: var(--lgs-mat-bg); backdrop-filter: var(--lgs-mat-blur, blur(22px) saturate(1.8)); } }
@keyframes lgs-mat-content-in { 0%, 35% { opacity: 0; filter: blur(8px); } 100% { opacity: 1; filter: blur(0px); } }
- Morph from source (in-page menus): a
clip-path: inset(… round …)from the source rect (T2 fills--sx --sy --sw --sh --srfrom the trigger's rect; T1 default a 64 × 48 capsule at the top-left) to the menu's box onmorph-openwith the b0 curve; content 15–50 %. In T5 the menu's slab materializes in place while this CSS clip-path draws the open morph, and glassd dematerializes it on close; glassd v3 has no morph, overshoot or liquid neck (contracts/glassd.md§6, P9's R1 decision; REQ P9->P4). - Cross-quad menus (bar "+" → its popup): a true morph is possible only in glassd over the union of both world rects; otherwise materialize the popup in place, scale origin at the edge nearest the button.
11.7 CEF rules [MO §6.3]
R1 never fade a parent of glass (it becomes a backdrop root); R2 Steam's keyframes beat declarations, so put motion on our pseudo-elements and registered properties and use !important for focus colours; R3 replace an entrance animation only by naming a new one and repeating Steam's functional entries; use independent scale/translate, never stacked transform; R4 exits need DOM (instant in T1 unless Steam keeps exit classes); R5 no mount animations on virtualised rows; R6 CSS retargets without velocity (fine for hover and focus); R7 hidden windows do not advance transitions, use keyframes for toasts; R8 filter and backdrop-filter animations are allowed one-shot on elements ≤ 600 × 600 px, ≤ 350 ms, only if perf shows no frames > 34 ms (amends Phase 1's ban); R9 clip-path and custom-property animations run on the main thread, menu-sized only; R10 respect React's timeouts; R11 leave nothing behind at rest (backwards fill; no resting filter: blur(0), scale: 1, clip-path: inset(0) on Steam nodes). View Transitions are not used (MO §6.5).
11.8 Scene graph and glassd [MO §7–9, SP §4]
- Scene-graph motion is push-driven. SP measured 30, 60 and 90 pushes/s (≈ 1.6 ms compositor CPU per push of the ≈ 22 KB graph): push at 60/s only while a value moves, push the exact final value once, then stop. Evaluate springs by time (closed-form function, MO §8), not per step.
- Animate only depth (≤ 20 mm) and dim/opacity in the graph; never crop x/y or metres-per-pixel. Keep content motion in CSS (the crops are live).
- glassd runs springs itself at ≤ 72 fps from targets in
glassd.json: per-shape materializem,press+ position (no morph: in-page menus morph in CSS, see above). Materialize ramps rim/spec (0 → 0.6 of m), lensing (0 → 0.7), frost and tint (0.2 → 0.92), dark edge (0.3 → 0.92), shadow (0.4 → 1), coverage alphasmoothstep(0, 0.3, m). Pad slab atlas regions by 12 Steam px × backdrop scale so swell and overshoot fit. Render at full rate only while a spring is unsettled. - Reduce Motion: push the end depth once at the lowest-visibility moment; glassd uses coverage alpha only (150–200 ms).
11.9 Motion verification [MO §10]
- Token conformance:
document.getAnimations()durations equal a token, iterations 1, easing a--lgs-ease-*curve or linear. - Nothing at rest:
getAnimations().length === 0one second after any interaction. - Filmstrips: pause and seek each animation at f = 0, .15, .35, .5, .75, 1 and CDP-screenshot
shots/p2_motion_<surface>_<interaction>_<f>.png: glass before content on entry, content before glass on exit, no text scaling, no closed outline in any frame. - Reduce Motion and High Contrast through CDP
Emulation.setEmulatedMediainside a lab lock (never Steam's settings). python glass.py perfwhile scripting the interaction: no frames > 34 ms, fps within 5 % of stock.- glassd: pinned
m,press,kper shape andglassd --once --dumpat m = 0, .15, .35, .6, 1. - Scene graph:
native/spike/sg_timeline.pywith a depth animation spec and hvgrab frames at fixed delays (look, then delete).
12. Layout and focus navigation (what T1 may change)
- D-pad direction is read from computed CSS. For every nav node without an explicit
flow-children, Steam'sGetLayout()infers row / column / grid fromgetComputedStyleat each key press (GP §0.5). A T1 reflow therefore changes the D-pad mapping consistently with what is drawn. - Never reorder focusable siblings with
order: inside a row or column the D-pad follows DOM order. Non-focusable siblings may move (GP C.1 moves the stats block withorder: 1). - Nodes with explicit handlers keep their axis: tab strips with
onMoveLeft/onMoveRightstay horizontal (that is why game-page tabs are a segmented control, not a vertical ornament). - Never turn a row into a column or the reverse where Steam declares
flow-children; the D-pad would invert (SN, SM rules). - Virtualised lists (library grid, Storage list) keep their JS-computed geometry: style rows, do not change row heights in CSS; larger posters need T3 props (LA LQ2). The friends list is not virtualized (measured, SM-D9), so its rows may change height in CSS [A7].
- Settings drill-down may hide a section in one view when it stays visible in exactly one other view and SET's T-CNT sums equal stock (exemption E-DRILL, PLAN §1.16; flag
settingsDrill) [A7, SET §4.3]. - New views (T3) use Steam's
Focusable,DialogButton,ToggleField,GamepadPage,MenuandshowContextMenu; each has anonCanceland an error boundary, and lives under/library/lgs/…(SR §3.4, §7). - Verification:
L.padtraversal afterBTakeFocus(3)/FocusApplicationRoot()(GP §0.4, SR §4): every target reachable; Down-Up and Left-Right return to the start; LB/RB still page; B closes or returns.
13. Accessibility
| Setting | Behaviour |
|---|---|
Steam high contrast (prefers-contrast: more) |
Glass near-opaque (#0b0d10 at .94), a 2 px solid white .70 edge is allowed (the only stroke), text pure white, no light spot |
Steam reduced animations (prefers-reduced-motion) |
§11.4 C8: bounce 0, movement → fades ≤ 200 ms, no blur ramps, no lift animation |
| Interface scaling | All sizes are CSS px/pt, so they scale with Steam's zoom; check the largest scale for overflow |
| Screen reader | Keep Steam's DOM order and roles; aria-label on every icon-only circle (T2) |
| Transparency | The glass intensity dial (--lgs-dial, lgs dial V) is the user's Reduce Transparency |
| Input parity | Every hover reveal has a gamepad path (§10.4) |
| Contrast | Body text ≥ 4.5:1 against the rendered glass (audit CONTRAST = 0) |
Never change Steam's accessibility settings to test them; emulate media features through CDP.
14. Implementation tiers: what each one delivers
| Tier | Delivers in this system | Status (capabilities) |
|---|---|---|
| T1 CSS incl. layout | Sizes, shapes, type, fills, edges, in-page Liquid Glass, ornament margin layouts, scroll edges, motion tokens | Layout changes allowed under §12 rules |
| T2 DOM augmentation | Light-spot coordinates, pressed class for gamepad A, selection pill, icon chips (data-lgs-page), aria-labels, decorative art variables |
Allowed: adds only, never removes or moves Steam nodes |
| T3 Steam React views | Home / launcher routes, Control Center side tiles, search sheet, folder art, A–Z program grid, taller header sync | [PROVEN] routes, overrides, Steam components, focus (SR, 19/19 selftest); Phase 2 builds them on P2's framework (contracts/react.md) [A13] |
| T4 scene graph | Depth for ornaments, menus, sheets, focused cards; popup z/scale; window dim and recede; frame-height override; enlarged frame controls |
Interactive crop registration, popups as ornaments, dim/recede, frame height, frame controls [PROVEN]; click on a moved crop, laser on transparent texels, cross-popup gamepad focus [UNPROVEN], so the default profile uses non-interactive, click-safe crops only (§3.8) [A13] |
| T5 glassd | Real frost, lensing, specular, adaptive tint and contact shadows over the room; tinted slabs; materialize; morph | Surfaces, slabs, partial covers [PROVEN-P1]; GM v2 material (edges from shape and light, prism lensing, glass-over-glass) built; plates (G1), hole treatment (G2), per-slab tint (G3) planned (P9); systemui covers needed (SP §11.2, GP GQ6/GQ8) [A13] |
15. The mockup kit
docs/phase2/mockups/kit.css + kit.js render this system as static HTML at true size, so every redesign can be judged in a picture before any Steam code changes.
15.1 What it simulates
| Reality | Kit stand-in |
|---|---|
| The headset view | .lgk-view, 1920 × 1080 px, a CC0 room photo behind everything (data-room="lounge" or "studio"; data-dim for a dim evening room). At 1:1 the view spans ≈ 59° at 0.0307°/px, close to the framing of the reference shots |
| Steam's main window | .lgk-overlay, 1280 × 720 CSS px at 1:1: every DESIGN2 number types in unchanged |
| Other quads | .lgk-pop with --pop-scale = 1 / m from §2.5: frame menu 1.10, bar and its popups 1.20, SteamVR frame controls 0.75, SteamVR settings / Now Playing 0.69 |
| glassd glass (T5) | `.lgk-glass[data-mat=window |
| Lensing | data-lens on a glass element: kit.js builds an SVG displacement map (the bible's engine, with a wider lens band) and sets it as the element's backdrop-filter (Chromium only) |
| Edges | The §6.2 recipe exactly: conic specular arcs with side gaps, darkened inner edge, thickness shading, depth shadow (--dz in mm) |
| T1 fallback | data-tier="t1" on the view: window and panel glass lose the room frost (approximate: ornament glass outside the window still frosts the room in the kit) |
| Depth | Not stereo: shown through the §3.8 shadow; #annot in the URL labels every [data-dz] element with its depth and tier |
| States | Classes is-hover (with --hx/--hy), is-focus, is-pressed, is-selected, is-nav-selected, is-disabled, plus is-nav-row (hover = spot only) and is-onfill (coloured fill: focus = glow). Same values and compositing as the theme's 04-states.css; python docs/phase2/fontkit.py --parity checks that every shared token is equal |
Not simulated: SteamVR's window curvature, the Frame's 19.6 ppd (judge legibility in an hvgrab frame, not in a mockup), stereo, motion.
15.2 Building a mockup
<link rel="stylesheet" href="kit.css">
<div class="lgk-view" data-room="lounge">
<div class="lgk-overlay" style="left:320px; top:66px"> <!-- Steam's 1280 x 720 overlay -->
<div class="lgk-glass" data-mat="window" style="left:0; top:0; width:1280px; height:656px">
<div class="t-large" style="position:absolute; left:40px; top:25px">Library</div>
…fills, rows, posters (never another .lgk-glass inside)…
</div>
<div class="lgk-glass lgk-toolbar" data-mat="liquid" data-lens data-dz="0" style="left:192px; top:628px; width:896px">…</div>
</div>
</div>
<script src="kit.js"></script>
- Glass elements are siblings in the functional layer, never nested, so each samples the room and the window (a
backdrop-filterinside another one would only see its parent). - Icons:
<i data-i="home"></i>. Art:<div class="lgk-art" data-art="poster:3:Game Name">(alsohero,avatar,program); all art is procedural, no third-party images or titles. - Classes:
lgk-btn circle|capsule|plain|lg|xl|sm|primary|play,lgk-seg,lgk-search,lgk-tabbar/lgk-tab,lgk-toolbar,lgk-sidebar,lgk-row,lgk-platter,lgk-toggle on,lgk-slider(--v),lgk-menu,lgk-poster,lgk-home-icon,lgk-badge,lgk-glyph,lgk-dots,lgk-windowbar,lgk-tooltip,lgk-laser-dot; typet-xl1 … t-caption, vibrancyv1 v2 v3. Kit tokens are named--lg-*; where §16 has a theme token with the same suffix (--lgs-*), the values are equal. - Render:
python tools/mockshot.py docs/phase2/mockups/<name>.html shots/p2_<name>.png(headless Chrome, 1920 × 1080). The kit sets<html data-lgk-ready="1">when its work is done, well inside the 3 s virtual-time budget. - Assets and licences:
mockups/assets/LICENSE.md(Poly Haven CC0 photos; the lounge view is a rectilinear reprojection of the "Lythwood Room" panorama).
15.3 The worked example
_example.html → shots/p2_example.png (variants: p2_example_studio.png, p2_example_dim.png, p2_example_t1.png, rendered before A1–A13). Re-rendered 2026-10-07 with the amendments. It shows:
- the Library window (
windowglass mode: 1280 × 656, radius 54) with the Back circle at (24, 24), a Large Title at x 100, a 520 px search capsule centred at x 640 with no microphone, an account circle at the concentric corner inset, a segmented control (white selected segment), 220 × 330 posters with an update chip and a download-progress capsule [A1]; - the tab-bar ornament (frame menu, 6 + 3 items, Library selected as the navigation pill with its arc, lensing at the bezel);
- the bottom ornament at y 628–712 straddling the glass by 28 px at depth 0: the Sort button white because its menu is open, a Filter capsule with a count, an
All · VR · Non-VRsegmented control; laser mode, so no controller glyph badges [A1, A3, A5]; - the Sort menu (thick glass at +10 mm, the default profile; five items: one column of 72 px rows under a 40 px header) grown upward out of its button, the current sort checked in the leading slot, the laser-hovered row lit at the hit point (hover look only: the laser never paints the focus look) [A3, A5, A6];
- SteamVR's frame controls (idle-dim), the dashboard bar as two liquid capsules (Steam button, running apps as circular art, +; clock, battery capsule, notification badge, Quick Access), and the grab pill under the bar.
16. Proposed tokens (for theme/00-tokens.nowrap.css, owned by Foundation)
Values replace the Phase 1 tokens of the same name; new names are added. Selectors and file ownership as in DESIGN.md §10.
As built (P4, 2026-10-07). The live tokens are theme/00-tokens.nowrap.css; docs/phase2/contracts/tokens.md is their contract. Differences from the proposal below: --lgs-focus-add is .32 (A5); the Phase 2 material values live under new names --lgs-mat-<m>-bg / -blur / -shade (window, panel, liquid, liquid-room, thick, clear, plate), because Phase 1 files use --lgs-glass-bg and --lgs-thick-bg inside linear-gradient() and background-color, where a gradient value would be invalid and make glass transparent; --lgs-glass-bg, --lgs-thick-bg, --lgs-glass-blur, --lgs-thick-blur keep their Phase 1 values until the last area migrates (PLAN §6). Motion tokens live in P5's theme/02-motion.nowrap.css. New: the state numbers (--lgs-hover, --lgs-focus, --lgs-within, --lgs-press, --lgs-lift, --lgs-dis), the hook switches --lgs-edge and --lgs-ill, the PLAN §1.3 size tokens, the selection recipe and --lgs-white-glow.
@property --lgs-hover { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --lgs-press { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --lgs-focus { syntax: "<number>"; inherits: false; initial-value: 0; }
html.lgs-on {
/* type */
--lgs-font: "LGS Inter", "Motiva Sans", Arial, sans-serif;
--lgs-text-1: rgb(255 255 255 / .96); --lgs-text-2: rgb(255 255 255 / .70);
--lgs-text-3: rgb(255 255 255 / .50); --lgs-text-disabled: rgb(255 255 255 / .32);
--lgs-text-on-selected: #0d0e12;
/* sizes (main-window px; 1 pt = 4/3 px) */
--lgs-hit: 80px; --lgs-btn: 60px; --lgs-btn-lg: 70px; --lgs-btn-xl: 86px; --lgs-btn-sm: 44px;
--lgs-row: 72px; --lgs-row-gap: 8px; --lgs-field: 64px; --lgs-ornament: 84px;
--lgs-ornament-overlap: 28px; --lgs-header: 108px;
--lgs-inset: 24px; --lgs-inset-text: 40px; --lgs-gap: 24px;
/* radii (concentric) */
--lgs-r-window: 54px; --lgs-r-sheet: 44px; --lgs-r-menu: 32px; --lgs-r-platter: 30px;
--lgs-r-row: 24px; --lgs-r-card: 20px; --lgs-r-small: 14px; --lgs-r-capsule: 999px;
/* colour */
--lgs-blue: rgb(0 145 255); --lgs-green: rgb(48 209 88); --lgs-red: rgb(255 66 69);
--lgs-orange: rgb(255 146 48); --lgs-yellow: rgb(255 214 0); --lgs-purple: rgb(219 52 242);
--lgs-tint-play: rgb(48 209 88 / .88); --lgs-tint-primary: rgb(0 145 255 / .86);
--lgs-tint-danger: rgb(255 66 69 / .84); --lgs-toggle-on: var(--lgs-green);
/* fills on glass */
--lgs-fill-thin: rgb(255 255 255 / .10); --lgs-fill-nav: rgb(255 255 255 / .18);
--lgs-hover-add: .08; --lgs-focus-add: .14;
--lgs-fill-regular: rgb(0 0 0 / .14); --lgs-fill-thick: rgb(0 0 0 / .30);
--lgs-well-shadow: inset 0 2px 5px rgb(0 0 0 / .26);
--lgs-selected-fill: rgb(255 255 255 / .94);
--lgs-scrim: rgb(0 0 0 / .35);
--lgs-field-focus: 0 0 0 3px rgb(255 255 255 / .55), 0 0 16px rgb(255 255 255 / .22);
/* edges (no closed rings: §6.2) */
--lgs-light: 340deg;
--lgs-rim-window: .78; --lgs-rim-panel: .90; --lgs-rim-liquid: 1.10; --lgs-rim-thick: .85;
--lgs-darkedge-window: inset 0 0 16px -2px rgb(0 0 0 / .20), inset 0 -2px 4px -1px rgb(0 0 0 / .12);
--lgs-darkedge-panel: inset 0 0 10px -2px rgb(0 0 0 / .16), inset 0 -2px 4px -1px rgb(0 0 0 / .12);
--lgs-darkedge-liquid: inset 0 0 6px -1px rgb(0 0 0 / .12), inset 0 -2px 4px -1px rgb(0 0 0 / .12);
--lgs-darkedge-thick: inset 0 0 12px -2px rgb(0 0 0 / .20), inset 0 -2px 4px -1px rgb(0 0 0 / .12);
--lgs-shadow-25mm: 0 10px 30px rgb(0 0 0 / .28); /* ornaments */
--lgs-shadow-30mm: 0 12px 36px rgb(0 0 0 / .32); /* menus, alerts */
--lgs-shadow-15mm: 0 6px 18px rgb(0 0 0 / .30); /* primary, lifted card */
/* CSS fallback glass (T1); glassd replaces the room-facing ones in T5 */
--lgs-glass-blur: blur(12px) saturate(1.7); /* liquid, over in-page content */
--lgs-thick-blur: blur(30px) saturate(1.5);
--lgs-glass-bg: linear-gradient(180deg, rgb(255 255 255 / .12), rgb(255 255 255 / .03) 55%), rgb(80 80 86 / .12);
--lgs-thick-bg: linear-gradient(180deg, rgb(255 255 255 / .08), rgb(255 255 255 / 0) 35%), rgb(36 37 42 / calc(.30 + var(--lgs-dial) * .2));
/* motion (MO §6.2) */
--lgs-ease-b0: linear(0, .005 1.2%, .020 2.3%, .084 5.2%, .159 7.7%, .461 16.9%, .556 20.2%, .639 23.5%, .709 26.9%, .767 30.2%, .817 33.7%, .861 37.6%, .900 42.1%, .930 46.9%, .954 52.4%, .971 58.6%, .991 73.8%, 1);
--lgs-ease-b15: linear(0, .006 1.3%, .024 2.7%, .054 4.2%, .093 5.7%, .188 8.7%, .518 18.2%, .613 21.4%, .696 24.5%, .767 27.7%, .825 30.9%, .872 34.1%, .913 37.6%, .945 41.4%, .970 45.6%, .988 50.3%, 1.000 55.8%, 1.006 67.9%, 1);
--lgs-ease-b20: linear(0, .006 1.3%, .025 2.8%, .056 4.3%, .099 6.0%, .199 9.2%, .536 18.9%, .633 22.0%, .717 25.2%, .785 28.2%, .844 31.4%, .894 34.7%, .933 38.1%, .964 41.7%, .987 45.7%, 1.003 50.3%, 1.012 55.3%, 1.015 64.8%, 1);
--lgs-d-interactive: 210ms; --lgs-d-hover-in: 294ms; --lgs-d-fade: 441ms; --lgs-d-snappy: 488ms;
--lgs-d-morph-open: 607ms; --lgs-d-morph-close: 441ms; --lgs-d-sheet-in: 735ms; --lgs-d-sheet-out: 514ms;
--lgs-d-page: 662ms; --lgs-d-mat-in: 250ms; --lgs-d-mat-out: 350ms;
--lgs-press-grow: 6; --lgs-press-max: 1.06;
}
@media (prefers-reduced-motion: reduce) {
html.lgs-on {
--lgs-ease-b15: var(--lgs-ease-b0); --lgs-ease-b20: var(--lgs-ease-b0);
--lgs-press-grow: 0; --lgs-d-mat-in: 180ms; --lgs-d-mat-out: 180ms;
}
}
@media (prefers-contrast: more) {
html.lgs-on {
--lgs-dial: 1; --lgs-text-2: rgb(255 255 255 / .86); --lgs-text-3: rgb(255 255 255 / .74);
--lgs-glass-bg: rgb(11 13 16 / .94); --lgs-thick-bg: rgb(11 13 16 / .96);
}
}
Depths for theme/layers.json (scene units at r = 1; the daemon should convert from mm with the live S × r): ornaments 0.068, menus and alerts 0.081, sheets 0.136, primary capsule 0.041, focused card lift 0.041, Home icons 0.027 / 0.068.
17. Verification checklist (agents only)
The gates are PLAN §4 [A11]: the global gates (G-AUD, G-SIZE, G-TYPE, G-OUTLINE, G-FOCUS, G-MOTION, G-PAD, G-DEPTH, G-HV, G-REMOVE), the route matrix, the native gate and the conformance run of VP §6, measured with the lab commands of docs/phase2/contracts/lab.md. Where a row below disagrees with PLAN §4 (for example #5's focus threshold, now G-FOCUS's +40 L, or #7's depths, now the §3.8 default profile), PLAN §4 wins. The table stays as the design-level summary.
| # | Check | How |
|---|---|---|
| 1 | Targets | Every focusable rect ≥ 80 × 80 px, or centre ≥ 80 px from others with ≥ 21 px clear gap (≥ 72 px where the quad is fixed); glass.py audit SHRUNK/UNCLICKABLE = 0 |
| 2 | Text | No computed font-size < 18 px; body ≥ 22 px; CONTRAST = 0; no text-transform: uppercase, font-style: italic or letter-spacing > 0 in chrome |
| 3 | No outlines | Glass containers: border-width: 0, outline-style: none, no 1 px box-shadow ring; edge pixel profiles in shots: a peak on the top edge, flat sides |
| 4 | Selection vocabulary | White fill only on toggled/selected buttons, segments and open-menu sources |
| 5 | Focus | With each element .gpfocus, its fill differs from rest by ≥ 0.06 luminance in a screenshot; nothing pulses (two shots 1.5 s apart identical) |
| 6 | Tabs | ≤ 6 items per tab-bar capsule |
| 7 | Depth | __LGS_SG.dump(): every dz matches §3.8 within 2 mm; each lift has its shadow; total ≤ 80 mm |
| 8 | Motion | §11.9 |
| 9 | Parity | Every hover-only reveal also appears under gamepad focus; every legend-only action has a visible button |
| 10 | Font | document.fonts.check('500 24px "LGS Inter"') true on every surface; CJK text renders; perf within 5 % |
| 11 | Glass tone | hvgrab frame (look, then delete): glass L 55–110 inside vs outside the window; room-texture sd reduced ≥ 80 % (T5) |
| 12 | Functions | glass.py audit GONE/HIDDEN = 0 per route; L.pad traversals of §12; launch paths logged, never executed |
18. Risks and open questions
| # | Risk | Mitigation |
|---|---|---|
| 1 | Inter at 1.5× Steam's sizes truncates and wraps Steam layouts | Audit every route; page or group content (§2.4); Callout 22 px for dense secondary text |
| 2 | data: fonts may be blocked by a CSP font-src in some Steam or SteamVR windows |
Check per surface (§5.2 step 6); fall back to Steam's stack where blocked |
| 3 | +163 KB per injected window | Measure memory and parse time; drop opsz (−42 KB) first |
| 4 | The 108 px header needs both a CSS variable and a JS store write (SN CQ1) | Until proven, keep Steam's 40 px header height and centre the larger controls in a scroll-edge band |
| 5 | Hexagonal Home grids change geometric D-pad neighbours | Explicit neighbour maps (T3) or the square-lattice fallback |
| 6 | Sheet push-back (visionOS geometry) moves the window | Default to MO §4.8 (sheet comes forward); compare both in hvgrab before deciding |
| 7 | Interactive crops: click routing on moved crops is unproven without a wearer | Keep every crop at its original x/y; ornaments by the ornament margin |
| 8 | The tab bar (9 Steam destinations in one column) is taller than the window (748 vs 720 px in the example) | Accept the 14 px overhang per side, or shrink the frame-menu popup with its request scale to ≥ 72 px pitch in main px |
| 9 | Unpublished visionOS numbers (corner radius, type sizes, ornament depths) | Tagged [ours]; adjust from hvgrab review and, if ever available, Apple's visionOS design kit |
| 10 | Display density: mockups show more detail than the headset | Judge legibility only in hvgrab frames |
| 11 | Stereo depths are justified by disparity math, not perception tests | Shadow paired with every lift; cap 80 mm; review in hvgrab |
| 12 | Gamepad press events for the press glow are unverified | T2 listener; without it gamepad A shows focus only |
Sources
Project documents: docs/phase2/research/visionos.md, liquid-glass-motion.md, references.md, springs.py; docs/phase2/capabilities/spatial.md, steam-react.md; docs/phase2/audit/shell-nav.md, library-apps.md, game-pages.md, system.md, social-media.md; docs/DESIGN.md, docs/LAB.md, docs/NATIVE.md, native/glassd/README.md, theme/00-tokens.nowrap.css, theme/layers.json, device/lgs.py, device/lgs_vr.py; the design bible ../bible/index.html and ../.claude/skills/run-liquid-glass-frame/references/design-language.md; the reference shots docs/refs/visionos/1–12.
External: Apple HIG and WWDC sessions as cited in the research files; Inter 4.1 (https://rsms.me/inter/, https://github.com/rsms/inter/releases/tag/v4.1, licence https://github.com/rsms/inter/blob/master/LICENSE.txt); Poly Haven CC0 licence (https://polyhaven.com/license), assets "Lythwood Room" (Greg Zaal) and "Photo Studio London Hall" (Sergej Majboroda).