- screens-admin.jsx S3SettingsCard: when /settings/s3 fails, log to console and surface the message in the existing SettingsMsg banner instead of silently returning empty fields. Also logs the response payload on success so the next "endpoint blank" report is easier to diagnose. (closes part of #15) - screens-ingest.jsx recorder row: wrap the signal value in a dot+text pair; add CSS so the dot pulses green when status=receiving and matches the value color otherwise. The pulse is the kind of cue the Live signal column was missing per #2.
543 lines
16 KiB
CSS
543 lines
16 KiB
CSS
/* responsive + polish fixes */
|
|
.page-header h1 { white-space: nowrap; }
|
|
.page-header .subtitle {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
min-width: 0;
|
|
flex-shrink: 1;
|
|
}
|
|
.page-header { gap: 12px; flex-wrap: wrap; }
|
|
.status-pip span { white-space: nowrap; }
|
|
.status-pip { white-space: nowrap; }
|
|
.rail-item { min-width: 0; }
|
|
.rail-item > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.rail-item > span.rail-count, .rail-item > .rail-color-dot { overflow: visible; flex-shrink: 0; }
|
|
|
|
@media (max-width: 1100px) {
|
|
.stat-row { grid-template-columns: repeat(2, 1fr); }
|
|
.jobs-stats { grid-template-columns: repeat(2, 1fr); }
|
|
}
|
|
|
|
@media (max-width: 1280px) {
|
|
.recorder-row {
|
|
grid-template-columns: 180px 1fr;
|
|
grid-template-rows: auto auto;
|
|
}
|
|
.recorder-stats { grid-column: 2 / 3; grid-row: 2; }
|
|
.recorder-actions { grid-column: 1 / 3; grid-row: 3; justify-content: flex-end; padding-top: 4px; border-top: 1px solid var(--border); }
|
|
}
|
|
|
|
.capture-stat-label, .recorder-stat .stat-label, .stat-card .label, .stat-card .delta {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.topbar > * { flex-shrink: 0; }
|
|
.topbar .crumb { min-width: 0; overflow: hidden; }
|
|
.topbar .crumb > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.topbar .spacer { flex: 1; min-width: 8px; }
|
|
|
|
@media (max-width: 1100px) {
|
|
.topbar .search-wrap { display: none; }
|
|
}
|
|
@media (max-width: 900px) {
|
|
.topbar .status-pip span:not(.dot) { display: none; }
|
|
}
|
|
|
|
.library-toolbar { flex-wrap: wrap; }
|
|
.library-toolbar .search { width: 200px; }
|
|
|
|
@media (max-width: 1280px) {
|
|
.page-body > div[style*="grid-template-columns: 440px"] {
|
|
grid-template-columns: 1fr !important;
|
|
}
|
|
}
|
|
|
|
.audio-meter.v {
|
|
border-radius: 99px;
|
|
background: rgba(255,255,255,0.04);
|
|
padding: 3px;
|
|
}
|
|
|
|
.recorder-preview { min-height: 56px; }
|
|
|
|
.activity-text .target { word-break: break-word; }
|
|
|
|
.asset-card .meta .sub { overflow: hidden; }
|
|
|
|
.stat-card .label, .stat-card .value, .stat-card .delta { position: relative; z-index: 1; }
|
|
.stat-card .spark { z-index: 0; }
|
|
|
|
/* ============================================================
|
|
Search bar polish — give it a real container so it doesn't
|
|
read as floating text on the topbar background.
|
|
============================================================ */
|
|
.topbar .search,
|
|
.search-wrap .search {
|
|
background: var(--bg-2);
|
|
border: 1px solid var(--border-strong);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.03),
|
|
0 1px 0 rgba(0, 0, 0, 0.25);
|
|
color: var(--text-1);
|
|
transition: background 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
|
|
}
|
|
.topbar .search:hover,
|
|
.search-wrap .search:hover {
|
|
background: var(--bg-3);
|
|
border-color: var(--border-stronger);
|
|
}
|
|
.topbar .search:focus-within,
|
|
.search-wrap .search:focus-within,
|
|
.topbar .search.is-open,
|
|
.search-wrap .search.is-open {
|
|
background: var(--bg-2);
|
|
border-color: var(--accent);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.04),
|
|
0 0 0 3px var(--accent-soft);
|
|
}
|
|
.topbar .search input::placeholder,
|
|
.search-wrap .search input::placeholder {
|
|
color: var(--text-3);
|
|
}
|
|
.topbar .search .search-icon,
|
|
.search-wrap .search .search-icon {
|
|
color: var(--text-2);
|
|
}
|
|
.topbar .search:focus-within .search-icon,
|
|
.search-wrap .search:focus-within .search-icon {
|
|
color: var(--accent-text);
|
|
}
|
|
.topbar .search .kbd,
|
|
.search-wrap .search .kbd {
|
|
background: var(--bg-1);
|
|
border-color: var(--border-stronger);
|
|
color: var(--text-2);
|
|
}
|
|
|
|
/* Library-local "Filter assets" search — same container treatment,
|
|
keep its compact width. */
|
|
.library-toolbar .search {
|
|
background: var(--bg-2);
|
|
border: 1px solid var(--border-strong);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.03),
|
|
0 1px 0 rgba(0, 0, 0, 0.25);
|
|
}
|
|
.library-toolbar .search:hover { background: var(--bg-3); border-color: var(--border-stronger); }
|
|
.library-toolbar .search:focus-within {
|
|
border-color: var(--accent);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.04),
|
|
0 0 0 3px var(--accent-soft);
|
|
}
|
|
|
|
/* Open-state dropdown: visually connect it to the input. */
|
|
.search-wrap .search.is-open {
|
|
border-bottom-left-radius: 0;
|
|
border-bottom-right-radius: 0;
|
|
}
|
|
.search-results {
|
|
background: var(--bg-2);
|
|
border-color: var(--border-stronger);
|
|
border-top-left-radius: 0;
|
|
border-top-right-radius: 0;
|
|
margin-top: -1px;
|
|
padding: 6px;
|
|
}
|
|
.search-result {
|
|
padding: 8px 10px;
|
|
}
|
|
.search-result + .search-result { margin-top: 1px; }
|
|
.search-result:hover { background: var(--hover-strong); }
|
|
.search-result.active {
|
|
background: var(--accent-soft);
|
|
outline: 1px solid var(--accent-soft-2);
|
|
outline-offset: -1px;
|
|
}
|
|
|
|
/* ============================================================
|
|
Right-click context menu — pop it forward off the page so it
|
|
reads as a menu, not a floating list.
|
|
============================================================ */
|
|
.ctx-menu {
|
|
background: var(--bg-2);
|
|
border: 1px solid var(--border-stronger);
|
|
box-shadow:
|
|
0 1px 0 rgba(255, 255, 255, 0.04) inset,
|
|
0 18px 40px rgba(0, 0, 0, 0.55),
|
|
0 4px 10px rgba(0, 0, 0, 0.35);
|
|
padding: 6px;
|
|
min-width: 240px;
|
|
animation: ctxFadeIn 90ms ease-out both;
|
|
}
|
|
@keyframes ctxFadeIn {
|
|
from { opacity: 0; transform: translateY(-2px) scale(0.985); }
|
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
}
|
|
.ctx-menu .ctx-header {
|
|
padding: 8px 10px 8px;
|
|
font-size: 10.5px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--text-3);
|
|
border-bottom: 1px solid var(--border);
|
|
margin: 0 -2px 6px;
|
|
}
|
|
.ctx-menu .ctx-divider {
|
|
background: var(--border-strong);
|
|
margin: 6px 2px;
|
|
}
|
|
.ctx-menu .ctx-section-label {
|
|
font-size: 9.5px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.09em;
|
|
color: var(--text-4);
|
|
padding: 8px 10px 4px;
|
|
}
|
|
.ctx-menu button {
|
|
padding: 7px 10px;
|
|
border-radius: 5px;
|
|
gap: 10px;
|
|
color: var(--text-1);
|
|
}
|
|
.ctx-menu button + button { margin-top: 1px; }
|
|
.ctx-menu button:hover:not(:disabled) {
|
|
background: var(--accent-soft);
|
|
color: var(--accent-text);
|
|
}
|
|
.ctx-menu button:hover:not(:disabled) svg { color: var(--accent); }
|
|
.ctx-menu button:disabled { color: var(--text-3); }
|
|
.ctx-menu button:disabled svg { color: var(--text-4); }
|
|
.ctx-menu button.danger:hover:not(:disabled) {
|
|
background: var(--danger-soft);
|
|
color: var(--danger);
|
|
}
|
|
.ctx-menu button.danger:hover:not(:disabled) svg { color: var(--danger); }
|
|
|
|
/* Row-popover menu (Users page etc.) — match the same polish so the
|
|
app feels consistent. */
|
|
.row-menu {
|
|
background: var(--bg-2);
|
|
border: 1px solid var(--border-stronger);
|
|
box-shadow:
|
|
0 1px 0 rgba(255, 255, 255, 0.04) inset,
|
|
0 14px 32px rgba(0, 0, 0, 0.5);
|
|
padding: 6px;
|
|
}
|
|
.row-menu button { padding: 7px 10px; border-radius: 5px; }
|
|
.row-menu button:hover { background: var(--accent-soft); color: var(--accent-text); }
|
|
.row-menu button.danger:hover { background: var(--danger-soft); color: var(--danger); }
|
|
|
|
/* ============================================================
|
|
Sidebar brand logo — replace the gradient "D" tile with the
|
|
actual dragon-coiled-D logo. mix-blend-mode: screen drops the
|
|
light-gray PNG background so only the black silhouette + blue
|
|
flame remain over the dark sidebar.
|
|
============================================================ */
|
|
.brand-logo {
|
|
width: 32px;
|
|
height: 32px;
|
|
flex-shrink: 0;
|
|
object-fit: contain;
|
|
/* The PNG has a light-gray background. Invert it so the black
|
|
dragon becomes white-ish, then multiply against the sidebar
|
|
so the (now-bright) gray background falls back out. Simpler
|
|
route: a subtle drop-shadow + mix-blend. */
|
|
mix-blend-mode: screen;
|
|
filter: drop-shadow(0 0 6px rgba(91, 124, 250, 0.25));
|
|
}
|
|
.sidebar-header:hover .brand-logo {
|
|
filter: drop-shadow(0 0 10px rgba(91, 124, 250, 0.45));
|
|
}
|
|
/* If you ever drop a transparent-PNG version into img/dragon-logo.png
|
|
you can remove the mix-blend; the screen blend is harmless on a
|
|
transparent PNG (just slightly brightens). */
|
|
|
|
/* ============================================================
|
|
Launcher home — full-bleed landing page with the logo as hero
|
|
and big section tiles.
|
|
============================================================ */
|
|
.launcher {
|
|
flex: 1;
|
|
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%),
|
|
var(--bg-0);
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
padding: 48px 32px 64px;
|
|
}
|
|
.launcher-inner {
|
|
width: 100%;
|
|
max-width: 1160px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 40px;
|
|
}
|
|
|
|
.launcher-hero {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 14px;
|
|
text-align: center;
|
|
margin-top: 8px;
|
|
}
|
|
.launcher-logo {
|
|
width: 180px;
|
|
height: 180px;
|
|
object-fit: contain;
|
|
/* Drop the gray PNG background — see .brand-logo comment for the
|
|
trick. Soft accent halo so the silhouette has presence. */
|
|
mix-blend-mode: screen;
|
|
filter:
|
|
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;
|
|
}
|
|
@keyframes launcherLogoIn {
|
|
from { opacity: 0; transform: translateY(8px) scale(0.96); }
|
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
}
|
|
.launcher-wordmark {
|
|
margin: 0;
|
|
font-size: 44px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.12em;
|
|
line-height: 1;
|
|
background: linear-gradient(180deg, var(--text-1) 0%, #B4C3FF 100%);
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
text-shadow: 0 0 32px rgba(91, 124, 250, 0.15);
|
|
}
|
|
.launcher-tagline {
|
|
margin: 0;
|
|
color: var(--text-3);
|
|
font-size: 13.5px;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
.launcher-grid {
|
|
width: 100%;
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 14px;
|
|
}
|
|
@media (max-width: 960px) { .launcher-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
@media (max-width: 620px) { .launcher-grid { grid-template-columns: 1fr; } }
|
|
|
|
.launcher-tile {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-areas:
|
|
"icon arrow"
|
|
"label label"
|
|
"sub sub"
|
|
"desc desc";
|
|
grid-template-columns: 1fr auto;
|
|
align-items: start;
|
|
gap: 6px;
|
|
text-align: left;
|
|
padding: 20px 22px 22px;
|
|
border-radius: var(--r-lg);
|
|
background:
|
|
linear-gradient(180deg, rgba(255,255,255,0.04), transparent 45%),
|
|
var(--bg-1);
|
|
border: 1px solid var(--border);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.03),
|
|
0 12px 28px rgba(0, 0, 0, 0.28);
|
|
color: var(--text-1);
|
|
cursor: pointer;
|
|
transition: transform 140ms ease, border-color 140ms ease, box-shadow 140ms ease, background 140ms ease;
|
|
min-height: 168px;
|
|
overflow: hidden;
|
|
isolation: isolate;
|
|
}
|
|
.launcher-tile::before {
|
|
/* Tinted accent halo that brightens on hover. Sits behind content
|
|
(z-index lower than children which inherit 1). */
|
|
content: "";
|
|
position: absolute;
|
|
inset: -1px;
|
|
border-radius: inherit;
|
|
pointer-events: none;
|
|
background: radial-gradient(120% 80% at 0% 0%, var(--tile-tint, transparent), transparent 60%);
|
|
opacity: 0;
|
|
transition: opacity 160ms ease;
|
|
z-index: 0;
|
|
}
|
|
.launcher-tile > * { position: relative; z-index: 1; }
|
|
.launcher-tile:hover {
|
|
transform: translateY(-2px);
|
|
border-color: var(--border-stronger);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.05),
|
|
0 18px 40px rgba(0, 0, 0, 0.42);
|
|
}
|
|
.launcher-tile:hover::before { opacity: 1; }
|
|
.launcher-tile:focus-visible {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: 2px;
|
|
}
|
|
.launcher-tile:active { transform: translateY(0); }
|
|
|
|
.launcher-tile-icon {
|
|
grid-area: icon;
|
|
width: 44px; height: 44px;
|
|
border-radius: 10px;
|
|
display: grid;
|
|
place-items: center;
|
|
background: var(--tile-icon-bg, var(--bg-3));
|
|
color: var(--tile-icon-fg, var(--text-1));
|
|
border: 1px solid var(--tile-icon-border, var(--border-strong));
|
|
margin-bottom: 6px;
|
|
}
|
|
.launcher-tile-icon svg { width: 22px; height: 22px; }
|
|
|
|
.launcher-tile-label {
|
|
grid-area: label;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
.launcher-tile-sub {
|
|
grid-area: sub;
|
|
font-size: 11.5px;
|
|
font-family: var(--font-mono);
|
|
color: var(--tile-sub-fg, var(--text-3));
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
.launcher-tile-desc {
|
|
grid-area: desc;
|
|
font-size: 12.5px;
|
|
color: var(--text-3);
|
|
line-height: 1.5;
|
|
margin-top: 4px;
|
|
}
|
|
.launcher-tile-arrow {
|
|
grid-area: arrow;
|
|
align-self: start;
|
|
color: var(--text-4);
|
|
transform: translateX(-4px);
|
|
transition: transform 140ms ease, color 140ms ease;
|
|
}
|
|
.launcher-tile:hover .launcher-tile-arrow {
|
|
transform: translateX(0);
|
|
color: var(--tile-icon-fg, var(--accent-text));
|
|
}
|
|
|
|
/* 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-icon-bg: var(--accent-soft);
|
|
--tile-icon-fg: var(--accent-text);
|
|
--tile-icon-border: rgba(91, 124, 250, 0.30);
|
|
}
|
|
.launcher-tile.tone-live {
|
|
--tile-tint: rgba(255, 59, 48, 0.18);
|
|
--tile-icon-bg: var(--live-soft);
|
|
--tile-icon-fg: var(--live);
|
|
--tile-icon-border: rgba(255, 59, 48, 0.30);
|
|
}
|
|
.launcher-tile.tone-purple {
|
|
--tile-tint: rgba(181, 124, 250, 0.18);
|
|
--tile-icon-bg: var(--purple-soft);
|
|
--tile-icon-fg: var(--purple);
|
|
--tile-icon-border: rgba(181, 124, 250, 0.30);
|
|
}
|
|
.launcher-tile.tone-success {
|
|
--tile-tint: rgba(45, 212, 168, 0.16);
|
|
--tile-icon-bg: var(--success-soft);
|
|
--tile-icon-fg: var(--success);
|
|
--tile-icon-border: rgba(45, 212, 168, 0.30);
|
|
}
|
|
.launcher-tile.tone-warn {
|
|
--tile-tint: rgba(245, 166, 35, 0.18);
|
|
--tile-icon-bg: var(--warning-soft);
|
|
--tile-icon-fg: var(--warning);
|
|
--tile-icon-border: rgba(245, 166, 35, 0.30);
|
|
}
|
|
.launcher-tile.tone-neutral {
|
|
--tile-tint: rgba(255, 255, 255, 0.06);
|
|
--tile-icon-bg: var(--bg-3);
|
|
--tile-icon-fg: var(--text-1);
|
|
--tile-icon-border: var(--border-stronger);
|
|
}
|
|
.launcher-tile.tone-ghost {
|
|
--tile-tint: rgba(255, 255, 255, 0.04);
|
|
--tile-icon-bg: transparent;
|
|
--tile-icon-fg: var(--text-2);
|
|
--tile-icon-border: var(--border);
|
|
background:
|
|
linear-gradient(180deg, rgba(255,255,255,0.02), transparent 50%),
|
|
var(--bg-1);
|
|
}
|
|
|
|
.launcher-status {
|
|
display: flex;
|
|
gap: 16px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
margin-top: 4px;
|
|
}
|
|
.launcher-status-pip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 11.5px;
|
|
font-family: var(--font-mono);
|
|
color: var(--text-3);
|
|
letter-spacing: 0.02em;
|
|
padding: 6px 12px;
|
|
border-radius: 99px;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg-1);
|
|
}
|
|
.launcher-status-pip .dot {
|
|
width: 6px; height: 6px; border-radius: 50%;
|
|
box-shadow: 0 0 0 3px var(--success-soft);
|
|
}
|
|
.launcher-status-pip .muted { color: var(--text-4); margin-left: 2px; }
|
|
.launcher-status-pip.live .dot {
|
|
box-shadow: 0 0 0 3px var(--live-soft);
|
|
animation: pulse 1.6s ease-in-out infinite;
|
|
}
|
|
|
|
/* ============================================================
|
|
Recorder row — signal indicator with a pulsing dot when
|
|
actually receiving frames. Closes part of #2.
|
|
============================================================ */
|
|
.signal-val {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.signal-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.04);
|
|
}
|
|
.signal-dot.receiving {
|
|
animation: signalPulse 1.4s ease-in-out infinite;
|
|
}
|
|
@keyframes signalPulse {
|
|
0%, 100% { box-shadow: 0 0 0 0 rgba(45, 212, 168, 0.6); }
|
|
50% { box-shadow: 0 0 0 6px rgba(45, 212, 168, 0); }
|
|
}
|