ui/lobbymenu.razor.scss
// Lobby — fullscreen overlay, laid out like the original's main menu:
// options down the top left, players on the right, over a full-bleed image.
.lobby
{
font-family: Inter;
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
width: 100%;
height: 100%;
// ⚠️ s&box PANELS ARE NOT HIT-TESTABLE UNLESS THEY OPT IN.
//
// This file declared no pointer-events at all, and that was the whole bug:
// no hover highlight, no clicks, nothing. DevMenu.razor.scss has always had
// the working pattern — `pointer-events: none` on the root, `all` on the
// interactive frame — which is exactly why the Q menu's buttons worked and
// the lobby's never did.
//
// It is NOT a cursor problem. Removing the player, and migrating
// Mouse.Visible -> Mouse.Visibility, both changed nothing, because the
// cursor was fine and there was simply nothing for it to hit. The giveaway
// was the hand cursor flickering on click while no button ever highlighted.
pointer-events: none;
&.hidden { display: none; }
// ── background ──────────────────────────────────────────────────────────
.bg
{
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
}
// Sits over the image so text stays readable on ANY art — without it a
// light background makes the whole menu vanish, and that is not something
// you notice until someone swaps the image.
.scrim
{
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
width: 100%;
height: 100%;
// Light touch — the art is already dark, and a heavy scrim would throw
// away the thing we put it there for. Enough to stop text sitting on a
// streetlamp, not enough to flatten the image.
background-color: rgba( 6, 8, 12, 0.42 );
}
.content
{
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
width: 100%;
height: 100%;
flex-direction: row;
padding: 56px 64px;
// The menu itself is what you click. Matches DevMenu's `.frame`.
// Deliberately here and not on the root, so the background and scrim
// stay pass-through rather than eating clicks meant for the world.
pointer-events: all;
}
// ── left: options, running downward ─────────────────────────────────────
.left
{
flex-direction: column;
width: 380px;
// ⛔ THE COLUMN SCROLLS; ITS ROWS DO NOT SHRINK. With the character list expanded the
// menu is taller than the screen leaves room for, and the flex default let every row
// absorb the difference — which does not look like an overfull menu, it looks like a
// rendering bug: `Creative` and `Build and edit the map config` printed ON TOP of each
// other, two rows of text in one row of space. Capping the LIST instead only moved the
// damage (the twelfth character fell off the bottom with no scrollbar to say so).
// ⚠️ SO THE OVERFLOW IS SPENT IN ONE PLACE, DELIBERATELY: the column scrolls, every row
// keeps the height it asked for, and nothing is silently missing or silently on top of
// anything else.
overflow-y: scroll;
> * { flex-shrink: 0; }
.brand { font-size: 44px; font-weight: 600; color: #fff; }
// ⚠️ A LABEL AGAIN. This was briefly a button; it was too quiet to find, and the Map row
// in the menu below does the job properly. Left as the subtitle under the brand, which is
// where you look to answer "what am I on" rather than "how do I change it".
.mapname { font-size: 17px; color: rgba( 255, 255, 255, 0.4 ); margin-bottom: 22px; }
// ⚠️ THE GAMEMODE BEING PLAYED, UNDER THE MAP NAME (published side, 2026-10-05): gold, the map browser's accent, and pulled
// up against the name it belongs to.
.modename
{
font-size: 14px;
font-weight: 600;
letter-spacing: 1.5px;
text-transform: uppercase;
color: #ffd479;
margin-top: -18px;
margin-bottom: 22px;
}
// ⚠️ THE EDITOR ON THE PLAYER'S SIDE (`nz_side`, 2026-10-05): amber, under the map's name, so it is the first thing
// read in a lobby that has lost its building rows on purpose
.sidetag
{
font-size: 12px;
letter-spacing: 1px;
color: #f0b44c;
background-color: rgba( 240, 180, 76, 0.12 );
border-left: 2px solid #f0b44c;
padding: 6px 10px;
margin-top: -12px;
margin-bottom: 18px;
}
.hint { margin-top: 24px; font-size: 13px; color: rgba( 255, 255, 255, 0.3 ); }
}
// ⛔ THE MENU READS OVER ANY PICTURE NOW (2026-10-05): *"i want to change the buttons to be more easily seen in the
// lobby"*. The rows were white at 5% over the map's art, with their second lines at 38% white, and on a bright picture
// both all but vanished. They are SOLID now, in the map select page's palette (MapBrowser.razor.scss): dark strips, the
// gold rule (#ffd479) on the row under the pointer, and bigger, brighter text.
.group
{
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 2px;
color: #c9cdd4;
// ⚠️ A SHADOW, NOT A BOX: the headings sit straight on the picture, and this keeps them readable on a light one
text-shadow: 0px 1px 4px rgba( 0, 0, 0, 0.9 );
margin-top: 20px;
margin-bottom: 8px;
.dim { color: #8d939c; }
}
// ⚠️ INDENTED AND QUIETER THAN `.opt`, deliberately. The list is a child of the row that
// opened it, and styling it like a sibling option would make the menu look like it grew four
// permanent entries — the exact thing collapsing it was meant to avoid.
// ⚠️ SOLID TOO (2026-10-05), a shade darker than the rows, so the open list still reads over the picture.
.charlist
{
flex-direction: column;
margin: -2px 0 8px 14px;
border-left: 2px solid #2a2e36;
// ⛔ `flex-shrink: 0`, AND WITHOUT IT THE TWELFTH CHARACTER WAS UNREACHABLE. The list is a
// flex child in a column that is already nearly full, so it was being SQUEEZED rather than
// capped: measured in the running lobby it got a 216px rect for 223px of rows, and Samuel
// Stuhlinger — last in the last set — was clipped off the bottom with no scrollbar to say
// so. A list that silently ends one row early is indistinguishable from a roster that is
// missing a character, which is exactly how it was reported.
flex-shrink: 0;
// ⚠️ THE CAP IS A BACKSTOP, NOT THE LAYOUT. Rows are tight enough that fifteen of them
// (twelve characters, three headers) fit without it; it only engages if the roster grows
// again, and then the list scrolls instead of eating the Build and Config rows below.
max-height: 360px;
overflow-y: scroll;
// The set name, not an option — sized and coloured like `.group` so it reads as a
// heading inside the list rather than a thirteenth thing you can click.
.charset
{
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #8d939c;
background-color: #0b0c0f;
padding: 7px 14px 3px 14px;
}
.charopt
{
padding: 8px 14px;
font-size: 16px;
color: #c3c8cf;
background-color: #0e1013;
margin-bottom: 2px;
cursor: pointer;
&:hover
{
background-color: #1c2027;
color: #fff;
}
&.on
{
background-color: #14251a;
color: #cdeccd;
}
&.clear { color: #8d939c; font-size: 15px; }
}
}
.opt
{
flex-direction: column;
padding: 14px 18px;
margin-bottom: 6px;
background-color: #111317;
border-left: 3px solid #2a2e36;
cursor: pointer;
.opt-label { font-size: 19px; font-weight: 600; color: #f2f4f7; }
.opt-desc { font-size: 14px; color: #8d939c; margin-top: 2px; }
// ⚠️ THE GOLD RULE MARKS THE ROW UNDER THE POINTER, as it marks the previewed map on the map select page
&:hover
{
background-color: #1c2027;
border-left: 3px solid #ffd479;
.opt-label { color: #fff; }
.opt-desc { color: #b9bec6; }
}
&.active
{
background-color: #102033;
border-left: 3px solid #4a9eff;
}
// Ready up: the one row the whole lobby is for, so the biggest
&.strong
{
padding: 18px 18px;
.opt-label { font-size: 22px; }
}
&.on
{
background-color: #14251a;
border-left: 3px solid #7ad17a;
.opt-label { color: #e3f5e3; }
.opt-desc { color: #9fd49f; }
}
// Nothing to ready up FOR — the map has no spawns placed.
//
// ⚠️ pointer-events: none is what makes it unclickable, so the hover
// sound cannot fire either. The click handler stays wired in the markup
// on purpose (LobbyCommands.Ready does the refusing) — this is the
// visible half of a rule enforced elsewhere, not the rule itself.
//
// ⚠️ The DESCRIPTION keeps full opacity while the label dims. It is
// carrying the reason, and a greyed-out control whose explanation is
// also greyed out is just a broken button.
&.disabled
{
background-color: #221214;
border-left: 3px solid #d94b4b;
pointer-events: none;
.opt-label { opacity: 0.45; }
.opt-desc { color: #e08a8a; }
}
}
// ── middle: character previews ──────────────────────────────────────────
.middle
{
flex-grow: 1;
align-items: center;
justify-content: flex-end;
.preview
{
width: 100%;
height: 78%;
}
}
// ── right: players ──────────────────────────────────────────────────────
// ⚠️ 400 WIDE, WAS 330 (2026-10-05): *"player names have little space to exist, so it starts creating more rows"*
.right
{
flex-direction: column;
margin-left: auto;
width: 400px;
}
.player
{
flex-direction: row;
align-items: center;
padding: 12px 16px;
margin-bottom: 6px;
background-color: rgba( 255, 255, 255, 0.05 );
border-radius: 4px;
.tick { font-size: 15px; color: rgba( 255, 255, 255, 0.3 ); width: 18px; flex-shrink: 0; }
// ⛔ ONE LINE, AND THE ROOM IS THE NAME'S (2026-10-05). The name took what the state left it and wrapped onto two or three
// lines. It now takes every pixel the tick and the state don't need and stays on one line. A name too long for 16 px
// is drawn smaller until it fits: the row sets its size (`NameFit`, `NameRoom` in LobbyMenu.razor). The "…" is a
// backstop only; at the 8 px floor even Steam's longest name fits.
.pname
{
font-size: 16px;
color: #fff;
margin-left: 6px;
flex-grow: 1;
flex-shrink: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pstate { margin-left: 12px; flex-shrink: 0; white-space: nowrap; font-size: 13px; color: rgba( 255, 255, 255, 0.35 ); }
// Which one is you. Obvious with one player, not with eight — and the
// list now shows real names rather than "You" in slot one.
&.you
{
border-left: 2px solid rgba( 255, 255, 255, 0.45 );
background-color: rgba( 255, 255, 255, 0.09 );
}
// Ready is the one state worth reading across a full roster at a
// glance, so it gets colour rather than only a word.
&.ready
{
background-color: rgba( 122, 209, 122, 0.12 );
.tick { color: #7ad17a; }
.pstate { color: #7ad17a; }
}
}
}