Files
MichaelScottsman--LiquidAss…/theme/03-material.css
T
MichaelScottsman 5af91fc1bd Glass leaves with its page; the sidebar keeps native glass in VR Settings; Friends clears the search row; a paintbrush on the bar tunes the glass
Route changes
- Plates fade with their content: the reporter reads a running opacity fade on a plate or its ancestors
  (Home -> Friends: the route's 150 ms exit) and reports exitAt/exitMs; glassd fades the plate's coverage
  linearly on that clock, optics unchanged (cap exitAt); the daemon keeps a leaving plate for exactly
  that fade. A plate whose element left the DOM (Home -> Library stays in one /library route: no fade,
  Home unmounts when Library has rendered) goes in 90 ms instead of the 350 ms dematerialize ramp that
  left Home's discs over the Library grid.
- The window cover arrives with the route: the shell sets the new route's glass mode in the history
  change itself, the reporter reports the mode flip at once (tickNow) with the time it happened, and
  glassd springs a kept surface's new cover from then (appearAt now also for main gaining its cover).
- A disc popped at the switch belongs to the page that leaves: it never flips the new page to the crops
  layout and goes with the page's fade (lgs_sg.js routeOut).

Sidebar in VR Settings
- The frame menu is a laser-only surface keyed on Steam's gamepad-nav flag, which goes stale on
  SteamVR's own pages (the laser works SteamVR's page): the native glass went on and off with every
  input Steam saw, its CSS glass in between. On a SteamVR page (the frame's VR Settings action active)
  the rule does not apply.

Friends
- The page starts at y 108 (C1a's pad + 40, as Settings' list): the sidebar platter no longer sits
  under the search capsule.

Glass tuner (flag glassTune, on)
- A paintbrush circle after the system capsule (its own glass piece and native cover) opens a bar popup
  through Steam's own bar popup button: colour (8 swatches) and tint strength, Glass intensity (the
  dial), Refraction, Frost, Highlights, Reset to default.
- Daemon action glass.tune (kind ui, its own 40 ms rate, outside the global window): glassd.json at once,
  ~/.config/glass-shell/tune.json 400 ms after the last change (kept across reboots by request; the
  defaults delete it; RT-6 allows exactly this file), the dial file mirrored for the SteamVR pages.
- glassd: tune {refract, frost, light, hue, hueK} applied in drawGlass, eased (40 ms) like the dial now
  is; the glass body's neutral moves toward the hue (uHue), per-piece tints stay on top (cap tune).
- The CSS glass follows live: a style per Steam window sets --lgs-dial / --lgs-tune-hue; the material
  tints mix the colour in.
2026-10-09 14:06:10 -04:00

385 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-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-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-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; }
}