diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json
index 11e4cf71c1..f516e00b9c 100644
--- a/internal/cloudcp/portal/dist/build_manifest.json
+++ b/internal/cloudcp/portal/dist/build_manifest.json
@@ -1,5 +1,5 @@
{
- "source_hash": "e0dc04d4f7be0d3c9048256e3dfff63acb71c236f2c8fd44b237612191a7d696",
+ "source_hash": "69bb383eb566fc7cd58d13fdd5aad0d3d5b2b43107d66fbebc9d700607af2d26",
"build_inputs": [
"package.json",
"tsconfig.json",
diff --git a/internal/cloudcp/portal/dist/portal_app.css b/internal/cloudcp/portal/dist/portal_app.css
index d492f3e93c..29cf3ac020 100644
--- a/internal/cloudcp/portal/dist/portal_app.css
+++ b/internal/cloudcp/portal/dist/portal_app.css
@@ -381,8 +381,8 @@ header .logout-btn:hover,
.portal-shell-main {
min-width: 0;
min-height: 820px;
- padding: 0 0 28px;
- background: #f5f7fa;
+ padding: 0 0 24px;
+ background: #f3f6fa;
}
.portal-shell-main > .portal-content-panel {
display: none !important;
@@ -391,15 +391,27 @@ header .logout-btn:hover,
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 18px;
- align-items: end;
- padding: 16px 22px 18px;
+ align-items: center;
+ padding: 18px 22px 16px;
border-bottom: 1px solid var(--line);
- background: rgba(244, 247, 250, 0.92);
+ background: rgba(255, 255, 255, 0.84);
}
.portal-account-bar-copy {
display: flex;
flex-direction: column;
- gap: 6px;
+ gap: 8px;
+}
+.portal-account-bar-meta {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+.portal-account-bar-separator,
+.portal-account-bar-access {
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--ink-muted);
}
.portal-account-bar-row {
display: flex;
@@ -410,9 +422,9 @@ header .logout-btn:hover,
}
.portal-account-bar-row h2 {
margin: 0;
- font-size: 28px;
- line-height: 0.98;
- letter-spacing: -0.05em;
+ font-size: 32px;
+ line-height: 0.96;
+ letter-spacing: -0.06em;
}
.portal-account-bar-copy p {
margin: 0;
@@ -459,13 +471,13 @@ header .logout-btn:hover,
gap: 26px;
}
.portal-content-panel {
- padding: 20px 22px 0;
+ padding: 16px 22px 0;
}
.portal-top-section,
.service-section {
display: flex;
flex-direction: column;
- gap: 16px;
+ gap: 14px;
}
.portal-top-section-header h2,
.service-header h2 {
@@ -548,11 +560,11 @@ header .logout-btn:hover,
.account-content-panel-overview,
.account-content-panel-workspaces,
.account-content-panel-team {
- padding: 18px;
+ padding: 16px;
border: 1px solid var(--line);
- border-radius: var(--radius-lg);
- background: #eef3f7;
- box-shadow: 0 12px 28px rgba(10, 25, 41, 0.045);
+ border-radius: 14px;
+ background: #edf2f6;
+ box-shadow: 0 10px 22px rgba(10, 25, 41, 0.04);
}
.account-command-deck,
.account-overview-card,
@@ -607,12 +619,12 @@ header .logout-btn:hover,
.account-command-deck {
display: grid;
grid-template-columns: 1fr;
- gap: 14px;
+ gap: 12px;
align-items: stretch;
- padding: 16px;
+ padding: 14px;
border: 1px solid var(--line);
- border-radius: 14px;
- background: rgba(255, 255, 255, 0.6);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.72);
}
.account-overview-card {
display: grid;
@@ -756,7 +768,7 @@ header .logout-btn:hover,
.portal-support-panel {
border: 1px solid var(--line);
border-radius: var(--radius-lg);
- padding: 22px;
+ padding: 18px 20px;
background: var(--panel);
box-shadow: var(--shadow-sm);
}
@@ -944,17 +956,16 @@ header .logout-btn:hover,
grid-template-columns: minmax(0, 1.42fr) minmax(360px, 1fr);
gap: 14px;
align-items: start;
- padding: 16px;
+ padding: 14px;
border: 1px solid var(--line);
- border-radius: 14px;
- background: rgba(255, 255, 255, 0.56);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.68);
}
.workspace-operations-main,
.workspace-operations-detail {
min-width: 0;
}
-.workspace-management-panel,
-.team-management-panel {
+.workspace-management-panel {
min-height: 100%;
}
.workspace-management-panel.visible,
@@ -1298,16 +1309,33 @@ header .logout-btn:hover,
}
.team-list-message {
display: flex;
- align-items: center;
+ flex-direction: column;
+ align-items: flex-start;
justify-content: center;
- min-height: 120px;
- padding: 16px;
+ gap: 6px;
+ min-height: 148px;
+ padding: 18px;
border: 1px dashed var(--line-strong);
border-radius: 10px;
- color: var(--ink-muted);
background: var(--panel-muted);
}
+.team-list-message-title {
+ font-size: 16px;
+ line-height: 1.1;
+ letter-spacing: -0.02em;
+ color: var(--ink);
+}
+.team-list-message-copy {
+ font-size: 13px;
+ line-height: 1.55;
+ color: var(--ink-soft);
+}
.team-list-message.error {
+ border-color: rgba(166, 27, 27, 0.2);
+ background: var(--danger-soft);
+}
+.team-list-message.error .team-list-message-title,
+.team-list-message.error .team-list-message-copy {
color: var(--danger);
}
.btn-remove {
diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js
index 950e2e0a54..692c877cae 100644
--- a/internal/cloudcp/portal/dist/portal_app.js
+++ b/internal/cloudcp/portal/dist/portal_app.js
@@ -138,11 +138,18 @@
actionButton.setAttribute("data-workspace-action", workspace.state === "active" ? "suspend" : "delete");
closeButton.disabled = entry.manageWorkspace.pending;
}
- function setContainerMessage(container, msg, isError) {
+ function setContainerMessage(container, title, msg, isError) {
container.textContent = "";
var message = document.createElement("div");
message.className = "team-list-message" + (isError ? " error" : "");
- message.textContent = msg;
+ var heading = document.createElement("strong");
+ heading.className = "team-list-message-title";
+ heading.textContent = title;
+ var copy = document.createElement("span");
+ copy.className = "team-list-message-copy";
+ copy.textContent = msg;
+ message.appendChild(heading);
+ message.appendChild(copy);
container.appendChild(message);
}
function countMembersByRole(members, role) {
@@ -160,11 +167,11 @@
return;
}
if (entry.teamQuery.status === "loading") {
- stats.innerHTML = '
Roster Loading\u2026
';
+ stats.innerHTML = 'Roster Loading\u2026
Invites Ready
';
return;
}
if (entry.teamQuery.status === "error") {
- stats.innerHTML = 'Roster Needs attention
';
+ stats.innerHTML = 'Roster Needs attention
Fallback Invite only
';
return;
}
var members = entry.teamQuery.data;
@@ -265,15 +272,15 @@
return;
}
if (entry.teamQuery.status === "loading") {
- setContainerMessage(roster, "Loading\u2026", false);
+ setContainerMessage(roster, "Loading roster", "Checking who currently has access to this account.", false);
return;
}
if (entry.teamQuery.status === "error") {
- setContainerMessage(roster, entry.teamQuery.error, true);
+ setContainerMessage(roster, "Roster needs attention", entry.teamQuery.error, true);
return;
}
if (!entry.teamQuery.data.length) {
- setContainerMessage(roster, "No team members.", false);
+ setContainerMessage(roster, "No operators yet", "Invite someone new when this hosted account needs shared access.", false);
return;
}
roster.textContent = "";
@@ -1692,13 +1699,6 @@
function hasHostedAccounts(accounts) {
return accounts.length > 0;
}
- function countWorkspaces(accounts) {
- var total = 0;
- for (var i = 0; i < accounts.length; i += 1) {
- total += Array.isArray(accounts[i].workspaces) ? accounts[i].workspaces.length : 0;
- }
- return total;
- }
function countWorkspacesByState(workspaces, state) {
var count = 0;
for (var i = 0; i < workspaces.length; i += 1) {
@@ -1761,12 +1761,6 @@
}).join("");
return 'Attention
Needs review These workspaces should be checked before you treat the hosted fleet as fully healthy.
' + items + "
";
}
- function renderOverviewBand(accounts) {
- var hosted = hasHostedAccounts(accounts);
- var workspaceTotal = countWorkspaces(accounts);
- var summary = hosted ? "Hosted operations, operator access, and commercial account services." : "Billing, license recovery, refunds, and privacy actions until hosted access is attached.";
- return 'Pulse Account
' + (hosted ? "Operator console" : "Account console") + ' ' + (hosted ? "Operator ready" : "Self-hosted only") + " " + summary + '
Hosted access ' + (hosted ? "Active" : "Not attached") + 'Accounts ' + (accounts.length === 1 ? "1 account" : String(accounts.length) + " accounts") + 'Workspace fleet ' + (workspaceTotal ? workspaceCountLabel(workspaceTotal) : "0 workspaces") + "
";
- }
function renderPrimaryAccountBar(account) {
var workspaceLabel = workspaceCountLabel((account.workspaces || []).length);
var actionHTML = "";
@@ -1779,7 +1773,7 @@
if (account.can_manage) {
actionHTML += 'Manage team ';
}
- return '' + escapeHTML(accountKindLabel(account)) + '
' + escapeHTML(account.name) + ' ' + escapeHTML(account.kind_label) + ' ' + escapeHTML(titleCase(account.role)) + ' ' + escapeHTML(workspaceLabel) + "
" + escapeHTML(account.kind === "msp" ? "Operator workspace account" : "Hosted account operations") + '
' + actionHTML + "
";
+ return '' + escapeHTML(accountKindLabel(account)) + ' / ' + escapeHTML(titleCase(account.role)) + ' access
' + escapeHTML(account.name) + ' ' + escapeHTML(account.kind_label) + ' ' + escapeHTML(titleCase(account.role)) + ' ' + escapeHTML(workspaceLabel) + "
" + escapeHTML(account.kind === "msp" ? "Operator workspace account" : "Hosted account operations") + '
' + actionHTML + "
";
}
function shellSectionButton(section, activeSection, title, copy) {
return '' + title + ' ' + copy + " ";
@@ -1878,7 +1872,7 @@
var hostedContent = accounts.map(function(account) {
return '' + renderAccountOverviewSection(account) + renderAccountWorkspaceSection(account, context.accountAPIBasePath) + renderAccountTeamSection(account) + "
";
}).join("");
- return '' + renderShellNavigation(accounts, context.bootstrap.support_email || "", activeSection) + '
' + renderOverviewBand(accounts) + (accounts.length === 1 ? renderPrimaryAccountBar(accounts[0]) : "") + '
Select a service
Choose the next commercial action Open a billing, license, refund, or privacy flow from the service navigator. The active request stays here so the account-services area behaves like one operating desk instead of a list of disconnected tools.
Start here
Billing Open Stripe customer portal access after verification.
Licenses Recover the latest active self-hosted license and invoice link.
Refunds Confirm eligibility before revoking active commercial access.
Privacy Request export or deletion without leaving Pulse Account.
What to expect
Verification first Each flow confirms the commercial email before opening sensitive account actions.
One task at a time The active commercial request stays in this panel until you finish or switch tools.
Support stays close If billing, licenses, refunds, or privacy behave unexpectedly, escalate from this surface.
Data and privacy Request export or deletion of the commercial data tied to an email address. Payment data held directly by Stripe still requires support handling.
Payment-card data stays with Stripe. For Stripe deletion support, contact
' + escapeHTML(context.bootstrap.support_email || "") + ' .
' + renderSupportSection(context) + " ";
+ return '' + renderShellNavigation(accounts, context.bootstrap.support_email || "", activeSection) + '
' + (accounts.length === 1 ? renderPrimaryAccountBar(accounts[0]) : "") + '
Select a service
Choose the next commercial action Open a billing, license, refund, or privacy flow from the service navigator. The active request stays here so the account-services area behaves like one operating desk instead of a list of disconnected tools.
Start here
Billing Open Stripe customer portal access after verification.
Licenses Recover the latest active self-hosted license and invoice link.
Refunds Confirm eligibility before revoking active commercial access.
Privacy Request export or deletion without leaving Pulse Account.
What to expect
Verification first Each flow confirms the commercial email before opening sensitive account actions.
One task at a time The active commercial request stays in this panel until you finish or switch tools.
Support stays close If billing, licenses, refunds, or privacy behave unexpectedly, escalate from this surface.
Data and privacy Request export or deletion of the commercial data tied to an email address. Payment data held directly by Stripe still requires support handling.
Payment-card data stays with Stripe. For Stripe deletion support, contact
' + escapeHTML(context.bootstrap.support_email || "") + ' .
' + renderSupportSection(context) + " ";
}
function renderSignedOutPortalHTML(context) {
var statusHTML = "";
diff --git a/internal/cloudcp/portal/frontend/src/account_view.test.ts b/internal/cloudcp/portal/frontend/src/account_view.test.ts
index b07a5fb8ed..335f0907ff 100644
--- a/internal/cloudcp/portal/frontend/src/account_view.test.ts
+++ b/internal/cloudcp/portal/frontend/src/account_view.test.ts
@@ -83,13 +83,16 @@ describe('account view', function() {
teamVisible: true,
teamQuery: { status: 'loading', error: '', data: [] },
}));
- expect(document.getElementById('team-list-acct_1')?.textContent).toContain('Loading');
+ expect(document.getElementById('team-list-acct_1')?.textContent).toContain('Loading roster');
+ expect(document.getElementById('team-stats-acct_1')?.textContent).toContain('Invites');
renderTeamSection('acct_1', createEntry({
teamVisible: true,
teamQuery: { status: 'error', error: 'Failed to load team.', data: [] },
}));
+ expect(document.getElementById('team-list-acct_1')?.textContent).toContain('Roster needs attention');
expect(document.getElementById('team-list-acct_1')?.textContent).toContain('Failed to load team.');
+ expect(document.getElementById('team-stats-acct_1')?.textContent).toContain('Invite only');
renderTeamSection(
'acct_1',
diff --git a/internal/cloudcp/portal/frontend/src/account_view.ts b/internal/cloudcp/portal/frontend/src/account_view.ts
index 2f5ae96dfa..b96f024eb5 100644
--- a/internal/cloudcp/portal/frontend/src/account_view.ts
+++ b/internal/cloudcp/portal/frontend/src/account_view.ts
@@ -163,11 +163,18 @@ export function renderWorkspaceManagement(account: PortalAccountSummary, entry:
closeButton.disabled = entry.manageWorkspace.pending;
}
-function setContainerMessage(container: HTMLElement, msg: string, isError: boolean): void {
+function setContainerMessage(container: HTMLElement, title: string, msg: string, isError: boolean): void {
container.textContent = '';
var message = document.createElement('div');
message.className = 'team-list-message' + (isError ? ' error' : '');
- message.textContent = msg;
+ var heading = document.createElement('strong');
+ heading.className = 'team-list-message-title';
+ heading.textContent = title;
+ var copy = document.createElement('span');
+ copy.className = 'team-list-message-copy';
+ copy.textContent = msg;
+ message.appendChild(heading);
+ message.appendChild(copy);
container.appendChild(message);
}
@@ -187,11 +194,15 @@ function renderTeamStats(accountID: string, entry: PortalAccountUIEntry): void {
return;
}
if (entry.teamQuery.status === 'loading') {
- stats.innerHTML = 'Roster Loading…
';
+ stats.innerHTML =
+ 'Roster Loading…
' +
+ 'Invites Ready
';
return;
}
if (entry.teamQuery.status === 'error') {
- stats.innerHTML = 'Roster Needs attention
';
+ stats.innerHTML =
+ 'Roster Needs attention
' +
+ 'Fallback Invite only
';
return;
}
@@ -317,15 +328,15 @@ export function renderTeamSection(accountID: string, entry: PortalAccountUIEntry
return;
}
if (entry.teamQuery.status === 'loading') {
- setContainerMessage(roster, 'Loading…', false);
+ setContainerMessage(roster, 'Loading roster', 'Checking who currently has access to this account.', false);
return;
}
if (entry.teamQuery.status === 'error') {
- setContainerMessage(roster, entry.teamQuery.error, true);
+ setContainerMessage(roster, 'Roster needs attention', entry.teamQuery.error, true);
return;
}
if (!entry.teamQuery.data.length) {
- setContainerMessage(roster, 'No team members.', false);
+ setContainerMessage(roster, 'No operators yet', 'Invite someone new when this hosted account needs shared access.', false);
return;
}
diff --git a/internal/cloudcp/portal/frontend/src/shell_view.test.ts b/internal/cloudcp/portal/frontend/src/shell_view.test.ts
index ce105a039e..03816d0fb4 100644
--- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts
+++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts
@@ -125,9 +125,6 @@ describe('shell view', function() {
})
);
- expect(html).toContain('portal-shell-head-kicker">Pulse Account');
- expect(html).toContain('portal-shell-head-title">Operator console');
- expect(html).toContain('Hosted operations, operator access, and commercial account services.');
expect(html).toContain('portal-shell-nav');
expect(html).toContain('Overview');
expect(html).toContain('Workspaces');
@@ -136,6 +133,7 @@ describe('shell view', function() {
expect(html).toContain('id="account-services-section"');
expect(html).toContain('Self-hosted licenses and billing');
expect(html).toContain('portal-account-bar');
+ expect(html).toContain('Owner access');
expect(html).toContain('id="accounts-root"');
expect(html).toContain('MSP account');
expect(html).toContain('Acme MSP');
@@ -196,9 +194,7 @@ describe('shell view', function() {
);
expect(html).toContain('Pulse Account');
- expect(html).toContain('Self-hosted only');
- expect(html).toContain('portal-shell-head-title">Account console');
- expect(html).toContain('Billing, license recovery, refunds, and privacy actions until hosted access is attached.');
+ expect(html).toContain('Account console');
expect(html).toContain('Use these tools for self-hosted commercial actions.');
expect(html).toContain('Account services');
expect(html).not.toContain('Self-hosted licenses and billing');
diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts
index 6dbd24f794..7e15172fed 100644
--- a/internal/cloudcp/portal/frontend/src/shell_view.ts
+++ b/internal/cloudcp/portal/frontend/src/shell_view.ts
@@ -193,36 +193,6 @@ function renderAttentionPanel(workspaces: PortalWorkspaceSummary[]): string {
);
}
-function renderOverviewBand(accounts: PortalAccountSummary[]): string {
- var hosted = hasHostedAccounts(accounts);
- var workspaceTotal = countWorkspaces(accounts);
- var summary = hosted
- ? 'Hosted operations, operator access, and commercial account services.'
- : 'Billing, license recovery, refunds, and privacy actions until hosted access is attached.';
-
- return (
- '' +
- '' +
- '
Pulse Account
' +
- '
' +
- '
' +
- '
' +
- '
' + (hosted ? 'Operator console' : 'Account console') + ' ' +
- '' + (hosted ? 'Operator ready' : 'Self-hosted only') + ' ' +
- '' +
- '
' + summary + '
' +
- '
' +
- '
' +
- 'Hosted access ' + (hosted ? 'Active' : 'Not attached') + ' ' +
- 'Accounts ' + (accounts.length === 1 ? '1 account' : String(accounts.length) + ' accounts') + ' ' +
- 'Workspace fleet ' + (workspaceTotal ? workspaceCountLabel(workspaceTotal) : '0 workspaces') + ' ' +
- '
' +
- '
' +
- '
' +
- ' '
- );
-}
-
function renderPrimaryAccountBar(account: PortalAccountSummary): string {
var workspaceLabel = workspaceCountLabel((account.workspaces || []).length);
var actionHTML = '';
@@ -249,7 +219,11 @@ function renderPrimaryAccountBar(account: PortalAccountSummary): string {
return (
'' +
'' +
- '
' + escapeHTML(accountKindLabel(account)) + '
' +
+ '
' +
+ '' + escapeHTML(accountKindLabel(account)) + ' ' +
+ '/ ' +
+ '' + escapeHTML(titleCase(account.role)) + ' access ' +
+ '
' +
'
' +
'
' + escapeHTML(account.name) + ' ' +
'
' +
@@ -657,28 +631,37 @@ function renderAccountTeamSection(account: PortalAccountSummary): string {
'
' +
'
' +
'
' +
- '
' +
- '
' +
- '
Invite someone new ' +
- '
Add another operator with the minimum role they need for this account.
' +
+ '
' +
+ '
' +
+ '
Access desk
' +
+ '
Invite and role policy ' +
+ '
Keep the roster deliberate. Invite the smallest role first, then tighten access as responsibilities become clearer.
' +
'
' +
- '
' +
- '
Email
' +
- '
Role Admin Tech Read-only
' +
- '
Invite ' +
+ '
' +
+ '
' +
+ '
' +
+ '
Invite someone new ' +
+ '
Add another operator with the minimum role they need for this account.
' +
+ '
' +
+ '
' +
+ '
Email
' +
+ '
Role Admin Tech Read-only
' +
+ '
Invite ' +
+ '
' +
+ '
' +
+ accessPolicy +
'
' +
'
' +
- accessPolicy +
'
' +
'
' +
'' +
@@ -765,7 +748,6 @@ export function renderAuthenticatedPortalHTML(context: ShellViewContext): string
'
' +
renderShellNavigation(accounts, context.bootstrap.support_email || '', activeSection) +
'
' +
- renderOverviewBand(accounts) +
(accounts.length === 1 ? renderPrimaryAccountBar(accounts[0]) : '') +
'
' +
'' + hostedContent + '
' +
diff --git a/internal/cloudcp/portal/frontend/src/styles.css b/internal/cloudcp/portal/frontend/src/styles.css
index 2f034ae4f4..7a08d9c68a 100644
--- a/internal/cloudcp/portal/frontend/src/styles.css
+++ b/internal/cloudcp/portal/frontend/src/styles.css
@@ -411,8 +411,8 @@ header .logout-btn:hover,
.portal-shell-main {
min-width: 0;
min-height: 820px;
- padding: 0 0 28px;
- background: #f5f7fa;
+ padding: 0 0 24px;
+ background: #f3f6fa;
}
.portal-shell-main > .portal-content-panel {
@@ -423,16 +423,30 @@ header .logout-btn:hover,
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 18px;
- align-items: end;
- padding: 16px 22px 18px;
+ align-items: center;
+ padding: 18px 22px 16px;
border-bottom: 1px solid var(--line);
- background: rgba(244, 247, 250, 0.92);
+ background: rgba(255, 255, 255, 0.84);
}
.portal-account-bar-copy {
display: flex;
flex-direction: column;
- gap: 6px;
+ gap: 8px;
+}
+
+.portal-account-bar-meta {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.portal-account-bar-separator,
+.portal-account-bar-access {
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--ink-muted);
}
.portal-account-bar-row {
@@ -445,9 +459,9 @@ header .logout-btn:hover,
.portal-account-bar-row h2 {
margin: 0;
- font-size: 28px;
- line-height: 0.98;
- letter-spacing: -0.05em;
+ font-size: 32px;
+ line-height: 0.96;
+ letter-spacing: -0.06em;
}
.portal-account-bar-copy p {
@@ -502,14 +516,14 @@ header .logout-btn:hover,
}
.portal-content-panel {
- padding: 20px 22px 0;
+ padding: 16px 22px 0;
}
.portal-top-section,
.service-section {
display: flex;
flex-direction: column;
- gap: 16px;
+ gap: 14px;
}
.portal-top-section-header h2,
@@ -606,11 +620,11 @@ header .logout-btn:hover,
.account-content-panel-overview,
.account-content-panel-workspaces,
.account-content-panel-team {
- padding: 18px;
+ padding: 16px;
border: 1px solid var(--line);
- border-radius: var(--radius-lg);
- background: #eef3f7;
- box-shadow: 0 12px 28px rgba(10, 25, 41, 0.045);
+ border-radius: 14px;
+ background: #edf2f6;
+ box-shadow: 0 10px 22px rgba(10, 25, 41, 0.04);
}
.account-command-deck,
@@ -670,12 +684,12 @@ header .logout-btn:hover,
.account-command-deck {
display: grid;
grid-template-columns: 1fr;
- gap: 14px;
+ gap: 12px;
align-items: stretch;
- padding: 16px;
+ padding: 14px;
border: 1px solid var(--line);
- border-radius: 14px;
- background: rgba(255, 255, 255, 0.6);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.72);
}
.account-overview-card {
@@ -838,7 +852,7 @@ header .logout-btn:hover,
.portal-support-panel {
border: 1px solid var(--line);
border-radius: var(--radius-lg);
- padding: 22px;
+ padding: 18px 20px;
background: var(--panel);
box-shadow: var(--shadow-sm);
}
@@ -1055,10 +1069,10 @@ header .logout-btn:hover,
grid-template-columns: minmax(0, 1.42fr) minmax(360px, 1fr);
gap: 14px;
align-items: start;
- padding: 16px;
+ padding: 14px;
border: 1px solid var(--line);
- border-radius: 14px;
- background: rgba(255, 255, 255, 0.56);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.68);
}
.workspace-operations-main,
@@ -1066,8 +1080,7 @@ header .logout-btn:hover,
min-width: 0;
}
-.workspace-management-panel,
-.team-management-panel {
+.workspace-management-panel {
min-height: 100%;
}
@@ -1465,17 +1478,37 @@ header .logout-btn:hover,
.team-list-message {
display: flex;
- align-items: center;
+ flex-direction: column;
+ align-items: flex-start;
justify-content: center;
- min-height: 120px;
- padding: 16px;
+ gap: 6px;
+ min-height: 148px;
+ padding: 18px;
border: 1px dashed var(--line-strong);
border-radius: 10px;
- color: var(--ink-muted);
background: var(--panel-muted);
}
+.team-list-message-title {
+ font-size: 16px;
+ line-height: 1.1;
+ letter-spacing: -0.02em;
+ color: var(--ink);
+}
+
+.team-list-message-copy {
+ font-size: 13px;
+ line-height: 1.55;
+ color: var(--ink-soft);
+}
+
.team-list-message.error {
+ border-color: rgba(166, 27, 27, 0.2);
+ background: var(--danger-soft);
+}
+
+.team-list-message.error .team-list-message-title,
+.team-list-message.error .team-list-message-copy {
color: var(--danger);
}