Merge pull request #233 from cita-777/codex/login-surface-pr

feat(web): redesign login surface
This commit is contained in:
Cita
2026-03-21 17:17:51 +08:00
committed by GitHub
5 changed files with 680 additions and 43 deletions
+78
View File
@@ -0,0 +1,78 @@
import { describe, expect, it, vi } from 'vitest';
import { create, type ReactTestInstance } from 'react-test-renderer';
import { Login } from './App.js';
import { SITE_DOCS_URL, SITE_GITHUB_URL } from './docsLink.js';
function collectText(node: ReactTestInstance): string {
return (node.children || []).map((child) => {
if (typeof child === 'string') return child;
return collectText(child);
}).join('');
}
describe('Login surface', () => {
it('uses the site root as the documentation URL', () => {
expect(SITE_DOCS_URL).toBe('https://metapi.cita777.me');
});
it('uses the author github profile for the login github shortcut', () => {
expect(SITE_GITHUB_URL).toBe('https://github.com/cita-777');
});
it('renders a poster-style hero with a floating admin login panel', () => {
const root = create(
<Login onLogin={vi.fn()} t={(text) => text} />,
);
try {
const pageText = collectText(root.root);
const lightBrandPanel = root.root.find((node) => (
node.type === 'section'
&& typeof node.props.className === 'string'
&& node.props.className.includes('login-brand-panel-light')
));
const authStage = root.root.find((node) => (
node.type === 'section'
&& typeof node.props.className === 'string'
&& node.props.className.includes('login-auth-stage')
));
const brandMarkCanvas = root.root.find((node) => (
node.type === 'div'
&& typeof node.props.className === 'string'
&& node.props.className.includes('brand-mark-canvas')
));
expect(pageText).toContain('Metapi');
expect(pageText).toContain('中转站的中转站');
expect(pageText).not.toContain('一个 API Key,一个入口');
expect(pageText).toContain('兼容 New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API');
expect(pageText).toContain('统一代理网关');
expect(pageText).toContain('智能路由引擎');
expect(pageText).toContain('自动模型发现');
expect(pageText).toContain('部署文档');
expect(lightBrandPanel).toBeTruthy();
expect(authStage).toBeTruthy();
expect(brandMarkCanvas).toBeTruthy();
const docsLink = root.root.find((node) => (
node.type === 'a'
&& node.props.href === SITE_DOCS_URL
));
const tokenInput = root.root.find((node) => (
node.type === 'input'
&& node.props.placeholder === '管理员令牌'
));
const githubLink = root.root.find((node) => (
node.type === 'a'
&& node.props.href === SITE_GITHUB_URL
));
expect(docsLink.props.target).toBe('_blank');
expect(githubLink.props['aria-label']).toBe('GitHub');
expect(githubLink.props.target).toBe('_blank');
expect(tokenInput.props.type).toBe('password');
} finally {
root.unmount();
}
});
});
+109 -32
View File
@@ -14,7 +14,7 @@ import {
} from './appLocalState.js';
import { I18nProvider, useI18n } from './i18n.js';
import { resolveLoginErrorMessage } from './loginError.js';
import { SITE_DOCS_URL } from './docsLink.js';
import { SITE_DOCS_URL, SITE_GITHUB_URL } from './docsLink.js';
import { useAnimatedVisibility } from './components/useAnimatedVisibility.js';
import { useIsMobile } from './components/useIsMobile.js';
import { MobileDrawer } from './components/MobileDrawer.js';
@@ -127,10 +127,24 @@ function resolveStoredProfile(): UserProfile {
}
}
function Login({ onLogin, t }: { onLogin: (token: string) => void; t: (text: string) => string }) {
export function Login({ onLogin, t }: { onLogin: (token: string) => void; t: (text: string) => string }) {
const [token, setToken] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const capabilityRows = [
{
title: t('统一代理网关'),
description: t('一个 Key、一个入口,兼容 OpenAI / Claude 下游格式'),
},
{
title: t('自动模型发现'),
description: t('上游新增模型自动出现在模型列表,零配置路由生成'),
},
{
title: t('智能路由引擎'),
description: t('按成本、延迟、成功率自动选择最优通道,故障自动转移'),
},
];
const handleLogin = async () => {
if (!token) return;
@@ -167,37 +181,100 @@ function Login({ onLogin, t }: { onLogin: (token: string) => void; t: (text: str
};
return (
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--color-bg)' }}>
<div className="animate-scale-in" style={{ background: 'var(--color-bg-card)', padding: 40, borderRadius: 'var(--radius-xl)', boxShadow: 'var(--shadow-lg)', width: 400, border: '1px solid var(--color-border-light)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
<img src="/logo.png" alt="Metapi" style={{ width: 40, height: 40, borderRadius: 12 }} />
<h1 style={{ fontSize: 24, fontWeight: 700 }}>Metapi</h1>
</div>
<p style={{ fontSize: 14, color: 'var(--color-text-muted)', marginBottom: 24 }}>{t('请输入管理员令牌后继续。')}</p>
<input
type="password"
placeholder={t('管理员令牌')}
value={token}
onChange={(e) => {
setToken(e.target.value);
setError('');
}}
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
style={{ width: '100%', padding: '10px 14px', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', fontSize: 14, outline: 'none', background: 'var(--color-bg)', color: 'var(--color-text-primary)', marginBottom: 12, transition: 'border-color 0.2s' }}
/>
{error && (
<div className="alert alert-error animate-shake" style={{ marginBottom: 12 }}>
{error}
<div className="login-shell">
<div className="login-surface animate-scale-in">
<section className="login-brand-panel login-brand-panel-light">
<div className="login-brand-header">
<div className="brand-mark-frame brand-mark-frame-hero">
<div className="brand-mark-canvas">
<img src="/logo.png" alt="Metapi" className="login-brand-logo" />
</div>
</div>
<div className="login-brand-summary">
<div className="login-brand-name">Metapi</div>
<div className="login-brand-kicker">{t('中转站的中转站')}</div>
</div>
</div>
)}
<button
onClick={handleLogin}
disabled={loading || !token}
className="btn btn-primary"
style={{ width: '100%', padding: '10px 0', fontSize: 14, fontWeight: 600 }}
>
{loading ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} />{t('验证中...')}</> : t('登录')}
</button>
<div className="login-brand-copy-block">
<p className="login-brand-copy">
{t('把分散的 New API / One API / OneHub 等站点聚合成统一网关,自动发现模型、智能路由、成本更优。')}
</p>
</div>
<div className="login-compat-line">{t('兼容 New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API')}</div>
<div className="login-capability-list">
{capabilityRows.map((feature, index) => (
<div key={feature.title} className="login-capability-row">
<div className="login-capability-index">{String(index + 1).padStart(2, '0')}</div>
<div className="login-capability-content">
<div className="login-capability-title">{feature.title}</div>
<p className="login-capability-desc">{feature.description}</p>
</div>
</div>
))}
</div>
<div className="login-brand-footer">
<a
href={SITE_GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
className="login-icon-link"
aria-label="GitHub"
title="GitHub"
>
<svg viewBox="0 0 24 24" aria-hidden="true" className="login-icon-link-svg">
<path
fill="currentColor"
d="M12 2C6.48 2 2 6.59 2 12.25c0 4.53 2.87 8.38 6.84 9.73.5.1.68-.22.68-.49 0-.24-.01-1.04-.01-1.88-2.78.62-3.37-1.22-3.37-1.22-.45-1.2-1.11-1.52-1.11-1.52-.91-.64.07-.63.07-.63 1 .07 1.53 1.06 1.53 1.06.9 1.57 2.35 1.12 2.92.85.09-.67.35-1.12.64-1.38-2.22-.26-4.55-1.15-4.55-5.13 0-1.13.39-2.05 1.03-2.77-.1-.26-.45-1.31.1-2.73 0 0 .84-.28 2.75 1.06A9.3 9.3 0 0 1 12 6.91c.85 0 1.71.12 2.51.35 1.91-1.34 2.75-1.06 2.75-1.06.55 1.42.2 2.47.1 2.73.64.72 1.03 1.64 1.03 2.77 0 3.99-2.34 4.86-4.57 5.12.36.33.68.97.68 1.96 0 1.42-.01 2.56-.01 2.91 0 .27.18.59.69.49A10.27 10.27 0 0 0 22 12.25C22 6.59 17.52 2 12 2Z"
/>
</svg>
</a>
<a
href={SITE_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
className="login-doc-link"
>
{t('部署文档')}
</a>
</div>
</section>
<section className="login-auth-stage">
<div className="login-auth-panel">
<div className="login-auth-eyebrow">{t('管理员入口')}</div>
<h2 className="login-auth-title">{t('登录')}</h2>
<p className="login-auth-copy">{t('请输入管理员令牌后继续。')}</p>
<label className="login-auth-label" htmlFor="admin-token-input">{t('管理员令牌')}</label>
<input
id="admin-token-input"
type="password"
placeholder={t('管理员令牌')}
value={token}
onChange={(e) => {
setToken(e.target.value);
setError('');
}}
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
className="login-auth-input"
/>
{error && (
<div className="alert alert-error animate-shake" style={{ marginBottom: 12 }}>
{error}
</div>
)}
<button
onClick={handleLogin}
disabled={loading || !token}
className="btn btn-primary login-auth-submit"
>
{loading ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} />{t('验证中...')}</> : t('登录')}
</button>
<div className="login-auth-note">{t('仅校验本地服务访问权限,不会把令牌发送到第三方。')}</div>
<div className="login-auth-footer">
<span>{t('管理员登录后继续。')}</span>
</div>
</div>
</section>
</div>
</div>
);
+2 -1
View File
@@ -1 +1,2 @@
export const SITE_DOCS_URL = 'https://metapi.cita777.me/faq';
export const SITE_DOCS_URL = 'https://metapi.cita777.me';
export const SITE_GITHUB_URL = 'https://github.com/cita-777';
+7
View File
@@ -13,8 +13,15 @@ const zhToEn: Record<string, string> = {
'无法连接到服务器': 'Unable to connect to server',
'请输入管理员令牌后继续。': 'Enter admin token to continue.',
'管理员令牌': 'Admin Token',
'管理员入口': 'Admin Access',
'部署文档': 'Deployment Docs',
'管理员登录后继续。': 'Continue with admin sign-in.',
'仅校验本地服务访问权限,不会把令牌发送到第三方。': 'Only checks local service access and never sends the token to a third party.',
'验证中...': 'Verifying...',
'登录': 'Sign In',
'中转站的中转站': 'The Hub of Hubs',
'把分散的 New API / One API / OneHub 等站点聚合成统一网关,自动发现模型、智能路由、成本更优。': 'Turn fragmented New API / One API / OneHub sites into one unified gateway with auto model discovery, smart routing, and better cost control.',
'兼容 New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API': 'Compatible with New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API',
'用户名不能为空': 'Username cannot be empty',
'用户名最多 24 个字符': 'Username can be at most 24 characters',
'个人信息': 'Profile',
+484 -10
View File
@@ -2016,11 +2016,6 @@ body {
color: var(--color-text-muted);
}
.badge-purple {
background: #ede9fe;
color: #6d28d9;
}
.badge-link {
display: inline-flex;
text-decoration: none;
@@ -2382,11 +2377,6 @@ body[data-tooltip-portal="true"] [data-tooltip]::before {
color: var(--color-text-muted);
}
[data-theme="dark"] .badge-purple {
background: #2e1065;
color: #c4b5fd;
}
[data-theme="dark"] .badge-link:hover .badge-muted,
[data-theme="dark"] .badge-link:focus-visible .badge-muted {
background: rgba(96, 165, 250, 0.2);
@@ -2692,6 +2682,490 @@ textarea {
padding: 20px;
}
/* ===== Frontend Skill Surfaces ===== */
.login-shell {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
background:
radial-gradient(circle at top left, rgba(59, 130, 246, 0.18), transparent 34%),
radial-gradient(circle at right 18%, rgba(34, 211, 238, 0.16), transparent 30%),
linear-gradient(180deg, #eef4fb 0%, #e9eef7 100%);
}
[data-theme="dark"] .login-shell {
background:
radial-gradient(circle at top left, rgba(59, 130, 246, 0.18), transparent 34%),
radial-gradient(circle at right 18%, rgba(34, 211, 238, 0.12), transparent 30%),
linear-gradient(180deg, #0d1320 0%, #0a1018 100%);
}
.login-surface {
width: min(1240px, 100%);
display: grid;
grid-template-columns: minmax(0, 1.16fr) minmax(360px, 440px);
gap: 24px;
padding: 20px;
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.78), rgba(244, 248, 253, 0.88));
border: 1px solid rgba(255, 255, 255, 0.58);
border-radius: 36px;
box-shadow:
0 24px 60px rgba(148, 163, 184, 0.24),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
[data-theme="dark"] .login-surface {
background:
linear-gradient(145deg, rgba(15, 23, 42, 0.88), rgba(10, 18, 30, 0.94));
border-color: rgba(148, 163, 184, 0.12);
box-shadow:
0 24px 80px rgba(2, 6, 23, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.03);
}
.login-brand-panel {
position: relative;
min-height: 760px;
padding: 56px 56px 44px;
display: flex;
flex-direction: column;
gap: 28px;
color: var(--color-text-primary);
border-radius: 30px;
overflow: hidden;
}
.login-brand-panel-light {
background:
radial-gradient(circle at top left, rgba(96, 165, 250, 0.22), transparent 26%),
radial-gradient(circle at right 20%, rgba(34, 211, 238, 0.16), transparent 22%),
linear-gradient(160deg, #f8fbff 0%, #edf4ff 52%, #e9f0fa 100%);
}
.login-brand-panel::before {
content: '';
position: absolute;
inset: auto auto -12% -8%;
width: 280px;
height: 280px;
border-radius: 50%;
background: radial-gradient(circle, rgba(125, 211, 252, 0.2), transparent 68%);
pointer-events: none;
}
.login-brand-panel::after {
content: '';
position: absolute;
inset: 28px 26px auto auto;
width: 220px;
height: 220px;
border-radius: 50%;
background: radial-gradient(circle, rgba(59, 130, 246, 0.12), transparent 66%);
pointer-events: none;
}
.login-brand-header {
display: flex;
align-items: center;
gap: 20px;
}
.login-brand-summary {
display: flex;
flex-direction: column;
gap: 6px;
}
.brand-mark-frame {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 10px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(227, 238, 252, 0.84));
border: 1px solid rgba(191, 219, 254, 0.6);
box-shadow:
0 24px 40px rgba(148, 163, 184, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.94);
}
.brand-mark-frame::after {
content: '';
position: absolute;
inset: 6px;
border-radius: inherit;
border: 1px solid rgba(255, 255, 255, 0.62);
pointer-events: none;
}
.brand-mark-frame-hero {
width: 118px;
height: 118px;
border-radius: 36px;
}
.brand-mark-canvas {
width: 100%;
height: 100%;
padding: 10px;
border-radius: 28px;
overflow: hidden;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(247, 250, 255, 0.92));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.96),
0 12px 28px rgba(59, 130, 246, 0.1);
}
.login-brand-logo {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 20px;
filter: drop-shadow(0 8px 18px rgba(37, 99, 235, 0.16));
}
.login-brand-name {
font-family: 'Avenir Next', 'Segoe UI Variable Display', 'PingFang SC', 'Noto Sans SC', sans-serif;
font-size: clamp(34px, 3.2vw, 46px);
font-weight: 800;
letter-spacing: -0.05em;
}
.login-brand-kicker {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #64748b;
}
.login-brand-copy-block {
display: grid;
max-width: 580px;
}
.login-brand-copy {
max-width: 42ch;
margin: 0;
font-size: 18px;
line-height: 1.92;
color: var(--color-text-secondary);
}
.login-compat-line {
max-width: 42ch;
font-size: 13px;
line-height: 1.8;
color: #475569;
}
.login-capability-list {
display: grid;
gap: 0;
max-width: 44rem;
margin-top: auto;
border-top: 1px solid rgba(148, 163, 184, 0.18);
}
.login-capability-row {
display: grid;
grid-template-columns: 36px minmax(0, 1fr);
gap: 16px;
align-items: start;
padding: 18px 0;
border-bottom: 1px solid rgba(148, 163, 184, 0.16);
}
.login-capability-index {
padding-top: 3px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.18em;
color: #64748b;
}
.login-capability-content {
display: grid;
gap: 6px;
}
.login-capability-title {
font-family: 'Avenir Next', 'Segoe UI Variable Display', 'PingFang SC', 'Noto Sans SC', sans-serif;
font-size: 18px;
font-weight: 700;
letter-spacing: -0.03em;
color: #0f172a;
}
.login-capability-desc {
margin: 0;
max-width: 34ch;
font-size: 14px;
line-height: 1.75;
color: var(--color-text-secondary);
}
.login-brand-footer {
display: flex;
align-items: center;
gap: 12px;
}
.login-icon-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 999px;
border: 1px solid rgba(99, 102, 241, 0.12);
background: rgba(255, 255, 255, 0.76);
color: #0f172a;
text-decoration: none;
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.login-icon-link:hover {
transform: translateY(-1px);
border-color: rgba(99, 102, 241, 0.2);
box-shadow: 0 10px 18px rgba(37, 99, 235, 0.12);
}
.login-icon-link-svg {
width: 18px;
height: 18px;
}
.login-doc-link {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 15px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.76);
color: var(--color-primary);
border: 1px solid rgba(99, 102, 241, 0.16);
font-size: 12px;
font-weight: 700;
text-decoration: none;
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.login-doc-link:hover {
transform: translateY(-1px);
box-shadow: 0 10px 18px rgba(37, 99, 235, 0.12);
}
.login-auth-stage {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
border-radius: 30px;
background:
radial-gradient(circle at top, rgba(125, 211, 252, 0.16), transparent 34%),
linear-gradient(180deg, rgba(255, 255, 255, 0.44), rgba(241, 245, 249, 0.58));
}
.login-auth-panel {
width: min(100%, 392px);
padding: 40px 34px 28px;
border-radius: 28px;
border: 1px solid rgba(226, 232, 240, 0.92);
background: rgba(255, 255, 255, 0.92);
box-shadow:
0 28px 50px rgba(148, 163, 184, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.94);
backdrop-filter: blur(10px);
}
.login-auth-eyebrow {
margin-bottom: 12px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-primary);
}
.login-auth-title {
margin: 0 0 10px;
font-family: 'Avenir Next', 'Segoe UI Variable Display', 'PingFang SC', 'Noto Sans SC', sans-serif;
font-size: 42px;
font-weight: 800;
line-height: 0.96;
letter-spacing: -0.05em;
}
.login-auth-copy {
margin: 0 0 24px;
font-size: 14px;
line-height: 1.7;
color: var(--color-text-secondary);
}
.login-auth-label {
margin-bottom: 8px;
font-size: 12px;
font-weight: 700;
color: var(--color-text-secondary);
}
.login-auth-input {
width: 100%;
padding: 14px 16px;
border: 1px solid color-mix(in srgb, var(--color-primary) 18%, var(--color-border));
border-radius: 16px;
font-size: 14px;
outline: none;
background: rgba(248, 250, 252, 0.92);
color: var(--color-text-primary);
margin-bottom: 14px;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
}
.login-auth-input:focus {
border-color: color-mix(in srgb, var(--color-primary) 56%, transparent);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-primary) 12%, transparent);
background: var(--color-bg-card);
}
.login-auth-submit {
width: 100%;
padding: 13px 0;
font-size: 14px;
font-weight: 700;
border-radius: 16px;
box-shadow: 0 16px 24px rgba(99, 102, 241, 0.2);
}
.login-auth-note {
margin-top: 16px;
font-size: 12px;
line-height: 1.7;
color: var(--color-text-muted);
}
.login-auth-footer {
margin-top: 18px;
padding-top: 14px;
border-top: 1px solid rgba(226, 232, 240, 0.86);
font-size: 12px;
color: var(--color-text-muted);
}
[data-theme="dark"] .login-surface,
[data-theme="dark"] .brand-mark-frame,
[data-theme="dark"] .brand-mark-canvas,
[data-theme="dark"] .login-auth-stage,
[data-theme="dark"] .login-auth-panel {
border-color: color-mix(in srgb, var(--color-border) 86%, transparent);
}
[data-theme="dark"] .login-shell,
[data-theme="dark"] .login-surface,
[data-theme="dark"] .brand-mark-frame,
[data-theme="dark"] .brand-mark-canvas,
[data-theme="dark"] .login-auth-stage,
[data-theme="dark"] .login-auth-panel {
background: color-mix(in srgb, var(--color-bg-card) 92%, #0f172a);
}
[data-theme="dark"] .login-brand-kicker,
[data-theme="dark"] .login-compat-line,
[data-theme="dark"] .login-capability-index,
[data-theme="dark"] .login-icon-link,
[data-theme="dark"] .login-auth-note,
[data-theme="dark"] .login-auth-footer {
color: #cbd5e1;
}
[data-theme="dark"] .login-capability-title {
color: #f8fafc;
}
[data-theme="dark"] .login-auth-footer {
border-top-color: rgba(51, 65, 85, 0.9);
}
@media (max-width: 1080px) {
.login-surface {
grid-template-columns: 1fr;
}
.login-brand-panel {
min-height: auto;
}
}
@media (max-width: 900px) {
.login-shell {
padding: 18px;
}
.login-brand-panel {
min-height: auto;
padding: 36px 32px;
}
.login-auth-stage {
padding: 20px;
}
.login-auth-panel {
padding: 34px 28px 26px;
}
.login-capability-row {
grid-template-columns: 32px minmax(0, 1fr);
gap: 12px;
}
}
@media (max-width: 640px) {
.login-shell {
padding: 12px;
}
.login-surface {
padding: 12px;
border-radius: 24px;
}
.login-brand-panel {
padding: 28px 20px;
border-radius: 24px;
}
.login-auth-stage {
padding: 14px;
border-radius: 24px;
}
.login-auth-panel {
padding: 28px 22px 22px;
border-radius: 22px;
}
.brand-mark-frame-hero {
width: 96px;
height: 96px;
border-radius: 28px;
}
.login-brand-copy {
font-size: 15px;
}
.login-brand-footer {
margin-top: 0;
}
}
/* ===== Empty State ===== */
.empty-state {
text-align: center;