dragonflight/services/web-ui/public/styles-rest.css

1384 lines
37 KiB
CSS
Raw Normal View History

/* ========== Projects ========== */
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 14px;
}
.project-card {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
cursor: pointer;
transition: transform 120ms, border 80ms;
}
.project-card:hover {
transform: translateY(-2px);
border-color: var(--border-stronger);
}
.project-thumb-grid {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 2px;
aspect-ratio: 16 / 9;
background: var(--bg-2);
}
.project-thumb-grid .project-thumb-cell:first-child { grid-row: span 2; }
.project-thumb-cell { position: relative; overflow: hidden; background: var(--bg-2); }
.project-thumb-cell .thumb-svg { width: 100%; height: 100%; }
.project-card-body { padding: 12px 14px; }
.project-meta {
display: flex; gap: 6px;
font-size: 11.5px; color: var(--text-3);
margin-top: 4px;
}
.project-bar {
margin-top: 10px;
display: flex;
height: 4px;
border-radius: 99px;
overflow: hidden;
background: var(--bg-3);
gap: 1px;
}
/* ========== Upload ========== */
.field-label {
font-size: 10.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-4);
display: block;
margin-bottom: 6px;
}
.select-faux, .field-input {
height: 34px;
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-sm);
padding: 0 10px;
display: flex; align-items: center; gap: 8px;
color: var(--text-1);
font-size: 12.5px;
width: 100%;
cursor: pointer;
}
.field-input { font-size: 12.5px; outline: 0; }
.field-input:focus { border-color: var(--accent); background: var(--bg-2); }
.field-input.select { justify-content: space-between; }
.dropzone {
background: var(--bg-1);
border: 1.5px dashed var(--border-stronger);
border-radius: var(--r-lg);
padding: 40px 24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
transition: background 120ms, border 120ms;
}
.dropzone:hover { background: var(--bg-2); border-color: var(--accent); }
.dropzone-formats { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; justify-content: center; }
.upload-row {
display: flex; align-items: center; gap: 12px;
padding: 12px 14px;
border-bottom: 1px solid var(--border);
}
.upload-row:last-child { border-bottom: 0; }
/* ========== Recorders ========== */
.recorders-list { display: flex; flex-direction: column; gap: 10px; }
.recorder-row {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 12px;
display: grid;
grid-template-columns: 220px 1fr 380px auto;
align-items: center;
gap: 16px;
transition: border 80ms;
}
.recorder-row:hover { border-color: var(--border-stronger); }
.recorder-row.recording { border-color: rgba(255,59,48,0.25); }
.recorder-row.error { border-color: rgba(255,91,91,0.25); }
.recorder-preview {
height: 56px;
border-radius: var(--r-sm);
background: var(--bg-2);
overflow: hidden;
position: relative;
}
.recorder-empty {
height: 100%;
display: grid; place-items: center;
color: var(--text-3);
}
.recorder-audio-prev {
height: 100%;
display: flex; align-items: center; gap: 10px;
padding: 0 12px;
}
.recorder-audio-prev .waveform { flex: 1; height: 80%; }
.recorder-info { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.recorder-sub {
font-size: 11.5px;
color: var(--text-3);
display: flex; gap: 6px;
}
.recorder-sub.mono { font-family: var(--font-mono); font-size: 11px; }
.recorder-stats {
display: grid;
grid-template-columns: 90px 90px 1fr;
gap: 12px;
}
.recorder-stat .stat-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 600;
color: var(--text-4);
margin-bottom: 3px;
}
.recorder-stat .stat-val { font-size: 12.5px; }
.recorder-actions { display: flex; align-items: center; gap: 4px; }
.rec-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: currentColor;
}
/* ========== Capture / DeckLink ========== */
.decklink-card {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
}
.decklink-head {
padding: 14px 16px;
display: flex; justify-content: space-between; align-items: center;
border-bottom: 1px solid var(--border);
}
.decklink-body {
padding: 24px;
display: grid;
grid-template-columns: 60px 1fr;
gap: 20px;
align-items: stretch;
background:
radial-gradient(ellipse at top, rgba(91,124,250,0.05), transparent 60%),
var(--bg-0);
}
.decklink-card-face {
background: linear-gradient(180deg, #1c1f28, #0d0e13);
border: 1px solid var(--border-strong);
border-radius: 6px;
display: flex; flex-direction: column;
justify-content: space-between;
padding: 12px 6px;
position: relative;
}
.decklink-label {
writing-mode: vertical-rl;
transform: rotate(180deg);
font-family: var(--font-mono);
font-size: 8.5px;
letter-spacing: 0.12em;
color: var(--text-3);
text-align: center;
margin: 0 auto;
}
.decklink-leds {
display: flex; gap: 6px;
justify-content: center;
}
.decklink-led {
width: 6px; height: 6px;
border-radius: 50%;
background: rgba(255,255,255,0.1);
}
.decklink-led.on {
background: var(--success);
box-shadow: 0 0 6px var(--success);
}
.bnc-ports { display: flex; flex-direction: column; gap: 10px; }
.bnc-port {
display: grid;
grid-template-columns: 32px 1fr auto;
align-items: center;
gap: 12px;
padding: 10px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--r-sm);
text-align: left;
cursor: pointer;
position: relative;
transition: background 80ms, border 80ms;
}
.bnc-port:hover { border-color: var(--border-stronger); }
.bnc-port.active { background: var(--bg-3); }
.bnc-port.active.live::before {
content: "";
position: absolute;
left: 0; top: 0; bottom: 0;
width: 2px;
background: var(--success);
}
.bnc-port.recording::before { background: var(--live); }
.bnc-port.active { box-shadow: inset 0 0 0 1px var(--accent); border-color: var(--accent); }
.bnc-connector {
width: 32px; height: 32px;
border-radius: 50%;
background: radial-gradient(circle, #1a1d24, #0a0c11);
display: grid; place-items: center;
border: 1.5px solid var(--border-stronger);
position: relative;
}
.bnc-pin {
width: 6px; height: 6px;
background: linear-gradient(135deg, #6b7280, #2a2f3a);
border-radius: 50%;
}
.bnc-ring {
position: absolute;
inset: 4px;
border: 1px dashed rgba(255,255,255,0.08);
border-radius: 50%;
}
.bnc-port.live .bnc-pin { background: linear-gradient(135deg, #FFC857, #C8862D); box-shadow: 0 0 8px rgba(255,200,87,0.4); }
.bnc-port.recording .bnc-pin { background: linear-gradient(135deg, #FF5C5C, #C8362D); box-shadow: 0 0 8px rgba(255,92,92,0.5); }
.bnc-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.bnc-num { font-size: 12.5px; font-weight: 600; }
.bnc-label { font-size: 11px; color: var(--text-2); }
.bnc-sig { font-size: 10.5px; color: var(--text-3); }
.bnc-port.live .bnc-sig { color: var(--success); }
.bnc-rec {
width: 10px; height: 10px;
border-radius: 50%;
background: var(--live);
animation: pulse 1.2s ease-in-out infinite;
}
.bnc-signal-bar {
grid-column: 2 / 4;
height: 3px;
background: var(--bg-3);
border-radius: 99px;
overflow: hidden;
margin-top: 4px;
}
.bnc-signal-fill {
height: 100%;
background: linear-gradient(90deg, var(--success), var(--warning), var(--danger));
background-size: 200% 100%;
background-position: 0% 0;
transition: width 200ms;
}
.decklink-foot {
padding: 10px 16px;
border-top: 1px solid var(--border);
display: flex; gap: 20px;
font-size: 11.5px;
color: var(--text-3);
}
.decklink-foot strong { color: var(--text-1); font-weight: 600; }
.capture-detail {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 16px;
}
.capture-detail.empty {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
padding: 80px 24px;
color: var(--text-3);
}
.capture-preview {
position: relative;
aspect-ratio: 16 / 9;
border-radius: var(--r-md);
overflow: hidden;
background: #000;
}
.capture-overlay-meters {
position: absolute;
right: 8px; top: 8px;
display: flex; gap: 4px;
}
.capture-tc {
position: absolute;
bottom: 8px; right: 8px;
background: rgba(0,0,0,0.7);
padding: 3px 8px;
border-radius: 3px;
font-family: var(--font-mono);
font-size: 11px;
color: white;
}
.capture-stats {
margin-top: 16px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.capture-stat {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--r-sm);
padding: 8px 10px;
}
.capture-stat-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 600;
color: var(--text-4);
}
.capture-stat-value { font-size: 13px; margin-top: 2px; }
/* ========== Monitors ========== */
.monitors-grid {
display: grid;
gap: 10px;
background: var(--bg-2);
border-radius: var(--r-lg);
padding: 10px;
}
.monitor-tile {
position: relative;
aspect-ratio: 16 / 9;
background: #000;
border-radius: var(--r-sm);
overflow: hidden;
border: 1px solid var(--border);
display: flex; flex-direction: column;
cursor: pointer;
}
.monitor-tile.audio { background: linear-gradient(135deg, hsl(180 30% 12%), hsl(200 25% 6%)); }
.monitor-tile-label {
position: absolute;
bottom: 0; left: 0; right: 0;
padding: 8px 10px;
background: linear-gradient(180deg, transparent, rgba(0,0,0,0.8));
display: flex; align-items: center; gap: 6px;
}
.monitor-tile-label .name {
color: white;
font-size: 11.5px;
font-weight: 500;
}
.monitor-tile-label .time {
margin-left: auto;
color: white;
font-family: var(--font-mono);
font-size: 10.5px;
}
/* ========== Jobs ========== */
.jobs-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.job-row {
display: grid;
/* status · Job · Asset · Node · Progress · Time · Priority · Actions
Time needs room for "done May 22 · 2:23 PM · 6h ago"; Progress hosts
the bar + percent; Node is just "primary" or "—" so it can be tight. */
grid-template-columns: 20px 110px 1fr 60px 240px 240px 70px 90px;
align-items: center;
gap: 12px;
padding: 10px 16px;
border-bottom: 1px solid var(--border);
font-size: 12.5px;
}
.job-row:last-child { border-bottom: 0; }
.job-row.head {
font-size: 10.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-4);
}
.job-progress-wrap {
display: flex; align-items: center; gap: 8px;
}
.job-progress-bar {
flex: 1;
height: 5px;
background: var(--bg-3);
border-radius: 99px;
overflow: hidden;
position: relative;
}
.job-progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent), #7C9EFF);
background-size: 200% 100%;
animation: shimmer 2s linear infinite;
transition: width 300ms;
}
/* ========== Editor ========== */
.editor-shell {
display: flex; flex-direction: column;
height: 100%;
background: var(--bg-0);
}
.editor-topbar {
height: 44px;
border-bottom: 1px solid var(--border);
padding: 0 14px;
display: flex; align-items: center; gap: 10px;
}
.editor-body {
flex: 1;
display: grid;
grid-template-columns: 240px 1fr 240px;
min-height: 0;
}
.editor-bins, .editor-insp { background: var(--bg-1); border-right: 1px solid var(--border); overflow-y: auto; }
.editor-insp { border-right: 0; border-left: 1px solid var(--border); }
.editor-bin-item {
display: flex; align-items: center; gap: 8px;
padding: 6px;
border-radius: var(--r-sm);
cursor: pointer;
}
.editor-bin-item:hover { background: var(--hover); }
.editor-bin-thumb { width: 48px; aspect-ratio: 16/9; border-radius: 3px; overflow: hidden; background: var(--bg-2); flex-shrink: 0; }
.editor-viewer { background: #000; display: flex; flex-direction: column; }
.editor-canvas {
flex: 1;
position: relative;
background: #050608;
display: grid; place-items: center;
overflow: hidden;
max-height: 50vh;
}
.editor-canvas .thumb-svg { width: 100%; height: 100%; }
.editor-transport {
height: 44px;
display: flex; align-items: center; gap: 8px;
padding: 0 14px;
background: var(--bg-1);
border-top: 1px solid var(--border);
}
.editor-timeline {
background: var(--bg-0);
border-top: 1px solid var(--border);
height: 240px;
display: flex; flex-direction: column;
position: relative;
overflow: hidden;
}
.editor-timeline-head {
height: 32px;
display: flex; align-items: center;
padding: 0 12px;
gap: 8px;
border-bottom: 1px solid var(--border);
}
.timeline-ruler {
height: 22px;
display: flex;
padding-left: 40px;
border-bottom: 1px solid var(--border);
background: var(--bg-1);
}
.ruler-tick {
flex: 1;
border-left: 1px solid var(--border);
font-size: 9.5px;
color: var(--text-4);
padding: 4px 4px 0;
}
.timeline-track {
display: grid;
grid-template-columns: 40px 1fr;
align-items: stretch;
height: 42px;
border-bottom: 1px solid var(--border);
}
.timeline-track-label {
background: var(--bg-1);
display: grid; place-items: center;
font-size: 10.5px;
font-family: var(--font-mono);
font-weight: 600;
color: var(--text-3);
border-right: 1px solid var(--border);
}
.timeline-track-lane {
position: relative;
background:
repeating-linear-gradient(90deg, transparent 0 49px, rgba(255,255,255,0.02) 49px 50px);
}
.clip {
position: absolute;
top: 4px; bottom: 4px;
border-radius: 3px;
overflow: hidden;
display: flex; align-items: center;
cursor: pointer;
border: 1px solid rgba(0,0,0,0.3);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.15);
}
.clip-label {
position: absolute;
left: 6px; top: 3px;
font-size: 10px;
font-weight: 600;
color: white;
z-index: 2;
text-shadow: 0 1px 1px rgba(0,0,0,0.4);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.clip-film { display: flex; flex: 1; height: 100%; }
.clip-film-cell { flex: 1; overflow: hidden; }
.clip-film-cell .thumb-svg { width: 100%; height: 100%; }
.clip.audio { background: var(--success) !important; }
.clip-wave { width: 100%; height: 100%; padding: 4px; opacity: 0.6; }
.clip-wave .waveform { width: 100%; height: 100%; }
.timeline-playhead {
position: absolute;
top: 32px; bottom: 0;
width: 1.5px;
background: var(--live);
pointer-events: none;
z-index: 5;
}
.timeline-playhead::before {
content: "";
position: absolute;
top: -4px; left: 50%;
width: 10px; height: 10px;
background: var(--live);
transform: translateX(-50%) rotate(45deg);
}
/* ========== Admin tables ========== */
/* ── Row popover menu (Users, etc.) ────────────────────────────────── */
.row-menu {
position: absolute;
right: 0;
top: calc(100% + 4px);
z-index: 20;
min-width: 180px;
background: var(--bg-1);
border: 1px solid var(--border-stronger);
border-radius: 6px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
padding: 4px;
display: flex;
flex-direction: column;
}
.row-menu button {
display: flex;
align-items: center;
gap: 8px;
background: transparent;
border: 0;
color: var(--text-1);
font-size: 12.5px;
padding: 7px 10px;
border-radius: 4px;
cursor: pointer;
text-align: left;
}
.row-menu button:hover { background: var(--bg-3); }
.row-menu button.danger:hover { background: var(--danger-soft); color: var(--danger); }
.user-row, .token-row, .container-row, .schedule-row {
display: grid;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
font-size: 12.5px;
}
.user-row { grid-template-columns: 1.5fr 100px 1.5fr 120px 40px; }
.token-row { grid-template-columns: 1.4fr 1.4fr 110px 110px 100px 40px; }
.container-row { grid-template-columns: 1.4fr 1.4fr 140px 140px 100px 1.4fr 110px; }
.schedule-row { grid-template-columns: 1.6fr 1.2fr 1.2fr 90px 110px 110px 150px; }
.user-row.head, .token-row.head, .container-row.head, .schedule-row.head {
font-size: 10.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-4);
}
.user-row:last-child, .token-row:last-child, .container-row:last-child, .schedule-row:last-child { border-bottom: 0; }
.token-row.revoked { opacity: 0.5; }
/* ========== Schedule (EPG timeline) ==========
Broadcast-control-room schedule. Recorders are rows, time is the horizontal
axis. The single scrollable .epg container uses a 2-column / 2-row grid
so the gutter (left) and ruler (top) stay sticky during scroll. */
.epg-page {
display: flex; flex-direction: column;
height: 100%;
min-height: 0;
--epg-pph: 88px; /* pixels per hour, overridden inline per view */
--epg-row-h: 60px;
--epg-gutter-w: 220px;
--epg-ruler-h: 32px;
}
/* ---- Status strip (always on top of the schedule screen) -------------- */
.epg-status {
padding: 10px 20px 12px;
background: linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 100%);
border-bottom: 1px solid var(--border);
}
.epg-status-row {
display: flex; align-items: center; gap: 10px;
min-height: 22px;
font-size: 13px;
}
.epg-status-row.sub { margin-top: 2px; font-size: 12px; color: var(--text-3); }
.epg-status-dot {
width: 8px; height: 8px; border-radius: 50%;
flex-shrink: 0;
}
.epg-status-dot.live {
background: var(--live);
box-shadow: 0 0 0 4px rgba(255, 59, 48, 0.16);
animation: _epg_live_pulse 1.6s ease-out infinite;
}
.epg-status-dot.idle { background: var(--text-4); }
.epg-status-label {
font-weight: 600; letter-spacing: 0.02em;
text-transform: uppercase; font-size: 10.5px;
color: var(--text-2);
}
.epg-status-label.muted { color: var(--text-4); }
.epg-status-active {
display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
flex: 1; min-width: 0;
}
.epg-status-pill {
display: inline-flex; align-items: center; gap: 8px;
padding: 2px 10px 2px 0;
background: var(--bg-2);
border: 1px solid var(--border-strong);
border-radius: 6px;
font-size: 12px;
overflow: hidden;
}
.epg-status-pill-bar {
width: 3px; align-self: stretch;
background: var(--text-3);
}
.epg-status-pill-name { font-weight: 600; padding-left: 8px; }
.epg-status-pill-rec { color: var(--text-3); font-size: 11px; padding-left: 8px; border-left: 1px solid var(--border); }
.epg-status-pill-time { color: var(--text-2); font-size: 11px; padding-left: 8px; border-left: 1px solid var(--border); }
.epg-status-next { font-weight: 500; color: var(--text-1); }
.epg-status-next-rec { color: var(--text-3); padding-left: 6px; }
.epg-status-next-time { color: var(--accent-text); padding-left: 6px; }
@keyframes _epg_live_pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 59, 48, 0.55); }
70% { box-shadow: 0 0 0 10px rgba(255, 59, 48, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 59, 48, 0); }
}
/* ---- Toolbar (date nav + view tabs + CTA) ------------------------------ */
.epg-toolbar {
display: flex; align-items: center; gap: 8px;
padding: 10px 20px;
border-bottom: 1px solid var(--border);
background: var(--bg-1);
}
.epg-toolbar .spacer { flex: 1; }
.epg-date {
display: flex; align-items: center; gap: 4px;
}
.epg-date-label {
font-size: 15px; font-weight: 600;
min-width: 240px; padding: 0 6px;
letter-spacing: -0.01em;
}
/* ---- Today: scrollable timeline ---------------------------------------- */
.epg {
flex: 1; min-height: 0;
display: grid;
grid-template-columns: var(--epg-gutter-w) 1fr;
grid-template-rows: var(--epg-ruler-h) 1fr;
overflow: auto;
background: var(--bg-0);
position: relative;
}
.epg-corner {
position: sticky; top: 0; left: 0;
grid-row: 1; grid-column: 1;
z-index: 4;
background: var(--bg-1);
border-right: 1px solid var(--border);
border-bottom: 1px solid var(--border);
display: flex; align-items: center;
padding: 0 14px;
font-size: 11px; color: var(--text-3);
letter-spacing: 0.02em;
}
.epg-gutter {
position: sticky; left: 0;
grid-row: 2; grid-column: 1;
z-index: 2;
background: var(--bg-1);
border-right: 1px solid var(--border);
}
.epg-gutter-rows { display: flex; flex-direction: column; }
.epg-gutter-row {
display: flex; align-items: center; gap: 10px;
height: var(--epg-row-h);
padding: 0 14px;
border-bottom: 1px solid var(--border);
position: relative;
}
.epg-gutter-row:last-child { border-bottom: 0; }
.epg-gutter-status {
width: 8px; height: 8px; border-radius: 50%;
flex-shrink: 0;
background: var(--text-4);
}
.epg-gutter-status.live {
background: var(--live);
box-shadow: 0 0 0 3px rgba(255, 59, 48, 0.18);
}
.epg-gutter-status.err { background: var(--danger); }
.epg-gutter-status.idle { background: var(--text-4); }
.epg-gutter-meta { display: flex; flex-direction: column; min-width: 0; }
.epg-gutter-name {
font-size: 12.5px; font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
color: var(--text-1);
}
.epg-gutter-sub {
display: flex; align-items: center; gap: 6px;
font-size: 10.5px; color: var(--text-3);
letter-spacing: 0.06em; text-transform: uppercase;
}
.epg-gutter-dot {
display: inline-block;
width: 7px; height: 7px; border-radius: 50%;
}
.epg-canvas-head {
position: sticky; top: 0;
grid-row: 1; grid-column: 2;
z-index: 3;
background: var(--bg-1);
border-bottom: 1px solid var(--border);
}
.epg-canvas {
grid-row: 2; grid-column: 2;
position: relative;
background:
/* hour-band rhythm — alternating subtle stripe every other hour */
repeating-linear-gradient(
to right,
transparent 0,
transparent var(--epg-pph),
rgba(255,255,255,0.012) var(--epg-pph),
rgba(255,255,255,0.012) calc(var(--epg-pph) * 2)
),
/* hour separator lines every hour */
repeating-linear-gradient(
to right,
var(--border) 0,
var(--border) 1px,
transparent 1px,
transparent var(--epg-pph)
);
}
/* Hour ruler */
.epg-ruler {
position: relative;
height: var(--epg-ruler-h);
}
.epg-ruler-tick {
position: absolute; top: 0; bottom: 0;
padding-left: 8px;
display: flex; align-items: center;
font-size: 10.5px; font-weight: 600;
letter-spacing: 0.04em;
color: var(--text-3);
border-left: 1px solid var(--border);
}
.epg-ruler-tick.end { border-left: 0; }
/* Recorder rows + event blocks */
.epg-rows { display: flex; flex-direction: column; position: relative; }
.epg-row {
position: relative;
height: var(--epg-row-h);
border-bottom: 1px solid var(--border);
cursor: copy;
}
.epg-row:last-child { border-bottom: 0; }
.epg-block {
position: absolute;
top: 8px;
height: calc(var(--epg-row-h) - 16px);
feat(schedule): right-click menu + drag-to-resize on EPG event blocks Right-click any event block to open a context menu (Edit, Cancel, Copy schedule ID, Delete) — actions per status mirror the List view so the two surfaces stay in lockstep. Menu is viewport-clamped and dismisses on outside click / scroll, same pattern as the asset menu in the Library. Drag-to-resize works for pending schedules only (the schedules PUT rejects edits to running rows, and terminal statuses are read-only): - Drag the left edge to move the start time - Drag the right edge to move the end time - Drag the body to shift the whole block in time All gestures snap to 15-minute increments to match the new-schedule click snap. Minimum duration is clamped to 5 minutes; the block clamps to the visible day on both edges. While dragging the title shows the preview range ("Start time → end time") and the block lifts with a project-tinted shadow. A short pointer click (< 4px travel) still opens the edit modal — the click and drag share the same pointerdown so the operator never has to know which gesture they made first. Implementation: replaces the <button> block with a <div> hosting three zones (left handle / body / right handle). Pointer events with setPointerCapture so drags survive losing the cursor over the block, and pointerup demotes back to click if travel was below threshold. Optimistic local update on resize, PUT /schedules/:id with just the two changed time fields, refetch to reconcile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:33:57 -04:00
display: flex; align-items: stretch;
background: var(--bg-2);
border: 1px solid var(--border-strong);
border-radius: 5px;
font-size: 11.5px;
text-align: left;
color: var(--text-1);
overflow: hidden;
feat(schedule): right-click menu + drag-to-resize on EPG event blocks Right-click any event block to open a context menu (Edit, Cancel, Copy schedule ID, Delete) — actions per status mirror the List view so the two surfaces stay in lockstep. Menu is viewport-clamped and dismisses on outside click / scroll, same pattern as the asset menu in the Library. Drag-to-resize works for pending schedules only (the schedules PUT rejects edits to running rows, and terminal statuses are read-only): - Drag the left edge to move the start time - Drag the right edge to move the end time - Drag the body to shift the whole block in time All gestures snap to 15-minute increments to match the new-schedule click snap. Minimum duration is clamped to 5 minutes; the block clamps to the visible day on both edges. While dragging the title shows the preview range ("Start time → end time") and the block lifts with a project-tinted shadow. A short pointer click (< 4px travel) still opens the edit modal — the click and drag share the same pointerdown so the operator never has to know which gesture they made first. Implementation: replaces the <button> block with a <div> hosting three zones (left handle / body / right handle). Pointer events with setPointerCapture so drags survive losing the cursor over the block, and pointerup demotes back to click if travel was below threshold. Optimistic local update on resize, PUT /schedules/:id with just the two changed time fields, refetch to reconcile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:33:57 -04:00
transition: background 80ms, border-color 80ms, transform 80ms, box-shadow 80ms;
/* Don't paint text selection while the operator drags the block. */
user-select: none; -webkit-user-select: none;
}
.epg-block:hover {
background: var(--bg-3);
border-color: var(--border-stronger);
transform: translateY(-1px);
z-index: 1;
}
feat(schedule): right-click menu + drag-to-resize on EPG event blocks Right-click any event block to open a context menu (Edit, Cancel, Copy schedule ID, Delete) — actions per status mirror the List view so the two surfaces stay in lockstep. Menu is viewport-clamped and dismisses on outside click / scroll, same pattern as the asset menu in the Library. Drag-to-resize works for pending schedules only (the schedules PUT rejects edits to running rows, and terminal statuses are read-only): - Drag the left edge to move the start time - Drag the right edge to move the end time - Drag the body to shift the whole block in time All gestures snap to 15-minute increments to match the new-schedule click snap. Minimum duration is clamped to 5 minutes; the block clamps to the visible day on both edges. While dragging the title shows the preview range ("Start time → end time") and the block lifts with a project-tinted shadow. A short pointer click (< 4px travel) still opens the edit modal — the click and drag share the same pointerdown so the operator never has to know which gesture they made first. Implementation: replaces the <button> block with a <div> hosting three zones (left handle / body / right handle). Pointer events with setPointerCapture so drags survive losing the cursor over the block, and pointerup demotes back to click if travel was below threshold. Optimistic local update on resize, PUT /schedules/:id with just the two changed time fields, refetch to reconcile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:33:57 -04:00
.epg-block.dragging {
transform: translateY(-1px);
z-index: 10;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5), 0 0 0 1px var(--epg-block-color, var(--accent));
transition: none; /* follow cursor 1:1, no easing during drag */
}
.epg-block-bar {
position: absolute;
left: 0; top: 0; bottom: 0;
width: 4px;
background: var(--epg-block-color, var(--accent));
feat(schedule): right-click menu + drag-to-resize on EPG event blocks Right-click any event block to open a context menu (Edit, Cancel, Copy schedule ID, Delete) — actions per status mirror the List view so the two surfaces stay in lockstep. Menu is viewport-clamped and dismisses on outside click / scroll, same pattern as the asset menu in the Library. Drag-to-resize works for pending schedules only (the schedules PUT rejects edits to running rows, and terminal statuses are read-only): - Drag the left edge to move the start time - Drag the right edge to move the end time - Drag the body to shift the whole block in time All gestures snap to 15-minute increments to match the new-schedule click snap. Minimum duration is clamped to 5 minutes; the block clamps to the visible day on both edges. While dragging the title shows the preview range ("Start time → end time") and the block lifts with a project-tinted shadow. A short pointer click (< 4px travel) still opens the edit modal — the click and drag share the same pointerdown so the operator never has to know which gesture they made first. Implementation: replaces the <button> block with a <div> hosting three zones (left handle / body / right handle). Pointer events with setPointerCapture so drags survive losing the cursor over the block, and pointerup demotes back to click if travel was below threshold. Optimistic local update on resize, PUT /schedules/:id with just the two changed time fields, refetch to reconcile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:33:57 -04:00
pointer-events: none; /* let the resize handle behind catch pointerdown */
}
.epg-block-body {
flex: 1; min-width: 0;
display: flex; align-items: center; gap: 8px;
padding: 0 10px 0 14px;
cursor: pointer;
overflow: hidden;
}
feat(schedule): right-click menu + drag-to-resize on EPG event blocks Right-click any event block to open a context menu (Edit, Cancel, Copy schedule ID, Delete) — actions per status mirror the List view so the two surfaces stay in lockstep. Menu is viewport-clamped and dismisses on outside click / scroll, same pattern as the asset menu in the Library. Drag-to-resize works for pending schedules only (the schedules PUT rejects edits to running rows, and terminal statuses are read-only): - Drag the left edge to move the start time - Drag the right edge to move the end time - Drag the body to shift the whole block in time All gestures snap to 15-minute increments to match the new-schedule click snap. Minimum duration is clamped to 5 minutes; the block clamps to the visible day on both edges. While dragging the title shows the preview range ("Start time → end time") and the block lifts with a project-tinted shadow. A short pointer click (< 4px travel) still opens the edit modal — the click and drag share the same pointerdown so the operator never has to know which gesture they made first. Implementation: replaces the <button> block with a <div> hosting three zones (left handle / body / right handle). Pointer events with setPointerCapture so drags survive losing the cursor over the block, and pointerup demotes back to click if travel was below threshold. Optimistic local update on resize, PUT /schedules/:id with just the two changed time fields, refetch to reconcile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:33:57 -04:00
.epg-block.resizable .epg-block-body { cursor: grab; }
.epg-block.dragging .epg-block-body { cursor: grabbing; }
.epg-block-name {
flex: 1; min-width: 0;
font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.epg-block-time {
font-size: 10px; color: var(--text-3);
flex-shrink: 0;
}
feat(schedule): right-click menu + drag-to-resize on EPG event blocks Right-click any event block to open a context menu (Edit, Cancel, Copy schedule ID, Delete) — actions per status mirror the List view so the two surfaces stay in lockstep. Menu is viewport-clamped and dismisses on outside click / scroll, same pattern as the asset menu in the Library. Drag-to-resize works for pending schedules only (the schedules PUT rejects edits to running rows, and terminal statuses are read-only): - Drag the left edge to move the start time - Drag the right edge to move the end time - Drag the body to shift the whole block in time All gestures snap to 15-minute increments to match the new-schedule click snap. Minimum duration is clamped to 5 minutes; the block clamps to the visible day on both edges. While dragging the title shows the preview range ("Start time → end time") and the block lifts with a project-tinted shadow. A short pointer click (< 4px travel) still opens the edit modal — the click and drag share the same pointerdown so the operator never has to know which gesture they made first. Implementation: replaces the <button> block with a <div> hosting three zones (left handle / body / right handle). Pointer events with setPointerCapture so drags survive losing the cursor over the block, and pointerup demotes back to click if travel was below threshold. Optimistic local update on resize, PUT /schedules/:id with just the two changed time fields, refetch to reconcile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:33:57 -04:00
/* Resize handles. 8px-wide invisible-by-default hit zones at each end;
a 2px tinted bar fades in on hover so the operator sees where the
resize affordance is. Hides for non-pending blocks (no .resizable). */
.epg-block-handle {
position: absolute;
top: 0; bottom: 0;
width: 8px;
cursor: ew-resize;
z-index: 3;
}
.epg-block-handle.left { left: 0; }
.epg-block-handle.right { right: 0; }
.epg-block-handle::after {
content: '';
position: absolute; top: 6px; bottom: 6px;
width: 2px;
background: var(--epg-block-color, var(--accent));
opacity: 0;
transition: opacity 80ms;
}
.epg-block-handle.left::after { left: 2px; }
.epg-block-handle.right::after { right: 2px; }
.epg-block-handle:hover::after,
.epg-block.dragging .epg-block-handle::after { opacity: 0.85; }
.epg-block-glyph {
display: inline-flex; align-items: center; justify-content: center;
flex-shrink: 0;
width: 14px; height: 14px;
border-radius: 3px;
font-size: 9px; font-weight: 700;
font-family: var(--font-mono);
}
.epg-block-glyph.live { color: var(--live); background: rgba(255, 59, 48, 0.16); }
.epg-block-glyph.failed { color: var(--danger); background: var(--danger-soft); }
.epg-block.live {
background: linear-gradient(180deg, rgba(255,59,48,0.16) 0%, rgba(255,59,48,0.08) 100%);
border-color: var(--live);
box-shadow: 0 0 0 1px rgba(255, 59, 48, 0.25);
}
.epg-block.failed {
background: var(--danger-soft);
border-color: var(--danger);
}
.epg-block.failed .epg-block-bar { background: var(--danger); }
.epg-block.past { opacity: 0.55; }
.epg-block.past:hover { opacity: 0.85; }
/* Now-line: vertical hot-red line that ticks across the timeline */
.epg-now {
position: absolute;
top: 0; bottom: 0;
width: 1px;
background: var(--live);
pointer-events: none;
z-index: 2;
box-shadow: 0 0 6px rgba(255, 59, 48, 0.45);
}
.epg-now-pip {
position: absolute;
top: -3px; left: -4px;
width: 9px; height: 9px;
border-radius: 50%;
background: var(--live);
box-shadow: 0 0 6px rgba(255, 59, 48, 0.7);
}
/* ---- Week view: 7 day-sections stacked vertically --------------------- */
.epg-week {
flex: 1; min-height: 0;
overflow: auto;
background: var(--bg-0);
padding: 0 0 24px;
}
.epg-week-day {
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-dayhead {
display: flex; align-items: baseline; gap: 10px;
padding: 10px 20px 6px;
font-size: 12px; font-weight: 600;
color: var(--text-2);
position: sticky; left: 0;
z-index: 1;
}
.epg-week-dayname { letter-spacing: 0.02em; }
.epg-week-todaypip {
font-size: 9.5px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.08em;
color: var(--accent-text);
background: var(--accent-soft);
padding: 1px 6px; border-radius: 99px;
}
.epg-week-row-wrap {
position: relative;
padding: 0 0 8px 20px;
background:
repeating-linear-gradient(
to right,
transparent 0,
transparent var(--epg-pph),
rgba(255,255,255,0.012) var(--epg-pph),
rgba(255,255,255,0.012) calc(var(--epg-pph) * 2)
);
}
/* ---- List view (panel reuses .schedule-row from the row-tables block) - */
.epg-list { padding: 20px; flex: 1; min-height: 0; overflow: auto; }
/* ---- Empty states ----------------------------------------------------- */
.epg-empty {
flex: 1;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 10px; padding: 80px 20px;
color: var(--text-3);
}
.epg-empty-title {
font-size: 15px; font-weight: 600;
color: var(--text-2);
}
.epg-empty-sub {
font-size: 12.5px;
}
/* ========== Cluster ========== */
.cluster-canvas {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
}
/* ========== Settings ========== */
.settings-nav {
display: flex; flex-direction: column; gap: 2px;
position: sticky; top: 0;
}
.settings-nav-item {
display: flex; align-items: center; gap: 8px;
padding: 0 10px;
height: 32px;
border-radius: var(--r-sm);
color: var(--text-2);
font-size: 12.5px;
cursor: pointer;
}
.settings-nav-item:hover { background: var(--hover); color: var(--text-1); }
.settings-nav-item.active { background: var(--accent-soft); color: var(--accent-text); }
.settings-card {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
}
.settings-card-head {
padding: 16px;
display: flex; align-items: flex-start; gap: 12px;
border-bottom: 1px solid var(--border);
}
.settings-card-icon {
width: 36px; height: 36px;
border-radius: var(--r-sm);
background: var(--accent-soft);
color: var(--accent);
display: grid; place-items: center;
flex-shrink: 0;
}
.settings-card-body { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.field { display: flex; flex-direction: column; gap: 4px; }
.field-input-wrap { display: flex; gap: 6px; align-items: center; }
.checkbox-row {
display: flex; align-items: center; gap: 8px;
font-size: 12.5px; color: var(--text-1);
cursor: pointer;
}
.checkbox-row input { accent-color: var(--accent); }
/* ========== Tokens parody ========== */
.token-hero {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 14px;
margin-bottom: 16px;
}
.token-burn-card, .token-actual-card {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 18px 20px;
position: relative;
overflow: hidden;
}
.token-burn-card {
background:
radial-gradient(ellipse at top right, rgba(255,91,91,0.12), transparent 60%),
var(--bg-1);
border-color: rgba(255,91,91,0.18);
}
.token-actual-card {
background:
radial-gradient(ellipse at bottom left, rgba(45,212,168,0.10), transparent 60%),
var(--bg-1);
border-color: rgba(45,212,168,0.18);
}
.token-card-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 700;
color: var(--text-4);
margin-bottom: 8px;
}
.token-counter {
display: flex; align-items: baseline; gap: 12px;
}
.token-flame { font-size: 32px; line-height: 1; }
.token-big {
font-size: 48px;
font-weight: 700;
letter-spacing: -0.03em;
font-variant-numeric: tabular-nums;
background: linear-gradient(180deg, #FF9D6B, #FF5B5B);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
line-height: 1.05;
}
.token-rate {
font-size: 12px;
display: flex; align-items: baseline;
font-family: var(--font-mono);
}
.token-actual-amount {
display: flex; align-items: baseline; gap: 0;
color: var(--success);
margin-bottom: 8px;
}
.token-comparison {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 0 0 12px;
margin-bottom: 16px;
overflow: hidden;
}
.token-compare-chart { padding: 0 16px; }
.token-compare-legend {
display: flex; gap: 24px;
padding: 8px 0 0;
font-size: 12px;
color: var(--text-2);
}
.token-compare-legend .dot {
display: inline-block;
width: 10px; height: 10px;
border-radius: 50%;
margin-right: 6px;
vertical-align: middle;
}
.token-grid {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 14px;
}
.token-event {
display: flex; align-items: center; gap: 12px;
padding: 10px 14px;
border-bottom: 1px solid var(--border);
}
.token-event:last-child { border-bottom: 0; }
.token-event.fresh { animation: tokenFresh 800ms ease; }
@keyframes tokenFresh {
0% { background: rgba(255,91,91,0.15); transform: translateX(-4px); }
100% { background: transparent; transform: none; }
}
.token-tiers {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.token-tier {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 16px 14px;
display: flex; flex-direction: column;
gap: 6px;
position: relative;
}
.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);
}
.token-tier-badge {
position: absolute;
top: -8px; right: 12px;
background: var(--accent);
color: white;
font-size: 9.5px;
font-weight: 700;
letter-spacing: 0.06em;
padding: 3px 8px;
border-radius: 99px;
}
.token-tier-name { font-size: 13px; font-weight: 700; letter-spacing: -0.01em; }
.token-tier-desc { font-size: 11px; color: var(--text-3); min-height: 32px; line-height: 1.4; }
.token-tier-price { margin-top: 6px; display: flex; align-items: baseline; }
.token-tier-tokens { font-size: 11px; color: var(--text-3); padding-top: 4px; border-top: 1px solid var(--border); }
.token-footnote {
margin-top: 20px;
padding: 14px 16px;
background: var(--warning-soft);
border: 1px solid rgba(245,166,35,0.2);
border-radius: var(--r-md);
display: flex; gap: 12px;
font-size: 12.5px;
color: var(--text-2);
line-height: 1.55;
}
.token-footnote svg { color: var(--warning); flex-shrink: 0; margin-top: 2px; }
.token-footnote strong { color: var(--warning); }
/* ========== Global Search ========== */
.search-wrap {
position: relative;
flex-shrink: 0;
}
.search-wrap .search {
width: 280px;
}
.search-wrap .search.is-open {
border-color: var(--accent);
background: var(--bg-2);
}
.search-results {
position: absolute;
top: calc(100% + 6px);
right: 0;
width: 440px;
max-width: 90vw;
max-height: 460px;
overflow-y: auto;
background: var(--bg-1);
border: 1px solid var(--border-stronger);
border-radius: var(--r-md);
box-shadow: var(--shadow-pop);
padding: 4px;
z-index: 60;
}
.search-empty {
padding: 18px 12px;
text-align: center;
font-size: 12px;
color: var(--text-3);
}
.search-result {
display: grid;
grid-template-columns: 24px 1fr auto;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: var(--r-sm);
cursor: pointer;
color: var(--text-1);
}
.search-result.active {
background: var(--accent-soft);
}
.search-result-icon {
width: 24px; height: 24px;
border-radius: 5px;
background: var(--bg-3);
color: var(--text-2);
display: grid; place-items: center;
flex-shrink: 0;
}
.search-result.active .search-result-icon {
background: var(--accent-soft-2);
color: var(--accent-text);
}
.search-result-text {
min-width: 0;
display: flex;
flex-direction: column;
gap: 1px;
}
.search-result-label {
font-size: 12.5px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-result-sub {
font-size: 11px;
color: var(--text-3);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-result-kind {
font-family: var(--font-mono);
font-size: 9.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-4);
background: var(--bg-2);
border: 1px solid var(--border);
padding: 2px 6px;
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-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); }
.search-result-kind.kind-user { color: #D5B8FF; border-color: rgba(181,124,250,0.25); }
.search-result-kind.kind-nav { color: var(--text-2); }
/* ========== Asset right-click menu ========== */
.ctx-menu {
position: fixed;
z-index: 80;
min-width: 220px;
max-width: 280px;
background: var(--bg-1);
border: 1px solid var(--border-stronger);
border-radius: var(--r-md);
box-shadow: var(--shadow-pop);
padding: 4px;
display: flex;
flex-direction: column;
font-size: 12.5px;
}
.ctx-menu .ctx-header {
padding: 8px 10px 6px;
font-size: 11px;
font-weight: 600;
color: var(--text-3);
border-bottom: 1px solid var(--border);
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ctx-menu .ctx-divider {
height: 1px;
background: var(--border);
margin: 4px 2px;
}
.ctx-menu .ctx-section-label {
font-size: 9.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-4);
padding: 6px 10px 4px;
}
.ctx-menu .ctx-empty {
padding: 6px 10px 10px;
font-size: 11.5px;
color: var(--text-3);
font-style: italic;
}
.ctx-menu button {
display: flex;
align-items: center;
gap: 8px;
background: transparent;
border: 0;
color: var(--text-1);
font-size: 12.5px;
padding: 7px 10px;
border-radius: 4px;
cursor: pointer;
text-align: left;
width: 100%;
}
.ctx-menu button svg { color: var(--text-3); flex-shrink: 0; }
.ctx-menu button:hover:not(:disabled) { background: var(--bg-3); }
.ctx-menu button:hover:not(:disabled) svg { color: var(--text-1); }
.ctx-menu button:disabled {
opacity: 0.5;
cursor: default;
}
.ctx-menu button.danger { color: var(--danger); }
.ctx-menu button.danger svg { color: var(--danger); }
.ctx-menu button.danger:hover:not(:disabled) {
background: var(--danger-soft);
}