Files
MichaelScottsman--LiquidAss…/theme/20-shell.css
T
MichaelScottsman 7ee1310d18 native spikes: scene-graph layering + dmabuf overlays proven
- IVROverlayView reads system overlays (systemui, HeadsetView) but not
  Steam's dmabuf-backed panels
- SteamVR systemui scene graph accepts injected nodes (DOM elements with
  buildNode; submit via module 5723's scheduler): reparent-to-panel +
  panel-anchor + transform + panel shows live crops of Steam's window at
  any 3D offset, and our own dmabuf overlays (needs SetOverlayMouseScale)
- hvgrab: headset-view capture for verification
2026-10-06 22:53:36 -04:00

499 lines
20 KiB
CSS

/* Glass Shell, area "shell": the dashboard window itself (surface main).
Window glass on the outermost container, transparent page roots so the
glass shows on every route, Liquid Glass header capsules (Back, search),
the in-window footer legend as a floating capsule, the app-details scrim,
and the search route's chrome and result tiles.
Wrapped as html.lgs-on { ... } by the injector. Look only: no geometry,
no transforms, no visibility changes. Two rendering-only hints are used
on purpose and explained where they appear: will-change on the window's
inner wrapper (backdrop root) and transform-style on Back (so its glass
can blur). Coverage, shots and audits: docs/coverage/shell.md. */
/* ------------------------------------------------------------------ */
/* 1. Window glass */
/* ------------------------------------------------------------------ */
/* The outermost window container already clips the whole UI with
overflow:hidden + border-radius; raise the radius to the window radius
and paint the visionOS window glass here, behind every route. */
%{BasicUiRoot} {
background: var(--lgs-window-sheen), var(--lgs-window-bg);
border-radius: var(--lgs-r-window);
}
/* The lit glass edge sits above all content (full-bleed art, modals,
header, footer) like a real rim; click-through and decorative only. */
%{BasicUiRoot}::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: var(--lgs-window-rim);
pointer-events: none;
z-index: 20000;
}
/* Backdrop root just inside the glass. backdrop-filter re-composites
everything painted before it up to its backdrop root; without this,
every blurred control over plain window glass would blur the glass tint
itself and double its opacity (measured: alpha .725 -> .93, a dark
blot over a bright room). With the root here, Liquid Glass blurs only
page content. will-change:opacity is a rendering hint only: no layout,
no containing block for fixed elements, and the stacking context it
makes wraps every Steam layer, so their relative z-order is unchanged. */
%{MainNavMenuMainSplit} {
will-change: opacity;
}
/* Overlay paths Steam renders on a transparent window (/apprunning,
/keyboard, in-game overlay, web views): keep them clear. */
%{BasicUiRoot}:has(> %{MainNavMenuMainSplit} > %{BasicHome}%{PopupBody>TransparentBackground}) {
background: none;
}
%{BasicUiRoot}:has(> %{MainNavMenuMainSplit} > %{BasicHome}%{PopupBody>TransparentBackground})::after {
box-shadow: none;
}
/* Every layer that repaints Steam's opaque #060a0e -> #0e141b gradient
(or black) over the window goes clear, so the window glass shows on
every route. Page roots owned by other areas are listed in the
coverage doc; these are the window-level ones. */
%{BasicHome}%{OpaqueBackground},
%{BasicHome}%{TrueBlackBackground},
%{BasicUiRoot}%{PopupBody>BlackBackground},
%{GamepadLibrary},
%{GamepadSearch},
%{DownloadsPage},
%{OverflowHidden>GamepadPage}%{DialogBackground},
%{ContentManagement} {
background: none;
}
%{BasicUiRoot}%{PopupBody>BlackBackground} {
background: var(--lgs-window-sheen), var(--lgs-window-bg);
}
/* ------------------------------------------------------------------ */
/* 2. Header: Back + search as Liquid Glass capsules over content */
/* ------------------------------------------------------------------ */
/* Steam's header bar is its ::before (50% black + 100px blur, opacity
driven by --gamepadui-header-background-opacity). The bar dissolves:
content scrolls under, and the controls float as their own capsules.
Opacity, position and size stay Steam's. */
#header%{Profile>Header}::before {
background: none;
backdrop-filter: none;
box-shadow: none;
}
/* Header capsules: Liquid Glass that really blurs the content scrolling
under the header. Steam's header is a 3D rendering context
(transform-style: preserve-3d, Steam-owned); in it, only layers that
take part in the 3D context see the page behind. A flat child (and
anything inside one, like a ::before of Back or of the overflow:hidden
search container) is flattened into its own surface, where
backdrop-filter sees nothing (measured). So:
- the search capsule is #header::after (a direct 3D participant),
anchored to the search container so it follows Steam's layout in
every language and mode;
- Back opts into the 3D context (preserve-3d: no transform, no fixed
children, no geometry change) so its ::before capsule participates.
The face layers a light glass tint over Apple's dimming scrim, so white
labels stay readable over bright art while the capsule still reads
lighter than the window glass around it. */
#header%{Profile>Header} {
--lgs-capsule-face: var(--lgs-panel-sheen),
linear-gradient(var(--lgs-glass-bg), var(--lgs-glass-bg)),
linear-gradient(var(--lgs-scrim), var(--lgs-scrim));
--lgs-capsule-hover: var(--lgs-panel-sheen),
linear-gradient(var(--lgs-hover-fill), var(--lgs-hover-fill)),
linear-gradient(var(--lgs-glass-bg), var(--lgs-glass-bg)),
linear-gradient(var(--lgs-scrim), var(--lgs-scrim));
--lgs-capsule-filled: var(--lgs-panel-sheen),
linear-gradient(var(--lgs-fill-2), var(--lgs-fill-2)),
linear-gradient(var(--lgs-glass-bg), var(--lgs-glass-bg)),
linear-gradient(var(--lgs-scrim), var(--lgs-scrim));
--lgs-capsule-focus: var(--lgs-panel-sheen),
linear-gradient(var(--lgs-focus-fill), var(--lgs-focus-fill)),
linear-gradient(var(--lgs-glass-bg), var(--lgs-glass-bg)),
linear-gradient(var(--lgs-scrim), var(--lgs-scrim));
--lgs-capsule-pressed: var(--lgs-panel-sheen),
linear-gradient(var(--lgs-pressed-fill), var(--lgs-pressed-fill)),
linear-gradient(var(--lgs-glass-bg), var(--lgs-glass-bg)),
linear-gradient(var(--lgs-scrim), var(--lgs-scrim));
}
/* Back: a glass capsule (Back is laser-only, not gamepad focusable). */
%{BackContainer} {
transform-style: preserve-3d;
color: var(--lgs-text-1);
text-shadow: var(--lgs-text-shadow);
}
%{BackContainer}::before {
content: "";
position: absolute;
top: 4px;
bottom: 4px;
left: 8px;
right: 6px;
z-index: -1;
border-radius: var(--lgs-r-capsule);
background: var(--lgs-capsule-face);
backdrop-filter: var(--lgs-glass-blur);
box-shadow: var(--lgs-glass-rim), var(--lgs-glass-shadow);
pointer-events: none;
transition: background var(--lgs-t-fast) var(--lgs-ease);
}
%{BackContainer}:hover {
color: var(--lgs-text-1);
}
%{BackContainer}:hover::before {
background: var(--lgs-capsule-hover);
}
%{BackContainer}:active::before {
background: var(--lgs-capsule-pressed);
}
%{BackContainer>BackButton} {
color: inherit;
}
/* the hairline divider between Back and search is replaced by the gap
between the two capsules (colour only; the pseudo-element stays) */
%{BackContainer}:not(:last-child)::after {
background: transparent;
}
/* Search: one capsule in every state (idle, filled, hover, focused). */
#header%{Profile>Header} > %{SearchAndTitleContainer}%{ShowingSearch} {
anchor-name: --lgs-search-field;
}
#header%{Profile>Header}:has(> %{SearchAndTitleContainer}%{ShowingSearch})::after {
content: "";
position: absolute;
top: 4px;
bottom: 4px;
left: calc(anchor(--lgs-search-field left, 100%) + 2px);
right: calc(anchor(--lgs-search-field right, 0px) + 8px);
z-index: -1;
border-radius: var(--lgs-r-capsule);
background: var(--lgs-capsule-face);
backdrop-filter: var(--lgs-glass-blur);
box-shadow: var(--lgs-glass-rim), var(--lgs-glass-shadow);
pointer-events: none;
transition: background var(--lgs-t-fast) var(--lgs-ease), box-shadow var(--lgs-t-fast) var(--lgs-ease);
}
#header%{Profile>Header}:has(> %{SearchAndTitleContainer}%{ShowingSearch}:hover)::after {
background: var(--lgs-capsule-hover);
}
/* filled (text present, or on /search): a slightly brighter face, so the
field reads as "active" the way Steam's grey field did */
#header%{Profile>Header}:has(> %{SearchAndTitleContainer}%{ShowingSearch} > %{SearchFieldBackground}%{WhiteBackground})::after {
background: var(--lgs-capsule-filled);
}
/* focused (the field takes gamepad focus): illuminated from within */
#header%{Profile>Header}:has(> %{SearchAndTitleContainer}%{ShowingSearch}.gpfocuswithin)::after {
background: var(--lgs-capsule-focus) !important;
box-shadow: var(--lgs-focus-ring) !important;
}
/* Steam's own field fill (grey #b8bcbf / white) is replaced by the
capsule above; its scaleX expand transform stays untouched.
(Steam's field rules reach (0,5,0); anchoring on Steam's literal
#header id outranks them without !important.) */
#header %{SearchAndTitleContainer} %{SearchFieldBackground},
#header %{SearchAndTitleContainer} %{SearchFieldBackground}%{WhiteBackground} {
background-color: transparent;
}
#header %{SearchAndTitleContainer} %{SearchIconLeft},
#header %{SearchAndTitleContainer} %{SearchIconLeft}%{WhiteBackground},
#header %{SearchAndTitleContainer} %{SearchIconRight},
#header %{SearchAndTitleContainer} %{SearchIconRight}%{WhiteBackground} {
color: var(--lgs-text-2);
}
#header %{SearchAndTitleContainer}.gpfocuswithin %{SearchIconLeft},
#header %{SearchAndTitleContainer}%{ShowingSearch}:hover %{SearchIconLeft} {
color: var(--lgs-text-1);
}
#header %{SearchAndTitleContainer} %{SearchBox},
#header %{SearchAndTitleContainer} %{SearchBox}%{WhiteBackground} {
color: var(--lgs-text-1);
caret-color: var(--lgs-text-1);
text-shadow: var(--lgs-text-shadow);
}
#header %{SearchAndTitleContainer} %{SearchBox}::placeholder {
color: var(--lgs-text-2);
}
#header %{SearchAndTitleContainer}%{ShowingSearch}:hover %{SearchBox}::placeholder {
color: var(--lgs-text-1);
}
#header %{SearchAndTitleContainer} %{SearchBox}::-webkit-search-cancel-button {
background-color: var(--lgs-fill-3);
border-radius: var(--lgs-r-capsule);
box-shadow: var(--lgs-control-rim);
}
#header %{SearchAndTitleContainer} %{SearchBox}::-webkit-search-cancel-button:hover {
background-color: var(--lgs-hover-fill);
}
#header %{SearchAndTitleContainer} %{SearchBox}::-webkit-search-cancel-button:active {
background-color: var(--lgs-pressed-fill);
}
/* Title mode (calibration, device support, captive portal ...):
the title stands on the window glass as a primary label. */
%{SearchAndTitleContainer}%{ShowingTitle},
%{SearchAndTitleContainer}%{ShowingTitle} > div {
color: var(--lgs-text-1);
text-shadow: var(--lgs-text-shadow);
}
/* Account-alert / avatar item (only with active support alerts). */
%{HeaderItem} {
color: var(--lgs-text-1);
border-radius: var(--lgs-r-capsule);
transition: background-color var(--lgs-t-fast) var(--lgs-ease);
}
%{HeaderItem}%{Profile>Clickable}:hover {
background-color: var(--lgs-hover-fill);
}
%{HeaderItem}.gpfocus,
%{HeaderItem}.gpfocuswithin {
background: var(--lgs-focus-fill) !important;
color: var(--lgs-text-1) !important;
box-shadow: var(--lgs-focus-ring) !important;
}
/* High contrast: give the header a solid bar again. */
@media (prefers-contrast: more) {
#header%{Profile>Header}::before {
background: var(--lgs-thick-bg);
}
}
/* ------------------------------------------------------------------ */
/* 3. Footer legend: one Liquid Glass capsule hugging the actions */
/* ------------------------------------------------------------------ */
/* The full-width black strip dissolves; the legend items (adjacent,
right-aligned) get a single capsule behind them, positioned with
anchors on the first and last item so the footer's own box, padding
and --gamepadui-current-footer-height stay exactly Steam's. */
#Footer%{BasicFooter} {
background: none;
backdrop-filter: none;
border-image: none;
border-top-color: transparent;
}
/* (the legend's first child is a flex spacer, so count legend items) */
%{FooterLegend} > :nth-child(1 of %{ActionButtonLegend}) {
anchor-name: --lgs-legend-first;
}
%{FooterLegend} > :nth-last-child(1 of %{ActionButtonLegend}) {
anchor-name: --lgs-legend-last;
}
%{FooterLegend} > :nth-child(1 of %{ActionButtonLegend}):nth-last-child(1 of %{ActionButtonLegend}) {
anchor-name: --lgs-legend-first, --lgs-legend-last;
}
#Footer%{BasicFooter}::after {
content: "";
position: absolute;
top: anchor(--lgs-legend-last top, 4px);
bottom: anchor(--lgs-legend-last bottom, 3px);
left: calc(anchor(--lgs-legend-first left, 100%) - 4px);
right: calc(anchor(--lgs-legend-last right, 0px) - 4px);
z-index: -1;
border-radius: var(--lgs-r-capsule);
/* Liquid Glass: blurs the content under the footer (not the window
tint, see the backdrop root above); the scrim layer keeps the white
labels readable over bright art */
background: var(--lgs-panel-sheen),
linear-gradient(var(--lgs-glass-bg), var(--lgs-glass-bg)),
linear-gradient(var(--lgs-scrim), var(--lgs-scrim));
backdrop-filter: var(--lgs-glass-blur);
box-shadow: var(--lgs-glass-rim), var(--lgs-glass-shadow);
pointer-events: none;
}
%{ActionButtonLegend} {
border-radius: var(--lgs-r-capsule);
transition: background-color var(--lgs-t-fast) var(--lgs-ease);
}
%{ActionButtonLegend}:hover {
background-color: var(--lgs-hover-fill);
}
%{ActionButtonLegend}:active {
background-color: var(--lgs-pressed-fill);
}
%{ActionButtonLegend} %{ActionButtonLabel} {
color: var(--lgs-text-1);
text-shadow: var(--lgs-text-shadow);
}
@media (prefers-contrast: more) {
#Footer%{BasicFooter}::after {
background: var(--lgs-thick-bg);
backdrop-filter: none;
}
}
/* ------------------------------------------------------------------ */
/* 4. Page transitions and the app-details scrim */
/* ------------------------------------------------------------------ */
/* Route transition wrappers paint nothing and keep Steam's opacity +
transform animations; only make sure nothing opaque is left there. */
%{TopLevelTransitionSwitch},
%{ContentWrapper}%{TopLevelTransition},
%{AbsoluteDiv} {
background-color: transparent;
}
/* App-details / partner-event overlay dimming layer: Apple's scrim
colour; Steam's opacity and backdrop-filter transitions stay. */
%{PartnerEventOverlayContainer} %{Container>TransitionWrapper} {
background-color: var(--lgs-scrim);
}
/* ------------------------------------------------------------------ */
/* 5. Search route (/search/tab/*) */
/* ------------------------------------------------------------------ */
/* Tab row (All / Library / Friends / Store / Tools / Hidden): the
segments sit in one Liquid Glass capsule; the selected segment is the
white pill. Scoped to the search page; the same tabbed-page component on
library routes belongs to the library area.
The capsule is an ::after of the row wrapper, which is Steam's 3D
context root (preserve-3d), so it takes part in that context and its
blur sees the results scrolling underneath. Anchored to the first and
last tab, it hugs the segments whatever the tab count or language. */
%{GamepadSearch} %{FixCenterAlignScroll} > :nth-child(1 of %{GamepadTabbedPage>Tab}) {
anchor-name: --lgs-search-tab-first;
}
%{GamepadSearch} %{FixCenterAlignScroll} > :nth-last-child(1 of %{GamepadTabbedPage>Tab}) {
anchor-name: --lgs-search-tab-last;
}
%{GamepadSearch} %{FixCenterAlignScroll} > :nth-child(1 of %{GamepadTabbedPage>Tab}):nth-last-child(1 of %{GamepadTabbedPage>Tab}) {
anchor-name: --lgs-search-tab-first, --lgs-search-tab-last;
}
%{GamepadSearch} %{TabHeaderRowWrapper}::after {
content: "";
position: absolute;
top: calc(anchor(--lgs-search-tab-first top, 0px) - 4px);
bottom: calc(anchor(--lgs-search-tab-first bottom, 100%) - 4px);
left: calc(anchor(--lgs-search-tab-first left, 100%) - 4px);
right: calc(anchor(--lgs-search-tab-last right, 0px) - 4px);
z-index: -1;
border-radius: var(--lgs-r-capsule);
background: var(--lgs-panel-sheen),
linear-gradient(var(--lgs-glass-bg), var(--lgs-glass-bg)),
linear-gradient(var(--lgs-scrim), var(--lgs-scrim));
backdrop-filter: var(--lgs-glass-blur);
box-shadow: var(--lgs-glass-rim), var(--lgs-glass-shadow);
pointer-events: none;
}
/* Steam's pinned band behind the row (dark slab + 100px blur, shown once
results scroll under it) becomes a soft scroll edge: window tint fading
out downwards, with a lighter blur faded by the same mask, so results
dissolve under the capsule instead of meeting a hard-edged bar. Its box,
z-index and show/hide stay Steam's. */
%{GamepadSearch} %{TabHeaderRowWrapper}::before {
background: linear-gradient(180deg, var(--lgs-window-bg) 0%, transparent 100%);
backdrop-filter: var(--lgs-glass-blur);
-webkit-mask-image: linear-gradient(180deg, #000 55%, transparent 100%);
mask-image: linear-gradient(180deg, #000 55%, transparent 100%);
box-shadow: none;
}
%{GamepadSearch} %{GamepadTabbedPage>Tab} {
border-radius: var(--lgs-r-capsule);
color: var(--lgs-text-1);
transition: background-color var(--lgs-t-fast) var(--lgs-ease),
color var(--lgs-t-fast) var(--lgs-ease), box-shadow var(--lgs-t-fast) var(--lgs-ease);
}
%{GamepadSearch} %{GamepadTabbedPage>Tab}:hover {
background-color: var(--lgs-hover-fill);
color: var(--lgs-text-1);
}
%{GamepadSearch} %{GamepadTabbedPage>Tab} %{TabCount} {
color: var(--lgs-text-2);
}
/* selected (and focused, which selects): the white pill, dark label */
%{GamepadSearch} %{GamepadTabbedPage>Tab}%{GamepadTabbedPage>Selected},
%{GamepadSearch} %{GamepadTabbedPage>Tab}%{GamepadTabbedPage>Selected}:hover,
%{GamepadSearch} %{GamepadTabbedPage>Tab}.gpfocus {
background-color: var(--lgs-selected-fill) !important;
color: var(--lgs-text-on-selected) !important;
}
%{GamepadSearch} %{GamepadTabbedPage>Tab}%{GamepadTabbedPage>Selected} %{TabCount},
%{GamepadSearch} %{GamepadTabbedPage>Tab}.gpfocus %{TabCount} {
color: var(--lgs-text-on-selected) !important;
opacity: 0.6;
}
/* controller focus: the white pill gets a separated bright ring (a scrim
gap, then the focus outline and glow), so focused differs clearly from
merely selected */
%{GamepadSearch} %{GamepadTabbedPage>Tab}.gpfocus {
box-shadow: 0 0 0 2px var(--lgs-scrim), 0 0 0 4px var(--lgs-focus-outline),
var(--lgs-focus-glow) !important;
}
/* row arrows (laser): secondary until hovered or focused */
%{GamepadSearch} %{TabHeaderRowWrapper} %{Arrows} {
color: var(--lgs-text-2);
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.32));
border-radius: var(--lgs-r-capsule);
transition: color var(--lgs-t-fast) var(--lgs-ease), background-color var(--lgs-t-fast) var(--lgs-ease);
}
%{GamepadSearch} %{TabHeaderRowWrapper} %{Arrows}:hover {
color: var(--lgs-text-1);
background-color: var(--lgs-hover-fill);
}
%{GamepadSearch} %{TabHeaderRowWrapper} %{Arrows}.gpfocus {
color: var(--lgs-text-1) !important;
background-color: var(--lgs-focus-fill) !important;
box-shadow: var(--lgs-focus-ring) !important;
}
/* Result art is content: opaque, rounded to the card radius. */
%{GamepadSearch} %{ResultTemplateImage} {
border-radius: var(--lgs-r-card);
transition: box-shadow var(--lgs-t-fast) var(--lgs-ease);
}
%{GamepadSearch} %{ResultTemplateImage} %{PortraitImage>Container},
%{GamepadSearch} %{ResultTemplateImage} %{PortraitImage>Image},
%{GamepadSearch} %{ResultTemplateImage} %{FriendResultImage},
%{GamepadSearch} %{ResultTemplateImage} %{ProfileBackground},
%{GamepadSearch} %{ResultTemplateImage} %{RedirectResultBackground} {
border-radius: var(--lgs-r-card);
}
%{GamepadSearch} %{ResultTemplateImage} %{PortraitImage>Container} {
background-color: var(--lgs-fill-1);
}
/* focus: a bright ring around the art; Steam's scale-up stays */
%{GamepadSearch} %{ResultTemplate}.gpfocuswithin %{ResultTemplateImage} {
box-shadow: var(--lgs-focus-ring-outer), var(--lgs-glass-shadow) !important;
}
/* label row under each tile */
%{GamepadSearch} %{GamepadSearch>Description} {
color: var(--lgs-text-2);
text-shadow: var(--lgs-text-shadow);
}
%{GamepadSearch} %{GamepadSearch>Icon} {
color: var(--lgs-text-2);
}
%{GamepadSearch} %{ResultTemplate}.gpfocuswithin %{GamepadSearch>Description},
%{GamepadSearch} %{ResultTemplate}.gpfocuswithin %{GamepadSearch>Icon} {
color: var(--lgs-text-1) !important;
}
/* "View more in the Store": a glass fill tile instead of a grey slab */
%{GamepadSearch} %{RedirectResultBackground} {
background: var(--lgs-panel-sheen), var(--lgs-fill-2);
box-shadow: var(--lgs-glass-rim);
}
%{GamepadSearch} %{OverlaidText} {
color: var(--lgs-text-1);
text-shadow: var(--lgs-text-shadow);
}
/* empty state */
%{GamepadSearch} %{NoResultsFound} {
color: var(--lgs-text-2);
text-shadow: var(--lgs-text-shadow);
}