diff --git a/server/pyproject.toml b/server/pyproject.toml
index da52924..8f6116b 100644
--- a/server/pyproject.toml
+++ b/server/pyproject.toml
@@ -36,6 +36,7 @@ dependencies = [
"python-json-logger>=3.1.0",
"python-dateutil>=2.9.0",
"python-multipart>=0.0.12",
+ "jinja2>=3.1.0",
"wmi>=1.5.1; sys_platform == 'win32'",
"zeroconf>=0.131.0",
"pyserial>=3.5",
diff --git a/server/src/wled_controller/main.py b/server/src/wled_controller/main.py
index 9f9a5b9..d5ee020 100644
--- a/server/src/wled_controller/main.py
+++ b/server/src/wled_controller/main.py
@@ -6,8 +6,10 @@ from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
-from fastapi.responses import JSONResponse, FileResponse
+from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
+from fastapi.templating import Jinja2Templates
+from starlette.requests import Request
from wled_controller import __version__
from wled_controller.api import router
@@ -262,6 +264,10 @@ if static_path.exists():
else:
logger.warning(f"Static files directory not found: {static_path}")
+# Jinja2 templates
+templates_path = Path(__file__).parent / "templates"
+templates = Jinja2Templates(directory=str(templates_path))
+
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
@@ -279,20 +285,9 @@ async def global_exception_handler(request, exc):
@app.get("/")
-async def root():
+async def root(request: Request):
"""Serve the web UI dashboard."""
- static_path = Path(__file__).parent / "static" / "index.html"
- if static_path.exists():
- return FileResponse(static_path)
-
- # Fallback to JSON if static files not found
- return {
- "name": "LED Grab",
- "version": __version__,
- "docs": "/docs",
- "health": "/health",
- "api": "/api/v1",
- }
+ return templates.TemplateResponse(request, "index.html")
if __name__ == "__main__":
diff --git a/server/src/wled_controller/static/css/base.css b/server/src/wled_controller/static/css/base.css
new file mode 100644
index 0000000..8070b3a
--- /dev/null
+++ b/server/src/wled_controller/static/css/base.css
@@ -0,0 +1,61 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --primary-color: #4CAF50;
+ --danger-color: #f44336;
+ --warning-color: #ff9800;
+ --info-color: #2196F3;
+}
+
+/* Dark theme (default) */
+[data-theme="dark"] {
+ --bg-color: #1a1a1a;
+ --card-bg: #2d2d2d;
+ --text-color: #e0e0e0;
+ --border-color: #404040;
+ --display-badge-bg: rgba(0, 0, 0, 0.4);
+ color-scheme: dark;
+}
+
+/* Light theme */
+[data-theme="light"] {
+ --bg-color: #f5f5f5;
+ --card-bg: #ffffff;
+ --text-color: #333333;
+ --border-color: #e0e0e0;
+ --display-badge-bg: rgba(255, 255, 255, 0.85);
+ color-scheme: light;
+}
+
+/* Default to dark theme */
+body {
+ background: var(--bg-color);
+ color: var(--text-color);
+}
+
+html {
+ background: var(--bg-color);
+ overflow-y: scroll;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ background: var(--bg-color);
+ color: var(--text-color);
+ line-height: 1.6;
+}
+
+body.modal-open {
+ position: fixed;
+ width: 100%;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
diff --git a/server/src/wled_controller/static/css/calibration.css b/server/src/wled_controller/static/css/calibration.css
new file mode 100644
index 0000000..ba68c05
--- /dev/null
+++ b/server/src/wled_controller/static/css/calibration.css
@@ -0,0 +1,423 @@
+/* Interactive Calibration Preview Edges */
+.calibration-preview {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 16 / 9;
+ margin: 40px auto 40px;
+ background: var(--card-bg);
+ border: 2px solid var(--border-color);
+ border-radius: 8px;
+ overflow: visible;
+}
+
+#calibration-preview-canvas {
+ position: absolute;
+ top: -40px;
+ left: -40px;
+ width: calc(100% + 80px);
+ height: calc(100% + 80px);
+ pointer-events: none;
+ z-index: 3;
+}
+
+.preview-screen {
+ position: absolute;
+ top: 37px;
+ left: 57px;
+ right: 57px;
+ bottom: 37px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 4px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ color: white;
+ font-size: 14px;
+}
+
+.preview-screen-border-width {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.preview-screen-border-width label {
+ white-space: nowrap;
+}
+
+.preview-screen-border-width input {
+ width: 52px;
+ padding: 2px 4px;
+ font-size: 12px;
+ border: 1px solid rgba(255, 255, 255, 0.4);
+ border-radius: 3px;
+ background: rgba(255, 255, 255, 0.15);
+ color: white;
+ text-align: center;
+}
+
+.preview-screen-border-width input:focus {
+ outline: none;
+ border-color: rgba(255, 255, 255, 0.7);
+ background: rgba(255, 255, 255, 0.25);
+}
+
+.preview-screen-total {
+ font-size: 16px;
+ font-weight: 600;
+ opacity: 0.9;
+ transition: color 0.2s;
+ cursor: pointer;
+ user-select: none;
+}
+
+.preview-screen-total:hover {
+ opacity: 1;
+}
+
+.preview-screen-total.mismatch {
+ color: #FFC107;
+}
+
+.inputs-dimmed .edge-led-input {
+ opacity: 0.2;
+ pointer-events: none;
+}
+
+.preview-screen-controls {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.preview-edge {
+ position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 11px;
+ color: var(--text-secondary);
+ background: rgba(128, 128, 128, 0.15);
+ transition: background 0.2s;
+ z-index: 2;
+ user-select: none;
+}
+
+/* Edge test toggle zones — positioned outside the container border */
+.edge-toggle {
+ position: absolute;
+ cursor: pointer;
+ z-index: 1;
+ background: rgba(128, 128, 128, 0.1);
+ border: 1px solid rgba(128, 128, 128, 0.35);
+ border-radius: 3px;
+ transition: background 0.2s, box-shadow 0.2s;
+}
+
+.edge-toggle:hover {
+ background: rgba(128, 128, 128, 0.25);
+}
+
+.preview-edge.edge-disabled {
+ opacity: 0.25;
+ pointer-events: none;
+}
+
+.preview-edge.edge-disabled .edge-led-input {
+ pointer-events: auto;
+ opacity: 1;
+}
+
+.edge-toggle.edge-disabled {
+ opacity: 0.15;
+ pointer-events: none;
+ cursor: default;
+}
+
+.toggle-top {
+ top: -16px;
+ left: 56px;
+ right: 56px;
+ height: 16px;
+}
+
+.toggle-bottom {
+ bottom: -16px;
+ left: 56px;
+ right: 56px;
+ height: 16px;
+}
+
+.toggle-left {
+ left: -16px;
+ top: 36px;
+ bottom: 36px;
+ width: 16px;
+}
+
+.toggle-right {
+ right: -16px;
+ top: 36px;
+ bottom: 36px;
+ width: 16px;
+}
+
+.edge-top {
+ top: 0;
+ left: 56px;
+ right: 56px;
+ height: 36px;
+ border-radius: 6px 6px 0 0;
+ flex-direction: row;
+ gap: 8px;
+}
+
+.edge-bottom {
+ bottom: 0;
+ left: 56px;
+ right: 56px;
+ height: 36px;
+ border-radius: 0 0 6px 6px;
+ flex-direction: row;
+ gap: 8px;
+}
+
+.edge-left {
+ left: 0;
+ top: 36px;
+ bottom: 36px;
+ width: 56px;
+ flex-direction: column;
+ border-radius: 6px 0 0 6px;
+ gap: 4px;
+}
+
+.edge-right {
+ right: 0;
+ top: 36px;
+ bottom: 36px;
+ width: 56px;
+ flex-direction: column;
+ border-radius: 0 6px 6px 0;
+ gap: 4px;
+}
+
+.edge-led-input {
+ width: 46px;
+ padding: 2px 2px;
+ font-size: 12px;
+ box-sizing: border-box;
+ max-height: 100%;
+ text-align: center;
+ background: rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 3px;
+ color: inherit;
+}
+
+.edge-led-input:focus {
+ border-color: var(--primary-color);
+ outline: none;
+}
+
+.edge-top .edge-led-input,
+.edge-bottom .edge-led-input {
+ width: 56px;
+}
+
+/* Hide spinner arrows on edge inputs to save space */
+.edge-led-input::-webkit-outer-spin-button,
+.edge-led-input::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+.edge-led-input[type=number] {
+ -moz-appearance: textfield;
+}
+
+/* Edge span bars */
+.edge-span-bar {
+ position: absolute;
+ background: rgba(76, 175, 80, 0.3);
+ border: 1px solid rgba(76, 175, 80, 0.5);
+ border-radius: 2px;
+ cursor: grab;
+ transition: background 0.15s;
+}
+
+.edge-span-bar:hover {
+ background: rgba(76, 175, 80, 0.45);
+}
+
+.edge-span-bar:active {
+ cursor: grabbing;
+}
+
+/* Horizontal edges: bar spans left-right */
+.edge-top .edge-span-bar,
+.edge-bottom .edge-span-bar {
+ top: 0;
+ bottom: 0;
+}
+
+/* Vertical edges: bar spans top-bottom */
+.edge-left .edge-span-bar,
+.edge-right .edge-span-bar {
+ left: 0;
+ right: 0;
+}
+
+/* Resize handles — large transparent hit area with narrow visible strip */
+.edge-span-handle {
+ position: absolute;
+ background: transparent;
+ z-index: 3;
+ opacity: 0;
+ transition: opacity 0.15s;
+}
+
+.edge-span-handle::after {
+ content: '';
+ position: absolute;
+ background: rgba(255, 255, 255, 0.75);
+ border: 1px solid rgba(76, 175, 80, 0.7);
+ border-radius: 2px;
+}
+
+.edge-span-bar:hover .edge-span-handle {
+ opacity: 1;
+}
+
+/* Horizontal handles */
+.edge-top .edge-span-handle,
+.edge-bottom .edge-span-handle {
+ top: 0;
+ bottom: 0;
+ width: 16px;
+ cursor: ew-resize;
+}
+
+.edge-top .edge-span-handle::after,
+.edge-bottom .edge-span-handle::after {
+ top: 3px;
+ bottom: 3px;
+ left: 6px;
+ width: 4px;
+}
+
+.edge-top .edge-span-handle-start,
+.edge-bottom .edge-span-handle-start {
+ left: -8px;
+}
+
+.edge-top .edge-span-handle-end,
+.edge-bottom .edge-span-handle-end {
+ right: -8px;
+}
+
+/* Vertical handles */
+.edge-left .edge-span-handle,
+.edge-right .edge-span-handle {
+ left: 0;
+ right: 0;
+ height: 16px;
+ cursor: ns-resize;
+}
+
+.edge-left .edge-span-handle::after,
+.edge-right .edge-span-handle::after {
+ left: 3px;
+ right: 3px;
+ top: 6px;
+ height: 4px;
+}
+
+.edge-left .edge-span-handle-start,
+.edge-right .edge-span-handle-start {
+ top: -8px;
+}
+
+.edge-left .edge-span-handle-end,
+.edge-right .edge-span-handle-end {
+ bottom: -8px;
+}
+
+/* Ensure LED input is above span bar */
+.edge-led-input {
+ position: relative;
+ z-index: 2;
+}
+
+/* Corner start-position buttons */
+.preview-corner {
+ position: absolute;
+ width: 56px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 18px;
+ line-height: 1;
+ color: rgba(128, 128, 128, 0.4);
+ cursor: pointer;
+ z-index: 5;
+ transition: color 0.2s, transform 0.2s, text-shadow 0.2s;
+ user-select: none;
+}
+
+.preview-corner:hover {
+ color: rgba(76, 175, 80, 0.6);
+}
+
+.preview-corner.active:hover {
+ transform: none;
+}
+
+.preview-corner.active {
+ color: #4CAF50;
+ text-shadow: 0 0 8px rgba(76, 175, 80, 0.6);
+ font-size: 22px;
+}
+
+.corner-top-left { top: 0; left: 0; }
+.corner-top-right { top: 0; right: 0; }
+.corner-bottom-left { bottom: 0; left: 0; }
+.corner-bottom-right { bottom: 0; right: 0; }
+
+/* Direction toggle inside screen */
+.direction-toggle {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ height: 26px;
+ padding: 0 10px;
+ background: rgba(255, 255, 255, 0.15);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ border-radius: 12px;
+ color: white;
+ font-family: inherit;
+ font-size: 12px;
+ box-sizing: border-box;
+ cursor: pointer;
+ transition: background 0.2s;
+ user-select: none;
+}
+
+.direction-toggle:hover {
+ background: rgba(255, 255, 255, 0.25);
+}
+
+.direction-toggle #direction-icon {
+ font-size: 14px;
+}
+
+.preview-hint {
+ text-align: center;
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ margin-top: 8px;
+}
diff --git a/server/src/wled_controller/static/css/cards.css b/server/src/wled_controller/static/css/cards.css
new file mode 100644
index 0000000..b810201
--- /dev/null
+++ b/server/src/wled_controller/static/css/cards.css
@@ -0,0 +1,592 @@
+section {
+ margin-bottom: 40px;
+}
+
+.displays-grid,
+.devices-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 20px;
+}
+
+.devices-grid > .loading,
+.devices-grid > .loading-spinner {
+ grid-column: 1 / -1;
+}
+
+.add-device-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border: 2px dashed var(--border-color);
+ background: transparent;
+ min-height: 160px;
+ transition: border-color 0.2s, background 0.2s;
+}
+
+.add-device-card:hover {
+ border-color: var(--primary-color);
+ background: rgba(33, 150, 243, 0.05);
+}
+
+.add-device-icon {
+ font-size: 2.5rem;
+ font-weight: 300;
+ color: var(--text-secondary);
+ line-height: 1;
+ transition: color 0.2s;
+}
+
+.add-device-card:hover .add-device-icon {
+ color: var(--primary-color);
+}
+
+.add-device-label {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ margin-top: 8px;
+}
+
+.card {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 12px 20px 20px;
+ position: relative;
+ transition: transform 0.2s, box-shadow 0.2s;
+ display: flex;
+ flex-direction: column;
+}
+
+.card:hover {
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+}
+
+.card-tutorial-btn {
+ position: absolute;
+ bottom: 10px;
+ right: 10px;
+ background: none;
+ border: 1.5px solid var(--border-color);
+ color: var(--text-muted, #777);
+ font-size: 0.7rem;
+ font-weight: bold;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border-radius: 50%;
+ transition: color 0.2s, background 0.2s, border-color 0.2s;
+ padding: 0;
+}
+
+.card-tutorial-btn:hover {
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+
+.card-top-actions {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ display: flex;
+ align-items: center;
+ gap: 2px;
+}
+
+.card-top-actions .card-remove-btn {
+ position: static;
+}
+
+.card-power-btn {
+ background: none;
+ border: none;
+ color: #777;
+ font-size: 1rem;
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border-radius: 4px;
+ transition: color 0.2s, background 0.2s;
+}
+
+.card-power-btn:hover {
+ color: var(--primary-color);
+ background: rgba(76, 175, 80, 0.1);
+}
+
+.card-remove-btn {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ background: none;
+ border: none;
+ color: #777;
+ font-size: 1rem;
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border-radius: 4px;
+ transition: color 0.2s, background 0.2s;
+}
+
+.card-remove-btn:hover {
+ color: var(--danger-color);
+ background: rgba(244, 67, 54, 0.1);
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+ padding-right: 30px;
+}
+
+.card-title {
+ font-size: 1.2rem;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+
+.device-url-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 0.7rem;
+ font-weight: 400;
+ color: var(--text-secondary);
+ background: var(--border-color);
+ padding: 2px 8px;
+ border-radius: 10px;
+ letter-spacing: 0.03em;
+ font-family: monospace;
+ text-decoration: none;
+ transition: background 0.2s;
+}
+
+.device-url-badge:hover {
+ background: var(--text-muted);
+}
+
+.device-url-icon {
+ font-size: 0.6rem;
+}
+
+.card-subtitle {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 15px;
+ flex-wrap: wrap;
+}
+
+.card-meta {
+ font-size: 0.8rem;
+ color: #999;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.device-type-badge {
+ font-size: 10px;
+ font-weight: 700;
+ padding: 1px 6px;
+ border-radius: 3px;
+ background: rgba(255, 255, 255, 0.15);
+ letter-spacing: 0.5px;
+}
+
+/* Device discovery */
+.discovery-section {
+ margin-bottom: 12px;
+}
+.btn-block {
+ width: 100%;
+}
+.discovery-list {
+ max-height: 200px;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ margin-top: 8px;
+}
+.discovery-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ background: var(--card-bg);
+ cursor: pointer;
+ transition: opacity 0.15s;
+}
+.discovery-item:not(.discovery-item--added):hover {
+ opacity: 0.8;
+}
+.discovery-item--added {
+ opacity: 0.5;
+ cursor: default;
+}
+.discovery-item-info {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+.discovery-badge {
+ font-size: 11px;
+ padding: 2px 8px;
+ border-radius: 10px;
+ background: var(--border-color);
+ white-space: nowrap;
+}
+.discovery-type-badge {
+ font-size: 10px;
+ padding: 1px 5px;
+ border-radius: 3px;
+ background: var(--primary-color);
+ color: #fff;
+ font-weight: 600;
+ vertical-align: middle;
+ margin-right: 2px;
+}
+.modal-divider {
+ border: none;
+ border-top: 1px solid var(--border-color);
+ margin: 12px 0;
+}
+.discovery-loading {
+ display: flex;
+ justify-content: center;
+ padding: 12px;
+}
+.discovery-spinner {
+ width: 20px;
+ height: 20px;
+ border: 2px solid var(--border-color);
+ border-top-color: var(--primary-color);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+.channel-indicator {
+ display: inline-flex;
+ gap: 2px;
+ align-items: center;
+}
+
+.channel-indicator .ch {
+ width: 8px;
+ height: 10px;
+ border-radius: 1px;
+}
+
+.display-card {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 15px;
+}
+
+.display-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+}
+
+.display-index {
+ font-size: 1.3rem;
+ font-weight: 700;
+ color: var(--info-color);
+}
+
+.primary-star {
+ color: var(--primary-color);
+ font-size: 1.2rem;
+}
+
+/* Display Layout Visualization */
+.layout-container {
+ position: relative;
+ background: transparent;
+}
+
+.layout-display {
+ position: absolute;
+ border: 3px solid;
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
+ transition: box-shadow 0.2s, border-color 0.2s;
+}
+
+.layout-display:hover {
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
+ z-index: 10;
+}
+
+.layout-display.primary {
+ border-color: var(--primary-color);
+ background: linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(76, 175, 80, 0.05));
+}
+
+.layout-display.secondary {
+ border-color: var(--border-color);
+ background: linear-gradient(135deg, rgba(128, 128, 128, 0.1), rgba(128, 128, 128, 0.05));
+}
+
+.layout-position-label {
+ position: absolute;
+ top: 4px;
+ left: 6px;
+ font-size: 0.7rem;
+ color: var(--text-secondary);
+}
+
+.layout-index-label {
+ position: absolute;
+ bottom: 6px;
+ left: 6px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--text-color);
+ background: var(--display-badge-bg);
+ padding: 1px 6px;
+ border-radius: 4px;
+ letter-spacing: 0.5px;
+}
+
+.layout-display-label {
+ text-align: center;
+ padding: 5px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.layout-display-label strong {
+ font-size: 0.9rem;
+ color: var(--text-color);
+ font-weight: 600;
+}
+
+.layout-display-label small {
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+}
+
+.primary-indicator {
+ position: absolute;
+ top: 2px;
+ right: 4px;
+ color: var(--primary-color);
+ font-size: 1.5rem;
+ line-height: 1;
+ text-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
+}
+
+
+/* Card brightness slider */
+.brightness-control {
+ padding: 0;
+ margin-bottom: 12px;
+}
+
+.brightness-slider {
+ width: 100%;
+}
+
+.brightness-loading .brightness-slider {
+ opacity: 0.3;
+ pointer-events: none;
+}
+
+/* Static color picker — inline in card-subtitle */
+.static-color-control {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.static-color-picker {
+ width: 22px;
+ height: 18px;
+ padding: 0;
+ border: 1px solid var(--border-color);
+ border-radius: 3px;
+ cursor: pointer;
+ background: none;
+ vertical-align: middle;
+}
+
+.btn-clear-color {
+ background: none;
+ border: none;
+ color: #777;
+ font-size: 0.75rem;
+ cursor: pointer;
+ padding: 0 2px;
+ line-height: 1;
+ border-radius: 3px;
+ transition: color 0.2s;
+}
+
+.btn-clear-color:hover {
+ color: var(--danger-color);
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 8px;
+}
+
+.section-header h2 {
+ margin-bottom: 0;
+}
+
+.section-tip {
+ font-size: 0.82rem;
+ color: var(--text-secondary);
+ margin: 0 0 15px 0;
+ line-height: 1.5;
+ padding: 8px 12px;
+ background: rgba(33, 150, 243, 0.08);
+ border-left: 3px solid var(--info-color, #2196F3);
+ border-radius: 0 6px 6px 0;
+}
+
+.section-tip a {
+ color: var(--info-color, #2196F3);
+ text-decoration: underline;
+}
+
+ul.section-tip {
+ list-style: disc;
+ padding-left: 28px;
+}
+
+ul.section-tip li {
+ margin: 2px 0;
+}
+
+.metrics-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 4px 12px;
+ margin-top: 8px;
+}
+
+.metric {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 3px 8px;
+ background: var(--bg-color);
+ border-radius: 4px;
+}
+
+.metric-value {
+ font-size: 0.9rem;
+ font-weight: 700;
+ color: var(--primary-color);
+}
+
+.metric-label {
+ font-size: 0.8rem;
+ color: #999;
+}
+
+/* Timing breakdown bar */
+.timing-breakdown {
+ margin-top: 8px;
+ padding: 6px 8px;
+ background: var(--bg-color);
+ border-radius: 4px;
+}
+
+.timing-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.timing-total {
+ font-size: 0.8rem;
+ color: var(--primary-color);
+}
+
+.timing-bar {
+ display: flex;
+ height: 8px;
+ border-radius: 4px;
+ overflow: hidden;
+ margin: 4px 0;
+ gap: 1px;
+}
+
+.timing-seg {
+ min-width: 2px;
+ transition: flex 0.3s ease;
+}
+
+.timing-extract { background: #4CAF50; }
+.timing-map { background: #FF9800; }
+.timing-smooth { background: #2196F3; }
+.timing-send { background: #E91E63; }
+
+.timing-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ font-size: 0.75rem;
+ color: #999;
+ margin-top: 4px;
+}
+
+.timing-legend-item {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+}
+
+.timing-dot {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 2px;
+}
+
+.timing-dot.timing-extract { background: #4CAF50; }
+.timing-dot.timing-map { background: #FF9800; }
+.timing-dot.timing-smooth { background: #2196F3; }
+.timing-dot.timing-send { background: #E91E63; }
+
+@media (max-width: 768px) {
+ .displays-grid,
+ .devices-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/server/src/wled_controller/static/css/components.css b/server/src/wled_controller/static/css/components.css
new file mode 100644
index 0000000..be15d1e
--- /dev/null
+++ b/server/src/wled_controller/static/css/components.css
@@ -0,0 +1,356 @@
+.badge {
+ padding: 4px 12px;
+ border-radius: 12px;
+ font-size: 0.85rem;
+ font-weight: 600;
+}
+
+.badge.processing {
+ background: var(--primary-color);
+ color: white;
+}
+
+.badge.idle {
+ background: var(--warning-color);
+ color: white;
+}
+
+.badge.error {
+ background: var(--danger-color);
+ color: white;
+}
+
+.card-content {
+ margin-bottom: 15px;
+}
+
+.info-row {
+ display: flex;
+ justify-content: space-between;
+ padding: 8px 0;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.info-row:last-child {
+ border-bottom: none;
+}
+
+.info-label {
+ color: #999;
+}
+
+.info-value {
+ font-weight: 600;
+}
+
+.card-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: auto;
+ padding-top: 12px;
+ border-top: 1px solid var(--border-color);
+ align-items: center;
+}
+
+.btn {
+ padding: 8px 16px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.9rem;
+ font-weight: 600;
+ transition: opacity 0.2s;
+ flex: 1 1 auto;
+ min-width: 100px;
+}
+
+.btn:hover {
+ opacity: 0.9;
+}
+
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.btn-primary {
+ background: var(--primary-color);
+ color: white;
+}
+
+.btn-danger {
+ background: var(--danger-color);
+ color: white;
+}
+
+.btn-secondary {
+ background: var(--border-color);
+ color: var(--text-color);
+}
+
+.btn-icon {
+ min-width: auto;
+ padding: 8px 12px;
+ font-size: 1.2rem;
+ flex: 0 0 auto;
+}
+
+.btn-icon:hover {
+ transform: scale(1.1);
+ opacity: 1;
+}
+
+.form-group {
+ margin-bottom: 15px;
+}
+
+.settings-toggle-group {
+ display: flex;
+ flex-direction: column;
+}
+
+.settings-toggle {
+ position: relative;
+ display: inline-block;
+ width: 34px;
+ height: 18px;
+ cursor: pointer;
+ margin-top: 4px;
+}
+
+.settings-toggle input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.settings-toggle-slider {
+ position: absolute;
+ inset: 0;
+ background: var(--border-color);
+ border-radius: 9px;
+ transition: background 0.2s;
+}
+
+.settings-toggle-slider::after {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 14px;
+ height: 14px;
+ background: white;
+ border-radius: 50%;
+ transition: transform 0.2s;
+}
+
+.settings-toggle input:checked + .settings-toggle-slider {
+ background: var(--primary-color);
+}
+
+.settings-toggle input:checked + .settings-toggle-slider::after {
+ transform: translateX(16px);
+}
+
+label {
+ display: block;
+ margin-bottom: 5px;
+ color: #999;
+ font-weight: 500;
+}
+
+input[type="text"],
+input[type="url"],
+input[type="number"],
+input[type="password"],
+select {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ background: var(--bg-color);
+ color: var(--text-color);
+ font-size: 1rem;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ transition: border-color 0.2s, box-shadow 0.2s, opacity 0.2s;
+}
+
+input[type="number"]:disabled,
+input[type="password"]:disabled,
+select:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+input[type="range"] {
+ width: 100%;
+ margin: 8px 0;
+}
+
+/* Better password field appearance */
+input[type="password"] {
+ letter-spacing: 0.15em;
+}
+
+input:focus,
+select:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
+}
+
+/* Remove browser autofill styling */
+input:-webkit-autofill,
+input:-webkit-autofill:hover,
+input:-webkit-autofill:focus {
+ -webkit-box-shadow: 0 0 0 1000px var(--bg-color) inset;
+ -webkit-text-fill-color: var(--text-color);
+ transition: background-color 5000s ease-in-out 0s;
+}
+
+.loading {
+ text-align: center;
+ padding: 40px;
+ color: #999;
+}
+
+.loading-spinner {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 40px;
+}
+
+.loading-spinner::after {
+ content: '';
+ width: 28px;
+ height: 28px;
+ border: 3px solid var(--border-color);
+ border-top-color: var(--primary-color);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* Full-page overlay spinner */
+.overlay-spinner {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ z-index: 9999;
+ backdrop-filter: blur(4px);
+}
+
+.overlay-spinner .progress-container {
+ position: relative;
+ width: 120px;
+ height: 120px;
+}
+
+.overlay-spinner .progress-ring {
+ transform: rotate(-90deg);
+}
+
+.overlay-spinner .progress-ring-circle {
+ transition: stroke-dashoffset 0.1s linear;
+ stroke: var(--primary-color);
+ stroke-width: 4;
+ fill: transparent;
+}
+
+.overlay-spinner .progress-ring-bg {
+ stroke: rgba(255, 255, 255, 0.1);
+ stroke-width: 4;
+ fill: transparent;
+}
+
+.overlay-spinner .progress-content {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+}
+
+.overlay-spinner .progress-percentage {
+ color: white;
+ font-size: 28px;
+ font-weight: 600;
+}
+
+.overlay-spinner .spinner-text {
+ margin-top: 24px;
+ color: white;
+ font-size: 16px;
+ font-weight: 500;
+}
+
+.overlay-spinner-close {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 32px;
+ cursor: pointer;
+ line-height: 1;
+ padding: 4px 8px;
+ transition: color 0.15s;
+}
+
+.overlay-spinner-close:hover {
+ color: white;
+}
+
+.toast {
+ position: fixed;
+ bottom: 40px;
+ left: 50%;
+ transform: translateX(-50%) translateY(100px);
+ padding: 16px 24px;
+ border-radius: 8px;
+ color: white;
+ font-weight: 600;
+ font-size: 15px;
+ opacity: 0;
+ transition: opacity 0.3s, transform 0.3s;
+ z-index: 2001;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
+ min-width: 300px;
+ text-align: center;
+}
+
+.toast.show {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ animation: toastShake 0.5s ease-in-out;
+}
+
+@keyframes toastShake {
+ 0%, 100% { transform: translateX(-50%) translateY(0); }
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-50%) translateY(-5px); }
+ 20%, 40%, 60%, 80% { transform: translateX(-50%) translateY(5px); }
+}
+
+.toast.success {
+ background: var(--primary-color);
+}
+
+.toast.error {
+ background: var(--danger-color);
+}
+
+.toast.info {
+ background: var(--info-color);
+}
diff --git a/server/src/wled_controller/static/css/dashboard.css b/server/src/wled_controller/static/css/dashboard.css
new file mode 100644
index 0000000..e1b9e32
--- /dev/null
+++ b/server/src/wled_controller/static/css/dashboard.css
@@ -0,0 +1,281 @@
+/* ── Dashboard ── */
+
+.dashboard-section {
+ margin-bottom: 16px;
+}
+
+.dashboard-section-header {
+ font-size: 0.8rem;
+ font-weight: 600;
+ margin-bottom: 6px;
+ color: var(--text-secondary);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ cursor: pointer;
+ user-select: none;
+}
+
+.dashboard-section-chevron {
+ font-size: 0.6rem;
+ color: var(--text-secondary);
+ width: 10px;
+ display: inline-block;
+}
+
+.dashboard-section-count {
+ background: var(--border-color);
+ color: var(--text-secondary);
+ border-radius: 10px;
+ padding: 0 6px;
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+
+.dashboard-subsection {
+ margin-bottom: 10px;
+ padding-left: 16px;
+}
+.dashboard-subsection .dashboard-section-header {
+ font-size: 0.72rem;
+}
+
+.dashboard-stop-all {
+ margin-left: auto;
+ padding: 2px 8px;
+ font-size: 0.7rem;
+ white-space: nowrap;
+ flex: 0 0 auto;
+}
+
+.dashboard-poll-wrap {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 3px;
+}
+
+.dashboard-poll-slider {
+ width: 48px;
+ height: 12px;
+ accent-color: var(--primary-color);
+ cursor: pointer;
+}
+
+.dashboard-poll-value {
+ font-size: 0.6rem;
+ color: var(--text-secondary);
+ min-width: 18px;
+}
+
+.dashboard-target {
+ display: grid;
+ grid-template-columns: 1fr auto auto;
+ align-items: center;
+ gap: 12px;
+ padding: 6px 12px;
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ margin-bottom: 4px;
+}
+
+.dashboard-target-info {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.dashboard-target-icon {
+ font-size: 1rem;
+ flex-shrink: 0;
+}
+
+.dashboard-target-name {
+ font-size: 0.85rem;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.dashboard-target-name .health-dot {
+ margin-right: 0;
+ flex-shrink: 0;
+}
+
+.dashboard-target-subtitle {
+ font-size: 0.7rem;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.dashboard-target-metrics {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+}
+
+.dashboard-metric {
+ text-align: center;
+ min-width: 48px;
+}
+
+.dashboard-metric-value {
+ font-size: 0.85rem;
+ font-weight: 700;
+ color: var(--primary-color);
+ line-height: 1.2;
+}
+
+.dashboard-metric-label {
+ font-size: 0.6rem;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
+}
+
+.dashboard-fps-metric {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-width: auto;
+}
+
+.dashboard-fps-sparkline {
+ position: relative;
+ width: 100px;
+ height: 36px;
+}
+
+.dashboard-fps-label {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ min-width: 36px;
+ line-height: 1.1;
+}
+
+.dashboard-fps-target {
+ font-weight: 400;
+ opacity: 0.5;
+ font-size: 0.75rem;
+}
+
+.dashboard-target-actions {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.dashboard-status-dot {
+ font-size: 1rem;
+ line-height: 1;
+}
+
+.dashboard-status-dot.active {
+ color: #4CAF50;
+ animation: pulse 2s infinite;
+}
+
+.dashboard-no-targets {
+ text-align: center;
+ padding: 32px 16px;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+}
+
+.dashboard-badge-stopped {
+ padding: 2px 8px;
+ border-radius: 10px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ background: var(--border-color);
+ color: var(--text-secondary);
+ flex-shrink: 0;
+}
+
+.dashboard-badge-active {
+ padding: 2px 8px;
+ border-radius: 10px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ background: var(--success-color, #28a745);
+ color: #fff;
+ flex-shrink: 0;
+}
+
+.dashboard-profile .dashboard-target-metrics {
+ min-width: 48px;
+}
+
+@media (max-width: 768px) {
+ .dashboard-target {
+ grid-template-columns: 1fr auto;
+ gap: 6px;
+ }
+
+ .dashboard-target-metrics {
+ display: none;
+ }
+}
+
+/* ===== PERFORMANCE CHARTS ===== */
+
+.perf-charts-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 12px;
+}
+
+.perf-chart-card {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ padding: 10px 12px;
+}
+
+.perf-chart-wrap {
+ position: relative;
+ height: 60px;
+}
+
+.perf-chart-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 4px;
+}
+
+.perf-chart-label {
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
+ color: var(--text-secondary);
+}
+
+.perf-chart-value {
+ font-size: 0.85rem;
+ font-weight: 700;
+}
+
+.perf-chart-value.cpu { color: #2196F3; }
+.perf-chart-value.ram { color: #4CAF50; }
+.perf-chart-value.gpu { color: #FF9800; }
+
+.perf-chart-unavailable {
+ text-align: center;
+ padding: 20px 0;
+ color: var(--text-secondary);
+ font-size: 0.8rem;
+}
diff --git a/server/src/wled_controller/static/css/layout.css b/server/src/wled_controller/static/css/layout.css
new file mode 100644
index 0000000..bbfcbfb
--- /dev/null
+++ b/server/src/wled_controller/static/css/layout.css
@@ -0,0 +1,209 @@
+header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px 0 10px;
+ margin-bottom: 10px;
+ position: relative;
+ z-index: 2100;
+}
+
+.header-title {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+h1 {
+ font-size: 2rem;
+ color: var(--primary-color);
+}
+
+h2 {
+ margin-bottom: 20px;
+ color: var(--text-color);
+ font-size: 1.5rem;
+}
+
+.server-info {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+}
+
+.header-link {
+ color: var(--text-secondary);
+ text-decoration: none;
+ font-size: 0.85rem;
+ font-weight: 500;
+ padding: 4px 8px;
+ border-radius: 4px;
+ transition: color 0.2s, background 0.2s;
+}
+
+.header-link:hover {
+ color: var(--text-color);
+ background: var(--bg-secondary);
+}
+
+#server-version {
+ font-size: 0.75rem;
+ font-weight: 400;
+ color: var(--text-secondary);
+ background: var(--border-color);
+ padding: 2px 8px;
+ border-radius: 10px;
+ letter-spacing: 0.03em;
+}
+
+.status-badge {
+ font-size: 1.5rem;
+ animation: pulse 2s infinite;
+}
+
+.status-badge.online {
+ color: var(--primary-color);
+}
+
+.status-badge.offline {
+ color: var(--danger-color);
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+/* WLED device health indicator */
+.health-dot {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ margin-right: 8px;
+ vertical-align: middle;
+ flex-shrink: 0;
+}
+
+.health-dot.health-online {
+ background-color: #4CAF50;
+ box-shadow: 0 0 6px rgba(76, 175, 80, 0.6);
+}
+
+.health-dot.health-offline {
+ background-color: var(--danger-color);
+ box-shadow: 0 0 6px rgba(244, 67, 54, 0.6);
+}
+
+.health-dot.health-unknown {
+ background-color: #9E9E9E;
+ animation: pulse 2s infinite;
+}
+
+.health-latency {
+ font-size: 0.7rem;
+ font-weight: 400;
+ color: #4CAF50;
+ margin-left: auto;
+ padding-left: 8px;
+ opacity: 0.85;
+}
+
+.health-latency.offline {
+ color: var(--danger-color);
+}
+
+/* Tabs */
+.tab-bar {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ border-bottom: 2px solid var(--border-color);
+ margin-bottom: 16px;
+}
+
+.tab-btn {
+ background: none;
+ border: none;
+ padding: 10px 18px;
+ font-size: 1rem;
+ font-weight: 500;
+ color: var(--text-secondary);
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ margin-bottom: -2px;
+ transition: color 0.2s, border-color 0.2s;
+}
+
+.tab-btn:hover {
+ color: var(--text-color);
+}
+
+.tab-btn.active {
+ color: var(--primary-color);
+ border-bottom-color: var(--primary-color);
+}
+
+.tab-panel {
+ display: none;
+}
+
+.tab-panel.active {
+ display: block;
+}
+
+/* Theme Toggle */
+.theme-toggle {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ padding: 6px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 1.2rem;
+ transition: transform 0.2s;
+ margin-left: 10px;
+}
+
+.theme-toggle:hover {
+ transform: scale(1.1);
+}
+
+/* Footer */
+.app-footer {
+ margin-top: 20px;
+ padding: 15px 0;
+ text-align: center;
+}
+
+.footer-content {
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+}
+
+.footer-content p {
+ margin: 0;
+}
+
+
+.footer-content strong {
+ color: var(--text-color);
+}
+
+.footer-content a {
+ color: var(--primary-color);
+ text-decoration: none;
+ transition: opacity 0.2s;
+}
+
+.footer-content a:hover {
+ opacity: 0.8;
+ text-decoration: underline;
+}
+
+@media (max-width: 768px) {
+ header {
+ flex-direction: column;
+ gap: 15px;
+ text-align: center;
+ }
+}
diff --git a/server/src/wled_controller/static/css/modal.css b/server/src/wled_controller/static/css/modal.css
new file mode 100644
index 0000000..5156752
--- /dev/null
+++ b/server/src/wled_controller/static/css/modal.css
@@ -0,0 +1,450 @@
+/* Modal Styles */
+.modal {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.8);
+ z-index: 2000;
+ align-items: center;
+ justify-content: center;
+ animation: fadeIn 0.2s ease-out;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+.modal-content {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 12px;
+ max-width: 500px;
+ width: 90%;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+ animation: slideUp 0.3s ease-out;
+}
+
+#template-modal .modal-content {
+ max-width: 500px !important;
+ width: 100% !important;
+}
+
+#test-template-modal .modal-content {
+ max-width: 420px;
+}
+
+@keyframes slideUp {
+ from {
+ transform: translateY(20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+.modal-header {
+ padding: 24px 24px 16px 24px;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.modal-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ color: var(--text-color);
+}
+
+.modal-header-actions {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.modal-header-btn {
+ background: none;
+ border: none;
+ font-size: 1.1rem;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border-radius: 4px;
+ transition: background 0.2s;
+ flex-shrink: 0;
+}
+.modal-header-btn:hover {
+ background: rgba(128, 128, 128, 0.15);
+}
+.modal-header-btn:disabled {
+ opacity: 0.4;
+ cursor: default;
+}
+
+.modal-close-btn {
+ background: none;
+ border: none;
+ color: #777;
+ font-size: 1.2rem;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border-radius: 4px;
+ transition: color 0.2s, background 0.2s;
+ flex-shrink: 0;
+}
+
+.modal-close-btn:hover {
+ color: var(--text-color);
+ background: rgba(128, 128, 128, 0.15);
+}
+
+.modal-body {
+ padding: 24px;
+}
+
+.modal-description {
+ color: #999;
+ margin-bottom: 20px;
+ line-height: 1.6;
+}
+
+.password-input-wrapper {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.password-input-wrapper input {
+ flex: 1;
+ padding-right: 45px;
+}
+
+.password-toggle {
+ position: absolute;
+ right: 8px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 1.2rem;
+ padding: 8px;
+ color: var(--text-color);
+ opacity: 0.6;
+ transition: opacity 0.2s;
+}
+
+.password-toggle:hover {
+ opacity: 1;
+}
+
+.label-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-bottom: 5px;
+}
+
+.label-row label {
+ margin-bottom: 0;
+}
+
+.hint-toggle {
+ background: none;
+ border: 1px solid var(--border-color);
+ border-radius: 50%;
+ width: 18px;
+ height: 18px;
+ font-size: 0.7rem;
+ line-height: 1;
+ color: var(--text-secondary, #888);
+ cursor: pointer;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.6;
+ transition: opacity 0.2s;
+ flex-shrink: 0;
+}
+
+.hint-toggle:hover {
+ opacity: 1;
+}
+
+.hint-toggle.active {
+ opacity: 1;
+ color: var(--primary-color, #4CAF50);
+ border-color: var(--primary-color, #4CAF50);
+}
+
+.input-hint {
+ display: block;
+ margin: 0 0 6px 0;
+ color: #666;
+ font-size: 0.85rem;
+}
+
+.fps-hint {
+ display: block;
+ margin-top: 4px;
+ font-size: 0.82rem;
+ color: var(--info-color, #2196F3);
+}
+
+.slider-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.slider-row input[type="range"] {
+ flex: 1;
+}
+
+.slider-value {
+ min-width: 28px;
+ text-align: center;
+ font-weight: 600;
+ font-size: 0.95rem;
+ color: var(--text-primary);
+}
+
+.error-message {
+ background: rgba(244, 67, 54, 0.1);
+ border: 1px solid var(--danger-color);
+ color: var(--danger-color);
+ padding: 12px;
+ border-radius: 4px;
+ margin-top: 15px;
+ font-size: 0.9rem;
+}
+
+.modal-footer {
+ padding: 16px 24px 24px 24px;
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+}
+
+.modal-footer .btn-icon {
+ min-width: 60px;
+ padding: 10px 20px;
+ font-size: 1.4rem;
+}
+
+.btn-sm {
+ padding: 4px 10px;
+ font-size: 0.8rem;
+ min-width: auto;
+}
+
+.btn-display-picker {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ background: var(--bg-color);
+ color: var(--text-color);
+ font-size: 1rem;
+ cursor: pointer;
+ text-align: left;
+ transition: border-color 0.2s, box-shadow 0.2s;
+ font-weight: 400;
+}
+
+.btn-display-picker:hover {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.15);
+}
+
+.modal-content-wide {
+ width: fit-content;
+ min-width: 500px;
+ max-width: calc(100vw - 40px);
+ max-height: calc(100vh - 40px);
+ display: flex;
+ flex-direction: column;
+}
+
+.modal-content-wide .modal-body {
+ overflow-y: auto;
+ scrollbar-gutter: stable;
+ flex: 1 1 auto;
+ min-height: 0;
+}
+
+@media (max-width: 768px) {
+ .modal-content {
+ width: 95%;
+ margin: 20px;
+ }
+
+ .modal-footer .btn {
+ min-width: 80px;
+ }
+}
+
+/* Image Lightbox */
+.lightbox {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.92);
+ z-index: 10000;
+ justify-content: center;
+ align-items: center;
+ cursor: zoom-out;
+}
+
+.lightbox.active {
+ display: flex;
+}
+
+.lightbox-content {
+ position: relative;
+ max-width: 95%;
+ max-height: 95%;
+ cursor: default;
+}
+
+.lightbox-content img {
+ max-width: 100%;
+ max-height: 90vh;
+ object-fit: contain;
+ border-radius: 4px;
+ display: block;
+}
+
+.lightbox-close {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ background: rgba(255, 255, 255, 0.15);
+ border: none;
+ color: white;
+ font-size: 1.5rem;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s;
+ z-index: 1;
+}
+
+.lightbox-close:hover {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+.lightbox-refresh-btn {
+ position: absolute;
+ top: 16px;
+ right: 64px;
+ background: rgba(255, 255, 255, 0.15);
+ border: none;
+ color: white;
+ font-size: 1.2rem;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s;
+ z-index: 1;
+}
+
+.lightbox-refresh-btn:hover {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+.lightbox-refresh-btn.active {
+ background: var(--primary-color);
+}
+
+.lightbox-stats {
+ position: absolute;
+ bottom: 8px;
+ left: 8px;
+ background: rgba(0, 0, 0, 0.7);
+ backdrop-filter: blur(8px);
+ color: white;
+ padding: 8px 14px;
+ border-radius: 6px;
+ font-size: 0.8rem;
+ display: flex;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.lightbox-stats .stat-item {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+}
+
+.lightbox-stats .stat-item span {
+ opacity: 0.7;
+}
+
+.lightbox-stats .stat-item strong {
+ font-weight: 600;
+}
+
+/* Display Picker Lightbox */
+.display-picker-content {
+ max-width: 900px;
+ width: 90%;
+ text-align: center;
+}
+
+.display-picker-title {
+ color: white;
+ font-size: 1.3rem;
+ margin-bottom: 20px;
+ font-weight: 500;
+}
+
+.display-picker-canvas {
+ background: rgba(255, 255, 255, 0.05);
+ border: 2px dashed rgba(255, 255, 255, 0.15);
+ border-radius: 8px;
+ padding: 24px;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.layout-display-pickable {
+ cursor: pointer !important;
+ border: 2px solid var(--border-color) !important;
+ background: linear-gradient(135deg, rgba(128, 128, 128, 0.08), rgba(128, 128, 128, 0.03)) !important;
+}
+
+.layout-display-pickable:hover {
+ box-shadow: 0 0 20px rgba(76, 175, 80, 0.4);
+ border-color: var(--primary-color) !important;
+}
+
+.layout-display-pickable.selected {
+ border-color: var(--primary-color) !important;
+ box-shadow: 0 0 16px rgba(76, 175, 80, 0.5);
+ background: rgba(76, 175, 80, 0.12) !important;
+}
diff --git a/server/src/wled_controller/static/css/patterns.css b/server/src/wled_controller/static/css/patterns.css
new file mode 100644
index 0000000..7ddbb5a
--- /dev/null
+++ b/server/src/wled_controller/static/css/patterns.css
@@ -0,0 +1,337 @@
+/* Static image stream styles */
+.image-preview-container {
+ text-align: center;
+ margin: 12px 0;
+ padding: 12px;
+ background: var(--bg-secondary);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+}
+.stream-image-preview {
+ max-width: 100%;
+ max-height: 200px;
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+}
+.stream-image-info {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ margin-top: 6px;
+}
+.validation-status {
+ font-size: 0.8rem;
+ margin-top: 4px;
+ padding: 4px 8px;
+ border-radius: 4px;
+}
+.validation-status.success {
+ color: #4caf50;
+}
+.validation-status.error {
+ color: #f44336;
+}
+.validation-status.loading {
+ color: var(--text-muted);
+}
+.stream-card-props {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 8px;
+}
+
+.stream-card-prop {
+ display: inline-block;
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ background: var(--border-color);
+ padding: 2px 8px;
+ border-radius: 10px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 180px;
+ vertical-align: middle;
+}
+
+.stream-card-prop-full {
+ max-width: 100%;
+ word-break: break-all;
+ white-space: normal;
+ font-size: 0.7rem;
+}
+
+/* Key Colors target styles */
+.kc-rect-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
+.kc-rect-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px;
+ background: var(--bg-color);
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+}
+
+.kc-rect-row input[type="text"] {
+ flex: 2;
+ min-width: 0;
+}
+
+.kc-rect-row input[type="number"] {
+ flex: 1;
+ min-width: 0;
+ width: 60px;
+}
+
+.kc-rect-row .kc-rect-remove-btn {
+ background: none;
+ border: none;
+ color: #777;
+ font-size: 1rem;
+ cursor: pointer;
+ padding: 4px;
+ border-radius: 4px;
+ flex-shrink: 0;
+ transition: color 0.2s, background 0.2s;
+}
+
+.kc-rect-row .kc-rect-remove-btn:hover {
+ color: var(--danger-color);
+ background: rgba(244, 67, 54, 0.1);
+}
+
+.kc-rect-labels {
+ display: flex;
+ gap: 6px;
+ padding: 0 8px;
+ margin-bottom: 4px;
+ font-size: 0.7rem;
+ color: var(--text-secondary);
+ font-weight: 600;
+}
+
+.kc-rect-labels span:first-child {
+ flex: 2;
+}
+
+.kc-rect-labels span {
+ flex: 1;
+ text-align: center;
+}
+
+.kc-rect-labels span:last-child {
+ width: 28px;
+ flex: 0 0 28px;
+}
+
+.kc-add-rect-btn {
+ width: 100%;
+ font-size: 0.85rem;
+ padding: 6px 12px;
+}
+
+.kc-rect-empty {
+ text-align: center;
+ color: var(--text-secondary);
+ font-size: 0.85rem;
+ padding: 12px;
+ font-style: italic;
+}
+
+.kc-color-swatches {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 8px;
+}
+
+.kc-swatch {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 3px;
+}
+
+.kc-swatch-color {
+ width: 32px;
+ height: 32px;
+ border-radius: 6px;
+ border: 2px solid var(--border-color);
+ transition: background-color 0.3s;
+}
+
+.kc-swatch-label {
+ font-size: 0.6rem;
+ color: var(--text-secondary);
+ max-width: 40px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ text-align: center;
+}
+
+.kc-no-colors {
+ color: var(--text-secondary);
+ font-size: 0.8rem;
+ font-style: italic;
+ padding: 4px 0;
+}
+
+/* Pattern Template Visual Editor */
+.pattern-canvas-container {
+ position: relative;
+ border-radius: 8px;
+ overflow: hidden;
+ background: var(--bg-color);
+ border: 1px solid var(--border-color);
+ margin-bottom: 12px;
+ resize: both;
+ width: 820px;
+ min-width: 400px;
+ max-width: 100%;
+ min-height: 200px;
+ height: 450px;
+ max-height: calc(100vh - 400px);
+}
+
+#pattern-canvas {
+ width: 100%;
+ height: 100%;
+ display: block;
+ cursor: default;
+}
+
+.pattern-canvas-toolbar {
+ display: flex;
+ gap: 2px;
+ padding: 4px;
+ align-items: center;
+ background: var(--bg-secondary);
+ border-radius: 6px;
+ margin-top: 4px;
+}
+
+.pattern-canvas-toolbar .btn {
+ flex: 0 0 auto;
+ min-width: 32px;
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ font-size: 1rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+}
+
+.pattern-bg-row {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+.pattern-bg-row select {
+ flex: 1;
+}
+
+.pattern-capture-btn {
+ flex: 0 0 auto;
+ min-width: 36px !important;
+ width: 36px;
+ height: 36px;
+ padding: 0 !important;
+ font-size: 1.1rem;
+ line-height: 36px;
+ text-align: center;
+}
+
+.pattern-rect-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ margin-top: 8px;
+ max-height: 200px;
+ overflow-y: auto;
+}
+
+.pattern-rect-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 8px;
+ background: var(--bg-color);
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ font-size: 0.85rem;
+ cursor: pointer;
+ transition: border-color 0.15s;
+}
+
+.pattern-rect-row.selected {
+ border-color: var(--primary-color);
+ background: rgba(76, 175, 80, 0.08);
+}
+
+.pattern-rect-row input[type="text"] {
+ flex: 2;
+ min-width: 0;
+ padding: 4px 6px;
+ font-size: 0.8rem;
+}
+
+.pattern-rect-row input[type="number"] {
+ flex: 1;
+ min-width: 0;
+ width: 55px;
+ padding: 4px 6px;
+ font-size: 0.8rem;
+}
+
+.pattern-rect-row .pattern-rect-remove-btn {
+ background: none;
+ border: none;
+ color: #777;
+ font-size: 0.9rem;
+ cursor: pointer;
+ padding: 2px 4px;
+ border-radius: 4px;
+ flex-shrink: 0;
+ transition: color 0.2s, background 0.2s;
+}
+
+.pattern-rect-row .pattern-rect-remove-btn:hover {
+ color: var(--danger-color);
+ background: rgba(244, 67, 54, 0.1);
+}
+
+.pattern-rect-labels {
+ display: flex;
+ gap: 6px;
+ padding: 0 8px;
+ margin-bottom: 2px;
+ font-size: 0.7rem;
+ color: var(--text-secondary);
+ font-weight: 600;
+}
+
+.pattern-rect-labels span:first-child {
+ flex: 2;
+}
+
+.pattern-rect-labels span {
+ flex: 1;
+ text-align: center;
+}
+
+.pattern-rect-labels span:last-child {
+ width: 24px;
+ flex: 0 0 24px;
+}
diff --git a/server/src/wled_controller/static/css/profiles.css b/server/src/wled_controller/static/css/profiles.css
new file mode 100644
index 0000000..491d190
--- /dev/null
+++ b/server/src/wled_controller/static/css/profiles.css
@@ -0,0 +1,185 @@
+/* ===== PROFILES ===== */
+
+.badge-profile-active {
+ background: var(--success-color, #28a745);
+ color: #fff;
+}
+
+.badge-profile-inactive {
+ background: var(--border-color);
+ color: var(--text-color);
+}
+
+.badge-profile-disabled {
+ background: var(--border-color);
+ color: var(--text-muted);
+ opacity: 0.7;
+}
+
+.profile-status-disabled {
+ opacity: 0.6;
+}
+
+.profile-logic-label {
+ font-size: 0.7rem;
+ font-weight: 600;
+ color: var(--text-muted);
+ padding: 0 4px;
+}
+
+/* Profile condition editor rows */
+.profile-condition-row {
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ padding: 10px;
+ margin-bottom: 8px;
+ background: var(--bg-secondary, var(--bg-color));
+}
+
+.condition-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+.condition-type-label {
+ font-weight: 600;
+ font-size: 0.9rem;
+}
+
+.btn-remove-condition {
+ background: none;
+ border: none;
+ color: var(--danger-color, #dc3545);
+ cursor: pointer;
+ font-size: 1rem;
+ padding: 2px 6px;
+}
+
+.condition-fields {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.condition-field label {
+ display: block;
+ font-size: 0.85rem;
+ margin-bottom: 3px;
+ color: var(--text-muted);
+}
+
+.condition-field select,
+.condition-field textarea {
+ width: 100%;
+ padding: 6px 8px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ background: var(--bg-color);
+ color: var(--text-color);
+ font-size: 0.9rem;
+ font-family: inherit;
+}
+
+.condition-apps {
+ resize: vertical;
+ min-height: 60px;
+}
+
+.condition-apps-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.btn-browse-apps {
+ background: none;
+ border: 1px solid var(--border-color);
+ color: var(--text-color);
+ font-size: 0.75rem;
+ padding: 2px 8px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: border-color 0.2s, background 0.2s;
+}
+
+.btn-browse-apps:hover {
+ border-color: var(--primary-color);
+ background: rgba(33, 150, 243, 0.1);
+}
+
+.process-picker {
+ margin-top: 6px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.process-picker-search {
+ width: 100%;
+ padding: 6px 8px;
+ border: none;
+ border-bottom: 1px solid var(--border-color);
+ background: var(--bg-color);
+ color: var(--text-color);
+ font-size: 0.85rem;
+ font-family: inherit;
+ outline: none;
+ box-sizing: border-box;
+}
+
+.process-picker-list {
+ max-height: 160px;
+ overflow-y: auto;
+}
+
+.process-picker-item {
+ padding: 4px 8px;
+ font-size: 0.8rem;
+ cursor: pointer;
+ transition: background 0.1s;
+}
+
+.process-picker-item:hover {
+ background: rgba(33, 150, 243, 0.15);
+}
+
+.process-picker-item.added {
+ color: var(--text-muted);
+ cursor: default;
+ opacity: 0.6;
+}
+
+.process-picker-loading {
+ padding: 8px;
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ text-align: center;
+}
+
+/* Profile target checklist */
+.profile-targets-checklist {
+ max-height: 200px;
+ overflow-y: auto;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ padding: 6px;
+}
+
+.profile-target-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 6px;
+ cursor: pointer;
+ border-radius: 3px;
+}
+
+.profile-target-item:hover {
+ background: var(--bg-secondary, var(--bg-color));
+}
+
+.profile-target-item input[type="checkbox"] {
+ margin: 0;
+}
diff --git a/server/src/wled_controller/static/css/streams.css b/server/src/wled_controller/static/css/streams.css
new file mode 100644
index 0000000..7b90a32
--- /dev/null
+++ b/server/src/wled_controller/static/css/streams.css
@@ -0,0 +1,611 @@
+/* ===========================
+ Capture Templates Styles
+ =========================== */
+
+.templates-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
+ gap: 20px;
+}
+
+.template-card {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 16px;
+ transition: box-shadow 0.2s;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+}
+
+.template-card:hover {
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.add-template-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 180px;
+ cursor: pointer;
+ border: 2px dashed var(--border-color);
+ background: transparent;
+ transition: border-color 0.2s, background 0.2s;
+}
+
+.add-template-card:hover {
+ border-color: var(--primary-color);
+ background: rgba(33, 150, 243, 0.05);
+ box-shadow: none;
+}
+
+.add-template-icon {
+ font-size: 2.5rem;
+ font-weight: 300;
+ color: var(--text-secondary);
+ line-height: 1;
+ transition: color 0.2s;
+}
+
+.add-template-card:hover .add-template-icon {
+ color: var(--primary-color);
+}
+
+.add-template-label {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ margin-top: 8px;
+ font-weight: 500;
+}
+
+.template-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.template-card .template-card-header {
+ padding-right: 24px;
+}
+
+.template-name {
+ font-size: 18px;
+ font-weight: bold;
+ color: var(--text-color);
+}
+
+.badge {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: bold;
+ text-transform: uppercase;
+}
+
+.template-description {
+ color: var(--text-secondary);
+ font-size: 14px;
+ margin-bottom: 12px;
+ line-height: 1.4;
+}
+
+.template-config {
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin-bottom: 8px;
+}
+
+.filter-chain {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-bottom: 8px;
+}
+
+.filter-chain-item {
+ font-size: 0.7rem;
+ background: var(--border-color);
+ color: var(--text-secondary);
+ padding: 2px 8px;
+ border-radius: 10px;
+}
+
+.filter-chain-arrow {
+ font-size: 0.7rem;
+ color: var(--text-muted);
+}
+
+.template-config-details {
+ margin: 12px 0;
+ font-size: 13px;
+}
+
+.template-config-details summary {
+ cursor: pointer;
+ color: var(--primary-color);
+ font-weight: 500;
+ padding: 4px 0;
+}
+
+.template-config-details summary:hover {
+ text-decoration: underline;
+}
+
+.template-no-config {
+ margin: 12px 0;
+ font-size: 13px;
+ color: var(--primary-color);
+ font-weight: 500;
+ padding: 4px 0;
+}
+
+.config-table {
+ width: 100%;
+ margin-top: 8px;
+ border-collapse: collapse;
+ font-size: 13px;
+}
+
+.config-table td {
+ padding: 4px 8px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.config-table tr:last-child td {
+ border-bottom: none;
+}
+
+.config-key {
+ color: var(--text-secondary);
+ white-space: nowrap;
+ width: 1%;
+}
+
+.config-value {
+ color: var(--text-primary);
+ font-family: monospace;
+}
+
+/* Engine config grid (property name left, input right) */
+.config-grid {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 8px 12px;
+ align-items: center;
+ margin-top: 12px;
+}
+
+.config-grid-label {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ text-align: right;
+}
+
+.config-grid-value input,
+.config-grid-value select {
+ width: 100%;
+ padding: 6px 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ background: var(--bg-color);
+ color: var(--text-color);
+ font-size: 0.85rem;
+}
+
+.template-card-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: auto;
+ padding-top: 12px;
+ border-top: 1px solid var(--border-color);
+ align-items: center;
+}
+
+.template-card-actions .btn:not(.btn-icon) {
+ flex: 1;
+}
+
+.template-card-actions .btn-icon {
+ flex-shrink: 0;
+}
+
+.text-muted {
+ color: var(--text-secondary);
+ font-style: italic;
+ font-size: 13px;
+}
+
+/* PP Filter List in Template Modal */
+.pp-filter-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 12px;
+}
+
+.pp-filter-empty {
+ color: var(--text-secondary);
+ font-size: 13px;
+ text-align: center;
+ padding: 16px;
+ border: 1px dashed var(--border-color);
+ border-radius: 8px;
+}
+
+.pp-filter-card {
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background: var(--bg-secondary);
+ padding: 10px 12px;
+}
+
+.pp-filter-card-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ cursor: pointer;
+ user-select: none;
+}
+
+.pp-filter-card.expanded .pp-filter-card-header {
+ margin-bottom: 8px;
+}
+
+.pp-filter-card-chevron {
+ font-size: 10px;
+ color: var(--text-secondary);
+ flex-shrink: 0;
+ width: 12px;
+}
+
+.pp-filter-card-name {
+ font-weight: 600;
+ font-size: 14px;
+ color: var(--text-primary);
+}
+
+.pp-filter-card-summary {
+ color: var(--text-secondary);
+ font-size: 12px;
+ margin-right: 8px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.pp-filter-card-actions {
+ display: flex;
+ gap: 4px;
+ flex-shrink: 0;
+ margin-left: auto;
+}
+
+.btn-filter-action {
+ background: none;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ width: 26px;
+ height: 26px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 11px;
+ padding: 0;
+}
+
+.btn-filter-action:hover:not(:disabled) {
+ background: var(--border-color);
+ color: var(--text-primary);
+}
+
+.btn-filter-action:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+}
+
+.btn-filter-remove:hover:not(:disabled) {
+ background: rgba(239, 68, 68, 0.15);
+ border-color: rgba(239, 68, 68, 0.4);
+ color: #ef4444;
+}
+
+.pp-filter-card-options {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.pp-filter-option {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.pp-filter-option label {
+ display: flex;
+ justify-content: space-between;
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.pp-filter-option input[type="range"] {
+ width: 100%;
+}
+
+.pp-filter-option-bool label {
+ justify-content: space-between;
+ gap: 8px;
+ align-items: center;
+ cursor: pointer;
+ padding: 4px 0;
+}
+
+.pp-filter-option-bool input[type="checkbox"] {
+ appearance: none;
+ -webkit-appearance: none;
+ width: 34px;
+ min-width: 34px;
+ height: 18px;
+ background: var(--border-color);
+ border-radius: 9px;
+ position: relative;
+ cursor: pointer;
+ transition: background 0.2s;
+ order: 1;
+ margin: 0;
+}
+
+.pp-filter-option-bool input[type="checkbox"]::after {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 14px;
+ height: 14px;
+ background: white;
+ border-radius: 50%;
+ transition: transform 0.2s;
+}
+
+.pp-filter-option-bool input[type="checkbox"]:checked {
+ background: var(--primary-color);
+}
+
+.pp-filter-option-bool input[type="checkbox"]:checked::after {
+ transform: translateX(16px);
+}
+
+.pp-filter-option-bool span {
+ order: 0;
+}
+
+.pp-add-filter-row {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ margin-bottom: 4px;
+}
+
+.pp-add-filter-select {
+ flex: 1;
+ padding: 6px 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ background: var(--card-bg);
+ color: var(--text-primary);
+ font-size: 13px;
+}
+
+.pp-add-filter-btn {
+ width: 34px;
+ height: 34px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ background: var(--card-bg);
+ color: var(--text-primary);
+ font-size: 20px;
+ cursor: pointer;
+ padding: 0;
+ line-height: 1;
+}
+
+.pp-add-filter-btn:hover {
+ background: var(--border-color);
+}
+
+/* Template Test Section */
+.template-test-section {
+ background: var(--bg-secondary);
+ border-radius: 8px;
+ padding: 16px;
+}
+
+.template-test-section h3 {
+ margin-top: 0;
+ margin-bottom: 12px;
+ font-size: 16px;
+}
+
+.test-results-container {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 16px;
+}
+
+.test-preview-section,
+.test-performance-section {
+ margin-bottom: 20px;
+}
+
+.test-preview-section:last-child,
+.test-performance-section:last-child {
+ margin-bottom: 0;
+}
+
+.test-preview-section h4,
+.test-performance-section h4 {
+ margin-top: 0;
+ margin-bottom: 12px;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+.test-preview-image {
+ border-radius: 4px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.test-preview-image img {
+ display: block;
+ width: 100%;
+ height: auto;
+}
+
+.test-performance-stats {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.stat-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 0;
+ border-bottom: 1px solid var(--border-color);
+ font-size: 14px;
+}
+
+.stat-item:last-child {
+ border-bottom: none;
+}
+
+.stat-item span {
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.stat-item strong {
+ color: var(--text-color);
+ font-weight: 600;
+ font-family: monospace;
+}
+
+/* Empty state */
+.empty-state {
+ text-align: center;
+ padding: 40px 20px;
+ color: var(--text-secondary);
+ font-size: 16px;
+}
+
+/* Stream type badges */
+.badge-raw {
+ background: #1976d2;
+ color: white;
+}
+
+.badge-processed {
+ background: #7b1fa2;
+ color: white;
+}
+
+/* Stream info panel in stream selector modal */
+.stream-info-panel {
+ padding: 4px 0 0 0;
+ font-size: 14px;
+ line-height: 1.6;
+}
+
+/* Stream sub-tabs */
+.stream-tab-bar {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ border-bottom: 2px solid var(--border-color);
+ margin-bottom: 16px;
+}
+
+.stream-tab-btn {
+ background: none;
+ border: none;
+ padding: 8px 14px;
+ font-size: 0.9rem;
+ font-weight: 500;
+ color: var(--text-secondary);
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ margin-bottom: -2px;
+ transition: color 0.2s, border-color 0.2s;
+}
+
+.stream-tab-btn:hover {
+ color: var(--text-color);
+}
+
+.stream-tab-btn.active {
+ color: var(--primary-color);
+ border-bottom-color: var(--primary-color);
+}
+
+.stream-tab-count {
+ background: var(--border-color);
+ color: var(--text-secondary);
+ font-size: 0.7rem;
+ font-weight: 600;
+ padding: 1px 6px;
+ border-radius: 8px;
+ margin-left: 4px;
+}
+
+.stream-tab-btn.active .stream-tab-count {
+ background: var(--primary-color);
+ color: #fff;
+}
+
+.stream-tab-panel {
+ display: none;
+}
+
+.stream-tab-panel.active {
+ display: block;
+}
+
+/* Sub-tab content sections */
+.subtab-section {
+ margin-bottom: 24px;
+}
+
+.subtab-section:last-child {
+ margin-bottom: 0;
+}
+
+.subtab-section-header {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--text-secondary);
+ margin: 0 0 12px 0;
+ padding-bottom: 8px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .templates-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/server/src/wled_controller/static/css/tutorials.css b/server/src/wled_controller/static/css/tutorials.css
new file mode 100644
index 0000000..a22db99
--- /dev/null
+++ b/server/src/wled_controller/static/css/tutorials.css
@@ -0,0 +1,197 @@
+/* Tutorial System */
+.tutorial-trigger-btn {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ border: 2px solid var(--primary-color);
+ background: transparent;
+ color: var(--primary-color);
+ font-size: 1rem;
+ font-weight: bold;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: auto;
+ margin-right: 8px;
+ flex-shrink: 0;
+}
+
+.tutorial-trigger-btn:hover {
+ background: var(--primary-color);
+ color: white;
+}
+
+#calibration-modal .modal-body {
+ position: relative;
+}
+
+.tutorial-overlay {
+ display: none;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 100;
+ pointer-events: none;
+}
+
+.tutorial-overlay.active {
+ display: block;
+ pointer-events: auto;
+}
+
+.tutorial-backdrop {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.7);
+ transition: clip-path 0.3s ease;
+}
+
+.tutorial-ring {
+ position: absolute;
+ border: 2px solid var(--primary-color);
+ border-radius: 6px;
+ pointer-events: none;
+ transition: all 0.3s ease;
+ animation: tutorial-pulse 2s infinite;
+}
+
+@keyframes tutorial-pulse {
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.6); }
+ 50% { box-shadow: 0 0 0 6px rgba(76, 175, 80, 0); }
+}
+
+.tutorial-tooltip {
+ position: absolute;
+ width: 260px;
+ background: var(--card-bg);
+ border: 2px solid var(--primary-color);
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
+ z-index: 102;
+ pointer-events: auto;
+ animation: tutorial-tooltip-in 0.25s ease-out;
+}
+
+@keyframes tutorial-tooltip-in {
+ from { opacity: 0; transform: translateY(-8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.tutorial-tooltip-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.tutorial-step-counter {
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--primary-color);
+}
+
+.tutorial-close-btn {
+ background: none;
+ border: none;
+ color: #777;
+ font-size: 1.3rem;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border-radius: 4px;
+ transition: color 0.2s, background 0.2s;
+ padding: 0;
+ line-height: 1;
+}
+
+.tutorial-close-btn:hover {
+ color: var(--text-color);
+ background: rgba(128, 128, 128, 0.15);
+}
+
+.tutorial-tooltip-text {
+ margin: 0;
+ padding: 12px;
+ line-height: 1.5;
+ color: var(--text-color);
+ font-size: 0.9rem;
+}
+
+.tutorial-tooltip-nav {
+ display: flex;
+ gap: 6px;
+ padding: 8px 12px;
+ border-top: 1px solid var(--border-color);
+}
+
+.tutorial-prev-btn,
+.tutorial-next-btn {
+ flex: 1;
+ padding: 6px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 1rem;
+ font-weight: 600;
+ transition: opacity 0.2s;
+}
+
+.tutorial-prev-btn {
+ background: var(--border-color);
+ color: var(--text-color);
+}
+
+.tutorial-next-btn {
+ background: var(--primary-color);
+ color: white;
+}
+
+.tutorial-prev-btn:hover:not(:disabled),
+.tutorial-next-btn:hover:not(:disabled) {
+ opacity: 0.85;
+}
+
+.tutorial-prev-btn:disabled,
+.tutorial-next-btn:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+}
+
+.tutorial-target {
+ position: relative;
+ z-index: 101 !important;
+}
+
+/* Fixed (viewport-level) tutorial overlay for device cards */
+.tutorial-overlay-fixed {
+ position: fixed;
+ z-index: 10000;
+}
+
+.tutorial-overlay-fixed .tutorial-backdrop {
+ position: fixed;
+}
+
+.tutorial-overlay-fixed .tutorial-ring {
+ position: fixed;
+}
+
+.tutorial-overlay-fixed .tutorial-tooltip {
+ position: absolute;
+ z-index: 10002;
+ animation: none;
+ opacity: 1;
+}
+
+/* target z-index for fixed overlay is set inline via JS (target is outside overlay DOM) */
diff --git a/server/src/wled_controller/static/index.html b/server/src/wled_controller/static/index.html
deleted file mode 100644
index 2b6284d..0000000
--- a/server/src/wled_controller/static/index.html
+++ /dev/null
@@ -1,1259 +0,0 @@
-
-
-
-
-
- LED Grab
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0 / 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
●
-
●
-
●
-
●
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- No devices found
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
![Full size preview]()
-
-
-
-
-
-
-
-
-
-
-
diff --git a/server/src/wled_controller/static/style.css b/server/src/wled_controller/static/style.css
deleted file mode 100644
index 092d459..0000000
--- a/server/src/wled_controller/static/style.css
+++ /dev/null
@@ -1,3712 +0,0 @@
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-:root {
- --primary-color: #4CAF50;
- --danger-color: #f44336;
- --warning-color: #ff9800;
- --info-color: #2196F3;
-}
-
-/* Dark theme (default) */
-[data-theme="dark"] {
- --bg-color: #1a1a1a;
- --card-bg: #2d2d2d;
- --text-color: #e0e0e0;
- --border-color: #404040;
- --display-badge-bg: rgba(0, 0, 0, 0.4);
- color-scheme: dark;
-}
-
-/* Light theme */
-[data-theme="light"] {
- --bg-color: #f5f5f5;
- --card-bg: #ffffff;
- --text-color: #333333;
- --border-color: #e0e0e0;
- --display-badge-bg: rgba(255, 255, 255, 0.85);
- color-scheme: light;
-}
-
-/* Default to dark theme */
-body {
- background: var(--bg-color);
- color: var(--text-color);
-}
-
-html {
- background: var(--bg-color);
- overflow-y: scroll;
-}
-
-body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
- background: var(--bg-color);
- color: var(--text-color);
- line-height: 1.6;
-}
-
-body.modal-open {
- position: fixed;
- width: 100%;
-}
-
-.container {
- max-width: 1200px;
- margin: 0 auto;
- padding: 20px;
-}
-
-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 20px 0 10px;
- margin-bottom: 10px;
- position: relative;
- z-index: 2100;
-}
-
-.header-title {
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-h1 {
- font-size: 2rem;
- color: var(--primary-color);
-}
-
-h2 {
- margin-bottom: 20px;
- color: var(--text-color);
- font-size: 1.5rem;
-}
-
-.server-info {
- display: flex;
- align-items: center;
- gap: 15px;
-}
-
-.header-link {
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 0.85rem;
- font-weight: 500;
- padding: 4px 8px;
- border-radius: 4px;
- transition: color 0.2s, background 0.2s;
-}
-
-.header-link:hover {
- color: var(--text-color);
- background: var(--bg-secondary);
-}
-
-#server-version {
- font-size: 0.75rem;
- font-weight: 400;
- color: var(--text-secondary);
- background: var(--border-color);
- padding: 2px 8px;
- border-radius: 10px;
- letter-spacing: 0.03em;
-}
-
-.status-badge {
- font-size: 1.5rem;
- animation: pulse 2s infinite;
-}
-
-.status-badge.online {
- color: var(--primary-color);
-}
-
-.status-badge.offline {
- color: var(--danger-color);
-}
-
-@keyframes pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.5; }
-}
-
-/* WLED device health indicator */
-.health-dot {
- display: inline-block;
- width: 10px;
- height: 10px;
- border-radius: 50%;
- margin-right: 8px;
- vertical-align: middle;
- flex-shrink: 0;
-}
-
-.health-dot.health-online {
- background-color: #4CAF50;
- box-shadow: 0 0 6px rgba(76, 175, 80, 0.6);
-}
-
-.health-dot.health-offline {
- background-color: var(--danger-color);
- box-shadow: 0 0 6px rgba(244, 67, 54, 0.6);
-}
-
-.health-dot.health-unknown {
- background-color: #9E9E9E;
- animation: pulse 2s infinite;
-}
-
-.health-latency {
- font-size: 0.7rem;
- font-weight: 400;
- color: #4CAF50;
- margin-left: auto;
- padding-left: 8px;
- opacity: 0.85;
-}
-
-.health-latency.offline {
- color: var(--danger-color);
-}
-
-section {
- margin-bottom: 40px;
-}
-
-.displays-grid,
-.devices-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
- gap: 20px;
-}
-
-.devices-grid > .loading,
-.devices-grid > .loading-spinner {
- grid-column: 1 / -1;
-}
-
-.add-device-card {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- border: 2px dashed var(--border-color);
- background: transparent;
- min-height: 160px;
- transition: border-color 0.2s, background 0.2s;
-}
-
-.add-device-card:hover {
- border-color: var(--primary-color);
- background: rgba(33, 150, 243, 0.05);
-}
-
-.add-device-icon {
- font-size: 2.5rem;
- font-weight: 300;
- color: var(--text-secondary);
- line-height: 1;
- transition: color 0.2s;
-}
-
-.add-device-card:hover .add-device-icon {
- color: var(--primary-color);
-}
-
-.add-device-label {
- font-size: 0.85rem;
- color: var(--text-secondary);
- margin-top: 8px;
-}
-
-.card {
- background: var(--card-bg);
- border: 1px solid var(--border-color);
- border-radius: 8px;
- padding: 12px 20px 20px;
- position: relative;
- transition: transform 0.2s, box-shadow 0.2s;
- display: flex;
- flex-direction: column;
-}
-
-.card:hover {
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
-}
-
-.card-tutorial-btn {
- position: absolute;
- bottom: 10px;
- right: 10px;
- background: none;
- border: 1.5px solid var(--border-color);
- color: var(--text-muted, #777);
- font-size: 0.7rem;
- font-weight: bold;
- width: 20px;
- height: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- border-radius: 50%;
- transition: color 0.2s, background 0.2s, border-color 0.2s;
- padding: 0;
-}
-
-.card-tutorial-btn:hover {
- border-color: var(--primary-color);
- color: var(--primary-color);
-}
-
-
-.card-top-actions {
- position: absolute;
- top: 8px;
- right: 8px;
- display: flex;
- align-items: center;
- gap: 2px;
-}
-
-.card-top-actions .card-remove-btn {
- position: static;
-}
-
-.card-power-btn {
- background: none;
- border: none;
- color: #777;
- font-size: 1rem;
- width: 28px;
- height: 28px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- border-radius: 4px;
- transition: color 0.2s, background 0.2s;
-}
-
-.card-power-btn:hover {
- color: var(--primary-color);
- background: rgba(76, 175, 80, 0.1);
-}
-
-.card-remove-btn {
- position: absolute;
- top: 10px;
- right: 10px;
- background: none;
- border: none;
- color: #777;
- font-size: 1rem;
- width: 28px;
- height: 28px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- border-radius: 4px;
- transition: color 0.2s, background 0.2s;
-}
-
-.card-remove-btn:hover {
- color: var(--danger-color);
- background: rgba(244, 67, 54, 0.1);
-}
-
-.card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
- padding-right: 30px;
-}
-
-.card-title {
- font-size: 1.2rem;
- font-weight: 600;
- display: flex;
- align-items: center;
- gap: 6px;
- flex-wrap: wrap;
-}
-
-.device-url-badge {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- font-size: 0.7rem;
- font-weight: 400;
- color: var(--text-secondary);
- background: var(--border-color);
- padding: 2px 8px;
- border-radius: 10px;
- letter-spacing: 0.03em;
- font-family: monospace;
- text-decoration: none;
- transition: background 0.2s;
-}
-
-.device-url-badge:hover {
- background: var(--text-muted);
-}
-
-.device-url-icon {
- font-size: 0.6rem;
-}
-
-.card-subtitle {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-bottom: 15px;
- flex-wrap: wrap;
-}
-
-.card-meta {
- font-size: 0.8rem;
- color: #999;
- display: inline-flex;
- align-items: center;
- gap: 4px;
-}
-
-.device-type-badge {
- font-size: 10px;
- font-weight: 700;
- padding: 1px 6px;
- border-radius: 3px;
- background: rgba(255, 255, 255, 0.15);
- letter-spacing: 0.5px;
-}
-
-/* Device discovery */
-.discovery-section {
- margin-bottom: 12px;
-}
-.btn-block {
- width: 100%;
-}
-.discovery-list {
- max-height: 200px;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- gap: 4px;
- margin-top: 8px;
-}
-.discovery-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 12px;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- background: var(--card-bg);
- cursor: pointer;
- transition: opacity 0.15s;
-}
-.discovery-item:not(.discovery-item--added):hover {
- opacity: 0.8;
-}
-.discovery-item--added {
- opacity: 0.5;
- cursor: default;
-}
-.discovery-item-info {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-.discovery-badge {
- font-size: 11px;
- padding: 2px 8px;
- border-radius: 10px;
- background: var(--border-color);
- white-space: nowrap;
-}
-.discovery-type-badge {
- font-size: 10px;
- padding: 1px 5px;
- border-radius: 3px;
- background: var(--primary-color);
- color: #fff;
- font-weight: 600;
- vertical-align: middle;
- margin-right: 2px;
-}
-.modal-divider {
- border: none;
- border-top: 1px solid var(--border-color);
- margin: 12px 0;
-}
-.discovery-loading {
- display: flex;
- justify-content: center;
- padding: 12px;
-}
-.discovery-spinner {
- width: 20px;
- height: 20px;
- border: 2px solid var(--border-color);
- border-top-color: var(--primary-color);
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
-}
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
-.channel-indicator {
- display: inline-flex;
- gap: 2px;
- align-items: center;
-}
-
-.channel-indicator .ch {
- width: 8px;
- height: 10px;
- border-radius: 1px;
-}
-
-.badge {
- padding: 4px 12px;
- border-radius: 12px;
- font-size: 0.85rem;
- font-weight: 600;
-}
-
-.badge.processing {
- background: var(--primary-color);
- color: white;
-}
-
-.badge.idle {
- background: var(--warning-color);
- color: white;
-}
-
-.badge.error {
- background: var(--danger-color);
- color: white;
-}
-
-.card-content {
- margin-bottom: 15px;
-}
-
-.info-row {
- display: flex;
- justify-content: space-between;
- padding: 8px 0;
- border-bottom: 1px solid var(--border-color);
-}
-
-.info-row:last-child {
- border-bottom: none;
-}
-
-.info-label {
- color: #999;
-}
-
-.info-value {
- font-weight: 600;
-}
-
-.card-actions {
- display: flex;
- gap: 8px;
- margin-top: auto;
- padding-top: 12px;
- border-top: 1px solid var(--border-color);
- align-items: center;
-}
-
-.btn {
- padding: 8px 16px;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 0.9rem;
- font-weight: 600;
- transition: opacity 0.2s;
- flex: 1 1 auto;
- min-width: 100px;
-}
-
-.btn:hover {
- opacity: 0.9;
-}
-
-.btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.btn-primary {
- background: var(--primary-color);
- color: white;
-}
-
-.btn-danger {
- background: var(--danger-color);
- color: white;
-}
-
-.btn-secondary {
- background: var(--border-color);
- color: var(--text-color);
-}
-
-.btn-icon {
- min-width: auto;
- padding: 8px 12px;
- font-size: 1.2rem;
- flex: 0 0 auto;
-}
-
-.btn-icon:hover {
- transform: scale(1.1);
- opacity: 1;
-}
-
-.display-card {
- background: var(--card-bg);
- border: 1px solid var(--border-color);
- border-radius: 8px;
- padding: 15px;
-}
-
-.display-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 15px;
-}
-
-.display-index {
- font-size: 1.3rem;
- font-weight: 700;
- color: var(--info-color);
-}
-
-.primary-star {
- color: var(--primary-color);
- font-size: 1.2rem;
-}
-
-/* Tabs */
-.tab-bar {
- display: flex;
- align-items: center;
- gap: 4px;
- border-bottom: 2px solid var(--border-color);
- margin-bottom: 16px;
-}
-
-.tab-btn {
- background: none;
- border: none;
- padding: 10px 18px;
- font-size: 1rem;
- font-weight: 500;
- color: var(--text-secondary);
- cursor: pointer;
- border-bottom: 2px solid transparent;
- margin-bottom: -2px;
- transition: color 0.2s, border-color 0.2s;
-}
-
-.tab-btn:hover {
- color: var(--text-color);
-}
-
-.tab-btn.active {
- color: var(--primary-color);
- border-bottom-color: var(--primary-color);
-}
-
-.tab-panel {
- display: none;
-}
-
-.tab-panel.active {
- display: block;
-}
-
-/* Display Layout Visualization */
-.layout-container {
- position: relative;
- background: transparent;
-}
-
-.layout-display {
- position: absolute;
- border: 3px solid;
- border-radius: 8px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
- transition: box-shadow 0.2s, border-color 0.2s;
-}
-
-.layout-display:hover {
- box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
- z-index: 10;
-}
-
-.layout-display.primary {
- border-color: var(--primary-color);
- background: linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(76, 175, 80, 0.05));
-}
-
-.layout-display.secondary {
- border-color: var(--border-color);
- background: linear-gradient(135deg, rgba(128, 128, 128, 0.1), rgba(128, 128, 128, 0.05));
-}
-
-.layout-position-label {
- position: absolute;
- top: 4px;
- left: 6px;
- font-size: 0.7rem;
- color: var(--text-secondary);
-}
-
-.layout-index-label {
- position: absolute;
- bottom: 6px;
- left: 6px;
- font-size: 0.85rem;
- font-weight: 600;
- color: var(--text-color);
- background: var(--display-badge-bg);
- padding: 1px 6px;
- border-radius: 4px;
- letter-spacing: 0.5px;
-}
-
-.layout-display-label {
- text-align: center;
- padding: 5px;
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.layout-display-label strong {
- font-size: 0.9rem;
- color: var(--text-color);
- font-weight: 600;
-}
-
-.layout-display-label small {
- font-size: 0.75rem;
- color: var(--text-secondary);
-}
-
-.primary-indicator {
- position: absolute;
- top: 2px;
- right: 4px;
- color: var(--primary-color);
- font-size: 1.5rem;
- line-height: 1;
- text-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
-}
-
-
-/* Card brightness slider */
-.brightness-control {
- padding: 0;
- margin-bottom: 12px;
-}
-
-.brightness-slider {
- width: 100%;
-}
-
-.brightness-loading .brightness-slider {
- opacity: 0.3;
- pointer-events: none;
-}
-
-/* Static color picker — inline in card-subtitle */
-.static-color-control {
- display: inline-flex;
- align-items: center;
- gap: 4px;
-}
-
-.static-color-picker {
- width: 22px;
- height: 18px;
- padding: 0;
- border: 1px solid var(--border-color);
- border-radius: 3px;
- cursor: pointer;
- background: none;
- vertical-align: middle;
-}
-
-.btn-clear-color {
- background: none;
- border: none;
- color: #777;
- font-size: 0.75rem;
- cursor: pointer;
- padding: 0 2px;
- line-height: 1;
- border-radius: 3px;
- transition: color 0.2s;
-}
-
-.btn-clear-color:hover {
- color: var(--danger-color);
-}
-
-.section-header {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 8px;
-}
-
-.section-header h2 {
- margin-bottom: 0;
-}
-
-.section-tip {
- font-size: 0.82rem;
- color: var(--text-secondary);
- margin: 0 0 15px 0;
- line-height: 1.5;
- padding: 8px 12px;
- background: rgba(33, 150, 243, 0.08);
- border-left: 3px solid var(--info-color, #2196F3);
- border-radius: 0 6px 6px 0;
-}
-
-.section-tip a {
- color: var(--info-color, #2196F3);
- text-decoration: underline;
-}
-
-ul.section-tip {
- list-style: disc;
- padding-left: 28px;
-}
-
-ul.section-tip li {
- margin: 2px 0;
-}
-
-.form-group {
- margin-bottom: 15px;
-}
-
-.settings-toggle-group {
- display: flex;
- flex-direction: column;
-}
-
-.settings-toggle {
- position: relative;
- display: inline-block;
- width: 34px;
- height: 18px;
- cursor: pointer;
- margin-top: 4px;
-}
-
-.settings-toggle input {
- opacity: 0;
- width: 0;
- height: 0;
-}
-
-.settings-toggle-slider {
- position: absolute;
- inset: 0;
- background: var(--border-color);
- border-radius: 9px;
- transition: background 0.2s;
-}
-
-.settings-toggle-slider::after {
- content: '';
- position: absolute;
- top: 2px;
- left: 2px;
- width: 14px;
- height: 14px;
- background: white;
- border-radius: 50%;
- transition: transform 0.2s;
-}
-
-.settings-toggle input:checked + .settings-toggle-slider {
- background: var(--primary-color);
-}
-
-.settings-toggle input:checked + .settings-toggle-slider::after {
- transform: translateX(16px);
-}
-
-label {
- display: block;
- margin-bottom: 5px;
- color: #999;
- font-weight: 500;
-}
-
-input[type="text"],
-input[type="url"],
-input[type="number"],
-input[type="password"],
-select {
- width: 100%;
- padding: 10px;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- background: var(--bg-color);
- color: var(--text-color);
- font-size: 1rem;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
- transition: border-color 0.2s, box-shadow 0.2s, opacity 0.2s;
-}
-
-input[type="number"]:disabled,
-input[type="password"]:disabled,
-select:disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-
-input[type="range"] {
- width: 100%;
- margin: 8px 0;
-}
-
-/* Better password field appearance */
-input[type="password"] {
- letter-spacing: 0.15em;
-}
-
-input:focus,
-select:focus {
- outline: none;
- border-color: var(--primary-color);
- box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
-}
-
-/* Remove browser autofill styling */
-input:-webkit-autofill,
-input:-webkit-autofill:hover,
-input:-webkit-autofill:focus {
- -webkit-box-shadow: 0 0 0 1000px var(--bg-color) inset;
- -webkit-text-fill-color: var(--text-color);
- transition: background-color 5000s ease-in-out 0s;
-}
-
-.loading {
- text-align: center;
- padding: 40px;
- color: #999;
-}
-
-.loading-spinner {
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 40px;
-}
-
-.loading-spinner::after {
- content: '';
- width: 28px;
- height: 28px;
- border: 3px solid var(--border-color);
- border-top-color: var(--primary-color);
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
-}
-
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
-/* Full-page overlay spinner */
-.overlay-spinner {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.7);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- z-index: 9999;
- backdrop-filter: blur(4px);
-}
-
-.overlay-spinner .progress-container {
- position: relative;
- width: 120px;
- height: 120px;
-}
-
-.overlay-spinner .progress-ring {
- transform: rotate(-90deg);
-}
-
-.overlay-spinner .progress-ring-circle {
- transition: stroke-dashoffset 0.1s linear;
- stroke: var(--primary-color);
- stroke-width: 4;
- fill: transparent;
-}
-
-.overlay-spinner .progress-ring-bg {
- stroke: rgba(255, 255, 255, 0.1);
- stroke-width: 4;
- fill: transparent;
-}
-
-.overlay-spinner .progress-content {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- text-align: center;
-}
-
-.overlay-spinner .progress-percentage {
- color: white;
- font-size: 28px;
- font-weight: 600;
-}
-
-.overlay-spinner .spinner-text {
- margin-top: 24px;
- color: white;
- font-size: 16px;
- font-weight: 500;
-}
-
-.overlay-spinner-close {
- position: absolute;
- top: 16px;
- right: 16px;
- background: none;
- border: none;
- color: rgba(255, 255, 255, 0.6);
- font-size: 32px;
- cursor: pointer;
- line-height: 1;
- padding: 4px 8px;
- transition: color 0.15s;
-}
-
-.overlay-spinner-close:hover {
- color: white;
-}
-
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
-.toast {
- position: fixed;
- bottom: 40px;
- left: 50%;
- transform: translateX(-50%) translateY(100px);
- padding: 16px 24px;
- border-radius: 8px;
- color: white;
- font-weight: 600;
- font-size: 15px;
- opacity: 0;
- transition: opacity 0.3s, transform 0.3s;
- z-index: 2001;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
- min-width: 300px;
- text-align: center;
-}
-
-.toast.show {
- opacity: 1;
- transform: translateX(-50%) translateY(0);
- animation: toastShake 0.5s ease-in-out;
-}
-
-@keyframes toastShake {
- 0%, 100% { transform: translateX(-50%) translateY(0); }
- 10%, 30%, 50%, 70%, 90% { transform: translateX(-50%) translateY(-5px); }
- 20%, 40%, 60%, 80% { transform: translateX(-50%) translateY(5px); }
-}
-
-.toast.success {
- background: var(--primary-color);
-}
-
-.toast.error {
- background: var(--danger-color);
-}
-
-.toast.info {
- background: var(--info-color);
-}
-
-.metrics-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 4px 12px;
- margin-top: 8px;
-}
-
-.metric {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 3px 8px;
- background: var(--bg-color);
- border-radius: 4px;
-}
-
-.metric-value {
- font-size: 0.9rem;
- font-weight: 700;
- color: var(--primary-color);
-}
-
-.metric-label {
- font-size: 0.8rem;
- color: #999;
-}
-
-/* Timing breakdown bar */
-.timing-breakdown {
- margin-top: 8px;
- padding: 6px 8px;
- background: var(--bg-color);
- border-radius: 4px;
-}
-
-.timing-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.timing-total {
- font-size: 0.8rem;
- color: var(--primary-color);
-}
-
-.timing-bar {
- display: flex;
- height: 8px;
- border-radius: 4px;
- overflow: hidden;
- margin: 4px 0;
- gap: 1px;
-}
-
-.timing-seg {
- min-width: 2px;
- transition: flex 0.3s ease;
-}
-
-.timing-extract { background: #4CAF50; }
-.timing-map { background: #FF9800; }
-.timing-smooth { background: #2196F3; }
-.timing-send { background: #E91E63; }
-
-.timing-legend {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- font-size: 0.75rem;
- color: #999;
- margin-top: 4px;
-}
-
-.timing-legend-item {
- display: flex;
- align-items: center;
- gap: 3px;
-}
-
-.timing-dot {
- display: inline-block;
- width: 8px;
- height: 8px;
- border-radius: 2px;
-}
-
-.timing-dot.timing-extract { background: #4CAF50; }
-.timing-dot.timing-map { background: #FF9800; }
-.timing-dot.timing-smooth { background: #2196F3; }
-.timing-dot.timing-send { background: #E91E63; }
-
-/* Modal Styles */
-.modal {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.8);
- z-index: 2000;
- align-items: center;
- justify-content: center;
- animation: fadeIn 0.2s ease-out;
-}
-
-@keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
-}
-
-.modal-content {
- background: var(--card-bg);
- border: 1px solid var(--border-color);
- border-radius: 12px;
- max-width: 500px;
- width: 90%;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
- animation: slideUp 0.3s ease-out;
-}
-
-#template-modal .modal-content {
- max-width: 500px !important;
- width: 100% !important;
-}
-
-#test-template-modal .modal-content {
- max-width: 420px;
-}
-
-@keyframes slideUp {
- from {
- transform: translateY(20px);
- opacity: 0;
- }
- to {
- transform: translateY(0);
- opacity: 1;
- }
-}
-
-.modal-header {
- padding: 24px 24px 16px 24px;
- border-bottom: 1px solid var(--border-color);
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.modal-header h2 {
- margin: 0;
- font-size: 1.5rem;
- color: var(--text-color);
-}
-
-.modal-header-actions {
- display: flex;
- align-items: center;
- gap: 4px;
-}
-
-.modal-header-btn {
- background: none;
- border: none;
- font-size: 1.1rem;
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- border-radius: 4px;
- transition: background 0.2s;
- flex-shrink: 0;
-}
-.modal-header-btn:hover {
- background: rgba(128, 128, 128, 0.15);
-}
-.modal-header-btn:disabled {
- opacity: 0.4;
- cursor: default;
-}
-
-.modal-close-btn {
- background: none;
- border: none;
- color: #777;
- font-size: 1.2rem;
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- border-radius: 4px;
- transition: color 0.2s, background 0.2s;
- flex-shrink: 0;
-}
-
-.modal-close-btn:hover {
- color: var(--text-color);
- background: rgba(128, 128, 128, 0.15);
-}
-
-.modal-body {
- padding: 24px;
-}
-
-.modal-description {
- color: #999;
- margin-bottom: 20px;
- line-height: 1.6;
-}
-
-.password-input-wrapper {
- position: relative;
- display: flex;
- align-items: center;
-}
-
-.password-input-wrapper input {
- flex: 1;
- padding-right: 45px;
-}
-
-.password-toggle {
- position: absolute;
- right: 8px;
- background: none;
- border: none;
- cursor: pointer;
- font-size: 1.2rem;
- padding: 8px;
- color: var(--text-color);
- opacity: 0.6;
- transition: opacity 0.2s;
-}
-
-.password-toggle:hover {
- opacity: 1;
-}
-
-.label-row {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-bottom: 5px;
-}
-
-.label-row label {
- margin-bottom: 0;
-}
-
-.hint-toggle {
- background: none;
- border: 1px solid var(--border-color);
- border-radius: 50%;
- width: 18px;
- height: 18px;
- font-size: 0.7rem;
- line-height: 1;
- color: var(--text-secondary, #888);
- cursor: pointer;
- padding: 0;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- opacity: 0.6;
- transition: opacity 0.2s;
- flex-shrink: 0;
-}
-
-.hint-toggle:hover {
- opacity: 1;
-}
-
-.hint-toggle.active {
- opacity: 1;
- color: var(--primary-color, #4CAF50);
- border-color: var(--primary-color, #4CAF50);
-}
-
-.input-hint {
- display: block;
- margin: 0 0 6px 0;
- color: #666;
- font-size: 0.85rem;
-}
-
-.fps-hint {
- display: block;
- margin-top: 4px;
- font-size: 0.82rem;
- color: var(--info-color, #2196F3);
-}
-
-.slider-row {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-.slider-row input[type="range"] {
- flex: 1;
-}
-
-.slider-value {
- min-width: 28px;
- text-align: center;
- font-weight: 600;
- font-size: 0.95rem;
- color: var(--text-primary);
-}
-
-.error-message {
- background: rgba(244, 67, 54, 0.1);
- border: 1px solid var(--danger-color);
- color: var(--danger-color);
- padding: 12px;
- border-radius: 4px;
- margin-top: 15px;
- font-size: 0.9rem;
-}
-
-.modal-footer {
- padding: 16px 24px 24px 24px;
- display: flex;
- justify-content: flex-end;
- gap: 8px;
-}
-
-.modal-footer .btn-icon {
- min-width: 60px;
- padding: 10px 20px;
- font-size: 1.4rem;
-}
-
-/* Theme Toggle */
-.theme-toggle {
- background: var(--card-bg);
- border: 1px solid var(--border-color);
- padding: 6px 12px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 1.2rem;
- transition: transform 0.2s;
- margin-left: 10px;
-}
-
-.theme-toggle:hover {
- transform: scale(1.1);
-}
-
-/* Footer */
-.app-footer {
- margin-top: 20px;
- padding: 15px 0;
- text-align: center;
-}
-
-.footer-content {
- color: var(--text-secondary);
- font-size: 0.9rem;
-}
-
-.footer-content p {
- margin: 0;
-}
-
-
-.footer-content strong {
- color: var(--text-color);
-}
-
-.footer-content a {
- color: var(--primary-color);
- text-decoration: none;
- transition: opacity 0.2s;
-}
-
-.footer-content a:hover {
- opacity: 0.8;
- text-decoration: underline;
-}
-
-/* Interactive Calibration Preview Edges */
-.calibration-preview {
- position: relative;
- width: 100%;
- aspect-ratio: 16 / 9;
- margin: 40px auto 40px;
- background: var(--card-bg);
- border: 2px solid var(--border-color);
- border-radius: 8px;
- overflow: visible;
-}
-
-#calibration-preview-canvas {
- position: absolute;
- top: -40px;
- left: -40px;
- width: calc(100% + 80px);
- height: calc(100% + 80px);
- pointer-events: none;
- z-index: 3;
-}
-
-.preview-screen {
- position: absolute;
- top: 37px;
- left: 57px;
- right: 57px;
- bottom: 37px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- border-radius: 4px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 6px;
- color: white;
- font-size: 14px;
-}
-
-.preview-screen-border-width {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 13px;
- font-weight: 600;
-}
-
-.preview-screen-border-width label {
- white-space: nowrap;
-}
-
-.preview-screen-border-width input {
- width: 52px;
- padding: 2px 4px;
- font-size: 12px;
- border: 1px solid rgba(255, 255, 255, 0.4);
- border-radius: 3px;
- background: rgba(255, 255, 255, 0.15);
- color: white;
- text-align: center;
-}
-
-.preview-screen-border-width input:focus {
- outline: none;
- border-color: rgba(255, 255, 255, 0.7);
- background: rgba(255, 255, 255, 0.25);
-}
-
-.preview-screen-total {
- font-size: 16px;
- font-weight: 600;
- opacity: 0.9;
- transition: color 0.2s;
- cursor: pointer;
- user-select: none;
-}
-
-.preview-screen-total:hover {
- opacity: 1;
-}
-
-.preview-screen-total.mismatch {
- color: #FFC107;
-}
-
-.inputs-dimmed .edge-led-input {
- opacity: 0.2;
- pointer-events: none;
-}
-
-.preview-screen-controls {
- display: flex;
- align-items: center;
- gap: 6px;
-}
-
-.preview-edge {
- position: absolute;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 11px;
- color: var(--text-secondary);
- background: rgba(128, 128, 128, 0.15);
- transition: background 0.2s;
- z-index: 2;
- user-select: none;
-}
-
-/* Edge test toggle zones — positioned outside the container border */
-.edge-toggle {
- position: absolute;
- cursor: pointer;
- z-index: 1;
- background: rgba(128, 128, 128, 0.1);
- border: 1px solid rgba(128, 128, 128, 0.35);
- border-radius: 3px;
- transition: background 0.2s, box-shadow 0.2s;
-}
-
-.edge-toggle:hover {
- background: rgba(128, 128, 128, 0.25);
-}
-
-.preview-edge.edge-disabled {
- opacity: 0.25;
- pointer-events: none;
-}
-
-.preview-edge.edge-disabled .edge-led-input {
- pointer-events: auto;
- opacity: 1;
-}
-
-.edge-toggle.edge-disabled {
- opacity: 0.15;
- pointer-events: none;
- cursor: default;
-}
-
-.toggle-top {
- top: -16px;
- left: 56px;
- right: 56px;
- height: 16px;
-}
-
-.toggle-bottom {
- bottom: -16px;
- left: 56px;
- right: 56px;
- height: 16px;
-}
-
-.toggle-left {
- left: -16px;
- top: 36px;
- bottom: 36px;
- width: 16px;
-}
-
-.toggle-right {
- right: -16px;
- top: 36px;
- bottom: 36px;
- width: 16px;
-}
-
-.edge-top {
- top: 0;
- left: 56px;
- right: 56px;
- height: 36px;
- border-radius: 6px 6px 0 0;
- flex-direction: row;
- gap: 8px;
-}
-
-.edge-bottom {
- bottom: 0;
- left: 56px;
- right: 56px;
- height: 36px;
- border-radius: 0 0 6px 6px;
- flex-direction: row;
- gap: 8px;
-}
-
-.edge-left {
- left: 0;
- top: 36px;
- bottom: 36px;
- width: 56px;
- flex-direction: column;
- border-radius: 6px 0 0 6px;
- gap: 4px;
-}
-
-.edge-right {
- right: 0;
- top: 36px;
- bottom: 36px;
- width: 56px;
- flex-direction: column;
- border-radius: 0 6px 6px 0;
- gap: 4px;
-}
-
-.edge-led-input {
- width: 46px;
- padding: 2px 2px;
- font-size: 12px;
- box-sizing: border-box;
- max-height: 100%;
- text-align: center;
- background: rgba(0, 0, 0, 0.3);
- border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 3px;
- color: inherit;
-}
-
-.edge-led-input:focus {
- border-color: var(--primary-color);
- outline: none;
-}
-
-.edge-top .edge-led-input,
-.edge-bottom .edge-led-input {
- width: 56px;
-}
-
-/* Hide spinner arrows on edge inputs to save space */
-.edge-led-input::-webkit-outer-spin-button,
-.edge-led-input::-webkit-inner-spin-button {
- -webkit-appearance: none;
- margin: 0;
-}
-.edge-led-input[type=number] {
- -moz-appearance: textfield;
-}
-
-/* Edge span bars */
-.edge-span-bar {
- position: absolute;
- background: rgba(76, 175, 80, 0.3);
- border: 1px solid rgba(76, 175, 80, 0.5);
- border-radius: 2px;
- cursor: grab;
- transition: background 0.15s;
-}
-
-.edge-span-bar:hover {
- background: rgba(76, 175, 80, 0.45);
-}
-
-.edge-span-bar:active {
- cursor: grabbing;
-}
-
-/* Horizontal edges: bar spans left-right */
-.edge-top .edge-span-bar,
-.edge-bottom .edge-span-bar {
- top: 0;
- bottom: 0;
-}
-
-/* Vertical edges: bar spans top-bottom */
-.edge-left .edge-span-bar,
-.edge-right .edge-span-bar {
- left: 0;
- right: 0;
-}
-
-/* Resize handles — large transparent hit area with narrow visible strip */
-.edge-span-handle {
- position: absolute;
- background: transparent;
- z-index: 3;
- opacity: 0;
- transition: opacity 0.15s;
-}
-
-.edge-span-handle::after {
- content: '';
- position: absolute;
- background: rgba(255, 255, 255, 0.75);
- border: 1px solid rgba(76, 175, 80, 0.7);
- border-radius: 2px;
-}
-
-.edge-span-bar:hover .edge-span-handle {
- opacity: 1;
-}
-
-/* Horizontal handles */
-.edge-top .edge-span-handle,
-.edge-bottom .edge-span-handle {
- top: 0;
- bottom: 0;
- width: 16px;
- cursor: ew-resize;
-}
-
-.edge-top .edge-span-handle::after,
-.edge-bottom .edge-span-handle::after {
- top: 3px;
- bottom: 3px;
- left: 6px;
- width: 4px;
-}
-
-.edge-top .edge-span-handle-start,
-.edge-bottom .edge-span-handle-start {
- left: -8px;
-}
-
-.edge-top .edge-span-handle-end,
-.edge-bottom .edge-span-handle-end {
- right: -8px;
-}
-
-/* Vertical handles */
-.edge-left .edge-span-handle,
-.edge-right .edge-span-handle {
- left: 0;
- right: 0;
- height: 16px;
- cursor: ns-resize;
-}
-
-.edge-left .edge-span-handle::after,
-.edge-right .edge-span-handle::after {
- left: 3px;
- right: 3px;
- top: 6px;
- height: 4px;
-}
-
-.edge-left .edge-span-handle-start,
-.edge-right .edge-span-handle-start {
- top: -8px;
-}
-
-.edge-left .edge-span-handle-end,
-.edge-right .edge-span-handle-end {
- bottom: -8px;
-}
-
-/* Ensure LED input is above span bar */
-.edge-led-input {
- position: relative;
- z-index: 2;
-}
-
-/* Corner start-position buttons */
-.preview-corner {
- position: absolute;
- width: 56px;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 18px;
- line-height: 1;
- color: rgba(128, 128, 128, 0.4);
- cursor: pointer;
- z-index: 5;
- transition: color 0.2s, transform 0.2s, text-shadow 0.2s;
- user-select: none;
-}
-
-.preview-corner:hover {
- color: rgba(76, 175, 80, 0.6);
-}
-
-.preview-corner.active:hover {
- transform: none;
-}
-
-.preview-corner.active {
- color: #4CAF50;
- text-shadow: 0 0 8px rgba(76, 175, 80, 0.6);
- font-size: 22px;
-}
-
-.corner-top-left { top: 0; left: 0; }
-.corner-top-right { top: 0; right: 0; }
-.corner-bottom-left { bottom: 0; left: 0; }
-.corner-bottom-right { bottom: 0; right: 0; }
-
-/* Direction toggle inside screen */
-.direction-toggle {
- display: flex;
- align-items: center;
- gap: 4px;
- height: 26px;
- padding: 0 10px;
- background: rgba(255, 255, 255, 0.15);
- border: 1px solid rgba(255, 255, 255, 0.3);
- border-radius: 12px;
- color: white;
- font-family: inherit;
- font-size: 12px;
- box-sizing: border-box;
- cursor: pointer;
- transition: background 0.2s;
- user-select: none;
-}
-
-.direction-toggle:hover {
- background: rgba(255, 255, 255, 0.25);
-}
-
-.direction-toggle #direction-icon {
- font-size: 14px;
-}
-
-.preview-hint {
- text-align: center;
- font-size: 0.8rem;
- color: var(--text-secondary);
- margin-top: 8px;
-}
-
-@media (max-width: 768px) {
- .displays-grid,
- .devices-grid {
- grid-template-columns: 1fr;
- }
-
- header {
- flex-direction: column;
- gap: 15px;
- text-align: center;
- }
-
- .modal-content {
- width: 95%;
- margin: 20px;
- }
-
- .modal-footer .btn {
- min-width: 80px;
- }
-}
-
-/* Tutorial System */
-.tutorial-trigger-btn {
- width: 28px;
- height: 28px;
- border-radius: 50%;
- border: 2px solid var(--primary-color);
- background: transparent;
- color: var(--primary-color);
- font-size: 1rem;
- font-weight: bold;
- cursor: pointer;
- transition: all 0.2s;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-left: auto;
- margin-right: 8px;
- flex-shrink: 0;
-}
-
-.tutorial-trigger-btn:hover {
- background: var(--primary-color);
- color: white;
-}
-
-#calibration-modal .modal-body {
- position: relative;
-}
-
-.tutorial-overlay {
- display: none;
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 100;
- pointer-events: none;
-}
-
-.tutorial-overlay.active {
- display: block;
- pointer-events: auto;
-}
-
-.tutorial-backdrop {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.7);
- transition: clip-path 0.3s ease;
-}
-
-.tutorial-ring {
- position: absolute;
- border: 2px solid var(--primary-color);
- border-radius: 6px;
- pointer-events: none;
- transition: all 0.3s ease;
- animation: tutorial-pulse 2s infinite;
-}
-
-@keyframes tutorial-pulse {
- 0%, 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.6); }
- 50% { box-shadow: 0 0 0 6px rgba(76, 175, 80, 0); }
-}
-
-.tutorial-tooltip {
- position: absolute;
- width: 260px;
- background: var(--card-bg);
- border: 2px solid var(--primary-color);
- border-radius: 8px;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
- z-index: 102;
- pointer-events: auto;
- animation: tutorial-tooltip-in 0.25s ease-out;
-}
-
-@keyframes tutorial-tooltip-in {
- from { opacity: 0; transform: translateY(-8px); }
- to { opacity: 1; transform: translateY(0); }
-}
-
-.tutorial-tooltip-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 12px;
- border-bottom: 1px solid var(--border-color);
-}
-
-.tutorial-step-counter {
- font-size: 0.8rem;
- font-weight: 600;
- color: var(--primary-color);
-}
-
-.tutorial-close-btn {
- background: none;
- border: none;
- color: #777;
- font-size: 1.3rem;
- width: 24px;
- height: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- border-radius: 4px;
- transition: color 0.2s, background 0.2s;
- padding: 0;
- line-height: 1;
-}
-
-.tutorial-close-btn:hover {
- color: var(--text-color);
- background: rgba(128, 128, 128, 0.15);
-}
-
-.tutorial-tooltip-text {
- margin: 0;
- padding: 12px;
- line-height: 1.5;
- color: var(--text-color);
- font-size: 0.9rem;
-}
-
-.tutorial-tooltip-nav {
- display: flex;
- gap: 6px;
- padding: 8px 12px;
- border-top: 1px solid var(--border-color);
-}
-
-.tutorial-prev-btn,
-.tutorial-next-btn {
- flex: 1;
- padding: 6px;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 1rem;
- font-weight: 600;
- transition: opacity 0.2s;
-}
-
-.tutorial-prev-btn {
- background: var(--border-color);
- color: var(--text-color);
-}
-
-.tutorial-next-btn {
- background: var(--primary-color);
- color: white;
-}
-
-.tutorial-prev-btn:hover:not(:disabled),
-.tutorial-next-btn:hover:not(:disabled) {
- opacity: 0.85;
-}
-
-.tutorial-prev-btn:disabled,
-.tutorial-next-btn:disabled {
- opacity: 0.3;
- cursor: not-allowed;
-}
-
-.tutorial-target {
- position: relative;
- z-index: 101 !important;
-}
-
-/* Fixed (viewport-level) tutorial overlay for device cards */
-.tutorial-overlay-fixed {
- position: fixed;
- z-index: 10000;
-}
-
-.tutorial-overlay-fixed .tutorial-backdrop {
- position: fixed;
-}
-
-.tutorial-overlay-fixed .tutorial-ring {
- position: fixed;
-}
-
-.tutorial-overlay-fixed .tutorial-tooltip {
- position: absolute;
- z-index: 10002;
- animation: none;
- opacity: 1;
-}
-
-/* target z-index for fixed overlay is set inline via JS (target is outside overlay DOM) */
-
-
-/* ===========================
- Capture Templates Styles
- =========================== */
-
-.templates-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
- gap: 20px;
-}
-
-.template-card {
- background: var(--card-bg);
- border: 1px solid var(--border-color);
- border-radius: 8px;
- padding: 16px;
- transition: box-shadow 0.2s;
- display: flex;
- flex-direction: column;
- position: relative;
-}
-
-.template-card:hover {
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-}
-
-.add-template-card {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- min-height: 180px;
- cursor: pointer;
- border: 2px dashed var(--border-color);
- background: transparent;
- transition: border-color 0.2s, background 0.2s;
-}
-
-.add-template-card:hover {
- border-color: var(--primary-color);
- background: rgba(33, 150, 243, 0.05);
- box-shadow: none;
-}
-
-.add-template-icon {
- font-size: 2.5rem;
- font-weight: 300;
- color: var(--text-secondary);
- line-height: 1;
- transition: color 0.2s;
-}
-
-.add-template-card:hover .add-template-icon {
- color: var(--primary-color);
-}
-
-.add-template-label {
- font-size: 0.85rem;
- color: var(--text-secondary);
- margin-top: 8px;
- font-weight: 500;
-}
-
-.template-card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
-}
-
-.template-card .template-card-header {
- padding-right: 24px;
-}
-
-.template-name {
- font-size: 18px;
- font-weight: bold;
- color: var(--text-color);
-}
-
-.badge {
- padding: 4px 8px;
- border-radius: 4px;
- font-size: 12px;
- font-weight: bold;
- text-transform: uppercase;
-}
-
-.template-description {
- color: var(--text-secondary);
- font-size: 14px;
- margin-bottom: 12px;
- line-height: 1.4;
-}
-
-.template-config {
- font-size: 14px;
- color: var(--text-secondary);
- margin-bottom: 8px;
-}
-
-.filter-chain {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 4px;
- margin-bottom: 8px;
-}
-
-.filter-chain-item {
- font-size: 0.7rem;
- background: var(--border-color);
- color: var(--text-secondary);
- padding: 2px 8px;
- border-radius: 10px;
-}
-
-.filter-chain-arrow {
- font-size: 0.7rem;
- color: var(--text-muted);
-}
-
-.template-config-details {
- margin: 12px 0;
- font-size: 13px;
-}
-
-.template-config-details summary {
- cursor: pointer;
- color: var(--primary-color);
- font-weight: 500;
- padding: 4px 0;
-}
-
-.template-config-details summary:hover {
- text-decoration: underline;
-}
-
-.template-no-config {
- margin: 12px 0;
- font-size: 13px;
- color: var(--primary-color);
- font-weight: 500;
- padding: 4px 0;
-}
-
-.config-table {
- width: 100%;
- margin-top: 8px;
- border-collapse: collapse;
- font-size: 13px;
-}
-
-.config-table td {
- padding: 4px 8px;
- border-bottom: 1px solid var(--border-color);
-}
-
-.config-table tr:last-child td {
- border-bottom: none;
-}
-
-.config-key {
- color: var(--text-secondary);
- white-space: nowrap;
- width: 1%;
-}
-
-.config-value {
- color: var(--text-primary);
- font-family: monospace;
-}
-
-/* Engine config grid (property name left, input right) */
-.config-grid {
- display: grid;
- grid-template-columns: auto 1fr;
- gap: 8px 12px;
- align-items: center;
- margin-top: 12px;
-}
-
-.config-grid-label {
- font-size: 0.85rem;
- color: var(--text-secondary);
- white-space: nowrap;
- text-align: right;
-}
-
-.config-grid-value input,
-.config-grid-value select {
- width: 100%;
- padding: 6px 10px;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- background: var(--bg-color);
- color: var(--text-color);
- font-size: 0.85rem;
-}
-
-.template-card-actions {
- display: flex;
- gap: 8px;
- margin-top: auto;
- padding-top: 12px;
- border-top: 1px solid var(--border-color);
- align-items: center;
-}
-
-.template-card-actions .btn:not(.btn-icon) {
- flex: 1;
-}
-
-.template-card-actions .btn-icon {
- flex-shrink: 0;
-}
-
-.text-muted {
- color: var(--text-secondary);
- font-style: italic;
- font-size: 13px;
-}
-
-/* PP Filter List in Template Modal */
-.pp-filter-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
- margin-bottom: 12px;
-}
-
-.pp-filter-empty {
- color: var(--text-secondary);
- font-size: 13px;
- text-align: center;
- padding: 16px;
- border: 1px dashed var(--border-color);
- border-radius: 8px;
-}
-
-.pp-filter-card {
- border: 1px solid var(--border-color);
- border-radius: 8px;
- background: var(--bg-secondary);
- padding: 10px 12px;
-}
-
-.pp-filter-card-header {
- display: flex;
- align-items: center;
- gap: 6px;
- cursor: pointer;
- user-select: none;
-}
-
-.pp-filter-card.expanded .pp-filter-card-header {
- margin-bottom: 8px;
-}
-
-.pp-filter-card-chevron {
- font-size: 10px;
- color: var(--text-secondary);
- flex-shrink: 0;
- width: 12px;
-}
-
-.pp-filter-card-name {
- font-weight: 600;
- font-size: 14px;
- color: var(--text-primary);
-}
-
-.pp-filter-card-summary {
- color: var(--text-secondary);
- font-size: 12px;
- margin-right: 8px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.pp-filter-card-actions {
- display: flex;
- gap: 4px;
- flex-shrink: 0;
- margin-left: auto;
-}
-
-.btn-filter-action {
- background: none;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- color: var(--text-secondary);
- cursor: pointer;
- width: 26px;
- height: 26px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 11px;
- padding: 0;
-}
-
-.btn-filter-action:hover:not(:disabled) {
- background: var(--border-color);
- color: var(--text-primary);
-}
-
-.btn-filter-action:disabled {
- opacity: 0.3;
- cursor: not-allowed;
-}
-
-.btn-filter-remove:hover:not(:disabled) {
- background: rgba(239, 68, 68, 0.15);
- border-color: rgba(239, 68, 68, 0.4);
- color: #ef4444;
-}
-
-.pp-filter-card-options {
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.pp-filter-option {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.pp-filter-option label {
- display: flex;
- justify-content: space-between;
- font-size: 12px;
- color: var(--text-secondary);
-}
-
-.pp-filter-option input[type="range"] {
- width: 100%;
-}
-
-.pp-filter-option-bool label {
- justify-content: space-between;
- gap: 8px;
- align-items: center;
- cursor: pointer;
- padding: 4px 0;
-}
-
-.pp-filter-option-bool input[type="checkbox"] {
- appearance: none;
- -webkit-appearance: none;
- width: 34px;
- min-width: 34px;
- height: 18px;
- background: var(--border-color);
- border-radius: 9px;
- position: relative;
- cursor: pointer;
- transition: background 0.2s;
- order: 1;
- margin: 0;
-}
-
-.pp-filter-option-bool input[type="checkbox"]::after {
- content: '';
- position: absolute;
- top: 2px;
- left: 2px;
- width: 14px;
- height: 14px;
- background: white;
- border-radius: 50%;
- transition: transform 0.2s;
-}
-
-.pp-filter-option-bool input[type="checkbox"]:checked {
- background: var(--primary-color);
-}
-
-.pp-filter-option-bool input[type="checkbox"]:checked::after {
- transform: translateX(16px);
-}
-
-.pp-filter-option-bool span {
- order: 0;
-}
-
-.pp-add-filter-row {
- display: flex;
- gap: 8px;
- align-items: center;
- margin-bottom: 4px;
-}
-
-.pp-add-filter-select {
- flex: 1;
- padding: 6px 10px;
- border: 1px solid var(--border-color);
- border-radius: 6px;
- background: var(--card-bg);
- color: var(--text-primary);
- font-size: 13px;
-}
-
-.pp-add-filter-btn {
- width: 34px;
- height: 34px;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- border: 1px solid var(--border-color);
- border-radius: 6px;
- background: var(--card-bg);
- color: var(--text-primary);
- font-size: 20px;
- cursor: pointer;
- padding: 0;
- line-height: 1;
-}
-
-.pp-add-filter-btn:hover {
- background: var(--border-color);
-}
-
-/* Template Test Section */
-.template-test-section {
- background: var(--bg-secondary);
- border-radius: 8px;
- padding: 16px;
-}
-
-.template-test-section h3 {
- margin-top: 0;
- margin-bottom: 12px;
- font-size: 16px;
-}
-
-.test-results-container {
- background: var(--card-bg);
- border: 1px solid var(--border-color);
- border-radius: 8px;
- padding: 16px;
-}
-
-.test-preview-section,
-.test-performance-section {
- margin-bottom: 20px;
-}
-
-.test-preview-section:last-child,
-.test-performance-section:last-child {
- margin-bottom: 0;
-}
-
-.test-preview-section h4,
-.test-performance-section h4 {
- margin-top: 0;
- margin-bottom: 12px;
- font-size: 14px;
- font-weight: 600;
- color: var(--text-secondary);
-}
-
-.test-preview-image {
- border-radius: 4px;
- overflow: hidden;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-}
-
-.test-preview-image img {
- display: block;
- width: 100%;
- height: auto;
-}
-
-.test-performance-stats {
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.stat-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 10px 0;
- border-bottom: 1px solid var(--border-color);
- font-size: 14px;
-}
-
-.stat-item:last-child {
- border-bottom: none;
-}
-
-.stat-item span {
- color: var(--text-secondary);
- font-weight: 500;
-}
-
-.stat-item strong {
- color: var(--text-color);
- font-weight: 600;
- font-family: monospace;
-}
-
-/* Empty state */
-.empty-state {
- text-align: center;
- padding: 40px 20px;
- color: var(--text-secondary);
- font-size: 16px;
-}
-
-/* Stream type badges */
-.badge-raw {
- background: #1976d2;
- color: white;
-}
-
-.badge-processed {
- background: #7b1fa2;
- color: white;
-}
-
-/* Stream info panel in stream selector modal */
-.stream-info-panel {
- padding: 4px 0 0 0;
- font-size: 14px;
- line-height: 1.6;
-}
-
-/* Stream sub-tabs */
-.stream-tab-bar {
- display: flex;
- align-items: center;
- gap: 4px;
- border-bottom: 2px solid var(--border-color);
- margin-bottom: 16px;
-}
-
-.stream-tab-btn {
- background: none;
- border: none;
- padding: 8px 14px;
- font-size: 0.9rem;
- font-weight: 500;
- color: var(--text-secondary);
- cursor: pointer;
- border-bottom: 2px solid transparent;
- margin-bottom: -2px;
- transition: color 0.2s, border-color 0.2s;
-}
-
-.stream-tab-btn:hover {
- color: var(--text-color);
-}
-
-.stream-tab-btn.active {
- color: var(--primary-color);
- border-bottom-color: var(--primary-color);
-}
-
-.stream-tab-count {
- background: var(--border-color);
- color: var(--text-secondary);
- font-size: 0.7rem;
- font-weight: 600;
- padding: 1px 6px;
- border-radius: 8px;
- margin-left: 4px;
-}
-
-.stream-tab-btn.active .stream-tab-count {
- background: var(--primary-color);
- color: #fff;
-}
-
-.stream-tab-panel {
- display: none;
-}
-
-.stream-tab-panel.active {
- display: block;
-}
-
-/* Sub-tab content sections */
-.subtab-section {
- margin-bottom: 24px;
-}
-
-.subtab-section:last-child {
- margin-bottom: 0;
-}
-
-.subtab-section-header {
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-secondary);
- margin: 0 0 12px 0;
- padding-bottom: 8px;
- border-bottom: 1px solid var(--border-color);
-}
-
-/* Image Lightbox */
-.lightbox {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.92);
- z-index: 10000;
- justify-content: center;
- align-items: center;
- cursor: zoom-out;
-}
-
-.lightbox.active {
- display: flex;
-}
-
-.lightbox-content {
- position: relative;
- max-width: 95%;
- max-height: 95%;
- cursor: default;
-}
-
-.lightbox-content img {
- max-width: 100%;
- max-height: 90vh;
- object-fit: contain;
- border-radius: 4px;
- display: block;
-}
-
-.lightbox-close {
- position: absolute;
- top: 16px;
- right: 16px;
- background: rgba(255, 255, 255, 0.15);
- border: none;
- color: white;
- font-size: 1.5rem;
- width: 40px;
- height: 40px;
- border-radius: 50%;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background 0.2s;
- z-index: 1;
-}
-
-.lightbox-close:hover {
- background: rgba(255, 255, 255, 0.3);
-}
-
-.lightbox-refresh-btn {
- position: absolute;
- top: 16px;
- right: 64px;
- background: rgba(255, 255, 255, 0.15);
- border: none;
- color: white;
- font-size: 1.2rem;
- width: 40px;
- height: 40px;
- border-radius: 50%;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background 0.2s;
- z-index: 1;
-}
-
-.lightbox-refresh-btn:hover {
- background: rgba(255, 255, 255, 0.3);
-}
-
-.lightbox-refresh-btn.active {
- background: var(--primary-color);
-}
-
-.lightbox-stats {
- position: absolute;
- bottom: 8px;
- left: 8px;
- background: rgba(0, 0, 0, 0.7);
- backdrop-filter: blur(8px);
- color: white;
- padding: 8px 14px;
- border-radius: 6px;
- font-size: 0.8rem;
- display: flex;
- gap: 16px;
- flex-wrap: wrap;
-}
-
-.lightbox-stats .stat-item {
- display: flex;
- gap: 4px;
- align-items: center;
-}
-
-.lightbox-stats .stat-item span {
- opacity: 0.7;
-}
-
-.lightbox-stats .stat-item strong {
- font-weight: 600;
-}
-
-/* Display Picker Lightbox */
-.display-picker-content {
- max-width: 900px;
- width: 90%;
- text-align: center;
-}
-
-.display-picker-title {
- color: white;
- font-size: 1.3rem;
- margin-bottom: 20px;
- font-weight: 500;
-}
-
-.display-picker-canvas {
- background: rgba(255, 255, 255, 0.05);
- border: 2px dashed rgba(255, 255, 255, 0.15);
- border-radius: 8px;
- padding: 24px;
- width: 100%;
- box-sizing: border-box;
-}
-
-.layout-display-pickable {
- cursor: pointer !important;
- border: 2px solid var(--border-color) !important;
- background: linear-gradient(135deg, rgba(128, 128, 128, 0.08), rgba(128, 128, 128, 0.03)) !important;
-}
-
-.layout-display-pickable:hover {
- box-shadow: 0 0 20px rgba(76, 175, 80, 0.4);
- border-color: var(--primary-color) !important;
-}
-
-.layout-display-pickable.selected {
- border-color: var(--primary-color) !important;
- box-shadow: 0 0 16px rgba(76, 175, 80, 0.5);
- background: rgba(76, 175, 80, 0.12) !important;
-}
-
-/* Display picker button in forms */
-.btn-display-picker {
- width: 100%;
- padding: 10px;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- background: var(--bg-color);
- color: var(--text-color);
- font-size: 1rem;
- cursor: pointer;
- text-align: left;
- transition: border-color 0.2s, box-shadow 0.2s;
- font-weight: 400;
-}
-
-.btn-display-picker:hover {
- border-color: var(--primary-color);
- box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.15);
-}
-
-/* Responsive adjustments */
-@media (max-width: 768px) {
- .templates-grid {
- grid-template-columns: 1fr;
- }
-}
-
-/* Static image stream styles */
-.image-preview-container {
- text-align: center;
- margin: 12px 0;
- padding: 12px;
- background: var(--bg-secondary);
- border-radius: 8px;
- border: 1px solid var(--border-color);
-}
-.stream-image-preview {
- max-width: 100%;
- max-height: 200px;
- border-radius: 4px;
- border: 1px solid var(--border-color);
-}
-.stream-image-info {
- font-size: 0.75rem;
- color: var(--text-muted);
- margin-top: 6px;
-}
-.validation-status {
- font-size: 0.8rem;
- margin-top: 4px;
- padding: 4px 8px;
- border-radius: 4px;
-}
-.validation-status.success {
- color: #4caf50;
-}
-.validation-status.error {
- color: #f44336;
-}
-.validation-status.loading {
- color: var(--text-muted);
-}
-.stream-card-props {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- margin-bottom: 8px;
-}
-
-.stream-card-prop {
- display: inline-block;
- font-size: 0.75rem;
- color: var(--text-secondary);
- background: var(--border-color);
- padding: 2px 8px;
- border-radius: 10px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 180px;
- vertical-align: middle;
-}
-
-.stream-card-prop-full {
- max-width: 100%;
- word-break: break-all;
- white-space: normal;
- font-size: 0.7rem;
-}
-
-/* Key Colors target styles */
-.kc-rect-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
- margin-bottom: 8px;
-}
-
-.kc-rect-row {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 8px;
- background: var(--bg-color);
- border: 1px solid var(--border-color);
- border-radius: 6px;
-}
-
-.kc-rect-row input[type="text"] {
- flex: 2;
- min-width: 0;
-}
-
-.kc-rect-row input[type="number"] {
- flex: 1;
- min-width: 0;
- width: 60px;
-}
-
-.kc-rect-row .kc-rect-remove-btn {
- background: none;
- border: none;
- color: #777;
- font-size: 1rem;
- cursor: pointer;
- padding: 4px;
- border-radius: 4px;
- flex-shrink: 0;
- transition: color 0.2s, background 0.2s;
-}
-
-.kc-rect-row .kc-rect-remove-btn:hover {
- color: var(--danger-color);
- background: rgba(244, 67, 54, 0.1);
-}
-
-.kc-rect-labels {
- display: flex;
- gap: 6px;
- padding: 0 8px;
- margin-bottom: 4px;
- font-size: 0.7rem;
- color: var(--text-secondary);
- font-weight: 600;
-}
-
-.kc-rect-labels span:first-child {
- flex: 2;
-}
-
-.kc-rect-labels span {
- flex: 1;
- text-align: center;
-}
-
-.kc-rect-labels span:last-child {
- width: 28px;
- flex: 0 0 28px;
-}
-
-.kc-add-rect-btn {
- width: 100%;
- font-size: 0.85rem;
- padding: 6px 12px;
-}
-
-.kc-rect-empty {
- text-align: center;
- color: var(--text-secondary);
- font-size: 0.85rem;
- padding: 12px;
- font-style: italic;
-}
-
-.kc-color-swatches {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- margin-bottom: 8px;
-}
-
-.kc-swatch {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 3px;
-}
-
-.kc-swatch-color {
- width: 32px;
- height: 32px;
- border-radius: 6px;
- border: 2px solid var(--border-color);
- transition: background-color 0.3s;
-}
-
-.kc-swatch-label {
- font-size: 0.6rem;
- color: var(--text-secondary);
- max-width: 40px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- text-align: center;
-}
-
-.kc-no-colors {
- color: var(--text-secondary);
- font-size: 0.8rem;
- font-style: italic;
- padding: 4px 0;
-}
-
-/* Pattern Template Visual Editor */
-.modal-content-wide {
- width: fit-content;
- min-width: 500px;
- max-width: calc(100vw - 40px);
- max-height: calc(100vh - 40px);
- display: flex;
- flex-direction: column;
-}
-
-.modal-content-wide .modal-body {
- overflow-y: auto;
- scrollbar-gutter: stable;
- flex: 1 1 auto;
- min-height: 0;
-}
-
-.pattern-canvas-container {
- position: relative;
- border-radius: 8px;
- overflow: hidden;
- background: var(--bg-color);
- border: 1px solid var(--border-color);
- margin-bottom: 12px;
- resize: both;
- width: 820px;
- min-width: 400px;
- max-width: 100%;
- min-height: 200px;
- height: 450px;
- max-height: calc(100vh - 400px);
-}
-
-#pattern-canvas {
- width: 100%;
- height: 100%;
- display: block;
- cursor: default;
-}
-
-.pattern-canvas-toolbar {
- display: flex;
- gap: 2px;
- padding: 4px;
- align-items: center;
- background: var(--bg-secondary);
- border-radius: 6px;
- margin-top: 4px;
-}
-
-.pattern-canvas-toolbar .btn {
- flex: 0 0 auto;
- min-width: 32px;
- width: 32px;
- height: 32px;
- padding: 0;
- font-size: 1rem;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
-}
-
-.pattern-bg-row {
- display: flex;
- gap: 0.5rem;
- align-items: center;
- margin-bottom: 8px;
-}
-
-.pattern-bg-row select {
- flex: 1;
-}
-
-.pattern-capture-btn {
- flex: 0 0 auto;
- min-width: 36px !important;
- width: 36px;
- height: 36px;
- padding: 0 !important;
- font-size: 1.1rem;
- line-height: 36px;
- text-align: center;
-}
-
-.pattern-rect-list {
- display: flex;
- flex-direction: column;
- gap: 6px;
- margin-top: 8px;
- max-height: 200px;
- overflow-y: auto;
-}
-
-.pattern-rect-row {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 6px 8px;
- background: var(--bg-color);
- border: 1px solid var(--border-color);
- border-radius: 6px;
- font-size: 0.85rem;
- cursor: pointer;
- transition: border-color 0.15s;
-}
-
-.pattern-rect-row.selected {
- border-color: var(--primary-color);
- background: rgba(76, 175, 80, 0.08);
-}
-
-.pattern-rect-row input[type="text"] {
- flex: 2;
- min-width: 0;
- padding: 4px 6px;
- font-size: 0.8rem;
-}
-
-.pattern-rect-row input[type="number"] {
- flex: 1;
- min-width: 0;
- width: 55px;
- padding: 4px 6px;
- font-size: 0.8rem;
-}
-
-.pattern-rect-row .pattern-rect-remove-btn {
- background: none;
- border: none;
- color: #777;
- font-size: 0.9rem;
- cursor: pointer;
- padding: 2px 4px;
- border-radius: 4px;
- flex-shrink: 0;
- transition: color 0.2s, background 0.2s;
-}
-
-.pattern-rect-row .pattern-rect-remove-btn:hover {
- color: var(--danger-color);
- background: rgba(244, 67, 54, 0.1);
-}
-
-.pattern-rect-labels {
- display: flex;
- gap: 6px;
- padding: 0 8px;
- margin-bottom: 2px;
- font-size: 0.7rem;
- color: var(--text-secondary);
- font-weight: 600;
-}
-
-.pattern-rect-labels span:first-child {
- flex: 2;
-}
-
-.pattern-rect-labels span {
- flex: 1;
- text-align: center;
-}
-
-.pattern-rect-labels span:last-child {
- width: 24px;
- flex: 0 0 24px;
-}
-
-/* ── Dashboard ── */
-
-.dashboard-section {
- margin-bottom: 16px;
-}
-
-.dashboard-section-header {
- font-size: 0.8rem;
- font-weight: 600;
- margin-bottom: 6px;
- color: var(--text-secondary);
- display: flex;
- align-items: center;
- gap: 6px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- cursor: pointer;
- user-select: none;
-}
-
-.dashboard-section-chevron {
- font-size: 0.6rem;
- color: var(--text-secondary);
- width: 10px;
- display: inline-block;
-}
-
-.dashboard-section-count {
- background: var(--border-color);
- color: var(--text-secondary);
- border-radius: 10px;
- padding: 0 6px;
- font-size: 0.75rem;
- font-weight: 600;
-}
-
-.dashboard-subsection {
- margin-bottom: 10px;
- padding-left: 16px;
-}
-.dashboard-subsection .dashboard-section-header {
- font-size: 0.72rem;
-}
-
-.dashboard-stop-all {
- margin-left: auto;
- padding: 2px 8px;
- font-size: 0.7rem;
- white-space: nowrap;
- flex: 0 0 auto;
-}
-
-.dashboard-poll-wrap {
- margin-left: auto;
- display: flex;
- align-items: center;
- gap: 3px;
-}
-
-.dashboard-poll-slider {
- width: 48px;
- height: 12px;
- accent-color: var(--primary-color);
- cursor: pointer;
-}
-
-.dashboard-poll-value {
- font-size: 0.6rem;
- color: var(--text-secondary);
- min-width: 18px;
-}
-
-.dashboard-target {
- display: grid;
- grid-template-columns: 1fr auto auto;
- align-items: center;
- gap: 12px;
- padding: 6px 12px;
- background: var(--card-bg);
- border: 1px solid var(--border-color);
- border-radius: 6px;
- margin-bottom: 4px;
-}
-
-.dashboard-target-info {
- display: flex;
- align-items: center;
- gap: 6px;
- min-width: 0;
- overflow: hidden;
-}
-
-.dashboard-target-icon {
- font-size: 1rem;
- flex-shrink: 0;
-}
-
-.dashboard-target-name {
- font-size: 0.85rem;
- font-weight: 600;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: flex;
- align-items: center;
- gap: 4px;
-}
-
-.dashboard-target-name .health-dot {
- margin-right: 0;
- flex-shrink: 0;
-}
-
-.dashboard-target-subtitle {
- font-size: 0.7rem;
- color: var(--text-secondary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.dashboard-target-metrics {
- display: flex;
- gap: 12px;
- align-items: center;
-}
-
-.dashboard-metric {
- text-align: center;
- min-width: 48px;
-}
-
-.dashboard-metric-value {
- font-size: 0.85rem;
- font-weight: 700;
- color: var(--primary-color);
- line-height: 1.2;
-}
-
-.dashboard-metric-label {
- font-size: 0.6rem;
- color: var(--text-secondary);
- text-transform: uppercase;
- letter-spacing: 0.3px;
-}
-
-.dashboard-fps-metric {
- display: flex;
- align-items: center;
- gap: 6px;
- min-width: auto;
-}
-
-.dashboard-fps-sparkline {
- position: relative;
- width: 100px;
- height: 36px;
-}
-
-.dashboard-fps-label {
- display: flex;
- flex-direction: column;
- align-items: center;
- min-width: 36px;
- line-height: 1.1;
-}
-
-.dashboard-fps-target {
- font-weight: 400;
- opacity: 0.5;
- font-size: 0.75rem;
-}
-
-.dashboard-target-actions {
- display: flex;
- align-items: center;
- gap: 4px;
-}
-
-.dashboard-status-dot {
- font-size: 1rem;
- line-height: 1;
-}
-
-.dashboard-status-dot.active {
- color: #4CAF50;
- animation: pulse 2s infinite;
-}
-
-.dashboard-no-targets {
- text-align: center;
- padding: 32px 16px;
- color: var(--text-secondary);
- font-size: 0.9rem;
-}
-
-.dashboard-badge-stopped {
- padding: 2px 8px;
- border-radius: 10px;
- font-size: 0.7rem;
- font-weight: 600;
- background: var(--border-color);
- color: var(--text-secondary);
- flex-shrink: 0;
-}
-
-.dashboard-badge-active {
- padding: 2px 8px;
- border-radius: 10px;
- font-size: 0.7rem;
- font-weight: 600;
- background: var(--success-color, #28a745);
- color: #fff;
- flex-shrink: 0;
-}
-
-.dashboard-profile .dashboard-target-metrics {
- min-width: 48px;
-}
-
-@media (max-width: 768px) {
- .dashboard-target {
- grid-template-columns: 1fr auto;
- gap: 6px;
- }
-
- .dashboard-target-metrics {
- display: none;
- }
-}
-
-/* ===== PERFORMANCE CHARTS ===== */
-
-.perf-charts-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 12px;
-}
-
-.perf-chart-card {
- background: var(--card-bg);
- border: 1px solid var(--border-color);
- border-radius: 6px;
- padding: 10px 12px;
-}
-
-.perf-chart-wrap {
- position: relative;
- height: 60px;
-}
-
-.perf-chart-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 4px;
-}
-
-.perf-chart-label {
- font-size: 0.75rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.3px;
- color: var(--text-secondary);
-}
-
-.perf-chart-value {
- font-size: 0.85rem;
- font-weight: 700;
-}
-
-.perf-chart-value.cpu { color: #2196F3; }
-.perf-chart-value.ram { color: #4CAF50; }
-.perf-chart-value.gpu { color: #FF9800; }
-
-.perf-chart-unavailable {
- text-align: center;
- padding: 20px 0;
- color: var(--text-secondary);
- font-size: 0.8rem;
-}
-
-/* ===== PROFILES ===== */
-
-.badge-profile-active {
- background: var(--success-color, #28a745);
- color: #fff;
-}
-
-.badge-profile-inactive {
- background: var(--border-color);
- color: var(--text-color);
-}
-
-.badge-profile-disabled {
- background: var(--border-color);
- color: var(--text-muted);
- opacity: 0.7;
-}
-
-.profile-status-disabled {
- opacity: 0.6;
-}
-
-.profile-logic-label {
- font-size: 0.7rem;
- font-weight: 600;
- color: var(--text-muted);
- padding: 0 4px;
-}
-
-/* Profile condition editor rows */
-.profile-condition-row {
- border: 1px solid var(--border-color);
- border-radius: 6px;
- padding: 10px;
- margin-bottom: 8px;
- background: var(--bg-secondary, var(--bg-color));
-}
-
-.condition-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 8px;
-}
-
-.condition-type-label {
- font-weight: 600;
- font-size: 0.9rem;
-}
-
-.btn-remove-condition {
- background: none;
- border: none;
- color: var(--danger-color, #dc3545);
- cursor: pointer;
- font-size: 1rem;
- padding: 2px 6px;
-}
-
-.condition-fields {
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.condition-field label {
- display: block;
- font-size: 0.85rem;
- margin-bottom: 3px;
- color: var(--text-muted);
-}
-
-.condition-field select,
-.condition-field textarea {
- width: 100%;
- padding: 6px 8px;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- background: var(--bg-color);
- color: var(--text-color);
- font-size: 0.9rem;
- font-family: inherit;
-}
-
-.condition-apps {
- resize: vertical;
- min-height: 60px;
-}
-
-.condition-apps-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.btn-browse-apps {
- background: none;
- border: 1px solid var(--border-color);
- color: var(--text-color);
- font-size: 0.75rem;
- padding: 2px 8px;
- border-radius: 4px;
- cursor: pointer;
- transition: border-color 0.2s, background 0.2s;
-}
-
-.btn-browse-apps:hover {
- border-color: var(--primary-color);
- background: rgba(33, 150, 243, 0.1);
-}
-
-.process-picker {
- margin-top: 6px;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- overflow: hidden;
-}
-
-.process-picker-search {
- width: 100%;
- padding: 6px 8px;
- border: none;
- border-bottom: 1px solid var(--border-color);
- background: var(--bg-color);
- color: var(--text-color);
- font-size: 0.85rem;
- font-family: inherit;
- outline: none;
- box-sizing: border-box;
-}
-
-.process-picker-list {
- max-height: 160px;
- overflow-y: auto;
-}
-
-.process-picker-item {
- padding: 4px 8px;
- font-size: 0.8rem;
- cursor: pointer;
- transition: background 0.1s;
-}
-
-.process-picker-item:hover {
- background: rgba(33, 150, 243, 0.15);
-}
-
-.process-picker-item.added {
- color: var(--text-muted);
- cursor: default;
- opacity: 0.6;
-}
-
-.process-picker-loading {
- padding: 8px;
- font-size: 0.8rem;
- color: var(--text-muted);
- text-align: center;
-}
-
-/* Profile target checklist */
-.profile-targets-checklist {
- max-height: 200px;
- overflow-y: auto;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- padding: 6px;
-}
-
-.profile-target-item {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 4px 6px;
- cursor: pointer;
- border-radius: 3px;
-}
-
-.profile-target-item:hover {
- background: var(--bg-secondary, var(--bg-color));
-}
-
-.profile-target-item input[type="checkbox"] {
- margin: 0;
-}
-
diff --git a/server/src/wled_controller/templates/index.html b/server/src/wled_controller/templates/index.html
new file mode 100644
index 0000000..abf95bd
--- /dev/null
+++ b/server/src/wled_controller/templates/index.html
@@ -0,0 +1,276 @@
+
+
+
+
+
+ LED Grab
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include 'modals/calibration.html' %}
+ {% include 'modals/device-settings.html' %}
+ {% include 'modals/target-editor.html' %}
+ {% include 'modals/kc-editor.html' %}
+ {% include 'modals/pattern-template.html' %}
+ {% include 'modals/api-key.html' %}
+ {% include 'modals/confirm.html' %}
+ {% include 'modals/add-device.html' %}
+ {% include 'modals/capture-template.html' %}
+ {% include 'modals/test-template.html' %}
+ {% include 'modals/test-stream.html' %}
+ {% include 'modals/test-pp-template.html' %}
+ {% include 'modals/stream.html' %}
+ {% include 'modals/pp-template.html' %}
+ {% include 'modals/profile-editor.html' %}
+
+ {% include 'partials/tutorial-overlay.html' %}
+ {% include 'partials/image-lightbox.html' %}
+ {% include 'partials/display-picker.html' %}
+
+
+
+
+
diff --git a/server/src/wled_controller/templates/modals/add-device.html b/server/src/wled_controller/templates/modals/add-device.html
new file mode 100644
index 0000000..3a27959
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/add-device.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+ No devices found
+
+
+
+
+
+
+
+
diff --git a/server/src/wled_controller/templates/modals/api-key.html b/server/src/wled_controller/templates/modals/api-key.html
new file mode 100644
index 0000000..b09f164
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/api-key.html
@@ -0,0 +1,40 @@
+
+
diff --git a/server/src/wled_controller/templates/modals/calibration.html b/server/src/wled_controller/templates/modals/calibration.html
new file mode 100644
index 0000000..ded8a84
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/calibration.html
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
0 / 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
●
+
●
+
●
+
●
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/src/wled_controller/templates/modals/capture-template.html b/server/src/wled_controller/templates/modals/capture-template.html
new file mode 100644
index 0000000..5e86374
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/capture-template.html
@@ -0,0 +1,45 @@
+
+
diff --git a/server/src/wled_controller/templates/modals/confirm.html b/server/src/wled_controller/templates/modals/confirm.html
new file mode 100644
index 0000000..ee1e5a0
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/confirm.html
@@ -0,0 +1,16 @@
+
+
diff --git a/server/src/wled_controller/templates/modals/device-settings.html b/server/src/wled_controller/templates/modals/device-settings.html
new file mode 100644
index 0000000..43531bc
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/device-settings.html
@@ -0,0 +1,90 @@
+
+
diff --git a/server/src/wled_controller/templates/modals/kc-editor.html b/server/src/wled_controller/templates/modals/kc-editor.html
new file mode 100644
index 0000000..4b1564c
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/kc-editor.html
@@ -0,0 +1,80 @@
+
+
diff --git a/server/src/wled_controller/templates/modals/pattern-template.html b/server/src/wled_controller/templates/modals/pattern-template.html
new file mode 100644
index 0000000..a5bbe48
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/pattern-template.html
@@ -0,0 +1,67 @@
+
+
diff --git a/server/src/wled_controller/templates/modals/pp-template.html b/server/src/wled_controller/templates/modals/pp-template.html
new file mode 100644
index 0000000..9d08124
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/pp-template.html
@@ -0,0 +1,40 @@
+
+
diff --git a/server/src/wled_controller/templates/modals/profile-editor.html b/server/src/wled_controller/templates/modals/profile-editor.html
new file mode 100644
index 0000000..2d14754
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/profile-editor.html
@@ -0,0 +1,74 @@
+
+
diff --git a/server/src/wled_controller/templates/modals/stream.html b/server/src/wled_controller/templates/modals/stream.html
new file mode 100644
index 0000000..f5d61ac
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/stream.html
@@ -0,0 +1,102 @@
+
+
diff --git a/server/src/wled_controller/templates/modals/target-editor.html b/server/src/wled_controller/templates/modals/target-editor.html
new file mode 100644
index 0000000..b7b5131
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/target-editor.html
@@ -0,0 +1,92 @@
+
+
diff --git a/server/src/wled_controller/templates/modals/test-pp-template.html b/server/src/wled_controller/templates/modals/test-pp-template.html
new file mode 100644
index 0000000..a2a6a5d
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/test-pp-template.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/src/wled_controller/templates/modals/test-stream.html b/server/src/wled_controller/templates/modals/test-stream.html
new file mode 100644
index 0000000..ae89c97
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/test-stream.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/src/wled_controller/templates/modals/test-template.html b/server/src/wled_controller/templates/modals/test-template.html
new file mode 100644
index 0000000..f3a008b
--- /dev/null
+++ b/server/src/wled_controller/templates/modals/test-template.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/src/wled_controller/templates/partials/display-picker.html b/server/src/wled_controller/templates/partials/display-picker.html
new file mode 100644
index 0000000..b9fb911
--- /dev/null
+++ b/server/src/wled_controller/templates/partials/display-picker.html
@@ -0,0 +1,10 @@
+
+
diff --git a/server/src/wled_controller/templates/partials/image-lightbox.html b/server/src/wled_controller/templates/partials/image-lightbox.html
new file mode 100644
index 0000000..3c89adc
--- /dev/null
+++ b/server/src/wled_controller/templates/partials/image-lightbox.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
![Full size preview]()
+
+
+
diff --git a/server/src/wled_controller/templates/partials/tutorial-overlay.html b/server/src/wled_controller/templates/partials/tutorial-overlay.html
new file mode 100644
index 0000000..b5b8047
--- /dev/null
+++ b/server/src/wled_controller/templates/partials/tutorial-overlay.html
@@ -0,0 +1,16 @@
+
+
diff --git a/server/tests/test_api.py b/server/tests/test_api.py
index 2501039..1cfbdda 100644
--- a/server/tests/test_api.py
+++ b/server/tests/test_api.py
@@ -10,13 +10,11 @@ client = TestClient(app)
def test_root_endpoint():
- """Test root endpoint."""
+ """Test root endpoint returns the HTML dashboard."""
response = client.get("/")
assert response.status_code == 200
- data = response.json()
- assert data["name"] == "WLED Screen Controller"
- assert data["version"] == __version__
- assert "/docs" in data["docs"]
+ assert "text/html" in response.headers["content-type"]
+ assert "LED Grab" in response.text
def test_health_check():