From 9530be2c1b970acaef79e2793d18c7217b38846a Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 27 Mar 2026 22:36:19 +0000 Subject: [PATCH] style(portal): simplify shell and team states --- .../cloudcp/portal/dist/build_manifest.json | 2 +- internal/cloudcp/portal/dist/portal_app.css | 86 ++++++++++++------ internal/cloudcp/portal/dist/portal_app.js | 38 ++++---- .../portal/frontend/src/account_view.test.ts | 5 +- .../portal/frontend/src/account_view.ts | 25 +++-- .../portal/frontend/src/shell_view.test.ts | 8 +- .../cloudcp/portal/frontend/src/shell_view.ts | 84 +++++++---------- .../cloudcp/portal/frontend/src/styles.css | 91 +++++++++++++------ 8 files changed, 193 insertions(+), 146 deletions(-) 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 = '
RosterLoading\u2026
'; + stats.innerHTML = '
RosterLoading\u2026
InvitesReady
'; return; } if (entry.teamQuery.status === "error") { - stats.innerHTML = '
RosterNeeds attention
'; + stats.innerHTML = '
RosterNeeds attention
FallbackInvite 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 '

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 += ''; } - return '
"; + return '
"; } function shellSectionButton(section, activeSection, title, copy) { return '"; @@ -1878,7 +1872,7 @@ var hostedContent = accounts.map(function(account) { return '
"; }).join(""); - return '
' + renderShellNavigation(accounts, context.bootstrap.support_email || "", activeSection) + '
' + renderOverviewBand(accounts) + (accounts.length === 1 ? renderPrimaryAccountBar(accounts[0]) : "") + '
' + hostedContent + '

' + serviceHeading + '

' + serviceNote + '

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
BillingOpen Stripe customer portal access after verification.
LicensesRecover the latest active self-hosted license and invoice link.
RefundsConfirm eligibility before revoking active commercial access.
PrivacyRequest export or deletion without leaving Pulse Account.
What to expect
Verification firstEach flow confirms the commercial email before opening sensitive account actions.
One task at a timeThe active commercial request stays in this panel until you finish or switch tools.
Support stays closeIf billing, licenses, refunds, or privacy behave unexpectedly, escalate from this surface.
Need help with billing, refund, privacy, or license actions? ' + escapeHTML(context.bootstrap.support_email || "") + '

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 || "") + '.

Use support if a billing, refund, privacy, or license flow does not behave as expected for this account.

' + escapeHTML(context.bootstrap.support_email || "") + '
' + renderSupportSection(context) + "
"; + return '
' + renderShellNavigation(accounts, context.bootstrap.support_email || "", activeSection) + '
' + (accounts.length === 1 ? renderPrimaryAccountBar(accounts[0]) : "") + '
' + hostedContent + '

' + serviceHeading + '

' + serviceNote + '

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
BillingOpen Stripe customer portal access after verification.
LicensesRecover the latest active self-hosted license and invoice link.
RefundsConfirm eligibility before revoking active commercial access.
PrivacyRequest export or deletion without leaving Pulse Account.
What to expect
Verification firstEach flow confirms the commercial email before opening sensitive account actions.
One task at a timeThe active commercial request stays in this panel until you finish or switch tools.
Support stays closeIf billing, licenses, refunds, or privacy behave unexpectedly, escalate from this surface.
Need help with billing, refund, privacy, or license actions? ' + escapeHTML(context.bootstrap.support_email || "") + '

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 || "") + '.

Use support if a billing, refund, privacy, or license flow does not behave as expected for this account.

' + 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 = '
RosterLoading…
'; + stats.innerHTML = + '
RosterLoading…
' + + '
InvitesReady
'; return; } if (entry.teamQuery.status === 'error') { - stats.innerHTML = '
RosterNeeds attention
'; + stats.innerHTML = + '
RosterNeeds attention
' + + '
FallbackInvite 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 ( '
' + '
' + @@ -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); }