Files
MichaelScottsman 4fc0cfffd6 assPod: a music and video player in the hand, on VLC
A music note button on the dashboard bar, left of the paintbrush, opens the
assPod in the hand whose laser pressed it. VLC plays its media library's music
and videos in the background; both sticks turn the click wheel, A and stick
clicks press the centre, B is Menu and the hamburger button puts it away. The
rest of the UI fades out while it is open.

- device/asspod: the pod's OS, screens, VLC engine and scene graph (bundled by
  tools/build_asspod.py into device/vr/systemui.asspod.js), with the model
  exported from tools/asspod-model
- native/podd: renders the 3D body with real shaders into a box of stereo
  portal panels, and the playing video's picture under the screen
- device/shell_ext/asspod.py: VLC, the library scan, art, podd, and video
  frames from ffmpeg paced to VLC's clock; videos keep their place across
  restarts
- Reposition and Black/White finish on the main menu
- install.sh offers to install VLC when it is missing
2026-10-09 22:27:35 -04:00

388 lines
17 KiB
CSS

/* Glass Shell foundation: materials (package P4).
Contract: docs/phase2/contracts/tokens.md §2. Sources: PLAN §1.6, DESIGN2
§6, WN §8.2 (edge lobe) and §8.4 (degraded glass), GM §1.7.
Wrapped as html.lgs-on { ... } by the bundler ("&" is html.lgs-on).
Edges come from light, never from lines (VP P-42): E3 is a conic arc from
the one key light with gaps on both sides plus a lobe at 26 % of the width;
E4/E5 (dark inner edge, thickness) are inset shadows in the
--lgs-mat-<m>-shade tokens; the fill is the --lgs-mat-<m>-bg token.
1. The edge hook: --lgs-edge: <material> on an element → E3 on its ::before.
2. Markup classes for elements we create (T2/T3).
3. Utilities: media dimming, scroll edges.
4. High Contrast: the one allowed stroke. */
/* ------------------------------------------------------------------ */
/* 1. The edge hook */
/* ------------------------------------------------------------------ */
/* The subjects are generated host lists, never "*" (G-PERF, review R1 M1:
a universal *::before inside @container makes Blink resolve ::before for
every element on every style recalc). Each "lgs-hosts" block is written by
`python docs/phase2/fontkit.py --hooks` from every theme rule that sets
--lgs-edge (to that material): the rightmost compound of its selector (a
superset; the style query still decides), one flat :where() entry per
host, so the hook keeps the specificity the universal subject had. Never
edit the blocks by hand. */
@container style(--lgs-edge: window) or style(--lgs-edge: panel) or style(--lgs-edge: liquid) or style(--lgs-edge: thick) or style(--lgs-edge: clear) or style(--lgs-edge: control) {
/* lgs-hosts edge ::before begin */
:where(.lgs-hosts-edge)::before,
:where(#Footer%{BasicFooter})::before,
:where(%{*BasicContextMenuModal>contextMenuContents})::before,
:where(%{*GamepadDialogContent>ModalPosition} > div.Panel:not(%{*GamepadDialogContent>ModalClickToDismiss}, %{FocusRing}, %{FocusRingRoot}, .BasicUIContextMenu, %{*GamepadDialogContent_InnerWidth>GamepadDialogContent}))::before,
:where(%{*GamepadDialogContent_InnerWidth>GamepadDialogContent})::before,
:where(%{BarSurfaceSection}%{LowBatteryGauge>Tabs})::before,
:where(%{BarSurfaceSection}%{Tray})::before,
:where(%{BarSurface}%{Bookend})::before,
:where(%{BasicUiRoot})::before,
:where(%{Container>MenuButton})::before,
:where(%{DashboardBarPopupContents})::before,
:where(%{DashboardBarPopupList})::before,
:where(%{DashboardMenu}%{Variant_TabMenu})::before,
:where(%{DialogWrapper})::before,
:where(%{GamepadTabbedPage})::before,
:where(%{HiddenFrame>InvitesList})::before,
:where(%{PlayButtonContainer>PlayButton})::before,
:where(%{PopupBody>Tooltip})::before,
:where(%{QuickAccessMenu})::before,
:where(%{RecentlyCompletedItem})::before,
:where(%{ShortTemplate})::before,
:where(%{TabRowTabs})::before,
:where(%{TextBoxCarouselContents})::before,
:where(%{VRVirtualKeyboardContents})::before,
:where(%{VirtualKeyboardStandaloneContainer>VirtualKeyboardContainer})::before,
:where(%{VolumeSliderLabel>VolumePopin}%{VolumeSliderLabel>VR})::before,
:where(.lgs-asspod)::before,
:where(.lgs-btn)::before,
:where(.lgs-cc-circle-face)::before,
:where(.lgs-cc-tile)::before,
:where(.lgs-edge)::before,
:where(.lgs-glass[data-lgs-mat="plate"])::before,
:where(.lgs-home-card)::before,
:where(.lgs-home-disc)::before,
:where(.lgs-home-plate)::before,
:where(.lgs-home-seg)::before,
:where(.lgs-home-wn-glass)::before,
:where(.lgs-search-glass)::before,
:where(.lgs-tabcap)::before,
:where(.lgs-tune)::before,
:where([data-lgs-mat="clear"])::before,
:where([data-lgs-mat="liquid"])::before,
:where([data-lgs-mat="panel"])::before,
:where([data-lgs-mat="thick"])::before,
:where([data-lgs-mat="window"])::before,
:where(button.DialogButton%{*GamepadDialogContent>Button})::before,
:where(div.lgs-c2b-panel[data-lgs-c2b])::before
/* lgs-hosts edge ::before end */
{
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
box-sizing: border-box;
/* under the host's children by default (controls: the label never
covers a 1.5 px rim); containers whose content reaches their edge set
--lgs-edge-z on the host (the window's default is above its header) */
--lgs-edge-z: inherit;
z-index: var(--lgs-edge-z, auto);
--lgs-focus: inherit;
--_lgs-rim: var(--lgs-rim-control);
/* --lgs-edge-dim (inherited, 0..1): how much a scrim over the slab dims
its rim */
--_lgs-k: calc(var(--_lgs-rim) * (1 + .5 * var(--lgs-focus)) * (1 - var(--lgs-edge-dim, 0)));
padding: var(--_lgs-rim-w, var(--lgs-rim-w));
background:
conic-gradient(from var(--lgs-light),
rgb(255 255 255 / calc(.62 * var(--_lgs-k))) 0deg,
rgb(255 255 255 / calc(.20 * var(--_lgs-k))) 38deg,
rgb(255 255 255 / 0) 72deg, rgb(255 255 255 / 0) 148deg,
rgb(255 255 255 / calc(.16 * var(--_lgs-k))) 180deg,
rgb(255 255 255 / 0) 214deg, rgb(255 255 255 / 0) 288deg,
rgb(255 255 255 / calc(.22 * var(--_lgs-k))) 322deg,
rgb(255 255 255 / calc(.62 * var(--_lgs-k))) 360deg),
radial-gradient(70% 120% at 26% 0%,
rgb(255 255 255 / calc(.18 * var(--_lgs-k))) 0,
rgb(255 255 255 / calc(.06 * var(--_lgs-k))) 46%,
rgb(255 255 255 / 0) 75%);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
transition: --lgs-focus var(--lgs-ill-d-out, 441ms) var(--lgs-ill-ease, ease-out);
}
}
@container style(--lgs-edge: window) {
/* the window's content (header, full-bleed art, modals) reaches its edge:
the rim sits above it like a real rim, as Phase 1's did (z 20000) */
/* lgs-hosts edge:window ::before begin */
:where(.lgs-hosts-edge-window)::before,
:where(%{BasicUiRoot})::before,
:where([data-lgs-mat="window"])::before
/* lgs-hosts edge:window ::before end */
{ --_lgs-rim: var(--lgs-rim-window); --_lgs-rim-w: var(--lgs-rim-w-window); z-index: var(--lgs-edge-z, 20000); }
/* ...so under an alert or sheet scrim (Steam's modal overlay, z 1500) it
dims with the window instead of glowing above it (review R1 m8); the
scrim is black .35 (--lgs-scrim). Steam keeps an empty, invisible
.ModalOverlayBackground in the page at all times: the scrim shows while
a .ModalOverlayContent is .active, except for a context menu alone (C1c:
menus have no scrim unless a dialog is .inactive below) */
&:is(:has(.ModalOverlayContent.inactive), :has(.ModalOverlayContent.active):not(:has(.ModalOverlayContent.active .BasicUIContextMenu)))
/* lgs-hosts edge:window where::before begin */
:where(.lgs-hosts-edge-window, %{BasicUiRoot}, [data-lgs-mat="window"])::before
/* lgs-hosts edge:window where::before end */
{ --lgs-edge-dim: .35; }
}
@container style(--lgs-edge: panel) {
/* lgs-hosts edge:panel ::before begin */
:where(.lgs-hosts-edge-panel)::before,
:where(#Footer%{BasicFooter}%{FloatingVRFooter})::before,
:where(%{DashboardBarPopupContents})::before,
:where(%{DashboardBarPopupList})::before,
:where(%{DashboardMenu}%{Variant_TabMenu})::before,
:where(%{HiddenFrame>InvitesList})::before,
:where(%{QuickAccessMenu})::before,
:where(%{ShortTemplate})::before,
:where(%{VolumeSliderLabel>VolumePopin}%{VolumeSliderLabel>VR})::before,
:where(.lgs-cc-tile)::before,
:where(.lgs-glass[data-lgs-mat="plate"])::before,
:where([data-lgs-mat="panel"])::before,
:where(div.lgs-c2b-panel[data-lgs-c2b])::before
/* lgs-hosts edge:panel ::before end */
{ --_lgs-rim: var(--lgs-rim-panel); }
}
@container style(--lgs-edge: liquid) {
/* lgs-hosts edge:liquid ::before begin */
:where(.lgs-hosts-edge-liquid)::before,
:where(#Footer%{BasicFooter})::before,
:where(%{BarSurfaceSection}%{LowBatteryGauge>Tabs})::before,
:where(%{BarSurfaceSection}%{Tray})::before,
:where(%{BarSurface}%{Bookend})::before,
:where(%{PlayButtonContainer>PlayButton})::before,
:where(%{RecentlyCompletedItem})::before,
:where(%{TabRowTabs})::before,
:where(%{TextBoxCarouselContents})::before,
:where(.lgs-asspod)::before,
:where(.lgs-cc-circle-face)::before,
:where(.lgs-home-card)::before,
:where(.lgs-home-disc)::before,
:where(.lgs-home-plate)::before,
:where(.lgs-home-seg)::before,
:where(.lgs-home-wn-glass)::before,
:where(.lgs-tabcap)::before,
:where(.lgs-tune)::before,
:where([data-lgs-mat="liquid"])::before
/* lgs-hosts edge:liquid ::before end */
{ --_lgs-rim: var(--lgs-rim-liquid); }
}
@container style(--lgs-edge: thick) {
/* lgs-hosts edge:thick ::before begin */
:where(.lgs-hosts-edge-thick)::before,
:where(%{*BasicContextMenuModal>contextMenuContents})::before,
:where(%{*GamepadDialogContent>ModalPosition} > div.Panel:not(%{*GamepadDialogContent>ModalClickToDismiss}, %{FocusRing}, %{FocusRingRoot}, .BasicUIContextMenu, %{*GamepadDialogContent_InnerWidth>GamepadDialogContent}))::before,
:where(%{*GamepadDialogContent_InnerWidth>GamepadDialogContent})::before,
:where(%{DialogWrapper})::before,
:where(%{GamepadTabbedPage})::before,
:where(%{PopupBody>Tooltip})::before,
:where(%{VRVirtualKeyboardContents})::before,
:where(%{VirtualKeyboardStandaloneContainer>VirtualKeyboardContainer})::before,
:where(.lgs-search-glass)::before,
:where([data-lgs-mat="thick"])::before
/* lgs-hosts edge:thick ::before end */
{ --_lgs-rim: var(--lgs-rim-thick); }
}
@container style(--lgs-edge: clear) {
/* lgs-hosts edge:clear ::before begin */
:where(.lgs-hosts-edge-clear)::before,
:where(%{Container>MenuButton})::before,
:where(%{TabRowTabs})::before,
:where([data-lgs-mat="clear"])::before
/* lgs-hosts edge:clear ::before end */
{ --_lgs-rim: var(--lgs-rim-clear); }
}
/* High Contrast: the one allowed stroke, 2 px solid white .70 (§4) */
@media (prefers-contrast: more) {
@container style(--lgs-edge: window) or style(--lgs-edge: panel) or style(--lgs-edge: liquid) or style(--lgs-edge: thick) or style(--lgs-edge: clear) or style(--lgs-edge: control) {
/* lgs-hosts edge ::before begin */
:where(.lgs-hosts-edge)::before,
:where(#Footer%{BasicFooter})::before,
:where(%{*BasicContextMenuModal>contextMenuContents})::before,
:where(%{*GamepadDialogContent>ModalPosition} > div.Panel:not(%{*GamepadDialogContent>ModalClickToDismiss}, %{FocusRing}, %{FocusRingRoot}, .BasicUIContextMenu, %{*GamepadDialogContent_InnerWidth>GamepadDialogContent}))::before,
:where(%{*GamepadDialogContent_InnerWidth>GamepadDialogContent})::before,
:where(%{BarSurfaceSection}%{LowBatteryGauge>Tabs})::before,
:where(%{BarSurfaceSection}%{Tray})::before,
:where(%{BarSurface}%{Bookend})::before,
:where(%{BasicUiRoot})::before,
:where(%{Container>MenuButton})::before,
:where(%{DashboardBarPopupContents})::before,
:where(%{DashboardBarPopupList})::before,
:where(%{DashboardMenu}%{Variant_TabMenu})::before,
:where(%{DialogWrapper})::before,
:where(%{GamepadTabbedPage})::before,
:where(%{HiddenFrame>InvitesList})::before,
:where(%{PlayButtonContainer>PlayButton})::before,
:where(%{PopupBody>Tooltip})::before,
:where(%{QuickAccessMenu})::before,
:where(%{RecentlyCompletedItem})::before,
:where(%{ShortTemplate})::before,
:where(%{TabRowTabs})::before,
:where(%{TextBoxCarouselContents})::before,
:where(%{VRVirtualKeyboardContents})::before,
:where(%{VirtualKeyboardStandaloneContainer>VirtualKeyboardContainer})::before,
:where(%{VolumeSliderLabel>VolumePopin}%{VolumeSliderLabel>VR})::before,
:where(.lgs-asspod)::before,
:where(.lgs-btn)::before,
:where(.lgs-cc-circle-face)::before,
:where(.lgs-cc-tile)::before,
:where(.lgs-edge)::before,
:where(.lgs-glass[data-lgs-mat="plate"])::before,
:where(.lgs-home-card)::before,
:where(.lgs-home-disc)::before,
:where(.lgs-home-plate)::before,
:where(.lgs-home-seg)::before,
:where(.lgs-home-wn-glass)::before,
:where(.lgs-search-glass)::before,
:where(.lgs-tabcap)::before,
:where(.lgs-tune)::before,
:where([data-lgs-mat="clear"])::before,
:where([data-lgs-mat="liquid"])::before,
:where([data-lgs-mat="panel"])::before,
:where([data-lgs-mat="thick"])::before,
:where([data-lgs-mat="window"])::before,
:where(button.DialogButton%{*GamepadDialogContent>Button})::before,
:where(div.lgs-c2b-panel[data-lgs-c2b])::before
/* lgs-hosts edge ::before end */
{
padding: 2px;
background: rgb(255 255 255 / .70);
}
}
}
/* ------------------------------------------------------------------ */
/* 2. Markup classes (elements we create, or Steam nodes T2 tags) */
/* ------------------------------------------------------------------ */
& .lgs-edge { --lgs-edge: control; }
& :is(.lgs-edge, .lgs-glass)[data-lgs-mat="window"] { --lgs-edge: window; }
& :is(.lgs-edge, .lgs-glass)[data-lgs-mat="panel"] { --lgs-edge: panel; }
& :is(.lgs-edge, .lgs-glass)[data-lgs-mat="liquid"] { --lgs-edge: liquid; }
& :is(.lgs-edge, .lgs-glass)[data-lgs-mat="thick"] { --lgs-edge: thick; }
& :is(.lgs-edge, .lgs-glass)[data-lgs-mat="clear"] { --lgs-edge: clear; }
& .lgs-glass {
position: relative;
box-sizing: border-box;
color: var(--lgs-text-1);
box-shadow: var(--_lgs-shade, none), var(--_lgs-dz, 0 0 0 transparent);
}
& .lgs-glass[data-lgs-mat="window"] {
--_lgs-shade: var(--lgs-mat-window-shade);
background: var(--lgs-mat-window-bg);
border-radius: var(--lgs-r-window);
}
& .lgs-glass[data-lgs-mat="panel"] {
--_lgs-shade: var(--lgs-mat-panel-shade);
background: var(--lgs-mat-panel-bg);
}
& .lgs-glass[data-lgs-mat="liquid"] {
--_lgs-shade: var(--lgs-mat-liquid-shade);
background: var(--lgs-mat-liquid-bg);
backdrop-filter: var(--lgs-mat-liquid-blur);
}
/* liquid glass with nothing of the page behind it (outside the window) */
& .lgs-glass[data-lgs-mat="liquid"][data-lgs-over="room"] {
background: var(--lgs-mat-liquid-room-bg);
backdrop-filter: none;
}
& .lgs-glass[data-lgs-mat="thick"] {
--_lgs-shade: var(--lgs-mat-thick-shade);
background: var(--lgs-mat-thick-bg);
backdrop-filter: var(--lgs-mat-thick-blur);
border-radius: var(--lgs-r-menu);
}
& .lgs-glass[data-lgs-mat="clear"] {
--_lgs-shade: var(--lgs-mat-clear-shade);
background: var(--lgs-mat-clear-bg);
backdrop-filter: var(--lgs-mat-clear-blur);
}
& .lgs-glass[data-lgs-mat="plate"] {
--_lgs-shade: var(--lgs-mat-panel-shade);
--lgs-edge: panel;
background: var(--lgs-mat-plate-bg);
}
/* depth shadow, sized by depth (DESIGN2 §3.8) */
& [data-lgs-dz="10"] { --_lgs-dz: var(--lgs-shadow-10mm); }
& [data-lgs-dz="15"] { --_lgs-dz: var(--lgs-shadow-15mm); }
& [data-lgs-dz="25"] { --_lgs-dz: var(--lgs-shadow-25mm); }
& [data-lgs-dz="30"] { --_lgs-dz: var(--lgs-shadow-30mm); }
/* selection vocabulary as markup (contract §3.3) */
& [data-lgs-sel="on"] {
background: var(--lgs-selected-fill) !important;
color: var(--lgs-text-on-selected) !important;
--lgs-ill: white;
}
& [data-lgs-sel="nav"] {
background: var(--lgs-fill-nav);
box-shadow: var(--lgs-sel-arc);
font-weight: var(--lgs-nav-weight);
}
/* ------------------------------------------------------------------ */
/* 3. Utilities */
/* ------------------------------------------------------------------ */
/* the 35 % dimming layer under clear glass over media (DESIGN2 §6.4) */
& .lgs-media-dim { background: var(--lgs-media-dim); }
/* scroll edges (DESIGN2 §6.7): a mask on a scroller we tag. Never on a
scroller that holds backdrop-filter (a mask makes it a backdrop root) */
& .lgs-scroll-fade-bottom { -webkit-mask-image: var(--lgs-scroll-mask-bottom); mask-image: var(--lgs-scroll-mask-bottom); }
& .lgs-scroll-fade-top { -webkit-mask-image: var(--lgs-scroll-mask-top); mask-image: var(--lgs-scroll-mask-top); }
& [data-lgs-scroll-band="top"] { --lgs-scroll-band: top; }
& [data-lgs-scroll-band="bottom"] { --lgs-scroll-band: bottom; }
/* the scroll-edge dimming band, on the scroller's non-scrolling container
(hosts generated like the edge's) */
@container style(--lgs-scroll-band: top) or style(--lgs-scroll-band: bottom) {
/* lgs-hosts band ::after begin */
:where(.lgs-hosts-band)::after,
:where([data-lgs-scroll-band="bottom"])::after,
:where([data-lgs-scroll-band="top"])::after
/* lgs-hosts band ::after end */
{
content: "";
position: absolute;
left: 0;
right: 0;
height: var(--lgs-scroll-band-h);
pointer-events: none;
z-index: 1;
}
}
@container style(--lgs-scroll-band: top) {
/* lgs-hosts band:top ::after begin */
:where(.lgs-hosts-band-top)::after,
:where([data-lgs-scroll-band="top"])::after
/* lgs-hosts band:top ::after end */
{ top: 0; background: var(--lgs-scroll-band-top); border-radius: inherit; }
}
@container style(--lgs-scroll-band: bottom) {
/* lgs-hosts band:bottom ::after begin */
:where(.lgs-hosts-band-bottom)::after,
:where([data-lgs-scroll-band="bottom"])::after
/* lgs-hosts band:bottom ::after end */
{ bottom: 0; background: var(--lgs-scroll-band-bottom); border-radius: inherit; }
}
/* ------------------------------------------------------------------ */
/* 4. High Contrast: the one allowed stroke, 2 px solid white .70 */
/* ------------------------------------------------------------------ */
/* (the edge's stroke is in §1, inside the host rule) */
@media (prefers-contrast: more) {
& .lgs-glass { backdrop-filter: none; }
}