Revamp CommonwealthOnline browser UI and styles

Rework the CommonwealthOnline browser UI: restructure HTML into a nav + subpanel layout, add SVG/PNG/font assets and new sprites, and overhaul styles with a new font-face and refreshed color/spacing variables. Refactor components: tabs use a left nav list; filters extraction (renderRow) and reset focus handling; direct-connect renders into a dedicated container; server-list switched to compact row layout and adds a scroll indicator; server-details now uses a preview area and simplified preview/stats rendering. Update app logic: adjust focus zone handling (nextFocusZone), limit zones per tab, update connection status/count display, and avoid rendering details for direct-connect. Overall changes improve responsiveness, visual hierarchy, and focus/navigation behavior.
This commit is contained in:
2026-06-08 14:40:00 +12:00
parent fe620020be
commit cfd1fab6b2
13 changed files with 555 additions and 506 deletions
+36 -11
View File
@@ -6,7 +6,7 @@
var joinOverlay = document.getElementById("coJoinOverlay");
var joinMessage = document.getElementById("coJoinMessage");
var FOCUS_ZONES = ["tabs", "filters", "server-list", "details"];
var FOCUS_ZONES = ["tabs", "filters", "server-list"];
var REPEAT_INITIAL_MS = 400;
var REPEAT_RATE_MS = 80;
@@ -144,9 +144,14 @@
}
function updateServerCount() {
var el = document.getElementById("coServerCount");
if (el) {
el.textContent = " \u2014 " + state.servers.length + " servers found";
var countEl = document.getElementById("coServerCount");
var titleEl = document.getElementById("coSubpanelTitle");
var count = state.servers.length;
if (countEl) {
countEl.textContent = count + " Found";
}
if (titleEl && state.activeTab === "server-browser") {
titleEl.textContent = count === 1 ? "1 Server" : count + " Servers";
}
}
@@ -223,7 +228,10 @@
CO_Tabs.render(root, state);
CO_ControllerPrompts.render(root, state);
document.getElementById("coConnectionStatus").textContent = state.connectionStatus;
var statusEl = document.getElementById("coConnectionStatus");
if (statusEl) {
statusEl.textContent = String(state.connectionStatus || "Online").toUpperCase();
}
document.querySelectorAll(".co-tab-pane").forEach(function (pane) {
pane.classList.toggle("active", pane.getAttribute("data-pane") === state.activeTab);
@@ -238,11 +246,14 @@
var listState = Object.assign({}, state, { servers: listServers });
if (state.activeTab === "favorites" || state.activeTab === "recent") {
listState.focusZone = state.focusZone === "details" || state.focusZone === "filters" ? "server-list" : state.focusZone;
listState.focusZone = state.focusZone === "filters" ? "server-list" : state.focusZone;
}
if (state.activeTab === "server-browser") {
CO_Filters.render(root, listState);
}
if (state.activeTab !== "direct-connect") {
CO_ServerDetails.render(root, listState);
}
@@ -366,20 +377,34 @@
joinServerLocal(sel.id);
}
function nextFocusZone(fromIdx, direction) {
var zones = FOCUS_ZONES.slice();
if (state.activeTab !== "server-browser") {
zones = zones.filter(function (z) { return z !== "filters"; });
}
var current = zones.indexOf(state.focusZone);
if (current < 0) current = fromIdx;
var next = current + direction;
if (next < 0 || next >= zones.length) return null;
return zones[next];
}
function navigateFocus(button) {
var zoneIdx = FOCUS_ZONES.indexOf(state.focusZone);
var list = getActiveList();
if (button === "dpad_left") {
if (zoneIdx > 0) {
state.focusZone = FOCUS_ZONES[zoneIdx - 1];
var prevZone = nextFocusZone(FOCUS_ZONES.indexOf(state.focusZone), -1);
if (prevZone) {
state.focusZone = prevZone;
render();
}
return;
}
if (button === "dpad_right") {
if (zoneIdx < FOCUS_ZONES.length - 1 && state.activeTab === "server-browser") {
state.focusZone = FOCUS_ZONES[zoneIdx + 1];
if (state.activeTab === "direct-connect") return;
var nextZone = nextFocusZone(FOCUS_ZONES.indexOf(state.focusZone), 1);
if (nextZone) {
state.focusZone = nextZone;
render();
}
return;
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Source artwork for inline borders in index.html (top + bottom strips) -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1075.97 11" aria-hidden="true">
<polyline
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-miterlimit="10"
vector-effect="non-scaling-stroke"
points="0 10.97 0 0 1075.97 0 1075.97 10.5"
/>
<polyline
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-miterlimit="10"
vector-effect="non-scaling-stroke"
transform="translate(0, 0)"
points="0 0.47 0 8.91 1075.97 8.97 1075.97 0.47"
/>
</svg>

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -6,12 +6,13 @@
var pane = root.querySelector('[data-pane="direct-connect"]');
if (!pane) return;
var container = pane.querySelector(".co-direct-connect");
if (!container) return;
var address = state.directConnect.address || "127.0.0.1";
var port = state.directConnect.port || "7777";
pane.innerHTML =
'<div class="co-direct-connect">' +
"<h2>Direct Connect</h2>" +
container.innerHTML =
'<div class="co-direct-row">' +
'<label for="co-dc-address">Server Address</label>' +
'<input class="co-filter-input" id="co-dc-address" type="text" value="' + escapeAttr(address) + '">' +
@@ -21,8 +22,7 @@
'<input class="co-filter-input" id="co-dc-port" type="text" value="' + escapeAttr(port) + '">' +
"</div>" +
'<button type="button" class="co-btn co-dc-connect">Connect</button>' +
'<p class="co-detail-desc" style="margin-top:16px">Enter a server address to connect directly without browsing the list.</p>' +
"</div>";
'<p class="co-detail-desc" style="margin-top:16px;color:var(--co-text-dim);font-size:var(--co-font-sm)">Enter a server address to connect directly without browsing the list.</p>';
},
bind: function (root, onConnect) {
@@ -12,6 +12,37 @@
{ key: "modded", type: "checkbox", label: "Modded" }
];
function renderRow(row, index, state) {
var rowFocused = state.focusZone === "filters" && state.selectedFilterIndex === index ? " focused" : "";
if (row.type === "text") {
return (
'<div class="co-filter-row' + rowFocused + '" data-filter-index="' + index + '">' +
'<div class="co-filter-label">' + row.label + '</div>' +
'<input class="co-filter-input" type="text" data-filter="' + row.key + '" placeholder="' + row.placeholder + '" value="' + escapeAttr(state.filters[row.key] || "") + '">' +
"</div>"
);
}
if (row.type === "select") {
var opts = row.options.map(function (opt) {
var sel = (state.filters[row.key] || row.options[0]) === opt ? " selected" : "";
return '<option value="' + escapeAttr(opt) + '"' + sel + ">" + opt + "</option>";
}).join("");
return (
'<div class="co-filter-row' + rowFocused + '" data-filter-index="' + index + '">' +
'<div class="co-filter-label">' + row.label + '</div>' +
'<select class="co-filter-select" data-filter="' + row.key + '">' + opts + "</select>" +
"</div>"
);
}
var checked = state.filters[row.key] ? " checked" : "";
return (
'<label class="co-checkbox-row co-filter-row co-filter-checks' + rowFocused + '" data-filter-index="' + index + '">' +
'<span class="co-checkbox' + checked + '" data-filter="' + row.key + '"></span>' +
row.label +
"</label>"
);
}
window.CO_Filters = {
FILTER_ROWS: FILTER_ROWS,
@@ -20,40 +51,23 @@
if (!el) return;
var focusedClass = state.focusZone === "filters" ? " focused" : "";
var html = FILTER_ROWS.map(function (row, index) {
var rowFocused = state.focusZone === "filters" && state.selectedFilterIndex === index ? " focused" : "";
if (row.type === "text") {
return (
'<div class="co-filter-row' + rowFocused + '" data-filter-index="' + index + '">' +
'<div class="co-filter-label">' + row.label + '</div>' +
'<input class="co-filter-input" type="text" data-filter="' + row.key + '" placeholder="' + row.placeholder + '" value="' + escapeAttr(state.filters[row.key] || "") + '">' +
"</div>"
);
}
if (row.type === "select") {
var opts = row.options.map(function (opt) {
var sel = (state.filters[row.key] || row.options[0]) === opt ? " selected" : "";
return '<option value="' + escapeAttr(opt) + '"' + sel + ">" + opt + "</option>";
}).join("");
return (
'<div class="co-filter-row' + rowFocused + '" data-filter-index="' + index + '">' +
'<div class="co-filter-label">' + row.label + '</div>' +
'<select class="co-filter-select" data-filter="' + row.key + '">' + opts + "</select>" +
"</div>"
);
}
var checked = state.filters[row.key] ? " checked" : "";
return (
'<label class="co-checkbox-row co-filter-row' + rowFocused + '" data-filter-index="' + index + '">' +
'<span class="co-checkbox' + checked + '" data-filter="' + row.key + '"></span>' +
row.label +
"</label>"
);
var searchHtml = renderRow(FILTER_ROWS[0], 0, state);
var selectHtml = FILTER_ROWS.slice(1, 5).map(function (row, i) {
return renderRow(row, i + 1, state);
}).join("");
var checkboxHtml = FILTER_ROWS.slice(5).map(function (row, i) {
return renderRow(row, i + 5, state);
}).join("");
var resetFocused = state.focusZone === "filters" && state.selectedFilterIndex === FILTER_ROWS.length ? " focused" : "";
html += '<button type="button" class="co-btn co-reset-btn">Reset Filters</button>';
el.className = "co-filters" + focusedClass;
el.innerHTML = html;
el.innerHTML =
'<div class="co-filters-row">' + searchHtml + "</div>" +
'<div class="co-filters-row co-filters-row-selects">' + selectHtml + "</div>" +
'<div class="co-filters-row co-filters-row-options">' +
checkboxHtml +
'<button type="button" class="co-btn co-reset-btn' + resetFocused + '">Reset Filters</button>' +
"</div>";
},
bind: function (root, onFilterChange, onReset) {
@@ -3,50 +3,35 @@
window.CO_ServerDetails = {
render: function (root, state) {
var el = root.querySelector(".co-details");
var activePane = root.querySelector('.co-tab-pane[data-pane="' + state.activeTab + '"]');
var el = activePane && activePane.querySelector(".co-preview-area");
if (!el) return;
el.className = "co-details" + (state.focusZone === "details" ? " focused" : "");
var server = state.servers[state.selectedServerIndex];
if (!server) {
el.innerHTML = '<div class="co-details-title">Server Details</div><div class="co-empty">Select a server</div>';
if (!server || !server.previewImage) {
el.innerHTML = "";
el.classList.add("co-preview-hidden");
return;
}
var rulesHtml = "";
if (server.rules && server.rules.length) {
rulesHtml = "<ul class=\"co-detail-rules\">" + server.rules.map(function (r) {
return "<li>" + escapeHtml(r) + "</li>";
}).join("") + "</ul>";
}
el.classList.remove("co-preview-hidden");
var previewHtml = server.previewImage
? '<img src="' + escapeAttr(server.previewImage) + '" alt="Preview">'
: "Preview Unavailable";
var stats = [
server.worldspace,
server.mode,
server.players + "/" + server.maxPlayers,
server.ping + " ms"
].filter(Boolean).join(" \u00b7 ");
el.innerHTML =
'<div class="co-details-title">Server Details</div>' +
'<div class="co-preview">' + previewHtml + "</div>" +
'<div class="co-detail-name">' + escapeHtml(server.name) + "</div>" +
detailRow("Region", server.region) +
detailRow("Players", server.players + "/" + server.maxPlayers) +
detailRow("Ping", server.ping + " ms") +
detailRow("Worldspace", server.worldspace) +
detailRow("Mode", server.mode) +
detailRow("Status", server.status) +
detailRow("Modded", server.modded ? "Yes" : "No") +
detailRow("Password", server.passworded ? "Required" : "None") +
'<div class="co-detail-desc">' + escapeHtml(server.description || "") + "</div>" +
rulesHtml +
'<div class="co-detail-desc" style="margin-top:8px;color:var(--co-text-dim)">Mod list: coming soon</div>';
'<div class="co-preview"><img src="' + escapeAttr(server.previewImage) + '" alt="Preview"></div>' +
'<div class="co-preview-caption">' +
'<div class="co-preview-name">' + escapeHtml(server.name) + "</div>" +
'<div class="co-preview-stats">' + escapeHtml(stats) + "</div>" +
"</div>";
}
};
function detailRow(label, value) {
return '<div class="co-detail-row"><span>' + label + '</span><span>' + escapeHtml(String(value)) + "</span></div>";
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&amp;")
@@ -17,6 +17,7 @@
if (!state.servers.length) {
list.innerHTML = '<div class="co-empty">No servers found</div>';
CO_ServerList.updateScrollIndicator(wrap, list);
return;
}
@@ -26,16 +27,26 @@
var pingCls = CO_ServerList.pingClass(server.ping);
return (
'<div class="co-server-row' + selected + '" data-index="' + index + '" data-id="' + escapeAttr(server.id) + '">' +
'<div class="co-server-row-left">' +
'<span class="co-fav' + fav + '">' + (server.favorite ? "\u2605" : "\u2606") + "</span>" +
"<span>" + escapeHtml(server.name) + "</span>" +
'<span class="co-server-row-name">' + escapeHtml(server.name) + "</span>" +
"</div>" +
'<div class="co-server-row-right">' +
"<span>" + server.players + "/" + server.maxPlayers + "</span>" +
'<span class="co-ping ' + pingCls + '">' + server.ping + " ms</span>" +
"<span>" + escapeHtml(server.worldspace) + "</span>" +
"<span>" + escapeHtml(server.mode) + "</span>" +
"<span>" + escapeHtml(server.status) + "</span>" +
"</div>" +
"</div>"
);
}).join("");
CO_ServerList.updateScrollIndicator(wrap, list);
},
updateScrollIndicator: function (wrap, list) {
var indicator = wrap && wrap.querySelector(".co-scroll-indicator");
if (!indicator || !list) return;
var hasOverflow = list.scrollHeight > list.clientHeight + 2;
indicator.classList.toggle("hidden", !hasOverflow);
},
bind: function (root, onSelect) {
@@ -64,6 +75,10 @@
if (row) {
row.scrollIntoView({ block: "nearest" });
}
var wrap = root.querySelector(".co-server-list-wrap");
if (wrap && list) {
CO_ServerList.updateScrollIndicator(wrap, list);
}
}
};
@@ -7,33 +7,35 @@
TAB_ORDER: TAB_ORDER,
render: function (root, state) {
var tabsEl = root.querySelector(".co-tabs");
if (!tabsEl) return;
var navEl = root.querySelector(".co-nav-list");
if (!navEl) return;
var labels = {
"direct-connect": "Direct Connect",
"server-browser": "Server Browser",
"server-browser": "Browse",
favorites: "Favorites",
recent: "Recent"
};
tabsEl.innerHTML = TAB_ORDER.map(function (tabId, index) {
navEl.innerHTML = TAB_ORDER.map(function (tabId, index) {
var active = state.activeTab === tabId ? " active" : "";
var focused = state.focusZone === "tabs" && state.selectedTabIndex === index ? " focused" : "";
return (
'<button type="button" class="co-tab' + active + focused + '" data-tab="' + tabId + '" data-index="' + index + '">' +
'<button type="button" class="co-nav-item' + active + focused + '" data-tab="' + tabId + '" data-index="' + index + '">' +
labels[tabId] +
"</button>"
);
}).join("");
navEl.className = "co-nav-list" + (state.focusZone === "tabs" ? " focused" : "");
},
bind: function (root, onTabClick) {
var tabsEl = root.querySelector(".co-tabs");
if (!tabsEl) return;
var navEl = root.querySelector(".co-nav-list");
if (!navEl) return;
tabsEl.addEventListener("click", function (e) {
var btn = e.target.closest(".co-tab");
navEl.addEventListener("click", function (e) {
var btn = e.target.closest(".co-nav-item");
if (!btn) return;
onTabClick(btn.getAttribute("data-tab"));
});
+41 -49
View File
@@ -9,63 +9,59 @@
<body>
<div class="co-root" id="coRoot">
<div class="co-panel" id="coPanel">
<header class="co-header">
<h1>Commonwealth Online</h1>
<div class="co-subtitle">Server Browser</div>
</header>
<svg class="co-panel-border co-panel-border-top" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1075.97 11" preserveAspectRatio="none" aria-hidden="true">
<polyline class="co-panel-border-line" points="0 10.97 0 0 1075.97 0 1075.97 10.5"/>
</svg>
<svg class="co-panel-border co-panel-border-bottom" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1075.97 11" preserveAspectRatio="none" aria-hidden="true">
<polyline class="co-panel-border-line" points="0 0.47 0 8.91 1075.97 8.97 1075.97 0.47"/>
</svg>
<nav class="co-tabs"></nav>
<div class="co-subpanel">
<nav class="co-nav-list"></nav>
<div class="co-body">
<div class="co-tab-content">
<div class="co-tab-pane" data-pane="direct-connect"></div>
<div class="co-tab-pane active" data-pane="server-browser">
<div class="co-browser-layout">
<aside class="co-filters"></aside>
<div class="co-server-list-wrap">
<div class="co-server-list-header">
<span></span>
<span>Server Name</span>
<span>Players</span>
<span>Ping</span>
<span>Worldspace</span>
<span>Mode</span>
<span>Status</span>
<div class="co-subpanel-content">
<div class="co-tab-content">
<div class="co-tab-pane" data-pane="direct-connect">
<header class="co-subpanel-header">
<h2 class="co-subpanel-title">Direct Connect</h2>
</header>
<div class="co-direct-connect"></div>
</div>
<div class="co-tab-pane active" data-pane="server-browser">
<header class="co-subpanel-header">
<h2 class="co-subpanel-title" id="coSubpanelTitle">Servers</h2>
<div class="co-subpanel-meta">
<span id="coConnectionStatus">Online</span>
<span class="co-count" id="coServerCount"></span>
</div>
</header>
<div class="co-filters"></div>
<div class="co-server-list-wrap">
<div class="co-server-list"></div>
<div class="co-scroll-indicator" aria-hidden="true">&raquo;&raquo;</div>
</div>
<aside class="co-details"></aside>
</div>
</div>
<div class="co-tab-pane" data-pane="favorites">
<div class="co-browser-layout co-favorites-layout">
<div class="co-tab-pane" data-pane="favorites">
<header class="co-subpanel-header">
<h2 class="co-subpanel-title">Favorites</h2>
</header>
<div class="co-preview-area"></div>
<div class="co-server-list-wrap">
<div class="co-server-list-header">
<span></span>
<span>Server Name</span>
<span>Players</span>
<span>Ping</span>
<span>Worldspace</span>
<span>Mode</span>
<span>Status</span>
</div>
<div class="co-server-list co-favorites-list"></div>
<div class="co-scroll-indicator" aria-hidden="true">&raquo;&raquo;</div>
</div>
</div>
</div>
<div class="co-tab-pane" data-pane="recent">
<div class="co-browser-layout co-recent-layout">
<div class="co-tab-pane" data-pane="recent">
<header class="co-subpanel-header">
<h2 class="co-subpanel-title">Recent</h2>
</header>
<div class="co-preview-area"></div>
<div class="co-server-list-wrap">
<div class="co-server-list-header">
<span></span>
<span>Server Name</span>
<span>Players</span>
<span>Ping</span>
<span>Worldspace</span>
<span>Mode</span>
<span>Status</span>
</div>
<div class="co-server-list co-recent-list"></div>
<div class="co-scroll-indicator" aria-hidden="true">&raquo;&raquo;</div>
</div>
</div>
</div>
@@ -73,10 +69,6 @@
</div>
<footer class="co-action-bar">
<div class="co-status-text">
Status: <span id="coConnectionStatus">Online</span>
<span class="co-count" id="coServerCount"></span>
</div>
<div class="co-prompts"></div>
</footer>
+360 -365
View File
@@ -1,36 +1,47 @@
@font-face {
font-family: "Roboto Condensed";
src: url("./assets/fonts/RobotoCondensed-SemiBold.ttf") format("truetype");
font-weight: 600 700;
font-style: normal;
font-display: block;
}
:root {
--co-green: #4dff3a;
--co-green-soft: rgba(77, 255, 58, 0.88);
--co-green-dim: rgba(57, 255, 34, 0.35);
--co-bg: rgba(0, 14, 0, 0.92);
--co-bg-dark: rgba(0, 8, 0, 0.96);
--co-bg-list: rgba(0, 12, 0, 0.88);
--co-selected: rgba(77, 255, 58, 0.92);
--co-selected-text: #041200;
--co-border: rgba(77, 255, 58, 0.9);
--co-text-dim: rgba(77, 255, 58, 0.68);
--co-ping-good: #4dff3a;
--co-ping-mid: #c8ff3a;
--co-ping-bad: #ffd43a;
--co-green: #2dff28;
--co-green-soft: #2dff28;
--co-green-dim: rgba(45, 255, 40, 0.22);
--co-bg: rgba(0, 12, 0, 0.62);
--co-bg-dark: rgba(0, 10, 0, 0.72);
--co-bg-list: transparent;
--co-selected: #2dff28;
--co-selected-text: #001400;
--co-border: #2dff28;
--co-text-dim: rgba(45, 255, 40, 0.5);
--co-ping-good: #2dff28;
--co-ping-mid: #c8ff28;
--co-ping-bad: #ffd428;
--co-danger: #ff6a6a;
--co-font: "Consolas", "Courier New", monospace;
--co-spacing: 10px;
--co-transition: 120ms ease;
--co-text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
--co-font: "Roboto Condensed", "Bahnschrift", "Arial Narrow", Arial, sans-serif;
--co-transition: 80ms linear;
--co-border-stroke: 5px;
--co-border-strip-height: 11px;
--co-border-cap: 10px;
--co-border-edge: 4px;
/*
* Fallout 4 main-menu safe zones (1280×720 UI scaled to viewport):
* Left — menu list + green selection bar (~046%)
* Top — "Fallout 4 Updates" / installed-content panel (~026% height)
* Bottom — Bethesda logo / controller hints (~08% height)
* Fallout 4 submenu alignment (1280×720, like ADD-ONS panel):
* Left — tight gap, no overlap (~454px)
* Top — MULTIPLAYER row (~286px)
* Bottom — main menu box bottom (~146px inset)
*/
--co-panel-left: max(44vw, 500px);
--co-panel-left: max(35.6vw, 454px);
--co-panel-right: max(2vw, 24px);
--co-panel-top: max(26vh, 220px);
--co-panel-bottom: max(8vh, 72px);
--co-font-sm: clamp(11px, 0.95vw, 13px);
--co-font-md: clamp(13px, 1.1vw, 16px);
--co-font-lg: clamp(15px, 1.25vw, 19px);
--co-font-xl: clamp(18px, 1.5vw, 24px);
--co-panel-top: max(39.5vh, 286px);
--co-panel-bottom: max(20.5vh, 146px);
--co-font-xs: clamp(9px, 0.75vw, 11px);
--co-font-sm: clamp(10px, 0.85vw, 12px);
--co-font-md: clamp(13px, 1.05vw, 16px);
--co-font-lg: clamp(16px, 1.3vw, 20px);
--co-nav-width: clamp(152px, 14vw, 196px);
}
* {
@@ -45,9 +56,11 @@ body {
height: 100vh;
overflow: hidden;
font-family: var(--co-font);
font-weight: 600;
color: var(--co-green);
background: transparent;
user-select: none;
-webkit-font-smoothing: antialiased;
}
.co-root {
@@ -58,151 +71,271 @@ body {
background: transparent;
}
/* Panel is inset from all Fallout main-menu chrome so vanilla UI stays visible. */
/* Submenu panel — SVG border overlay + translucent fill. */
.co-panel {
position: absolute;
left: var(--co-panel-left);
right: var(--co-panel-right);
top: var(--co-panel-top);
bottom: var(--co-panel-bottom);
width: auto;
height: auto;
min-width: 0;
min-height: 360px;
min-height: 0;
display: flex;
flex-direction: column;
border: 2px solid var(--co-border);
border: none;
background: var(--co-bg);
box-shadow:
0 0 0 1px rgba(77, 255, 58, 0.12),
0 0 48px rgba(0, 0, 0, 0.55),
inset 0 0 80px rgba(0, 0, 0, 0.25);
pointer-events: auto;
transition: opacity var(--co-transition);
backdrop-filter: blur(2px);
padding: var(--co-border-cap) var(--co-border-edge);
overflow: visible;
}
.co-panel.co-loading {
opacity: 0.92;
opacity: 0.85;
}
.co-header {
padding: 18px 22px 14px;
border-bottom: 1px solid var(--co-green-dim);
background: rgba(0, 6, 0, 0.45);
/*
* Top/bottom border strips (from assets/MenuBoarder.svg).
* Split so each edge anchors correctly — a single stretched SVG misaligns in Ultralight.
*/
.co-panel-border {
position: absolute;
left: 0;
width: 100%;
height: var(--co-border-strip-height);
pointer-events: none;
z-index: 10;
}
.co-header h1 {
font-size: var(--co-font-xl);
letter-spacing: 0.18em;
text-transform: uppercase;
font-weight: 700;
text-shadow: var(--co-text-shadow), 0 0 12px rgba(77, 255, 58, 0.2);
.co-panel-border-top {
top: 0;
}
.co-header .co-subtitle {
margin-top: 6px;
font-size: var(--co-font-sm);
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--co-text-dim);
.co-panel-border-bottom {
bottom: 0;
}
.co-tabs {
.co-panel-border-line {
fill: none;
stroke: var(--co-border);
stroke-width: var(--co-border-stroke);
stroke-miterlimit: 10;
vector-effect: non-scaling-stroke;
}
.co-subpanel {
flex: 1;
min-height: 0;
display: flex;
gap: 4px;
border-bottom: 1px solid var(--co-green-dim);
padding: 0 16px;
background: rgba(0, 6, 0, 0.3);
overflow: hidden;
}
.co-tab {
/* Left nav — mirrors Settings / Load category list. */
.co-nav-list {
flex: 0 0 var(--co-nav-width);
display: flex;
flex-direction: column;
padding: 6px 0;
border-right: 1px solid var(--co-green-dim);
overflow-y: auto;
}
.co-nav-item {
appearance: none;
border: none;
background: transparent;
color: var(--co-text-dim);
font: inherit;
font-size: var(--co-font-sm);
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 12px 18px;
cursor: pointer;
border: 1px solid transparent;
border-bottom: none;
margin-bottom: -1px;
transition: color var(--co-transition), border-color var(--co-transition);
}
.co-tab:hover {
color: var(--co-green-soft);
font: inherit;
font-size: var(--co-font-md);
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
text-align: left;
padding: 10px 16px;
cursor: pointer;
transition: background var(--co-transition), color var(--co-transition);
}
.co-tab.active {
color: var(--co-green);
border-color: var(--co-border);
background: rgba(57, 255, 34, 0.06);
.co-nav-item:hover:not(.active) {
background: rgba(45, 255, 40, 0.08);
}
.co-tab.focused {
outline: 1px solid var(--co-green);
outline-offset: -2px;
.co-nav-item.active {
background: var(--co-selected);
color: var(--co-selected-text);
}
.co-body {
.co-nav-item.focused:not(.active) {
background: rgba(45, 255, 40, 0.12);
}
.co-nav-list.focused .co-nav-item.focused:not(.active) {
background: rgba(45, 255, 40, 0.12);
}
/* Right content */
.co-subpanel-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.co-tab-content {
flex: 1;
min-height: 0;
overflow: hidden;
}
.co-browser-layout {
display: grid;
grid-template-columns: 1fr minmax(240px, 30%);
grid-template-rows: auto 1fr;
.co-tab-pane {
display: none;
flex-direction: column;
height: 100%;
min-height: 0;
}
.co-filters {
grid-column: 1 / -1;
border-bottom: 1px solid var(--co-green-dim);
padding: 12px 16px;
.co-tab-pane.active {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-end;
gap: 10px 14px;
overflow: visible;
background: rgba(0, 8, 0, 0.55);
}
.co-filters.focused {
.co-subpanel-header {
flex-shrink: 0;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
padding: 10px 16px 8px;
}
.co-subpanel-title {
font-size: var(--co-font-sm);
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--co-text-dim);
}
.co-subpanel-meta {
display: flex;
gap: 10px;
font-size: var(--co-font-xs);
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--co-text-dim);
white-space: nowrap;
}
.co-subpanel-meta .co-count {
color: var(--co-green-soft);
}
/* Preview — Load-menu thumbnail (favorites / recent only). */
.co-preview-area {
flex-shrink: 0;
margin: 0 12px 8px;
border: 1px solid var(--co-green-dim);
background: var(--co-bg-dark);
overflow: hidden;
}
.co-preview-area:empty,
.co-preview-area.co-preview-hidden {
display: none;
}
.co-preview {
width: 100%;
height: clamp(72px, 14vh, 120px);
display: flex;
align-items: center;
justify-content: center;
color: var(--co-text-dim);
font-size: var(--co-font-sm);
letter-spacing: 0.1em;
text-transform: uppercase;
overflow: hidden;
}
.co-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.co-preview-caption {
padding: 6px 10px;
border-top: 1px solid var(--co-green-dim);
font-size: var(--co-font-xs);
letter-spacing: 0.08em;
text-transform: uppercase;
line-height: 1.35;
}
.co-preview-caption .co-preview-name {
font-size: var(--co-font-sm);
font-weight: 700;
color: var(--co-green);
}
.co-preview-caption .co-preview-stats {
color: var(--co-text-dim);
}
/* Filters */
.co-filters {
flex-shrink: 0;
padding: 0 12px 10px;
display: flex;
flex-direction: column;
gap: 6px;
}
.co-filters.focused .co-filter-row.focused .co-filter-input,
.co-filters.focused .co-filter-row.focused .co-filter-select,
.co-filters.focused .co-filter-row.focused .co-checkbox {
outline: 1px solid var(--co-green);
outline-offset: -2px;
outline-offset: 0;
}
.co-filters-row {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 6px 10px;
}
.co-filters-row-selects .co-filter-row {
flex: 1 1 90px;
min-width: 80px;
}
.co-filters-row-options {
align-items: center;
}
.co-filter-row {
display: flex;
flex-direction: column;
gap: 4px;
gap: 3px;
min-width: 0;
}
.co-filter-row[data-filter-index="0"] {
flex: 1 1 180px;
min-width: 160px;
width: 100%;
}
.co-filter-row[data-filter-index="1"],
.co-filter-row[data-filter-index="2"],
.co-filter-row[data-filter-index="3"],
.co-filter-row[data-filter-index="4"] {
flex: 0 1 130px;
min-width: 110px;
.co-filter-row.co-filter-checks {
flex-direction: row;
align-items: center;
gap: 5px;
flex: 0 0 auto;
}
.co-filter-label {
font-size: var(--co-font-sm);
letter-spacing: 0.1em;
font-size: var(--co-font-xs);
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--co-text-dim);
}
@@ -215,8 +348,13 @@ body {
color: var(--co-green);
font: inherit;
font-size: var(--co-font-sm);
padding: 8px 10px;
text-shadow: var(--co-text-shadow);
font-weight: 600;
padding: 5px 8px;
border-radius: 0;
}
.co-filter-input::placeholder {
color: var(--co-text-dim);
}
.co-filter-input:focus,
@@ -225,30 +363,22 @@ body {
outline: none;
}
.co-filter-row.focused .co-filter-input,
.co-filter-row.focused .co-filter-select,
.co-filter-row.focused .co-checkbox {
outline: 1px solid var(--co-green);
}
.co-checkbox-row {
display: flex;
align-items: center;
gap: 8px;
font-size: var(--co-font-sm);
letter-spacing: 0.05em;
gap: 5px;
font-size: var(--co-font-xs);
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
cursor: pointer;
padding: 8px 0 0;
flex: 0 0 auto;
align-self: center;
}
.co-checkbox {
width: 16px;
height: 16px;
width: 13px;
height: 13px;
border: 1px solid var(--co-border);
background: var(--co-bg-dark);
background: transparent;
display: inline-flex;
align-items: center;
justify-content: center;
@@ -257,109 +387,81 @@ body {
.co-checkbox.checked::after {
content: "X";
font-size: 11px;
font-size: 9px;
font-weight: 700;
color: var(--co-green);
line-height: 1;
}
.co-btn {
appearance: none;
border: 1px solid var(--co-border);
background: rgba(77, 255, 58, 0.1);
border: 1px solid var(--co-green-dim);
background: transparent;
color: var(--co-green);
font: inherit;
font-size: var(--co-font-sm);
letter-spacing: 0.08em;
font-size: var(--co-font-xs);
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 10px 14px;
padding: 5px 10px;
cursor: pointer;
margin-left: auto;
align-self: flex-end;
flex-shrink: 0;
transition: background var(--co-transition);
}
.co-btn:hover {
background: rgba(57, 255, 34, 0.18);
background: rgba(45, 255, 40, 0.1);
}
.co-btn.focused {
background: rgba(45, 255, 40, 0.18);
outline: 1px solid var(--co-green);
background: rgba(57, 255, 34, 0.22);
}
/* Server list — save-file style rows */
.co-server-list-wrap {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
min-height: 0;
border-right: 1px solid var(--co-green-dim);
background: var(--co-bg-list);
position: relative;
}
.co-server-list-wrap.focused {
outline: 1px solid var(--co-green);
outline-offset: -2px;
}
.co-server-list-header {
display: grid;
grid-template-columns: 32px 2.2fr 72px 64px 1fr 88px 72px;
gap: 8px;
padding: 12px 16px;
font-size: var(--co-font-sm);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--co-text-dim);
border-bottom: 1px solid var(--co-green-dim);
background: rgba(0, 6, 0, 0.5);
background: rgba(45, 255, 40, 0.03);
}
.co-server-list {
flex: 1;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--co-green-dim) var(--co-bg-dark);
scrollbar-color: var(--co-green-dim) transparent;
padding: 0 4px;
}
.co-server-list::-webkit-scrollbar {
width: 10px;
}
.co-server-list::-webkit-scrollbar-track {
background: var(--co-bg-dark);
width: 4px;
}
.co-server-list::-webkit-scrollbar-thumb {
background: var(--co-green-dim);
border: 1px solid var(--co-border);
}
.co-server-row {
display: grid;
grid-template-columns: 32px 2.2fr 72px 64px 1fr 88px 72px;
gap: 8px;
padding: 12px 16px;
font-size: var(--co-font-md);
line-height: 1.35;
display: flex;
align-items: center;
cursor: pointer;
border-bottom: 1px solid rgba(77, 255, 58, 0.1);
transition: background var(--co-transition);
text-shadow: var(--co-text-shadow);
}
.co-server-row > span:nth-child(2) {
font-size: var(--co-font-lg);
font-weight: 700;
letter-spacing: 0.04em;
}
.co-server-row > span:nth-child(3) {
justify-content: space-between;
gap: 16px;
padding: 8px 12px;
font-size: var(--co-font-md);
font-weight: 600;
line-height: 1.2;
cursor: pointer;
transition: background var(--co-transition);
}
.co-server-row:hover {
background: rgba(57, 255, 34, 0.06);
.co-server-row:hover:not(.selected) {
background: rgba(45, 255, 40, 0.06);
}
.co-server-row.selected {
@@ -367,15 +469,44 @@ body {
color: var(--co-selected-text);
}
.co-server-row.selected .co-ping,
.co-server-row.selected .co-fav {
.co-server-row-left {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
flex: 1;
}
.co-server-row-name {
font-size: var(--co-font-lg);
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.co-server-row-right {
display: flex;
align-items: center;
gap: 20px;
flex-shrink: 0;
font-size: var(--co-font-md);
font-weight: 700;
letter-spacing: 0.04em;
}
.co-server-row.selected .co-server-row-right,
.co-server-row.selected .co-fav,
.co-server-row.selected .co-ping {
color: var(--co-selected-text);
}
.co-fav {
text-align: center;
color: var(--co-text-dim);
font-size: 16px;
font-size: 15px;
flex-shrink: 0;
}
.co-fav.on {
@@ -396,120 +527,32 @@ body {
color: var(--co-selected-text);
}
.co-details {
padding: 14px 16px;
overflow-y: auto;
font-size: var(--co-font-md);
background: rgba(0, 8, 0, 0.45);
line-height: 1.4;
}
.co-details.focused {
outline: 1px solid var(--co-green);
outline-offset: -2px;
}
.co-details-title {
font-size: var(--co-font-sm);
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--co-text-dim);
margin-bottom: 10px;
}
.co-preview {
width: 100%;
height: 96px;
border: 1px solid var(--co-green-dim);
background: var(--co-bg-dark);
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: center;
color: var(--co-text-dim);
font-size: var(--co-font-sm);
letter-spacing: 0.06em;
text-transform: uppercase;
overflow: hidden;
}
.co-preview img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.85;
}
.co-detail-name {
font-size: var(--co-font-lg);
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 10px;
font-weight: 700;
text-shadow: var(--co-text-shadow);
}
.co-detail-row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid rgba(77, 255, 58, 0.08);
}
.co-detail-row span:first-child {
color: var(--co-text-dim);
text-transform: uppercase;
font-size: var(--co-font-sm);
letter-spacing: 0.05em;
.co-scroll-indicator {
flex-shrink: 0;
}
.co-detail-row span:last-child {
text-align: right;
font-weight: 600;
}
.co-detail-desc {
margin-top: 12px;
line-height: 1.5;
color: var(--co-green-soft);
font-size: var(--co-font-sm);
}
.co-detail-rules {
margin-top: 10px;
padding-left: 16px;
text-align: center;
padding: 2px 0 4px;
font-size: var(--co-font-xs);
letter-spacing: 0.15em;
color: var(--co-text-dim);
font-size: var(--co-font-sm);
}
.co-detail-rules li {
margin-bottom: 3px;
.co-scroll-indicator.hidden {
display: none;
}
/* Footer prompts */
.co-action-bar {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 18px;
justify-content: flex-end;
padding: 6px 14px 8px;
border-top: 1px solid var(--co-green-dim);
font-size: var(--co-font-sm);
letter-spacing: 0.05em;
background: rgba(0, 6, 0, 0.45);
}
.co-status-text {
text-transform: uppercase;
}
.co-status-text .co-count {
color: var(--co-text-dim);
}
.co-prompts {
display: flex;
gap: 20px;
gap: 16px;
flex-wrap: wrap;
justify-content: flex-end;
}
@@ -517,62 +560,59 @@ body {
.co-prompt {
display: flex;
align-items: center;
gap: 8px;
font-size: var(--co-font-sm);
letter-spacing: 0.06em;
gap: 6px;
font-size: var(--co-font-xs);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--co-green-soft);
}
.co-prompt-key {
width: 22px;
height: 22px;
min-width: 18px;
height: 18px;
border: 1px solid var(--co-border);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-size: 8px;
font-weight: 700;
padding: 0 3px;
}
.co-direct-connect {
padding: 28px 36px;
max-width: 520px;
}
.co-direct-connect h2 {
font-size: var(--co-font-lg);
letter-spacing: 0.12em;
text-transform: uppercase;
margin-bottom: 18px;
padding: 16px 20px;
max-width: 480px;
}
.co-direct-row {
margin-bottom: 14px;
margin-bottom: 12px;
}
.co-direct-row label {
display: block;
font-size: var(--co-font-sm);
letter-spacing: 0.08em;
font-size: var(--co-font-xs);
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--co-text-dim);
margin-bottom: 6px;
margin-bottom: 4px;
}
.co-empty {
padding: 32px;
padding: 24px 16px;
text-align: center;
color: var(--co-text-dim);
font-size: var(--co-font-md);
letter-spacing: 0.06em;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.co-join-overlay {
position: absolute;
inset: 0;
background: rgba(0, 10, 0, 0.88);
background: rgba(0, 0, 0, 0.75);
display: flex;
flex-direction: column;
align-items: center;
@@ -597,10 +637,11 @@ body {
}
.co-spinner {
width: 24px;
height: 24px;
width: 22px;
height: 22px;
border: 2px solid var(--co-green-dim);
border-top-color: var(--co-green);
border-radius: 50%;
animation: co-spin 0.8s linear infinite;
}
@@ -608,68 +649,22 @@ body {
to { transform: rotate(360deg); }
}
.co-tab-content {
height: 100%;
overflow: hidden;
}
.co-tab-pane {
display: none;
height: 100%;
}
.co-tab-pane.active {
display: block;
}
.co-filters::-webkit-scrollbar,
.co-details::-webkit-scrollbar {
width: 8px;
}
.co-filters::-webkit-scrollbar-thumb,
.co-details::-webkit-scrollbar-thumb {
background: var(--co-green-dim);
}
.co-favorites-layout,
.co-recent-layout {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.co-favorites-layout .co-server-list-wrap,
.co-recent-layout .co-server-list-wrap {
grid-column: 1;
border-right: none;
}
@media (max-width: 1280px) {
:root {
--co-panel-left: max(46vw, 520px);
--co-panel-left: max(35.8vw, 450px);
--co-panel-right: max(2vw, 16px);
--co-panel-top: max(28vh, 200px);
--co-panel-bottom: max(9vh, 64px);
--co-panel-top: max(40vh, 280px);
--co-panel-bottom: max(21vh, 142px);
--co-nav-width: clamp(136px, 15vw, 168px);
}
.co-browser-layout {
grid-template-columns: 1fr minmax(200px, 34%);
}
.co-filter-row[data-filter-index="1"],
.co-filter-row[data-filter-index="2"],
.co-filter-row[data-filter-index="3"],
.co-filter-row[data-filter-index="4"] {
flex: 0 1 100px;
min-width: 90px;
}
}
@media (min-width: 2560px) {
:root {
--co-panel-left: max(44vw, 960px);
--co-panel-left: max(35.6vw, 908px);
--co-panel-right: max(2.5vw, 48px);
--co-panel-top: max(24vh, 280px);
--co-panel-bottom: max(7vh, 96px);
--co-panel-top: max(39.5vh, 572px);
--co-panel-bottom: max(20.5vh, 292px);
}
}