:root { --bg-main: #0e0e10; --bg-sidebar: #18181b; --bg-card: #1f1f23; --accent: #9146FF; --accent-hover: #772ce8; --text: #efeff1; --text-secondary: #adadb8; --success: #00c853; --error: #ff4444; --warning: #ffab00; /* Soft border that adapts to theme — used by post-4.5.x UI additions (filter input, sort select, bulk bar) so they don't visually break in light theme. */ --border-soft: rgba(255, 255, 255, 0.1); } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg-main); color: var(--text); height: 100vh; overflow: hidden; } .app { display: flex; height: 100vh; } /* Sidebar */ .sidebar { width: 300px; background: var(--bg-sidebar); display: flex; flex-direction: column; border-right: 1px solid rgba(255,255,255,0.1); overflow-y: auto; overflow-x: hidden; } .logo { padding: 20px; font-size: 18px; font-weight: bold; color: var(--accent); display: flex; align-items: center; gap: 10px; border-bottom: 1px solid rgba(255,255,255,0.1); } .logo svg { width: 28px; height: 28px; fill: currentColor; } .nav { padding: 15px; } .nav-item { display: flex; align-items: center; gap: 12px; padding: 12px 15px; border-radius: 6px; cursor: pointer; color: var(--text-secondary); transition: all 0.2s; margin-bottom: 4px; font-size: 14px; } .nav-item:hover { background: rgba(145, 71, 255, 0.15); color: var(--text); } .nav-item.active { background: var(--accent); color: white; } .nav-item:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(145, 70, 255, 0.55); } .nav-item svg { width: 20px; height: 20px; } .section-title { font-size: 11px; text-transform: uppercase; color: var(--text-secondary); padding: 15px 15px 8px; letter-spacing: 0.5px; display: flex; align-items: center; gap: 6px; justify-content: space-between; } /* Inner label group inside .section-title — keeps the title text and the streamer-section-counter aligned on the text baseline as a single unit, separate from the bulk-remove X button that pins to the right. */ .section-title-label { display: flex; align-items: baseline; gap: 8px; } /* Compact spacing variant — applied when the sidebar's streamer-list filter input is visible directly below the title, so the default padding-bottom + the filter's own margin don't double up into a visually loose gap. */ .section-title.compact { margin-bottom: 4px; } .streamers { flex: 1; overflow-y: auto; padding: 0 10px; } .streamer-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 6px; cursor: pointer; color: var(--text-secondary); transition: all 0.2s; font-size: 14px; } .streamer-item:hover { background: rgba(255,255,255,0.05); color: var(--text); } .streamer-item:focus-visible { outline: none; box-shadow: inset 0 0 0 2px rgba(145, 70, 255, 0.55); } .streamer-item.active { background: linear-gradient(90deg, rgba(145, 71, 255, 0.28) 0%, rgba(145, 71, 255, 0.08) 100%); color: var(--text); border-left: 3px solid var(--accent); box-shadow: inset 0 0 0 1px rgba(145, 71, 255, 0.18); position: relative; } .streamer-item.active::after { content: ''; position: absolute; right: 0; top: 50%; transform: translateY(-50%); width: 3px; height: 60%; background: var(--accent); border-radius: 2px 0 0 2px; opacity: 0.85; } .streamer-item.active .streamer-name { font-weight: 600; } /* VOD filter row — sits above the grid: filter input, clear, sort, count, hide-toggle. Previously every property was inline-styled on the
in index.html. */ .vod-filter-row { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; } .vod-filter-row .vod-sort-label { margin-left: 8px; } .vod-filter-row .vod-filter-count { min-width: 80px; } /* ============================================ VOD BULK-ACTION BAR — slides in when 1+ VOD is selected ============================================ Lives between the filter row and the VOD grid. Used to be all inline-styled in HTML; extracted to a class so the slide-in animation has somewhere to live and the styling stays consistent with the rest of the action surfaces. */ .vod-bulk-bar { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: linear-gradient(135deg, rgba(145, 70, 255, 0.15) 0%, rgba(145, 70, 255, 0.08) 100%); border: 1px solid rgba(145, 70, 255, 0.42); border-radius: 8px; margin-bottom: 12px; flex-wrap: wrap; box-shadow: 0 4px 16px rgba(145, 70, 255, 0.10); /* Animation fires whenever the bar transitions from display:none (.is-hidden present) back to display:flex (.is-hidden removed), because Animation events restart on each display change. */ animation: vod-bulk-bar-slide 0.22s cubic-bezier(0.16, 1, 0.3, 1); } @keyframes vod-bulk-bar-slide { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .vod-bulk-count { color: var(--text); font-size: 13px; font-weight: 600; letter-spacing: 0.2px; } .vod-bulk-spacer { flex: 1; } /* ============================================ CLIP-CUTTER MODAL — themed to match the rest of the app ============================================ This modal had a stack of hard-coded #2b2b2b / #E5A00D / #888 / #333 colors from before the Twitch-purple theme was a thing. Extracted to classes and re-themed using CSS vars + accent. */ .clip-modal { max-width: 500px; background: var(--bg-card); } .clip-modal-title { text-align: center; margin-bottom: 20px; color: var(--text); } .clip-modal-field { margin-bottom: 15px; } .clip-modal-label { display: block; margin-bottom: 6px; color: var(--text); font-size: 13px; font-weight: 500; } .clip-modal-meta { color: var(--text-secondary); font-size: 12px; } .clip-modal-time-row { display: flex; align-items: center; gap: 10px; margin-top: 8px; } .clip-modal-time-input { width: 100px; background: var(--bg-elevated); border: 1px solid var(--border-soft); border-radius: 6px; padding: 6px 10px; color: var(--text); font-family: 'Consolas', 'Segoe UI Mono', monospace; text-align: center; } .clip-modal-part-input { width: 100px; background: var(--bg-elevated); border: 1px solid var(--border-soft); border-radius: 6px; padding: 8px 12px; color: var(--text); } .clip-modal-template-input { width: 100%; background: var(--bg-elevated); border: 1px solid var(--border-soft); border-radius: 6px; padding: 8px 12px; color: var(--text); font-family: 'Consolas', 'Segoe UI Mono', monospace; } .clip-modal-duration { text-align: center; margin-bottom: 20px; padding: 10px; background: rgba(0, 200, 83, 0.06); border: 1px solid rgba(0, 200, 83, 0.18); border-radius: 6px; } .clip-modal-duration-value { color: var(--success); font-weight: 600; font-family: 'Consolas', 'Segoe UI Mono', monospace; font-size: 14px; } /* updateClipDuration flips this class when end <= start so the value reads as a clear "Ungueltig!" / error message in red. */ .clip-modal-duration-value.invalid { color: var(--error); } .clip-modal-hint { color: var(--text-secondary); font-size: 12px; margin-top: 6px; line-height: 1.4; } .clip-template-wrap { display: none; margin-top: 10px; } .clip-template-wrap.shown { display: block; } /* Template-Guide button below the clip-template input — small offset from the lint badge that sits directly above it. Was a one-off inline style on the button. */ .clip-template-wrap .btn-secondary { margin-top: 8px; } .clip-radio-row { display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 8px; padding: 6px 8px; border-radius: 6px; transition: background 0.15s; } .clip-radio-row:hover { background: rgba(145, 70, 255, 0.06); } .clip-radio-label { color: var(--text-secondary); font-size: 13px; flex: 1; transition: color 0.15s; } .clip-radio-row:has(input[type="radio"]:checked) .clip-radio-label { color: var(--text); font-weight: 500; } .clip-modal-actions { text-align: center; margin-top: 10px; } /* ============================================ EVENTS / CHAT VIEWER MODALS — shared structure ============================================ Both viewers used to roll their own inline-styled list container + status row. Extracted into a small shared family so the look stays consistent if either one is touched independently later. */ .viewer-modal { display: flex; flex-direction: column; } .viewer-modal-events { max-width: 700px; max-height: 80vh; } .viewer-modal-chat { max-width: 800px; height: 80vh; } .viewer-modal-title { margin-top: 0; } .viewer-modal-status { color: var(--text-secondary); font-size: 12px; margin-bottom: 8px; } .viewer-modal-status-inline { margin-bottom: 0; } .viewer-modal-list { flex: 1; overflow-y: auto; background: var(--bg-main); border: 1px solid var(--border-soft); border-radius: 6px; padding: 8px; } .viewer-modal-list-chat { font-family: 'Consolas', 'Segoe UI Mono', monospace; font-size: 12px; } .viewer-modal-filter-row { display: flex; margin-bottom: 8px; gap: 8px; flex-wrap: wrap; align-items: center; } .viewer-modal-filter-input { flex: 1; min-width: 160px; background: var(--bg-card); border: 1px solid var(--border-soft); border-radius: 6px; padding: 6px 10px; color: var(--text); font-size: 13px; } /* ============================================ FILTER INPUTS — shared family for search/filter boxes ============================================ Used by the primary VOD filter above the grid, the archive search, and (in a compact variant) the sidebar streamer-list filter that only shows once the list crosses the threshold. */ .filter-input { flex: 1; min-width: 180px; background: var(--bg-card); border: 1px solid var(--border-soft); border-radius: 6px; padding: 8px 12px; color: var(--text); font-size: 13px; } .filter-input:hover:not(:focus):not(:disabled) { border-color: rgba(145, 70, 255, 0.45); } .filter-input.compact { width: calc(100% - 16px); margin: 0 8px 8px; padding: 4px 8px; border-radius: 4px; font-size: 12px; flex: none; min-width: 0; } .filter-input.flex-1-1-240 { flex: 1 1 240px; min-width: 200px; border-radius: 4px; padding: 6px 10px; } /* Monospace input utility — for filename-template fields and similar places where the value is expected to be a code-shaped string. */ .input-monospace { font-family: 'Consolas', 'Segoe UI Mono', monospace; } .streamer-item .remove { margin-left: auto; opacity: 0; color: var(--error); cursor: pointer; } .streamer-item:hover .remove, .streamer-item .remove:focus-visible { opacity: 1; } .streamer-item .remove:focus-visible, .queue-item .remove:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(255, 70, 70, 0.5); border-radius: 3px; } /* Keyboard focus rings for the AUTO / VOD / REC chip buttons. Each picks up its own accent: AUTO + VOD use semantic green/blue tints matching their active state, REC the red of the live dot. */ .streamer-auto:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(0, 200, 83, 0.55); } .streamer-vod:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.55); } .streamer-rec:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(255, 68, 68, 0.55); } /* Live-dot — red pulsing indicator shown next to a streamer's name in the sidebar when they are currently broadcasting on Twitch. */ .streamer-live-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #e91916; flex-shrink: 0; animation: streamer-live-pulse 1.6s ease-in-out infinite; box-shadow: 0 0 0 0 rgba(233, 25, 22, 0.55); } @keyframes streamer-live-pulse { 0% { box-shadow: 0 0 0 0 rgba(233, 25, 22, 0.55); } 70% { box-shadow: 0 0 0 6px rgba(233, 25, 22, 0); } 100% { box-shadow: 0 0 0 0 rgba(233, 25, 22, 0); } } .streamer-name.is-live { color: var(--text); font-weight: 600; } /* ============================================ VOD SKELETON CARDS — shown while VODs load ============================================ */ .vod-card-skeleton { cursor: default; pointer-events: none; border-color: transparent !important; box-shadow: none !important; transform: none !important; } .vod-card-skeleton .vod-skel-thumb { width: 100%; aspect-ratio: 16/9; background: linear-gradient(90deg, var(--bg-elevated) 0%, rgba(255,255,255,0.04) 50%, var(--bg-elevated) 100%); background-size: 200% 100%; animation: skel-shimmer 1.5s linear infinite; } .vod-card-skeleton .vod-skel-line { height: 14px; background: linear-gradient(90deg, var(--bg-elevated) 0%, rgba(255,255,255,0.06) 50%, var(--bg-elevated) 100%); background-size: 200% 100%; border-radius: 3px; animation: skel-shimmer 1.5s linear infinite; } /* Three skeleton-line variants for the VOD card placeholder — match the visual rhythm of a real card (title line, then two shorter meta lines). Replaces inline width/height/margin-top declarations. */ .vod-card-skeleton .vod-skel-line.title { width: 85%; } .vod-card-skeleton .vod-skel-line.meta-1 { width: 55%; height: 10px; margin-top: 8px; } .vod-card-skeleton .vod-skel-line.meta-2 { width: 40%; height: 10px; margin-top: 6px; } @keyframes skel-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } /* ============================================ TAB SWITCH FADE — subtle 180ms ease-in ============================================ */ .tab-content.active { animation: tab-fade-in 0.18s ease-out; } @keyframes tab-fade-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } /* ============================================ SCROLLBAR — twitch-themed, thin, subtle ============================================ */ *::-webkit-scrollbar { width: 10px; height: 10px; } *::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.02); } *::-webkit-scrollbar-thumb { background: rgba(145, 70, 255, 0.25); border-radius: 5px; border: 2px solid transparent; background-clip: padding-box; transition: background 0.2s; } *::-webkit-scrollbar-thumb:hover { background: rgba(145, 70, 255, 0.55); background-clip: padding-box; border: 2px solid transparent; } *::-webkit-scrollbar-corner { background: transparent; } .add-streamer { padding: 10px; display: flex; gap: 8px; } .add-streamer input { flex: 1; background: var(--bg-card); border: 1px solid var(--border-soft); border-radius: 6px; padding: 8px 12px; color: var(--text); font-size: 13px; transition: border-color 0.18s, box-shadow 0.18s, background 0.18s; } .add-streamer input:focus { outline: none; border-color: var(--accent); background: rgba(145, 70, 255, 0.08); box-shadow: 0 0 0 3px rgba(145, 70, 255, 0.18); } .add-streamer input::placeholder { color: var(--text-secondary); } .add-streamer button { background: var(--accent); border: none; border-radius: 6px; color: white; width: 36px; cursor: pointer; font-size: 18px; font-weight: 600; transition: background 0.18s, transform 0.12s, box-shadow 0.18s; } .add-streamer button:hover { background: var(--accent-hover); box-shadow: 0 4px 14px rgba(145, 70, 255, 0.35); } .add-streamer button:active { transform: scale(0.96); } /* ============================================ GLOBAL TEXT-INPUT POLISH — focus ring + smooth transitions ============================================ */ input[type="text"], input[type="search"], input[type="number"], input[type="password"], input[type="email"], textarea, select { transition: border-color 0.18s, box-shadow 0.18s, background 0.18s; } input[type="text"]:focus, input[type="search"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="email"]:focus, textarea:focus, select:focus { outline: none; border-color: var(--accent) !important; box-shadow: 0 0 0 3px rgba(145, 70, 255, 0.18); } /* ============================================ CUSTOM CHECKBOX — modern Twitch-purple ============================================ Replaces the OS gray box with a 16px rounded square that fills purple + draws a white check when checked. Smooth 0.18s transition on every state change. Focus ring matches inputs. */ input[type="checkbox"] { appearance: none; -webkit-appearance: none; width: 16px; height: 16px; flex-shrink: 0; border: 1.5px solid var(--border-soft); border-radius: 4px; background: var(--bg-card); cursor: pointer; position: relative; transition: background 0.18s, border-color 0.18s, box-shadow 0.18s, transform 0.12s; vertical-align: middle; } input[type="checkbox"]:hover:not(:disabled) { border-color: rgba(145, 70, 255, 0.6); } input[type="checkbox"]:checked { background: var(--accent); border-color: var(--accent); } input[type="checkbox"]:checked::after { content: ''; position: absolute; left: 4px; top: 0.5px; width: 5px; height: 9px; border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg); } input[type="checkbox"]:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(145, 70, 255, 0.25); } input[type="checkbox"]:active:not(:disabled) { transform: scale(0.92); } input[type="checkbox"]:disabled { opacity: 0.45; cursor: not-allowed; } /* ============================================ CUSTOM RADIO — matches the checkbox visual ============================================ */ input[type="radio"] { appearance: none; -webkit-appearance: none; width: 16px; height: 16px; flex-shrink: 0; border: 1.5px solid var(--border-soft); border-radius: 50%; background: var(--bg-card); cursor: pointer; position: relative; transition: background 0.18s, border-color 0.18s, box-shadow 0.18s, transform 0.12s; vertical-align: middle; } input[type="radio"]:hover:not(:disabled) { border-color: rgba(145, 70, 255, 0.6); } input[type="radio"]:checked { border-color: var(--accent); background: var(--bg-card); } input[type="radio"]:checked::after { content: ''; position: absolute; inset: 3px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 6px rgba(145, 70, 255, 0.45); } input[type="radio"]:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(145, 70, 255, 0.25); } input[type="radio"]:active:not(:disabled) { transform: scale(0.92); } /* ============================================ CUSTOM SELECT — chevron via inline SVG background ============================================ */ select { appearance: none; -webkit-appearance: none; -moz-appearance: none; background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23999'%3E%3Cpath d='M4 6l4 4 4-4z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; background-size: 14px; padding-right: 28px !important; cursor: pointer; } select:hover:not(:disabled) { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%239146FF'%3E%3Cpath d='M4 6l4 4 4-4z'/%3E%3C/svg%3E"); } select option { background: var(--bg-card); color: var(--text); } /* Compact-row select — used in tool/filter rows that sit OUTSIDE the .form-group container (which has its own select padding rules). Without this class the bare