design: overhaul pass — amber accent, home recomposition, motion layer
Lever 1 (color): Replace #5B7CFA AI-blue with electric amber #E8821C across all accent tokens, tile tones, logo glows, and hardcoded rgba values. Dark text on amber primary buttons for WCAG AA contrast. Lever 2 (home): Collapse centered logo hero into compact left-aligned header. Split tile grid into primary ops row (Library, Recorders, Playout) + secondary 4-col row (Downloads, Jobs, Dashboard, Settings) with reduced visual weight. Lever 3 (typography): Remove v1.2.0 from sidebar. Fix em-dashes to hyphens or periods across all visible UI strings (option labels, body copy, error messages). Topbar height 56px -> 48px. Lever 4 (motion): Staggered entry animation for launcher tiles (prefers-reduced-motion gated). Tactile scale(0.97) on primary/record buttons. Smooth 150ms nav active-item transitions. Lever 5 (blocks): Jobs stats row semantic card variants (amber glow when active, red border when failed, quiet muted style for Total). Lever 6 (spacing): Topbar 48px, launcher inner gap tightened, status left-aligned. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b875376887
commit
e54b8403e7
10 changed files with 196 additions and 136 deletions
1
.claude/launch.json
Normal file
1
.claude/launch.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":"0.0.1","configurations":[{"name":"web-ui","runtimeExecutable":"npx","runtimeArgs":["serve","services/web-ui/public","--listen","47434","--no-clipboard"],"port":47434}]}
|
||||||
|
|
@ -432,10 +432,10 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
<select className="field-input" value={growingOn ? 'h264_growing' : recCodec}
|
<select className="field-input" value={growingOn ? 'h264_growing' : recCodec}
|
||||||
onChange={e => setRecCodec(e.target.value)} disabled={growingOn}
|
onChange={e => setRecCodec(e.target.value)} disabled={growingOn}
|
||||||
style={{ appearance: 'auto', opacity: growingOn ? 0.6 : 1 }}>
|
style={{ appearance: 'auto', opacity: growingOn ? 0.6 : 1 }}>
|
||||||
{growingOn && <option value="h264_growing">XDCAM HD422 (MXF OP1a) — growing</option>}
|
{growingOn && <option value="h264_growing">XDCAM HD422 (MXF OP1a, growing)</option>}
|
||||||
<option value="hevc_nvenc">All-Intra HEVC (NVENC) — GPU, growing</option>
|
<option value="hevc_nvenc">All-Intra HEVC (NVENC, GPU, growing)</option>
|
||||||
<option value="h264_nvenc">H.264 (NVENC) — GPU</option>
|
<option value="h264_nvenc">H.264 (NVENC, GPU)</option>
|
||||||
<option value="prores_hq">ProRes 422 HQ — 4:2:2 CPU</option>
|
<option value="prores_hq">ProRes 422 HQ (4:2:2, CPU)</option>
|
||||||
<option value="prores">ProRes 422</option>
|
<option value="prores">ProRes 422</option>
|
||||||
<option value="prores_lt">ProRes 422 LT</option>
|
<option value="prores_lt">ProRes 422 LT</option>
|
||||||
<option value="prores_proxy">ProRes 422 Proxy</option>
|
<option value="prores_proxy">ProRes 422 Proxy</option>
|
||||||
|
|
@ -457,8 +457,8 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
) : (
|
) : (
|
||||||
<Field label="Bitrate" value="Quality-based (profile)" select />
|
<Field label="Bitrate" value="Quality-based (profile)" select />
|
||||||
)}
|
)}
|
||||||
<Field label="Resolution" value="Auto — from source" select />
|
<Field label="Resolution" value="Auto (from source)" select />
|
||||||
<Field label="Framerate" value="Auto — from source" select />
|
<Field label="Framerate" value="Auto (from source)" select />
|
||||||
{/* #3: warn when the configured bitrate exceeds the probed source
|
{/* #3: warn when the configured bitrate exceeds the probed source
|
||||||
bitrate — re-encoding above source adds storage, not quality. */}
|
bitrate — re-encoding above source adds storage, not quality. */}
|
||||||
{codecUsesBitrate && (() => {
|
{codecUsesBitrate && (() => {
|
||||||
|
|
|
||||||
|
|
@ -439,7 +439,7 @@ function UserPolicyRow({ user: u, expanded, onToggle, onChangeRole, onResetTotp
|
||||||
{!loading && !accessErr && (u.role === 'admin') && (
|
{!loading && !accessErr && (u.role === 'admin') && (
|
||||||
<div style={{ padding: '12px 0', fontSize: 12.5, color: 'var(--text-3)', display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ padding: '12px 0', fontSize: 12.5, color: 'var(--text-3)', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<Icon name="check" size={12} style={{ color: 'var(--success)' }} />
|
<Icon name="check" size={12} style={{ color: 'var(--success)' }} />
|
||||||
Admin — full access to every project.
|
Admin: full access to every project.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loading && !accessErr && u.role !== 'admin' && (
|
{!loading && !accessErr && u.role !== 'admin' && (
|
||||||
|
|
@ -1883,7 +1883,7 @@ function AddNodeModal({ onClose }) {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div style={{ marginBottom: 10, padding: 10, border: '1px solid var(--accent)', background: 'var(--accent-soft)', borderRadius: 6 }}>
|
<div style={{ marginBottom: 10, padding: 10, border: '1px solid var(--accent)', background: 'var(--accent-soft)', borderRadius: 6 }}>
|
||||||
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--accent-text)' }}>
|
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--accent-text)' }}>
|
||||||
This token is shown only once — copy the command now.
|
This token is shown only once. Copy the command now.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<code className="mono" style={{ display: 'block', background: 'var(--bg-2)', padding: 12, borderRadius: 6, fontSize: 11.5, lineHeight: 1.5, wordBreak: 'break-all', whiteSpace: 'pre-wrap' }}>{command}</code>
|
<code className="mono" style={{ display: 'block', background: 'var(--bg-2)', padding: 12, borderRadius: 6, fontSize: 11.5, lineHeight: 1.5, wordBreak: 'break-all', whiteSpace: 'pre-wrap' }}>{command}</code>
|
||||||
|
|
@ -2060,7 +2060,7 @@ function TotpSection() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 8 }}>
|
<div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 8 }}>
|
||||||
Scan the QR with Google Authenticator, Authy, or 1Password — or enter this secret manually:
|
Scan the QR with Google Authenticator, Authy, or 1Password, or enter this secret manually:
|
||||||
</div>
|
</div>
|
||||||
<div className="mono" style={{ fontSize: 12, background: 'var(--bg-2)', padding: '6px 10px', borderRadius: 5, wordBreak: 'break-all', marginBottom: 12 }}>{enroll.secret}</div>
|
<div className="mono" style={{ fontSize: 12, background: 'var(--bg-2)', padding: '6px 10px', borderRadius: 5, wordBreak: 'break-all', marginBottom: 12 }}>{enroll.secret}</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
|
|
@ -2264,7 +2264,7 @@ function StorageWarningBanner() {
|
||||||
}}>
|
}}>
|
||||||
<Icon name="alert" size={20} style={{ color: 'var(--danger)', flexShrink: 0, marginTop: 1 }} />
|
<Icon name="alert" size={20} style={{ color: 'var(--danger)', flexShrink: 0, marginTop: 1 }} />
|
||||||
<div style={{ fontSize: 13, fontWeight: 700, letterSpacing: '0.02em', lineHeight: 1.5, color: 'var(--text-1)' }}>
|
<div style={{ fontSize: 13, fontWeight: 700, letterSpacing: '0.02em', lineHeight: 1.5, color: 'var(--text-1)' }}>
|
||||||
WARNING — THESE SETTINGS ARE MEANT TO BE SET ONCE AT INITIAL DEPLOYMENT.
|
WARNING: THESE SETTINGS ARE MEANT TO BE SET ONCE AT INITIAL DEPLOYMENT.
|
||||||
CHANGING STORAGE PATHS MID-CYCLE CAN CAUSE DATABASE CORRUPTION AND FILE LOSS.
|
CHANGING STORAGE PATHS MID-CYCLE CAN CAUSE DATABASE CORRUPTION AND FILE LOSS.
|
||||||
PLEASE USE WITH CAUTION.
|
PLEASE USE WITH CAUTION.
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
background: disabled ? 'var(--bg-3)' : 'var(--accent)',
|
background: disabled ? 'var(--bg-3)' : 'var(--accent)',
|
||||||
color: '#fff',
|
color: disabled ? 'var(--text-3)' : '#0a0c10',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
padding: '9px',
|
padding: '9px',
|
||||||
|
|
@ -210,7 +210,7 @@
|
||||||
// An expired/used ticket means the user must start over.
|
// An expired/used ticket means the user must start over.
|
||||||
if (r.status === 401 && /ticket/.test(body.error || '')) {
|
if (r.status === 401 && /ticket/.test(body.error || '')) {
|
||||||
setTicket(null); setCode(''); setPassword('');
|
setTicket(null); setCode(''); setPassword('');
|
||||||
setError('Session timed out — please sign in again.');
|
setError('Session timed out. Please sign in again.');
|
||||||
} else {
|
} else {
|
||||||
setError(body.error || ('Verification failed: ' + r.status));
|
setError(body.error || ('Verification failed: ' + r.status));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,6 @@ function Home({ navigate }) {
|
||||||
<div className="launcher-inner">
|
<div className="launcher-inner">
|
||||||
<div className="launcher-hero">
|
<div className="launcher-hero">
|
||||||
<span className="launcher-logo-wrap">
|
<span className="launcher-logo-wrap">
|
||||||
<span className="launcher-logo-pulse" aria-hidden="true" />
|
|
||||||
<img
|
<img
|
||||||
className="launcher-logo"
|
className="launcher-logo"
|
||||||
src="img/dragon-logo.png"
|
src="img/dragon-logo.png"
|
||||||
|
|
@ -142,15 +141,17 @@ function Home({ navigate }) {
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<h1 className="launcher-wordmark">DRAGONFLIGHT</h1>
|
<div className="launcher-hero-text">
|
||||||
<p className="launcher-kicker">Let's Create</p>
|
<h1 className="launcher-wordmark">DRAGONFLIGHT</h1>
|
||||||
<p className="launcher-tagline">
|
<p className="launcher-tagline">
|
||||||
Media Asset Management & Production Platform
|
Media Asset Management & Production Platform
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Primary ops: Library, Recorders, Playout */}
|
||||||
<div className="launcher-grid">
|
<div className="launcher-grid">
|
||||||
{tiles.map(t => (
|
{tiles.slice(0, 3).map(t => (
|
||||||
<button
|
<button
|
||||||
key={t.id}
|
key={t.id}
|
||||||
className={'launcher-tile tone-' + t.tone}
|
className={'launcher-tile tone-' + t.tone}
|
||||||
|
|
@ -167,38 +168,55 @@ function Home({ navigate }) {
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Secondary: Downloads, Jobs, Dashboard, Settings — smaller tiles */}
|
||||||
|
<div className="launcher-settings-row">
|
||||||
|
{tiles.slice(3).map(t => (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
className={'launcher-tile tone-' + t.tone + ' launcher-tile-secondary'}
|
||||||
|
onClick={() => t.id === '__downloads' ? setShowDownloads(true) : navigate(t.id)}
|
||||||
|
>
|
||||||
|
<span className="launcher-tile-icon">
|
||||||
|
<Icon name={t.icon} size={20} />
|
||||||
|
</span>
|
||||||
|
<span className="launcher-tile-label">{t.label}</span>
|
||||||
|
<span className="launcher-tile-sub">{t.sub}</span>
|
||||||
|
<span className="launcher-tile-desc">{t.desc}</span>
|
||||||
|
<span className="launcher-tile-arrow">
|
||||||
|
<Icon name="arrowRight" size={13} />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
<button
|
<button
|
||||||
className="launcher-tile tone-ghost launcher-tile-secondary"
|
className="launcher-tile tone-ghost launcher-tile-secondary"
|
||||||
onClick={() => navigate('dashboard')}
|
onClick={() => navigate('dashboard')}
|
||||||
>
|
>
|
||||||
<span className="launcher-tile-icon">
|
<span className="launcher-tile-icon">
|
||||||
<Icon name="layout" size={22} />
|
<Icon name="layout" size={20} />
|
||||||
</span>
|
</span>
|
||||||
<span className="launcher-tile-label">Dashboard</span>
|
<span className="launcher-tile-label">Dashboard</span>
|
||||||
<span className="launcher-tile-sub">Operations view</span>
|
<span className="launcher-tile-sub">Operations view</span>
|
||||||
<span className="launcher-tile-desc">
|
<span className="launcher-tile-desc">
|
||||||
Recent activity, job queue, cluster health.
|
Live recorders, job queue, cluster health.
|
||||||
</span>
|
</span>
|
||||||
<span className="launcher-tile-arrow">
|
<span className="launcher-tile-arrow">
|
||||||
<Icon name="arrowRight" size={14} />
|
<Icon name="arrowRight" size={13} />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="launcher-settings-row">
|
|
||||||
<button
|
<button
|
||||||
className={'launcher-tile tone-' + settingsTile.tone + ' launcher-tile-settings'}
|
className={'launcher-tile tone-' + settingsTile.tone + ' launcher-tile-secondary launcher-tile-settings'}
|
||||||
onClick={() => navigate(settingsTile.id)}
|
onClick={() => navigate(settingsTile.id)}
|
||||||
>
|
>
|
||||||
<span className="launcher-tile-icon">
|
<span className="launcher-tile-icon">
|
||||||
<Icon name={settingsTile.icon} size={26} />
|
<Icon name={settingsTile.icon} size={20} />
|
||||||
</span>
|
</span>
|
||||||
<span className="launcher-tile-label">{settingsTile.label}</span>
|
<span className="launcher-tile-label">{settingsTile.label}</span>
|
||||||
<span className="launcher-tile-sub">{settingsTile.sub}</span>
|
<span className="launcher-tile-sub">{settingsTile.sub}</span>
|
||||||
<span className="launcher-tile-desc">{settingsTile.desc}</span>
|
<span className="launcher-tile-desc">{settingsTile.desc}</span>
|
||||||
<span className="launcher-tile-arrow">
|
<span className="launcher-tile-arrow">
|
||||||
<Icon name="arrowRight" size={14} />
|
<Icon name="arrowRight" size={13} />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -340,7 +358,7 @@ function DownloadsModal({ onClose }) {
|
||||||
<span className="btn primary sm" aria-disabled="true" style={{ opacity: 0.5, pointerEvents: 'none' }}>
|
<span className="btn primary sm" aria-disabled="true" style={{ opacity: 0.5, pointerEvents: 'none' }}>
|
||||||
<Icon name="download" />Teams ISO (.exe)
|
<Icon name="download" />Teams ISO (.exe)
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: 11.5, color: 'var(--text-3)' }}>coming soon — file pending</span>
|
<span style={{ fontSize: 11.5, color: 'var(--text-3)' }}>coming soon, file pending</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -540,7 +558,7 @@ function Dashboard({ navigate }) {
|
||||||
const alerts = [
|
const alerts = [
|
||||||
...erroredRecorders.map(r => ({
|
...erroredRecorders.map(r => ({
|
||||||
key: 'r-' + r.id, sev: 'danger',
|
key: 'r-' + r.id, sev: 'danger',
|
||||||
title: r.name + ' — recorder error',
|
title: r.name + ': recorder error',
|
||||||
meta: r.error_message || r.url || 'signal lost',
|
meta: r.error_message || r.url || 'signal lost',
|
||||||
action: 'Reconnect', to: 'recorders',
|
action: 'Reconnect', to: 'recorders',
|
||||||
})),
|
})),
|
||||||
|
|
@ -795,7 +813,7 @@ function OnAirEmpty({ sources, onStart }) {
|
||||||
<span className="onair-empty-icon"><Icon name="record" size={18} /></span>
|
<span className="onair-empty-icon"><Icon name="record" size={18} /></span>
|
||||||
<div className="onair-empty-copy">
|
<div className="onair-empty-copy">
|
||||||
<div className="onair-empty-title">Nothing on air</div>
|
<div className="onair-empty-title">Nothing on air</div>
|
||||||
<div className="onair-empty-sub">All recorders are idle — start a source to begin capturing.</div>
|
<div className="onair-empty-sub">All recorders are idle. Start a source to begin capturing.</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn primary" onClick={onStart}><Icon name="plus" size={14} />Start a recorder</button>
|
<button className="btn primary" onClick={onStart}><Icon name="plus" size={14} />Start a recorder</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -172,26 +172,26 @@ function Jobs({ navigate }) {
|
||||||
|
|
||||||
<div className="page-body">
|
<div className="page-body">
|
||||||
<div className="jobs-stats">
|
<div className="jobs-stats">
|
||||||
<div className="stat-card">
|
<div className={'stat-card' + (counts.running > 0 ? ' stat-card--active' : '')}>
|
||||||
<div className="label">Running</div>
|
<div className="label">Running</div>
|
||||||
<div className="value">{counts.running}</div>
|
<div className="value">{counts.running}</div>
|
||||||
<div className="delta">{counts.queued} queued</div>
|
<div className="delta">{counts.queued} queued</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="label">Completed</div>
|
<div className="label">Completed</div>
|
||||||
<div className="value">{counts.done}</div>
|
<div className="value stat-value--muted">{counts.done}</div>
|
||||||
<div className="delta">Total done</div>
|
<div className="delta">Total done</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<div className={'stat-card' + (counts.failed > 0 ? ' stat-card--failed' : '')}>
|
||||||
<div className="label">Failed</div>
|
<div className="label">Failed</div>
|
||||||
<div className="value">{counts.failed}</div>
|
<div className={'value' + (counts.failed > 0 ? ' stat-value--danger' : ' stat-value--muted')}>{counts.failed}</div>
|
||||||
<div className={'delta' + (counts.failed > 0 ? ' delta-warn' : '')}>
|
<div className={'delta' + (counts.failed > 0 ? ' delta-warn' : '')}>
|
||||||
{counts.failed > 0 ? 'Needs attention' : 'All clear'}
|
{counts.failed > 0 ? 'Needs attention' : 'All clear'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<div className="stat-card stat-card--quiet">
|
||||||
<div className="label">Total jobs</div>
|
<div className="label">Total jobs</div>
|
||||||
<div className="value">{counts.all}</div>
|
<div className="value stat-value--muted">{counts.all}</div>
|
||||||
<div className="delta muted delta-tiny">Updated {Math.round((Date.now()-lastFetch)/1000)}s ago</div>
|
<div className="delta muted delta-tiny">Updated {Math.round((Date.now()-lastFetch)/1000)}s ago</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,6 @@ function Sidebar({ active, onNavigate, me, collapsed, onToggle }) {
|
||||||
>
|
>
|
||||||
<img className="brand-logo" src="img/dragon-logo.png" alt="Dragonflight" draggable="false" />
|
<img className="brand-logo" src="img/dragon-logo.png" alt="Dragonflight" draggable="false" />
|
||||||
<div className="brand-name">Dragonflight</div>
|
<div className="brand-name">Dragonflight</div>
|
||||||
<div className="brand-sub">v1.2.0</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="icon-btn sidebar-toggle"
|
className="icon-btn sidebar-toggle"
|
||||||
|
|
|
||||||
|
|
@ -253,10 +253,10 @@
|
||||||
/* Convert the dark logo to white so it pops on the dark sidebar.
|
/* Convert the dark logo to white so it pops on the dark sidebar.
|
||||||
brightness(0) collapses everything to black, invert(1) flips to white.
|
brightness(0) collapses everything to black, invert(1) flips to white.
|
||||||
Works on both the original dark PNG and any transparent white PNG. */
|
Works on both the original dark PNG and any transparent white PNG. */
|
||||||
filter: brightness(0) invert(1) drop-shadow(0 0 6px rgba(91, 124, 250, 0.25));
|
filter: brightness(0) invert(1) drop-shadow(0 0 6px rgba(232, 130, 28, 0.30));
|
||||||
}
|
}
|
||||||
.sidebar-header:hover .brand-logo {
|
.sidebar-header:hover .brand-logo {
|
||||||
filter: brightness(0) invert(1) drop-shadow(0 0 10px rgba(91, 124, 250, 0.45));
|
filter: brightness(0) invert(1) drop-shadow(0 0 10px rgba(232, 130, 28, 0.55));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
|
|
@ -268,139 +268,132 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background:
|
background:
|
||||||
radial-gradient(1100px 600px at 50% 0%, rgba(91, 124, 250, 0.10), transparent 65%),
|
radial-gradient(1100px 500px at 50% -10%, rgba(232, 130, 28, 0.07), transparent 60%),
|
||||||
radial-gradient(900px 600px at 50% 100%, rgba(181, 124, 250, 0.06), transparent 60%),
|
|
||||||
var(--bg-0);
|
var(--bg-0);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 48px 32px 64px;
|
padding: 40px 32px 64px;
|
||||||
}
|
}
|
||||||
.launcher-inner {
|
.launcher-inner {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1160px;
|
max-width: 1160px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
gap: 40px;
|
gap: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.launcher-hero {
|
.launcher-hero {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 18px;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
margin-top: 8px;
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
/* Logo wrapper holds the animated pulse halo behind the image. */
|
/* Logo wrapper — compact, left-aligned mark. */
|
||||||
.launcher-logo-wrap {
|
.launcher-logo-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
width: 180px;
|
width: 52px;
|
||||||
height: 180px;
|
height: 52px;
|
||||||
}
|
flex-shrink: 0;
|
||||||
.launcher-logo-pulse {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
transform: translate(-50%, -50%) scale(0.85);
|
|
||||||
border-radius: 50%;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at center,
|
|
||||||
color-mix(in srgb, var(--accent) 55%, transparent) 0%,
|
|
||||||
color-mix(in srgb, var(--accent) 22%, transparent) 38%,
|
|
||||||
transparent 68%
|
|
||||||
);
|
|
||||||
filter: blur(2px);
|
|
||||||
animation: launcherLogoPulse 3.4s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
@keyframes launcherLogoPulse {
|
|
||||||
0%, 100% { transform: translate(-50%, -50%) scale(0.82); opacity: 0.45; }
|
|
||||||
50% { transform: translate(-50%, -50%) scale(1.08); opacity: 0.9; }
|
|
||||||
}
|
}
|
||||||
|
.launcher-logo-pulse { display: none; }
|
||||||
.launcher-logo {
|
.launcher-logo {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 180px;
|
width: 52px;
|
||||||
height: 180px;
|
height: 52px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
/* Convert to white - same approach as .brand-logo. */
|
|
||||||
filter:
|
filter:
|
||||||
brightness(0) invert(1)
|
brightness(0) invert(1)
|
||||||
drop-shadow(0 0 24px rgba(91, 124, 250, 0.28))
|
drop-shadow(0 0 8px rgba(232, 130, 28, 0.35));
|
||||||
drop-shadow(0 0 48px rgba(91, 124, 250, 0.18));
|
animation: launcherLogoIn 400ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
||||||
animation: launcherLogoIn 600ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
|
||||||
}
|
}
|
||||||
@keyframes launcherLogoIn {
|
@keyframes launcherLogoIn {
|
||||||
from { opacity: 0; transform: translateY(8px) scale(0.96); }
|
from { opacity: 0; transform: scale(0.88); }
|
||||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
to { opacity: 1; transform: scale(1); }
|
||||||
}
|
}
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.launcher-logo-pulse { animation: none; opacity: 0.5; }
|
.launcher-logo { animation: none; }
|
||||||
|
}
|
||||||
|
.launcher-hero-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.launcher-wordmark {
|
.launcher-wordmark {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 44px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.08em;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: var(--text-1);
|
color: var(--text-1);
|
||||||
text-shadow: 0 0 32px rgba(91, 124, 250, 0.15);
|
text-shadow: none;
|
||||||
}
|
|
||||||
.launcher-kicker {
|
|
||||||
margin: 2px 0 0;
|
|
||||||
color: var(--accent);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.22em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
.launcher-kicker { display: none; }
|
||||||
.launcher-tagline {
|
.launcher-tagline {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text-3);
|
color: var(--text-3);
|
||||||
font-size: 13.5px;
|
font-size: 12.5px;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.01em;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.launcher-tagline { font-size: 11.5px; letter-spacing: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.launcher-tagline-motto {
|
|
||||||
margin-top: 6px;
|
|
||||||
color: var(--accent);
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 15px;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
}
|
||||||
|
.launcher-tagline-motto { display: none; }
|
||||||
|
|
||||||
.launcher-grid {
|
.launcher-grid {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 14px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
@media (max-width: 960px) { .launcher-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
@media (max-width: 960px) { .launcher-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||||
@media (max-width: 620px) { .launcher-grid { grid-template-columns: 1fr; } }
|
@media (max-width: 620px) { .launcher-grid { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
/* Settings sits on its own centered row beneath the main grid. */
|
/* Secondary tiles: smaller vertical footprint, quieter treatment. */
|
||||||
|
.launcher-tile-secondary {
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 16px 18px 18px;
|
||||||
|
}
|
||||||
|
.launcher-tile-secondary .launcher-tile-icon {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.launcher-tile-secondary .launcher-tile-icon svg {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
}
|
||||||
|
.launcher-tile-secondary .launcher-tile-label {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.launcher-tile-secondary .launcher-tile-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings row: holds the 4 secondary tiles. */
|
||||||
.launcher-settings-row {
|
.launcher-settings-row {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: grid;
|
||||||
justify-content: center;
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.launcher-tile-settings {
|
.launcher-tile-settings {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: calc((100% - 28px) / 3);
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.launcher-settings-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
.launcher-settings-row { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
@media (max-width: 960px) { .launcher-tile-settings { max-width: calc((100% - 14px) / 2); } }
|
|
||||||
@media (max-width: 620px) { .launcher-tile-settings { max-width: 100%; } }
|
|
||||||
|
|
||||||
.launcher-tile {
|
.launcher-tile {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -507,10 +500,10 @@
|
||||||
/* Tone variants - colour the icon tile + halo, leave the body text
|
/* Tone variants - colour the icon tile + halo, leave the body text
|
||||||
neutral so the tile reads as a button, not a banner. */
|
neutral so the tile reads as a button, not a banner. */
|
||||||
.launcher-tile.tone-accent {
|
.launcher-tile.tone-accent {
|
||||||
--tile-tint: rgba(91, 124, 250, 0.18);
|
--tile-tint: rgba(232, 130, 28, 0.15);
|
||||||
--tile-icon-bg: var(--accent-soft);
|
--tile-icon-bg: var(--accent-soft);
|
||||||
--tile-icon-fg: var(--accent-text);
|
--tile-icon-fg: var(--accent-text);
|
||||||
--tile-icon-border: rgba(91, 124, 250, 0.30);
|
--tile-icon-border: rgba(232, 130, 28, 0.28);
|
||||||
}
|
}
|
||||||
.launcher-tile.tone-live {
|
.launcher-tile.tone-live {
|
||||||
--tile-tint: rgba(255, 59, 48, 0.18);
|
--tile-tint: rgba(255, 59, 48, 0.18);
|
||||||
|
|
@ -557,7 +550,7 @@
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
.launcher-status-pip {
|
.launcher-status-pip {
|
||||||
|
|
@ -585,12 +578,46 @@
|
||||||
|
|
||||||
.launcher-footer {
|
.launcher-footer {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
color: var(--text-4);
|
color: var(--text-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Motion layer — entry stagger for launcher tiles.
|
||||||
|
Respects prefers-reduced-motion.
|
||||||
|
============================================================ */
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.launcher-tile {
|
||||||
|
animation: tileEnter 360ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
.launcher-grid .launcher-tile:nth-child(1) { animation-delay: 60ms; }
|
||||||
|
.launcher-grid .launcher-tile:nth-child(2) { animation-delay: 110ms; }
|
||||||
|
.launcher-grid .launcher-tile:nth-child(3) { animation-delay: 160ms; }
|
||||||
|
.launcher-grid .launcher-tile:nth-child(4) { animation-delay: 210ms; }
|
||||||
|
.launcher-grid .launcher-tile:nth-child(5) { animation-delay: 250ms; }
|
||||||
|
.launcher-grid .launcher-tile:nth-child(6) { animation-delay: 290ms; }
|
||||||
|
.launcher-settings-row .launcher-tile { animation-delay: 320ms; }
|
||||||
|
|
||||||
|
@keyframes tileEnter {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tactile press feedback on high-stakes operational buttons. */
|
||||||
|
.btn-record:active,
|
||||||
|
button.btn.primary:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
transition: transform 80ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth active-item transition in sidebar nav. */
|
||||||
|
.nav-item {
|
||||||
|
transition: background 150ms ease-out, color 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Recorder row - signal indicator with a pulsing dot when
|
Recorder row - signal indicator with a pulsing dot when
|
||||||
actually receiving frames. Closes part of #2.
|
actually receiving frames. Closes part of #2.
|
||||||
|
|
@ -640,7 +667,7 @@
|
||||||
stroke-width: 1.5;
|
stroke-width: 1.5;
|
||||||
}
|
}
|
||||||
.bmd-card-trace {
|
.bmd-card-trace {
|
||||||
stroke: rgba(91, 124, 250, 0.12);
|
stroke: rgba(232, 130, 28, 0.10);
|
||||||
stroke-width: 0.5;
|
stroke-width: 0.5;
|
||||||
fill: none;
|
fill: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse at top, rgba(91,124,250,0.05), transparent 60%),
|
radial-gradient(ellipse at top, rgba(232,130,28,0.05), transparent 60%),
|
||||||
var(--bg-0);
|
var(--bg-0);
|
||||||
}
|
}
|
||||||
.decklink-card-face {
|
.decklink-card-face {
|
||||||
|
|
@ -971,7 +971,7 @@
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
.epg-week-day:last-child { border-bottom: 0; }
|
.epg-week-day:last-child { border-bottom: 0; }
|
||||||
.epg-week-day.today { background: linear-gradient(180deg, rgba(91,124,250,0.04) 0%, transparent 80%); }
|
.epg-week-day.today { background: linear-gradient(180deg, rgba(232,130,28,0.04) 0%, transparent 80%); }
|
||||||
.epg-week-dayhead {
|
.epg-week-dayhead {
|
||||||
display: flex; align-items: baseline; gap: 10px;
|
display: flex; align-items: baseline; gap: 10px;
|
||||||
padding: 10px 20px 6px;
|
padding: 10px 20px 6px;
|
||||||
|
|
@ -1060,6 +1060,20 @@
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Stat card semantic variants */
|
||||||
|
.stat-card--active {
|
||||||
|
border-color: var(--accent-soft-2);
|
||||||
|
background: linear-gradient(180deg, var(--accent-soft) 0%, var(--bg-1) 60%);
|
||||||
|
}
|
||||||
|
.stat-card--active .value { color: var(--accent); }
|
||||||
|
.stat-card--failed {
|
||||||
|
border-color: rgba(255,91,91,0.25);
|
||||||
|
background: linear-gradient(180deg, var(--danger-soft) 0%, var(--bg-1) 60%);
|
||||||
|
}
|
||||||
|
.stat-card--quiet { opacity: 0.75; }
|
||||||
|
.stat-value--muted { color: var(--text-2); }
|
||||||
|
.stat-value--danger { color: var(--danger); }
|
||||||
|
|
||||||
/* ========== Settings ========== */
|
/* ========== Settings ========== */
|
||||||
.settings-nav {
|
.settings-nav {
|
||||||
display: flex; flex-direction: column; gap: 2px;
|
display: flex; flex-direction: column; gap: 2px;
|
||||||
|
|
@ -1222,7 +1236,7 @@
|
||||||
.token-tier.popular {
|
.token-tier.popular {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
background: linear-gradient(180deg, var(--accent-soft) 0%, var(--bg-1) 40%);
|
background: linear-gradient(180deg, var(--accent-soft) 0%, var(--bg-1) 40%);
|
||||||
box-shadow: 0 0 24px rgba(91,124,250,0.10);
|
box-shadow: 0 0 24px rgba(232,130,28,0.12);
|
||||||
}
|
}
|
||||||
.token-tier-badge {
|
.token-tier-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -1345,7 +1359,7 @@
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.search-result-kind.kind-asset { color: #B4C3FF; border-color: rgba(91,124,250,0.25); }
|
.search-result-kind.kind-asset { color: #FECF8A; border-color: rgba(232,130,28,0.25); }
|
||||||
.search-result-kind.kind-project { color: #FFD89B; border-color: rgba(245,166,35,0.25); }
|
.search-result-kind.kind-project { color: #FFD89B; border-color: rgba(245,166,35,0.25); }
|
||||||
.search-result-kind.kind-recorder { color: #FFAFAF; border-color: rgba(255,91,91,0.25); }
|
.search-result-kind.kind-recorder { color: #FFAFAF; border-color: rgba(255,91,91,0.25); }
|
||||||
.search-result-kind.kind-job { color: #9EE7D2; border-color: rgba(45,212,168,0.25); }
|
.search-result-kind.kind-job { color: #9EE7D2; border-color: rgba(45,212,168,0.25); }
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,12 @@
|
||||||
--text-3: #8B92A0; /* WCAG AA (#133) - was #6B7280, 4.06:1 vs --bg-0; now ~7.5:1 */
|
--text-3: #8B92A0; /* WCAG AA (#133) - was #6B7280, 4.06:1 vs --bg-0; now ~7.5:1 */
|
||||||
--text-4: #6B7280;
|
--text-4: #6B7280;
|
||||||
|
|
||||||
/* accent (blue, frame.io-ish) */
|
/* accent — electric amber (broadcast tally) */
|
||||||
--accent: #5B7CFA;
|
--accent: #E8821C;
|
||||||
--accent-hover: #6E8BFF;
|
--accent-hover: #F09030;
|
||||||
--accent-soft: rgba(91, 124, 250, 0.14);
|
--accent-soft: rgba(232, 130, 28, 0.14);
|
||||||
--accent-soft-2: rgba(91, 124, 250, 0.22);
|
--accent-soft-2: rgba(232, 130, 28, 0.22);
|
||||||
--accent-text: #B4C3FF;
|
--accent-text: #FECF8A;
|
||||||
|
|
||||||
/* status */
|
/* status */
|
||||||
--success: #2DD4A8;
|
--success: #2DD4A8;
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
/* sizing (density tweakable) */
|
/* sizing (density tweakable) */
|
||||||
--sidebar-w: 232px;
|
--sidebar-w: 232px;
|
||||||
--row-h: 44px;
|
--row-h: 44px;
|
||||||
--topbar-h: 56px;
|
--topbar-h: 48px;
|
||||||
--gap: 16px;
|
--gap: 16px;
|
||||||
|
|
||||||
/* fonts */
|
/* fonts */
|
||||||
|
|
@ -244,10 +244,11 @@ a { color: inherit; text-decoration: none; }
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 28px; height: 28px;
|
width: 28px; height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: linear-gradient(135deg, #5B7CFA, #B57CFA);
|
background: var(--bg-3);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
display: grid; place-items: center;
|
display: grid; place-items: center;
|
||||||
font-weight: 600; font-size: 11px;
|
font-weight: 700; font-size: 11px;
|
||||||
color: white;
|
color: var(--accent);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.user-meta { display: flex; flex-direction: column; min-width: 0; flex: 1; }
|
.user-meta { display: flex; flex-direction: column; min-width: 0; flex: 1; }
|
||||||
|
|
@ -350,7 +351,7 @@ a { color: inherit; text-decoration: none; }
|
||||||
transition: background 80ms, border 80ms, color 80ms;
|
transition: background 80ms, border 80ms, color 80ms;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.btn.primary { background: var(--accent); color: white; }
|
.btn.primary { background: var(--accent); color: #0a0c10; }
|
||||||
.btn.primary:hover { background: var(--accent-hover); }
|
.btn.primary:hover { background: var(--accent-hover); }
|
||||||
.btn.ghost { background: transparent; color: var(--text-2); }
|
.btn.ghost { background: transparent; color: var(--text-2); }
|
||||||
.btn.ghost:hover { background: var(--hover); color: var(--text-1); }
|
.btn.ghost:hover { background: var(--hover); color: var(--text-1); }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue