feat(portal): add hosted account operations surface

This commit is contained in:
rcourtman
2026-03-27 13:58:36 +00:00
parent 8074221f1c
commit 9e6299dbb8
7 changed files with 216 additions and 17 deletions
@@ -1,6 +1,6 @@
# Pulse Account Portal Spec
Last updated: 2026-03-26
Last updated: 2026-03-27
Status: ACTIVE
## Purpose
@@ -135,6 +135,11 @@ Core rules:
7. Self-hosted billing/license tools belong in the same account shell, but
they must present as a secondary account-services area when hosted access
is active.
8. Hosted and MSP accounts must expose account-level operations separately from
individual workspace cards so operator actions are visible before a user
starts drilling into one workspace at a time.
9. Workspace fleets must summarize health and attention state at the account
level, not force the user to scan badges one card at a time.
## Screen Model
@@ -158,9 +163,14 @@ The signed-in shell should be treated as four first-class states:
Each signed-in state should render:
1. a concise overview band that summarizes the account posture
2. account cards with role, account kind, and workspace totals
3. explicit action groups, not anonymous menu affordances
4. a clearly separated `Self-hosted licenses and billing` section when those
2. account cards with role, account kind, workspace totals, and account-level
fleet status
3. an explicit `Account operations` area for billing, team, and workspace
management entry points
4. a `Workspace fleet` area that makes health and lifecycle state legible
before the user opens a workspace
5. explicit action groups, not anonymous menu affordances
6. a clearly separated `Self-hosted licenses and billing` section when those
tools are present below hosted content
## Product-Specific Boundaries
+1 -1
View File
@@ -1,5 +1,5 @@
{
"source_hash": "ade0e3202886602f0f46ca656041930d213b8dc6ea3d681664a6596f18da0c12",
"source_hash": "f091b6dbbdf775a750bbba5c4047fbba2553fa0393420916e2e9e5ca315e1437",
"build_inputs": [
"package.json",
"tsconfig.json",
+91 -2
View File
@@ -110,6 +110,43 @@ header .logout-btn:hover {
color: #64748b;
max-width: 520px;
}
.account-status-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin: 0 0 16px;
}
.account-stat-card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 6px;
box-shadow: 0 1px 3px rgba(15, 23, 42, .04);
}
.account-stat-label {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #64748b;
}
.account-stat-value {
font-size: 24px;
font-weight: 700;
color: #0f172a;
}
.account-stat-healthy {
color: #166534;
}
.account-stat-checking {
color: #1d4ed8;
}
.account-stat-unhealthy {
color: #b91c1c;
}
.badge {
font-size: 11px;
font-weight: 600;
@@ -194,10 +231,16 @@ header .logout-btn:hover {
.ws-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.ws-title-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
width: 100%;
}
.ws-name {
font-weight: 600;
@@ -209,6 +252,11 @@ header .logout-btn:hover {
gap: 6px;
flex-wrap: wrap;
}
.ws-summary {
font-size: 13px;
color: #475569;
line-height: 1.45;
}
.ws-created {
font-size: 11px;
color: #94a3b8;
@@ -305,10 +353,39 @@ header .logout-btn:hover {
.link-button {
text-decoration: none;
}
.account-operations-panel {
margin: 0 0 18px;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 16px 18px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(15, 23, 42, .04);
}
.account-operations-copy {
display: flex;
flex-direction: column;
gap: 4px;
max-width: 540px;
}
.account-operations-copy h3 {
margin: 0;
font-size: 15px;
font-weight: 700;
}
.account-operations-copy p {
margin: 0;
font-size: 13px;
color: #64748b;
line-height: 1.5;
}
.account-actions {
margin-top: 14px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
flex-wrap: wrap;
}
@@ -794,6 +871,15 @@ header .logout-btn:hover {
.account-badges {
justify-content: flex-start;
}
.account-status-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.account-operations-panel {
flex-direction: column;
}
.account-actions {
justify-content: flex-start;
}
.account-subsection-header {
flex-direction: column;
align-items: flex-start;
@@ -803,6 +889,9 @@ header .logout-btn:hover {
}
}
@media (max-width: 560px) {
.account-status-grid {
grid-template-columns: 1fr;
}
.workspace-card {
flex-direction: column;
align-items: flex-start;
+23 -3
View File
@@ -1464,6 +1464,20 @@
function hasHostedAccounts(accounts) {
return accounts.length > 0;
}
function countWorkspacesByHealth(workspaces, status) {
var count = 0;
for (var i = 0; i < workspaces.length; i += 1) {
if (String(workspaces[i].health_status || "") === status) count += 1;
}
return count;
}
function countWorkspacesByState(workspaces, state) {
var count = 0;
for (var i = 0; i < workspaces.length; i += 1) {
if (String(workspaces[i].state || "") === state) count += 1;
}
return count;
}
function renderOverviewBand(context, accounts) {
var hosted = hasHostedAccounts(accounts);
var workspaceTotal = countWorkspaces(accounts);
@@ -1478,6 +1492,7 @@
var state = String(workspace.state || "");
var safeState = escapeHTML(state);
var createdLabel = formatWorkspaceDate(workspace.created_at);
var healthSummary = workspace.health_status === "healthy" ? "Live updates and health checks are currently good." : workspace.health_status === "unhealthy" ? "This workspace needs attention before it is trustworthy." : "This workspace is still waiting on a completed health check.";
var openAction = "";
if (state === "active") {
openAction = '<form method="POST" action="' + escapeAttr(accountAPIBasePath + "/" + account.id + "/tenants/" + workspace.id + "/handoff") + '"><button type="submit" class="btn-primary">Open workspace</button></form>';
@@ -1491,11 +1506,16 @@
manageAction = '<div class="workspace-menu-wrap"><button type="button" class="btn-secondary btn-workspace-menu" id="workspace-menu-button-' + escapeAttr(account.id) + "-" + escapeAttr(workspace.id) + '" data-action="toggle-workspace-menu" data-account-id="' + escapeAttr(account.id) + '" data-workspace-id="' + escapeAttr(workspace.id) + '" aria-haspopup="menu" aria-expanded="false">Manage</button><div class="workspace-menu" id="workspace-menu-' + escapeAttr(account.id) + "-" + escapeAttr(workspace.id) + '" data-workspace-menu-account-id="' + escapeAttr(account.id) + '" data-workspace-id="' + escapeAttr(workspace.id) + '" role="menu" hidden><button type="button" class="workspace-menu-item workspace-menu-item-danger" data-action="workspace-action" data-account-id="' + escapeAttr(account.id) + '" data-workspace-id="' + escapeAttr(workspace.id) + '" data-workspace-action="' + escapeAttr(menuAction) + '" data-workspace-name="' + escapeAttr(workspace.display_name) + '">' + escapeHTML(menuLabel) + "</button></div></div>";
}
var createdMeta = createdLabel ? '<span class="ws-created">Created ' + escapeHTML(createdLabel) + "</span>" : "";
return '<div class="workspace-card"><div class="ws-info"><span class="ws-name">' + escapeHTML(workspace.display_name) + '</span><div class="ws-meta">' + healthBadgeHTML(workspace) + '<span class="badge badge-' + safeState + '">' + safeState + "</span>" + createdMeta + '</div></div><div class="ws-actions">' + openAction + manageAction + "</div></div>";
return '<div class="workspace-card"><div class="ws-info"><div class="ws-title-row"><span class="ws-name">' + escapeHTML(workspace.display_name) + '</span></div><div class="ws-meta">' + healthBadgeHTML(workspace) + '<span class="badge badge-' + safeState + '">' + safeState + "</span>" + createdMeta + '</div><div class="ws-summary">' + escapeHTML(healthSummary) + '</div></div><div class="ws-actions">' + openAction + manageAction + "</div></div>";
}
function renderAccountSection(account, accountAPIBasePath) {
var workspaces = Array.isArray(account.workspaces) ? account.workspaces : [];
var summaryText = accountKindLabel(account) + " \xB7 " + titleCase(account.role) + " \xB7 " + workspaceCountLabel(workspaces.length);
var healthyCount = countWorkspacesByHealth(workspaces, "healthy");
var checkingCount = countWorkspacesByHealth(workspaces, "checking");
var unhealthyCount = countWorkspacesByHealth(workspaces, "unhealthy");
var activeCount = countWorkspacesByState(workspaces, "active");
var operationsCopy = account.kind === "msp" ? "Manage the client fleet from this account surface. Workspace creation, billing, and team actions belong here." : "Use this account surface to open hosted workspaces, manage billing, and control access for this hosted account.";
var workspaceHTML = "";
if (workspaces.length === 0) {
workspaceHTML = '<div class="empty-state"><p>No hosted workspaces yet. Create one to get started.</p></div>';
@@ -1508,13 +1528,13 @@
var teamSection = "";
var addWorkspaceForm = "";
if (account.can_manage) {
actions = '<div class="account-actions">' + (account.kind === "msp" ? '<button type="button" class="btn-secondary" id="add-ws-btn-' + escapeAttr(account.id) + '" data-action="toggle-add-workspace" data-account-id="' + escapeAttr(account.id) + '">+ Add workspace</button>' : "") + (account.has_billing ? '<button type="button" class="btn-secondary" data-action="open-billing" data-account-id="' + escapeAttr(account.id) + '">Manage billing</button>' : "") + '<button type="button" class="btn-secondary" id="team-btn-' + escapeAttr(account.id) + '" data-action="toggle-team" data-account-id="' + escapeAttr(account.id) + '">Manage team</button></div>';
actions = '<div class="account-operations-panel"><div class="account-operations-copy"><h3>Account operations</h3><p>' + escapeHTML(operationsCopy) + '</p></div><div class="account-actions">' + (account.kind === "msp" ? '<button type="button" class="btn-secondary" id="add-ws-btn-' + escapeAttr(account.id) + '" data-action="toggle-add-workspace" data-account-id="' + escapeAttr(account.id) + '">Add workspace</button>' : "") + (account.has_billing ? '<button type="button" class="btn-secondary" data-action="open-billing" data-account-id="' + escapeAttr(account.id) + '">Manage billing</button>' : "") + '<button type="button" class="btn-secondary" id="team-btn-' + escapeAttr(account.id) + '" data-action="toggle-team" data-account-id="' + escapeAttr(account.id) + '">Manage team</button></div></div>';
teamSection = '<div class="team-section" id="team-section-' + escapeAttr(account.id) + '" data-actor-role="' + escapeAttr(account.role) + '"><h3>Team members</h3><table class="team-table"><thead><tr><th>Email</th><th>Role</th><th></th></tr></thead><tbody id="team-list-' + escapeAttr(account.id) + '"><tr><td colspan="3" class="team-message-cell">Loading\u2026</td></tr></tbody></table><div class="team-invite"><div><label for="invite-email-' + escapeAttr(account.id) + '">Email</label><input type="email" id="invite-email-' + escapeAttr(account.id) + '" placeholder="user@example.com" autocomplete="off"></div><div><label for="invite-role-' + escapeAttr(account.id) + '">Role</label><select id="invite-role-' + escapeAttr(account.id) + '"><option value="admin">Admin</option><option value="tech">Tech</option><option value="read_only">Read-only</option></select></div><button type="button" class="btn-primary btn-compact" data-action="invite-member" data-account-id="' + escapeAttr(account.id) + '">Invite</button></div></div>';
if (account.kind === "msp") {
addWorkspaceForm = '<div class="add-workspace-form" id="add-ws-form-' + escapeAttr(account.id) + '"><label for="ws-name-' + escapeAttr(account.id) + '">Workspace name (e.g. client name)</label><input type="text" id="ws-name-' + escapeAttr(account.id) + '" placeholder="Acme Corp" maxlength="80" autocomplete="off"><div class="form-actions"><button type="button" class="btn-primary" data-action="create-workspace" data-account-id="' + escapeAttr(account.id) + '">Create workspace</button><button type="button" class="btn-secondary" data-action="toggle-add-workspace" data-account-id="' + escapeAttr(account.id) + '">Cancel</button><div class="spinner" id="ws-spinner-' + escapeAttr(account.id) + '" hidden></div></div></div>';
}
}
return '<section class="account-section"><div class="account-header"><div class="account-heading"><div class="account-eyebrow">' + escapeHTML(accountKindLabel(account)) + "</div><h2>" + escapeHTML(account.name) + '</h2><div class="account-summary">' + escapeHTML(summaryText) + '</div></div><div class="account-badges"><span class="badge badge-' + escapeHTML(account.kind) + '">' + escapeHTML(account.kind_label) + "</span>" + roleBadgeHTML(account.role) + '</div></div><div class="account-subsection-header"><h3>Workspaces</h3><p>Open hosted Pulse workspaces and manage lifecycle actions for this account.</p></div>' + workspaceHTML + actions + teamSection + addWorkspaceForm + "</section>";
return '<section class="account-section"><div class="account-header"><div class="account-heading"><div class="account-eyebrow">' + escapeHTML(accountKindLabel(account)) + "</div><h2>" + escapeHTML(account.name) + '</h2><div class="account-summary">' + escapeHTML(summaryText) + '</div></div><div class="account-badges"><span class="badge badge-' + escapeHTML(account.kind) + '">' + escapeHTML(account.kind_label) + "</span>" + roleBadgeHTML(account.role) + '</div></div><div class="account-status-grid"><div class="account-stat-card"><span class="account-stat-label">Active workspaces</span><span class="account-stat-value">' + String(activeCount) + '</span></div><div class="account-stat-card"><span class="account-stat-label">Healthy</span><span class="account-stat-value account-stat-healthy">' + String(healthyCount) + '</span></div><div class="account-stat-card"><span class="account-stat-label">Checking</span><span class="account-stat-value account-stat-checking">' + String(checkingCount) + '</span></div><div class="account-stat-card"><span class="account-stat-label">Needs attention</span><span class="account-stat-value account-stat-unhealthy">' + String(unhealthyCount) + "</span></div></div>" + actions + '<div class="account-subsection-header"><h3>Workspace fleet</h3><p>Open hosted Pulse workspaces, review fleet health, and manage lifecycle actions for this account.</p></div>' + workspaceHTML + teamSection + addWorkspaceForm + "</section>";
}
function renderHeaderHTML(context) {
if (context.bootstrap.authenticated) {
@@ -131,12 +131,20 @@ describe('shell view', function() {
expect(html).toContain('MSP account');
expect(html).toContain('MSP account · Owner · 3 workspaces');
expect(html).toContain('Acme MSP');
expect(html).toContain('Account operations');
expect(html).toContain('Manage the client fleet from this account surface.');
expect(html).toContain('Workspace fleet');
expect(html).toContain('Alpha Workspace');
expect(html).toContain('Beta Workspace');
expect(html).toContain('Gamma Workspace');
expect(html).toContain('Active workspaces');
expect(html).toContain('Needs attention');
expect(html).toContain('Healthy</span>');
expect(html).toContain('Unhealthy</span>');
expect(html).toContain('Checking</span>');
expect(html).toContain('Live updates and health checks are currently good.');
expect(html).toContain('This workspace needs attention before it is trustworthy.');
expect(html).toContain('This workspace is still waiting on a completed health check.');
expect(html).toContain('/api/accounts/acct_1/tenants/ws_active/handoff');
expect(html).toContain('Open workspace');
expect(html).toContain('data-action="toggle-workspace-menu"');
@@ -77,6 +77,22 @@ function hasHostedAccounts(accounts: PortalAccountSummary[]): boolean {
return accounts.length > 0;
}
function countWorkspacesByHealth(workspaces: PortalWorkspaceSummary[], status: string): number {
var count = 0;
for (var i = 0; i < workspaces.length; i += 1) {
if (String(workspaces[i].health_status || '') === status) count += 1;
}
return count;
}
function countWorkspacesByState(workspaces: PortalWorkspaceSummary[], state: string): number {
var count = 0;
for (var i = 0; i < workspaces.length; i += 1) {
if (String(workspaces[i].state || '') === state) count += 1;
}
return count;
}
function renderOverviewBand(context: ShellViewContext, accounts: PortalAccountSummary[]): string {
var hosted = hasHostedAccounts(accounts);
var workspaceTotal = countWorkspaces(accounts);
@@ -117,6 +133,11 @@ function renderWorkspaceCard(account: PortalAccountSummary, workspace: PortalWor
var state = String(workspace.state || '');
var safeState = escapeHTML(state);
var createdLabel = formatWorkspaceDate(workspace.created_at);
var healthSummary = workspace.health_status === 'healthy'
? 'Live updates and health checks are currently good.'
: workspace.health_status === 'unhealthy'
? 'This workspace needs attention before it is trustworthy.'
: 'This workspace is still waiting on a completed health check.';
var openAction = '';
if (state === 'active') {
openAction =
@@ -172,12 +193,15 @@ function renderWorkspaceCard(account: PortalAccountSummary, workspace: PortalWor
return (
'<div class="workspace-card">' +
'<div class="ws-info">' +
'<span class="ws-name">' + escapeHTML(workspace.display_name) + '</span>' +
'<div class="ws-title-row">' +
'<span class="ws-name">' + escapeHTML(workspace.display_name) + '</span>' +
'</div>' +
'<div class="ws-meta">' +
healthBadgeHTML(workspace) +
'<span class="badge badge-' + safeState + '">' + safeState + '</span>' +
createdMeta +
'</div>' +
'<div class="ws-summary">' + escapeHTML(healthSummary) + '</div>' +
'</div>' +
'<div class="ws-actions">' +
openAction +
@@ -190,6 +214,13 @@ function renderWorkspaceCard(account: PortalAccountSummary, workspace: PortalWor
function renderAccountSection(account: PortalAccountSummary, accountAPIBasePath: string): string {
var workspaces = Array.isArray(account.workspaces) ? account.workspaces : [];
var summaryText = accountKindLabel(account) + ' · ' + titleCase(account.role) + ' · ' + workspaceCountLabel(workspaces.length);
var healthyCount = countWorkspacesByHealth(workspaces, 'healthy');
var checkingCount = countWorkspacesByHealth(workspaces, 'checking');
var unhealthyCount = countWorkspacesByHealth(workspaces, 'unhealthy');
var activeCount = countWorkspacesByState(workspaces, 'active');
var operationsCopy = account.kind === 'msp'
? 'Manage the client fleet from this account surface. Workspace creation, billing, and team actions belong here.'
: 'Use this account surface to open hosted workspaces, manage billing, and control access for this hosted account.';
var workspaceHTML = '';
if (workspaces.length === 0) {
workspaceHTML = '<div class="empty-state"><p>No hosted workspaces yet. Create one to get started.</p></div>';
@@ -207,13 +238,18 @@ function renderAccountSection(account: PortalAccountSummary, accountAPIBasePath:
var addWorkspaceForm = '';
if (account.can_manage) {
actions =
'<div class="account-operations-panel">' +
'<div class="account-operations-copy">' +
'<h3>Account operations</h3>' +
'<p>' + escapeHTML(operationsCopy) + '</p>' +
'</div>' +
'<div class="account-actions">' +
(account.kind === 'msp'
? '<button type="button" class="btn-secondary" id="add-ws-btn-' +
escapeAttr(account.id) +
'" data-action="toggle-add-workspace" data-account-id="' +
escapeAttr(account.id) +
'">+ Add workspace</button>'
'">Add workspace</button>'
: '') +
(account.has_billing
? '<button type="button" class="btn-secondary" data-action="open-billing" data-account-id="' +
@@ -225,6 +261,7 @@ function renderAccountSection(account: PortalAccountSummary, accountAPIBasePath:
'" data-action="toggle-team" data-account-id="' +
escapeAttr(account.id) +
'">Manage team</button>' +
'</div>' +
'</div>';
teamSection =
@@ -298,12 +335,30 @@ function renderAccountSection(account: PortalAccountSummary, accountAPIBasePath:
roleBadgeHTML(account.role) +
'</div>' +
'</div>' +
'<div class="account-status-grid">' +
'<div class="account-stat-card">' +
'<span class="account-stat-label">Active workspaces</span>' +
'<span class="account-stat-value">' + String(activeCount) + '</span>' +
'</div>' +
'<div class="account-stat-card">' +
'<span class="account-stat-label">Healthy</span>' +
'<span class="account-stat-value account-stat-healthy">' + String(healthyCount) + '</span>' +
'</div>' +
'<div class="account-stat-card">' +
'<span class="account-stat-label">Checking</span>' +
'<span class="account-stat-value account-stat-checking">' + String(checkingCount) + '</span>' +
'</div>' +
'<div class="account-stat-card">' +
'<span class="account-stat-label">Needs attention</span>' +
'<span class="account-stat-value account-stat-unhealthy">' + String(unhealthyCount) + '</span>' +
'</div>' +
'</div>' +
actions +
'<div class="account-subsection-header">' +
'<h3>Workspaces</h3>' +
'<p>Open hosted Pulse workspaces and manage lifecycle actions for this account.</p>' +
'<h3>Workspace fleet</h3>' +
'<p>Open hosted Pulse workspaces, review fleet health, and manage lifecycle actions for this account.</p>' +
'</div>' +
workspaceHTML +
actions +
teamSection +
addWorkspaceForm +
'</section>'
@@ -17,6 +17,13 @@
.account-subsection-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin: 0 0 14px; }
.account-subsection-header h3 { margin: 0; font-size: 15px; font-weight: 700; }
.account-subsection-header p { margin: 0; font-size: 13px; color: #64748b; max-width: 520px; }
.account-status-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 0 0 16px; }
.account-stat-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 14px 16px; display: flex; flex-direction: column; gap: 6px; box-shadow: 0 1px 3px rgba(15,23,42,.04); }
.account-stat-label { font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: #64748b; }
.account-stat-value { font-size: 24px; font-weight: 700; color: #0f172a; }
.account-stat-healthy { color: #166534; }
.account-stat-checking { color: #1d4ed8; }
.account-stat-unhealthy { color: #b91c1c; }
.badge { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.5px; }
.badge-msp { background: #dbeafe; color: #1e40af; }
.badge-cloud { background: #dcfce7; color: #166534; }
@@ -33,9 +40,11 @@
.workspace-list { display: flex; flex-direction: column; gap: 10px; }
.workspace-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 10px; padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; gap: 12px; box-shadow: 0 1px 3px rgba(15,23,42,.04); }
.workspace-card:hover { border-color: #cbd5e1; }
.ws-info { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.ws-info { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
.ws-title-row { display: flex; align-items: center; gap: 10px; width: 100%; }
.ws-name { font-weight: 600; font-size: 15px; }
.ws-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.ws-summary { font-size: 13px; color: #475569; line-height: 1.45; }
.ws-created { font-size: 11px; color: #94a3b8; }
.ws-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.workspace-menu-wrap { position: relative; }
@@ -53,7 +62,11 @@
.btn-danger:hover { background: #fef2f2; }
.btn-compact { padding: 8px 14px; font-size: 13px; }
.link-button { text-decoration: none; }
.account-actions { margin-top: 14px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.account-operations-panel { margin: 0 0 18px; display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 16px 18px; background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; box-shadow: 0 1px 3px rgba(15,23,42,.04); }
.account-operations-copy { display: flex; flex-direction: column; gap: 4px; max-width: 540px; }
.account-operations-copy h3 { margin: 0; font-size: 15px; font-weight: 700; }
.account-operations-copy p { margin: 0; font-size: 13px; color: #64748b; line-height: 1.5; }
.account-actions { display: flex; align-items: center; justify-content: flex-end; gap: 12px; flex-wrap: wrap; }
.add-workspace-form { margin-top: 12px; display: none; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; padding: 16px; }
.add-workspace-form.visible { display: block; }
.add-workspace-form label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
@@ -141,10 +154,14 @@
@media (max-width: 720px) {
.account-header { flex-direction: column; }
.account-badges { justify-content: flex-start; }
.account-status-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.account-operations-panel { flex-direction: column; }
.account-actions { justify-content: flex-start; }
.account-subsection-header { flex-direction: column; align-items: flex-start; }
.overview-stats { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
.account-status-grid { grid-template-columns: 1fr; }
.workspace-card { flex-direction: column; align-items: flex-start; }
.ws-actions { align-self: stretch; }
.ws-actions form, .ws-actions .btn-primary { width: 100%; text-align: center; }