Merge design/overhaul-tasteskill: Flame Blue accent, home recomposition, motion layer
UI overhaul:
- Flame Blue (#1025A1) accent — official Wild Dragon brand standard
- Home: compact left-aligned header, primary ops + secondary 4-col rows
- Typography: hyphen fixes, sidebar version removed, topbar 48px
- Motion: staggered tile entry, button scale press, nav transitions
- Jobs stats: semantic card variants (active/failed/total)
🤖 Generated with Claude Code
This commit is contained in:
commit
f8eda7fc37
11 changed files with 197 additions and 138 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}]}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// app.jsx - main shell
|
||||
|
||||
const ACCENT = '#5B7CFA';
|
||||
const ACCENT = '#1025A1';
|
||||
|
||||
function App() {
|
||||
const [route, setRoute] = React.useState('home');
|
||||
|
|
@ -41,8 +41,8 @@ function App() {
|
|||
document.documentElement.style.setProperty('--accent', ACCENT);
|
||||
document.documentElement.style.setProperty('--accent-soft', hexToRgba(ACCENT, 0.14));
|
||||
document.documentElement.style.setProperty('--accent-soft-2', hexToRgba(ACCENT, 0.22));
|
||||
document.documentElement.style.setProperty('--accent-text', lighten(ACCENT, 0.25));
|
||||
document.documentElement.style.setProperty('--accent-hover', lighten(ACCENT, 0.08));
|
||||
document.documentElement.style.setProperty('--accent-text', '#C7CFEA'); // Halo — readable on dark
|
||||
document.documentElement.style.setProperty('--accent-hover', '#1830B8');
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -436,10 +436,10 @@ function NewRecorderModal({ open, onClose }) {
|
|||
<select className="field-input" value={growingOn ? 'h264_growing' : recCodec}
|
||||
onChange={e => setRecCodec(e.target.value)} disabled={growingOn}
|
||||
style={{ appearance: 'auto', opacity: growingOn ? 0.6 : 1 }}>
|
||||
{growingOn && <option value="h264_growing">XDCAM HD422 (MXF OP1a) — growing</option>}
|
||||
<option value="hevc_nvenc">All-Intra HEVC (NVENC) — GPU, growing</option>
|
||||
<option value="h264_nvenc">H.264 (NVENC) — GPU</option>
|
||||
<option value="prores_hq">ProRes 422 HQ — 4:2:2 CPU</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="h264_nvenc">H.264 (NVENC, GPU)</option>
|
||||
<option value="prores_hq">ProRes 422 HQ (4:2:2, CPU)</option>
|
||||
<option value="prores">ProRes 422</option>
|
||||
<option value="prores_lt">ProRes 422 LT</option>
|
||||
<option value="prores_proxy">ProRes 422 Proxy</option>
|
||||
|
|
@ -461,8 +461,8 @@ function NewRecorderModal({ open, onClose }) {
|
|||
) : (
|
||||
<Field label="Bitrate" value="Quality-based (profile)" select />
|
||||
)}
|
||||
<Field label="Resolution" value="Auto — from source" select />
|
||||
<Field label="Framerate" value="Auto — from source" select />
|
||||
<Field label="Resolution" value="Auto (from source)" select />
|
||||
<Field label="Framerate" value="Auto (from source)" select />
|
||||
{/* #3: warn when the configured bitrate exceeds the probed source
|
||||
bitrate — re-encoding above source adds storage, not quality. */}
|
||||
{codecUsesBitrate && (() => {
|
||||
|
|
|
|||
|
|
@ -439,7 +439,7 @@ function UserPolicyRow({ user: u, expanded, onToggle, onChangeRole, onResetTotp
|
|||
{!loading && !accessErr && (u.role === 'admin') && (
|
||||
<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)' }} />
|
||||
Admin — full access to every project.
|
||||
Admin: full access to every project.
|
||||
</div>
|
||||
)}
|
||||
{!loading && !accessErr && u.role !== 'admin' && (
|
||||
|
|
@ -1883,7 +1883,7 @@ function AddNodeModal({ onClose }) {
|
|||
<React.Fragment>
|
||||
<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)' }}>
|
||||
This token is shown only once — copy the command now.
|
||||
This token is shown only once. Copy the command now.
|
||||
</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>
|
||||
|
|
@ -2060,7 +2060,7 @@ function TotpSection() {
|
|||
</div>
|
||||
<div>
|
||||
<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 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">
|
||||
|
|
@ -2264,7 +2264,7 @@ function StorageWarningBanner() {
|
|||
}}>
|
||||
<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)' }}>
|
||||
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.
|
||||
PLEASE USE WITH CAUTION.
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@
|
|||
// An expired/used ticket means the user must start over.
|
||||
if (r.status === 401 && /ticket/.test(body.error || '')) {
|
||||
setTicket(null); setCode(''); setPassword('');
|
||||
setError('Session timed out — please sign in again.');
|
||||
setError('Session timed out. Please sign in again.');
|
||||
} else {
|
||||
setError(body.error || ('Verification failed: ' + r.status));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,6 @@ function Home({ navigate }) {
|
|||
<div className="launcher-inner">
|
||||
<div className="launcher-hero">
|
||||
<span className="launcher-logo-wrap">
|
||||
<span className="launcher-logo-pulse" aria-hidden="true" />
|
||||
<img
|
||||
className="launcher-logo"
|
||||
src="img/dragon-logo.png"
|
||||
|
|
@ -142,15 +141,17 @@ function Home({ navigate }) {
|
|||
draggable="false"
|
||||
/>
|
||||
</span>
|
||||
<h1 className="launcher-wordmark">DRAGONFLIGHT</h1>
|
||||
<p className="launcher-kicker">Let's Create</p>
|
||||
<p className="launcher-tagline">
|
||||
Media Asset Management & Production Platform
|
||||
</p>
|
||||
<div className="launcher-hero-text">
|
||||
<h1 className="launcher-wordmark">DRAGONFLIGHT</h1>
|
||||
<p className="launcher-tagline">
|
||||
Media Asset Management & Production Platform
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Primary ops: Library, Recorders, Playout */}
|
||||
<div className="launcher-grid">
|
||||
{tiles.map(t => (
|
||||
{tiles.slice(0, 3).map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
className={'launcher-tile tone-' + t.tone}
|
||||
|
|
@ -167,38 +168,55 @@ function Home({ navigate }) {
|
|||
</span>
|
||||
</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
|
||||
className="launcher-tile tone-ghost launcher-tile-secondary"
|
||||
onClick={() => navigate('dashboard')}
|
||||
>
|
||||
<span className="launcher-tile-icon">
|
||||
<Icon name="layout" size={22} />
|
||||
<Icon name="layout" size={20} />
|
||||
</span>
|
||||
<span className="launcher-tile-label">Dashboard</span>
|
||||
<span className="launcher-tile-sub">Operations view</span>
|
||||
<span className="launcher-tile-desc">
|
||||
Recent activity, job queue, cluster health.
|
||||
Live recorders, job queue, cluster health.
|
||||
</span>
|
||||
<span className="launcher-tile-arrow">
|
||||
<Icon name="arrowRight" size={14} />
|
||||
<Icon name="arrowRight" size={13} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="launcher-settings-row">
|
||||
<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)}
|
||||
>
|
||||
<span className="launcher-tile-icon">
|
||||
<Icon name={settingsTile.icon} size={26} />
|
||||
<Icon name={settingsTile.icon} size={20} />
|
||||
</span>
|
||||
<span className="launcher-tile-label">{settingsTile.label}</span>
|
||||
<span className="launcher-tile-sub">{settingsTile.sub}</span>
|
||||
<span className="launcher-tile-desc">{settingsTile.desc}</span>
|
||||
<span className="launcher-tile-arrow">
|
||||
<Icon name="arrowRight" size={14} />
|
||||
<Icon name="arrowRight" size={13} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -340,7 +358,7 @@ function DownloadsModal({ onClose }) {
|
|||
<span className="btn primary sm" aria-disabled="true" style={{ opacity: 0.5, pointerEvents: 'none' }}>
|
||||
<Icon name="download" />Teams ISO (.exe)
|
||||
</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>
|
||||
|
|
@ -540,7 +558,7 @@ function Dashboard({ navigate }) {
|
|||
const alerts = [
|
||||
...erroredRecorders.map(r => ({
|
||||
key: 'r-' + r.id, sev: 'danger',
|
||||
title: r.name + ' — recorder error',
|
||||
title: r.name + ': recorder error',
|
||||
meta: r.error_message || r.url || 'signal lost',
|
||||
action: 'Reconnect', to: 'recorders',
|
||||
})),
|
||||
|
|
@ -795,7 +813,7 @@ function OnAirEmpty({ sources, onStart }) {
|
|||
<span className="onair-empty-icon"><Icon name="record" size={18} /></span>
|
||||
<div className="onair-empty-copy">
|
||||
<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>
|
||||
<button className="btn primary" onClick={onStart}><Icon name="plus" size={14} />Start a recorder</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -172,26 +172,26 @@ function Jobs({ navigate }) {
|
|||
|
||||
<div className="page-body">
|
||||
<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="value">{counts.running}</div>
|
||||
<div className="delta">{counts.queued} queued</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<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>
|
||||
<div className="stat-card">
|
||||
<div className={'stat-card' + (counts.failed > 0 ? ' stat-card--failed' : '')}>
|
||||
<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' : '')}>
|
||||
{counts.failed > 0 ? 'Needs attention' : 'All clear'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-card stat-card--quiet">
|
||||
<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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -166,7 +166,6 @@ function Sidebar({ active, onNavigate, me, collapsed, onToggle }) {
|
|||
>
|
||||
<img className="brand-logo" src="img/dragon-logo.png" alt="Dragonflight" draggable="false" />
|
||||
<div className="brand-name">Dragonflight</div>
|
||||
<div className="brand-sub">v1.2.0</div>
|
||||
</div>
|
||||
<button
|
||||
className="icon-btn sidebar-toggle"
|
||||
|
|
|
|||
|
|
@ -253,10 +253,10 @@
|
|||
/* Convert the dark logo to white so it pops on the dark sidebar.
|
||||
brightness(0) collapses everything to black, invert(1) flips to white.
|
||||
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(16, 37, 161, 0.35));
|
||||
}
|
||||
.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(16, 37, 161, 0.55));
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
|
@ -268,139 +268,132 @@
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background:
|
||||
radial-gradient(1100px 600px at 50% 0%, rgba(91, 124, 250, 0.10), transparent 65%),
|
||||
radial-gradient(900px 600px at 50% 100%, rgba(181, 124, 250, 0.06), transparent 60%),
|
||||
radial-gradient(1100px 500px at 50% -10%, rgba(16, 37, 161, 0.08), transparent 60%),
|
||||
var(--bg-0);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 48px 32px 64px;
|
||||
padding: 40px 32px 64px;
|
||||
}
|
||||
.launcher-inner {
|
||||
width: 100%;
|
||||
max-width: 1160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
align-items: stretch;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.launcher-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
gap: 18px;
|
||||
text-align: left;
|
||||
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 {
|
||||
position: relative;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
.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; }
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.launcher-logo-pulse { display: none; }
|
||||
.launcher-logo {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
object-fit: contain;
|
||||
/* Convert to white - same approach as .brand-logo. */
|
||||
filter:
|
||||
brightness(0) invert(1)
|
||||
drop-shadow(0 0 24px rgba(91, 124, 250, 0.28))
|
||||
drop-shadow(0 0 48px rgba(91, 124, 250, 0.18));
|
||||
animation: launcherLogoIn 600ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
||||
drop-shadow(0 0 8px rgba(16, 37, 161, 0.35));
|
||||
animation: launcherLogoIn 400ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
||||
}
|
||||
@keyframes launcherLogoIn {
|
||||
from { opacity: 0; transform: translateY(8px) scale(0.96); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
from { opacity: 0; transform: scale(0.88); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
@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 {
|
||||
margin: 0;
|
||||
font-size: 44px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
letter-spacing: 0.08em;
|
||||
line-height: 1;
|
||||
color: var(--text-1);
|
||||
text-shadow: 0 0 32px rgba(91, 124, 250, 0.15);
|
||||
}
|
||||
.launcher-kicker {
|
||||
margin: 2px 0 0;
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
text-shadow: none;
|
||||
}
|
||||
.launcher-kicker { display: none; }
|
||||
.launcher-tagline {
|
||||
margin: 0;
|
||||
color: var(--text-3);
|
||||
font-size: 13.5px;
|
||||
letter-spacing: 0.02em;
|
||||
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;
|
||||
font-size: 12.5px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.launcher-tagline-motto { display: none; }
|
||||
|
||||
.launcher-grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
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: 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 {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.launcher-tile-settings {
|
||||
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 {
|
||||
position: relative;
|
||||
|
|
@ -507,10 +500,10 @@
|
|||
/* Tone variants - colour the icon tile + halo, leave the body text
|
||||
neutral so the tile reads as a button, not a banner. */
|
||||
.launcher-tile.tone-accent {
|
||||
--tile-tint: rgba(91, 124, 250, 0.18);
|
||||
--tile-tint: rgba(16, 37, 161, 0.18);
|
||||
--tile-icon-bg: var(--accent-soft);
|
||||
--tile-icon-fg: var(--accent-text);
|
||||
--tile-icon-border: rgba(91, 124, 250, 0.30);
|
||||
--tile-icon-border: rgba(16, 37, 161, 0.30);
|
||||
}
|
||||
.launcher-tile.tone-live {
|
||||
--tile-tint: rgba(255, 59, 48, 0.18);
|
||||
|
|
@ -557,7 +550,7 @@
|
|||
gap: 16px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.launcher-status-pip {
|
||||
|
|
@ -585,12 +578,46 @@
|
|||
|
||||
.launcher-footer {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
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
|
||||
actually receiving frames. Closes part of #2.
|
||||
|
|
@ -640,7 +667,7 @@
|
|||
stroke-width: 1.5;
|
||||
}
|
||||
.bmd-card-trace {
|
||||
stroke: rgba(91, 124, 250, 0.12);
|
||||
stroke: rgba(16, 37, 161, 0.12);
|
||||
stroke-width: 0.5;
|
||||
fill: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@
|
|||
gap: 20px;
|
||||
align-items: stretch;
|
||||
background:
|
||||
radial-gradient(ellipse at top, rgba(91,124,250,0.05), transparent 60%),
|
||||
radial-gradient(ellipse at top, rgba(16,37,161,0.05), transparent 60%),
|
||||
var(--bg-0);
|
||||
}
|
||||
.decklink-card-face {
|
||||
|
|
@ -971,7 +971,7 @@
|
|||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.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(16,37,161,0.04) 0%, transparent 80%); }
|
||||
.epg-week-dayhead {
|
||||
display: flex; align-items: baseline; gap: 10px;
|
||||
padding: 10px 20px 6px;
|
||||
|
|
@ -1060,6 +1060,20 @@
|
|||
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-text); }
|
||||
.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-nav {
|
||||
display: flex; flex-direction: column; gap: 2px;
|
||||
|
|
@ -1222,7 +1236,7 @@
|
|||
.token-tier.popular {
|
||||
border-color: var(--accent);
|
||||
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(16,37,161,0.12);
|
||||
}
|
||||
.token-tier-badge {
|
||||
position: absolute;
|
||||
|
|
@ -1345,7 +1359,7 @@
|
|||
border-radius: 99px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.search-result-kind.kind-asset { color: #B4C3FF; border-color: rgba(91,124,250,0.25); }
|
||||
.search-result-kind.kind-asset { color: #C7CFEA; border-color: rgba(16,37,161,0.30); }
|
||||
.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-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-4: #6B7280;
|
||||
|
||||
/* accent (blue, frame.io-ish) */
|
||||
--accent: #5B7CFA;
|
||||
--accent-hover: #6E8BFF;
|
||||
--accent-soft: rgba(91, 124, 250, 0.14);
|
||||
--accent-soft-2: rgba(91, 124, 250, 0.22);
|
||||
--accent-text: #B4C3FF;
|
||||
/* accent — Flame Blue (Wild Dragon brand, Pantone 286 C) */
|
||||
--accent: #1025A1;
|
||||
--accent-hover: #1830B8;
|
||||
--accent-soft: rgba(16, 37, 161, 0.14);
|
||||
--accent-soft-2: rgba(16, 37, 161, 0.22);
|
||||
--accent-text: #C7CFEA;
|
||||
|
||||
/* status */
|
||||
--success: #2DD4A8;
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
/* sizing (density tweakable) */
|
||||
--sidebar-w: 232px;
|
||||
--row-h: 44px;
|
||||
--topbar-h: 56px;
|
||||
--topbar-h: 48px;
|
||||
--gap: 16px;
|
||||
|
||||
/* fonts */
|
||||
|
|
@ -244,10 +244,10 @@ a { color: inherit; text-decoration: none; }
|
|||
.avatar {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #5B7CFA, #B57CFA);
|
||||
background: var(--accent);
|
||||
display: grid; place-items: center;
|
||||
font-weight: 600; font-size: 11px;
|
||||
color: white;
|
||||
font-weight: 700; font-size: 11px;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.user-meta { display: flex; flex-direction: column; min-width: 0; flex: 1; }
|
||||
|
|
@ -350,7 +350,7 @@ a { color: inherit; text-decoration: none; }
|
|||
transition: background 80ms, border 80ms, color 80ms;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn.primary { background: var(--accent); color: white; }
|
||||
.btn.primary { background: var(--accent); color: #fff; }
|
||||
.btn.primary:hover { background: var(--accent-hover); }
|
||||
.btn.ghost { background: transparent; color: var(--text-2); }
|
||||
.btn.ghost:hover { background: var(--hover); color: var(--text-1); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue