ui/gamelauncher.razor.scss
@import "Theme.scss";
GameLauncher
{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
flex-direction: row;
align-items: stretch;
pointer-events: all;
backdrop-filter: blur(5px);
background-color: rgba(0, 0, 0, .45);
// Just a fade for the full-screen scrim - it must stay at 100% size the whole time, or the blur
// shrinks in from scale(0.95) and exposes a sliver of unblurred screen at the edges.
animation: cg-launch-fade .4s ease-out;
// --- Sidebar -------------------------------------------------------------
.sidebar
{
width: 210px;
flex-shrink: 0;
height: 100%;
flex-direction: column;
align-items: stretch;
gap: 16px;
padding: 28px 22px;
background-color: rgba(6, 12, 9, .8);
border-right: 0.15px solid $line;
// The scale-in "pop" lives on the content, not the full-screen scrim above.
transform-origin: 50% 50%;
animation: cg-launch-in .4s ease-out;
// Only the game list scrolls - "Browse Games" sits pinned below it, always visible.
.tiles
{
flex-grow: 1;
min-height: 0;
flex-direction: column;
align-items: stretch;
gap: 14px;
overflow-y: scroll;
overflow-x: unset;
}
.tile
{
flex-shrink: 0;
height: 138px;
flex-direction: column;
align-items: stretch;
gap: 8px;
padding: 10px;
background-color: $panel-2;
border: 0.15px solid $line;
border-radius: $r-md;
cursor: pointer;
pointer-events: all;
transition: all .12s ease;
&:hover
{
border-color: $line-soft;
filter: brightness(1.15);
}
.art
{
flex-grow: 1;
position: relative;
align-items: center;
justify-content: center;
border-radius: $r-sm;
background-color: $panel-3;
overflow: hidden;
.shot
{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-position: center;
object-fit: cover;
}
}
.pip
{
font-size: 32px;
line-height: 1;
color: $text-off;
&.red
{
color: rgba(178, 51, 48, .55);
}
&.black
{
color: rgba(28, 37, 48, .55);
}
}
.label
{
font-family: $sans;
font-size: 11px;
font-weight: 700;
letter-spacing: .2px;
color: $text-mut;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
// The focused game - the "your seat" gold treatment.
&.selected
{
background-color: rgba(34, 28, 16, .92);
border-color: $gold;
.art
{
background-color: $card-face;
}
.pip
{
&.red
{
color: $card-red;
}
&.black
{
color: $card-black;
}
}
.label
{
color: $text;
}
}
// Sits outside .tiles, pinned below the scrolling list - the escape hatch into the full games browser.
&.browse
{
background-image: linear-gradient(to bottom, $gold-bright, $gold-deep);
border-color: transparent;
.art
{
background-color: transparent;
}
// Reset uppercase + tracking, same as every other icon glyph in the theme - both
// break the ligature lookup and it falls back to rendering literal letters.
.ico
{
font-family: "Material Icons";
font-size: 34px;
letter-spacing: 0;
text-transform: lowercase;
color: $on-gold;
}
.text
{
font-weight: 800;
letter-spacing: 1px;
text-transform: uppercase;
color: $on-gold;
}
// Matches .cg-btn.primary's press feedback - brighten on hover, dim + settle on click.
&:hover
{
filter: brightness(1.12);
border-color: transparent;
}
&:active
{
filter: brightness(0.94);
border-color: transparent;
transform: translateY(1px);
}
}
}
}
// --- Hero ------------------------------------------------------------------
.hero
{
flex-grow: 1;
min-width: 0;
height: 100%;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
padding: $deadzone;
pointer-events: all;
transform-origin: 50% 50%;
animation: cg-launch-in .4s ease-out;
.brand
{
flex-direction: column;
align-items: flex-start;
gap: 10px;
.marks
{
flex-direction: row;
align-items: flex-start;
gap: 8px;
}
.mark
{
font-family: $serif;
font-size: 48px;
font-weight: 700;
color: linear-gradient( 45deg, #ffffff, #d4d9df 48%, #97a0ab );
text-shadow: 0 0px 4px rgba(0,0,0,0.8);
&.gold
{
color: linear-gradient( 45deg, #f8edc2, $gold-bright 38%, $gold 66%, $gold-deep );
}
}
.tagline
{
font-family: $sans;
font-size: 14px;
font-weight: 500;
color: $text-dim;
}
}
.focus
{
flex-direction: column;
align-items: flex-start;
gap: 14px;
max-width: 620px;
.tags
{
gap: 16px;
flex-direction: row;
}
.mode
{
// .cg-tag defaults to align-self: center (for centring over card art elsewhere) -
// here it sits in a left-aligned column, so pin it to the start instead.
align-self: flex-start;
text-shadow: none;
}
.title
{
font-family: $serif;
font-size: 68px;
font-weight: 700;
line-height: 1;
color: $text;
text-shadow: 0 2px 10px rgba(0,0,0,.5);
}
.desc
{
font-family: $sans;
font-size: 15px;
font-weight: 400;
line-height: 1.6;
color: $text-dim;
}
.meta
{
flex-direction: row;
align-items: center;
gap: 18px;
.item
{
flex-direction: row;
align-items: center;
gap: 7px;
font-family: $sans;
font-size: 13px;
font-weight: 600;
color: $text-mut;
.ico
{
font-family: "Material Icons";
font-size: 16px;
letter-spacing: 0;
text-transform: none;
color: $text-mut;
}
}
}
.actions
{
flex-direction: row;
align-items: center;
gap: 14px;
margin-top: 4px;
// Full pill buttons for the hero's primary actions, rounder than the standard $r-sm
// buttons used elsewhere (menus, modals) - matches the main-menu concept art.
.cg-btn
{
padding: 15px 32px;
border-radius: 999px;
}
}
.cg-note
{
margin-top: 4px;
}
// Stand-in for the .meta players line while a cloud game's package is still downloading+mounting -
// Package alone doesn't carry an authoritative player count, so this waits on the real GameDefinition.
.sk
{
background-color: $panel-3;
border-radius: $r-sm;
animation: cg-skeleton-pulse 1.1s ease-in-out infinite;
}
.sk-meta { width: 120px; height: 16px; }
}
}
// --- Bottom-right column - open tables for the focused game, then its stats -------------------------
.side
{
position: absolute;
right: $deadzone;
bottom: $deadzone;
width: 330px;
flex-direction: column;
align-items: stretch;
gap: 12px;
// Shared card chrome for both the sessions list and the stats card below it.
.sessions,
.stats
{
flex-direction: column;
align-items: stretch;
background-color: $panel-deep;
border: 0.15px solid $line;
border-radius: $r-lg;
overflow: hidden;
transform-origin: 50% 50%;
animation: cg-launch-in .4s ease-out;
@include elevate-panel;
}
// Same uppercase eyebrow style marks both cards; only Sessions gets a refresh button.
.sessions-head,
.stats-head
{
flex-direction: row;
align-items: center;
justify-content: space-between;
min-height: 48px; // matches the refresh button's box, so Stats' text-only header isn't shorter
padding: 12px 16px;
border-bottom: 0.15px solid $line;
.cg-btn
{
padding: 4px;
.ico { font-size: 13px; }
}
}
// Zebra stripe - shared so a session row and a stats row alternate the same way. Scoped to .rows
// (not just .row) so the header row sharing a parent with the stats rows doesn't throw off the
// odd/even count.
.rows .row:nth-child(even)
{
background-color: rgba(255, 255, 255, .009);
}
.sessions
{
.rows
{
flex-direction: column;
align-items: stretch;
max-height: 180px; // ~3 rows before it scrolls
overflow-y: scroll;
}
.row
{
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 10px;
min-height: 49px; // full row height (padding + tallest bit), not just the content area
padding: 12px 16px;
border-bottom: 0.15px solid $line;
&:last-child
{
border-bottom-width: 0;
}
&.empty
{
justify-content: center;
font-family: $sans;
font-size: 13px;
color: $text-dim;
}
.who
{
flex-direction: row;
align-items: center;
gap: 8px;
min-width: 0;
.dot
{
width: 7px;
height: 7px;
border-radius: 50%;
background-color: $green;
flex-shrink: 0;
}
.name
{
font-family: $sans;
font-size: 13px;
font-weight: 700;
color: $text;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
// Marks a table hosted by one of your Steam friends - these sort to the top of the list.
.ico.friend
{
font-family: "Material Icons";
font-size: 14px;
color: $gold;
flex-shrink: 0;
}
}
.join
{
flex-direction: row;
align-items: center;
gap: 8px;
flex-shrink: 0;
.meta
{
font-family: $sans;
font-size: 12px;
color: $text-mut;
}
.cg-btn
{
padding: 6px 14px;
font-size: 11px;
}
}
// Placeholder rows while the debounced query is still in flight, so the card doesn't pop
// in empty then jump once real rows land.
&.skeleton
{
.sk
{
background-color: rgba(236, 230, 214, .16);
border-radius: $r-sm;
animation: cg-skeleton-pulse 1.1s ease-in-out infinite;
}
.sk-dot { width: 7px; height: 7px; border-radius: 50%; }
.sk-name { width: 90px; height: 12px; }
.sk-btn { width: 54px; height: 24px; }
}
}
}
.stats
{
.rows
{
flex-direction: column;
align-items: stretch;
}
.row
{
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
border-bottom: 0.15px solid $line;
&:last-child
{
border-bottom-width: 0;
}
.k
{
font-family: $sans;
font-size: 13px;
font-weight: 500;
color: $text-dim;
}
.v
{
font-family: $serif;
font-size: 18px;
font-weight: 700;
color: $text;
}
}
}
}
}
@keyframes cg-launch-fade {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes cg-launch-in {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
@keyframes cg-skeleton-pulse {
0%, 100% { opacity: .5; }
50% { opacity: 1; }
}