diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9000ddf..5bfaed3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -171,6 +171,96 @@ deploy: "$DEPLOY_IMAGE" - docker ps --filter name="$CONTAINER_NAME" --format "ID={{.ID}} Status={{.Status}} Image={{.Image}}" +deploy-nightly: + stage: deploy + image: docker:latest + needs: [docker-build] + rules: + - if: $CI_COMMIT_BRANCH == "nightly" + variables: + DEPLOY_IMAGE: "$INTERNAL_REGISTRY/root/gaming-hub:nightly" + CONTAINER_NAME: "gaming-hub-nightly" + script: + - echo "[Nightly Deploy] Logging into registry..." + - echo "$CI_REGISTRY_PASSWORD" | docker login "$INTERNAL_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin + - echo "[Nightly Deploy] Pulling $DEPLOY_IMAGE..." + - docker pull "$DEPLOY_IMAGE" + - echo "[Nightly Deploy] Stopping main container..." + - docker stop gaming-hub || true + - docker rm gaming-hub || true + - echo "[Nightly Deploy] Stopping old nightly container..." + - docker stop "$CONTAINER_NAME" || true + - docker rm "$CONTAINER_NAME" || true + - echo "[Nightly Deploy] Starting $CONTAINER_NAME..." + - | + docker run -d \ + --name "$CONTAINER_NAME" \ + --network pangolin \ + --restart unless-stopped \ + --label "channel=nightly" \ + -p 8085:8080 \ + -e TZ=Europe/Berlin \ + -e NODE_ENV=production \ + -e PORT=8080 \ + -e DATA_DIR=/data \ + -e SOUNDS_DIR=/data/sounds \ + -e "NODE_OPTIONS=--dns-result-order=ipv4first" \ + -e ADMIN_PWD="$GAMING_HUB_ADMIN_PWD" \ + -e PCM_CACHE_MAX_MB=2048 \ + -e DISCORD_TOKEN_JUKEBOX="$GAMING_HUB_DISCORD_JUKEBOX" \ + -e DISCORD_TOKEN_RADIO="$GAMING_HUB_DISCORD_RADIO" \ + -e DISCORD_TOKEN_NOTIFICATIONS="$GAMING_HUB_DISCORD_NOTIFICATIONS" \ + -e PUBLIC_URL="$GAMING_HUB_PUBLIC_URL" \ + -e STEAM_API_KEY="$STEAM_API_KEY" \ + -v /mnt/cache/appdata/gaming-hub/data:/data:rw \ + -v /mnt/cache/appdata/dockge/container/jukebox/sounds/:/data/sounds:rw \ + "$DEPLOY_IMAGE" + - docker ps --filter name="$CONTAINER_NAME" --format "ID={{.ID}} Status={{.Status}} Image={{.Image}}" + +restore-main: + stage: deploy + image: docker:latest + needs: [docker-build] + rules: + - if: $CI_COMMIT_BRANCH == "nightly" + when: manual + allow_failure: true + variables: + DEPLOY_IMAGE: "$INTERNAL_REGISTRY/root/gaming-hub:latest" + CONTAINER_NAME: "gaming-hub" + script: + - echo "[Restore Main] Logging into registry..." + - echo "$CI_REGISTRY_PASSWORD" | docker login "$INTERNAL_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin + - echo "[Restore Main] Stopping nightly container..." + - docker stop gaming-hub-nightly || true + - docker rm gaming-hub-nightly || true + - echo "[Restore Main] Pulling $DEPLOY_IMAGE..." + - docker pull "$DEPLOY_IMAGE" + - echo "[Restore Main] Starting $CONTAINER_NAME..." + - | + docker run -d \ + --name "$CONTAINER_NAME" \ + --network pangolin \ + --restart unless-stopped \ + -p 8085:8080 \ + -e TZ=Europe/Berlin \ + -e NODE_ENV=production \ + -e PORT=8080 \ + -e DATA_DIR=/data \ + -e SOUNDS_DIR=/data/sounds \ + -e "NODE_OPTIONS=--dns-result-order=ipv4first" \ + -e ADMIN_PWD="$GAMING_HUB_ADMIN_PWD" \ + -e PCM_CACHE_MAX_MB=2048 \ + -e DISCORD_TOKEN_JUKEBOX="$GAMING_HUB_DISCORD_JUKEBOX" \ + -e DISCORD_TOKEN_RADIO="$GAMING_HUB_DISCORD_RADIO" \ + -e DISCORD_TOKEN_NOTIFICATIONS="$GAMING_HUB_DISCORD_NOTIFICATIONS" \ + -e PUBLIC_URL="$GAMING_HUB_PUBLIC_URL" \ + -e STEAM_API_KEY="$STEAM_API_KEY" \ + -v /mnt/cache/appdata/gaming-hub/data:/data:rw \ + -v /mnt/cache/appdata/dockge/container/jukebox/sounds/:/data/sounds:rw \ + "$DEPLOY_IMAGE" + - docker ps --filter name="$CONTAINER_NAME" --format "ID={{.ID}} Status={{.Status}} Image={{.Image}}" + bump-version: stage: bump-version image: diff --git a/web/src/App.tsx b/web/src/App.tsx index eeed0b4..fb12660 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -40,6 +40,12 @@ export default function App() { const [showVersionModal, setShowVersionModal] = useState(false); const [pluginData, setPluginData] = useState>({}); + // Admin state + const [adminLoggedIn, setAdminLoggedIn] = useState(false); + const [showAdminModal, setShowAdminModal] = useState(false); + const [adminPassword, setAdminPassword] = useState(''); + const [adminError, setAdminError] = useState(''); + // Electron auto-update state const isElectron = !!(window as any).electronAPI?.isElectron; const electronVersion = isElectron ? (window as any).electronAPI.version : null; @@ -130,13 +136,43 @@ export default function App() { const version = (import.meta as any).env?.VITE_APP_VERSION ?? 'dev'; - // Close version modal on Escape + // Close modals on Escape useEffect(() => { - if (!showVersionModal) return; - const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') setShowVersionModal(false); }; + if (!showVersionModal && !showAdminModal) return; + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setShowVersionModal(false); + setShowAdminModal(false); + } + }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); - }, [showVersionModal]); + }, [showVersionModal, showAdminModal]); + + // Admin login handler + const handleAdminLogin = () => { + if (!adminPassword) return; + fetch('/api/admin/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password: adminPassword }), + }) + .then(r => { + if (r.ok) { + setAdminLoggedIn(true); + setAdminPassword(''); + setAdminError(''); + } else { + setAdminError('Falsches Passwort'); + } + }) + .catch(() => setAdminError('Verbindungsfehler')); + }; + + const handleAdminLogout = () => { + setAdminLoggedIn(false); + setShowAdminModal(false); + }; // Tab icon mapping @@ -198,7 +234,6 @@ export default function App() { { - // Status vom Main-Prozess synchronisieren bevor Modal öffnet if (isElectron) { const api = (window as any).electronAPI; const s = api.getUpdateStatus?.(); @@ -212,6 +247,15 @@ export default function App() { > v{version} + +
DK
@@ -307,6 +351,46 @@ export default function App() { )} + {showAdminModal && ( +
setShowAdminModal(false)}> +
e.stopPropagation()}> + {adminLoggedIn ? ( + <> +
Admin Panel
+
+
A
+
+ Administrator + Eingeloggt +
+
+ + + ) : ( + <> +
{'\u{1F511}'} Admin Login
+
Passwort eingeben um Einstellungen freizuschalten
+ {adminError &&
{adminError}
} + setAdminPassword(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleAdminLogin(); }} + autoFocus + /> + + + )} +
+
+ )} +
{plugins.length === 0 ? (
diff --git a/web/src/plugins/game-library/game-library.css b/web/src/plugins/game-library/game-library.css index 372c8f0..2ed46b4 100644 --- a/web/src/plugins/game-library/game-library.css +++ b/web/src/plugins/game-library/game-library.css @@ -472,24 +472,30 @@ /* ── Empty state ── */ .gl-empty { - text-align: center; - padding: 60px 20px; + flex: 1; display: flex; flex-direction: column; + align-items: center; justify-content: center; gap: 16px; + padding: 40px; height: 100%; } .gl-empty-icon { - font-size: 48px; - margin-bottom: 16px; + font-size: 64px; line-height: 1; + filter: drop-shadow(0 0 20px rgba(230,126,34,0.5)); + animation: gl-empty-float 3s ease-in-out infinite; +} + +@keyframes gl-empty-float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-8px); } } .gl-empty h3 { - color: var(--text-normal); - margin: 0 0 8px; + font-size: 26px; font-weight: 700; color: #f2f3f5; + letter-spacing: -0.5px; margin: 0; } .gl-empty p { - color: var(--text-faint); - margin: 0; - font-size: 14px; + font-size: 15px; color: #80848e; + text-align: center; max-width: 360px; line-height: 1.5; margin: 0; } /* ── Common game playtime chips ── */ diff --git a/web/src/plugins/lolstats/lolstats.css b/web/src/plugins/lolstats/lolstats.css index 59503c4..f4b552d 100644 --- a/web/src/plugins/lolstats/lolstats.css +++ b/web/src/plugins/lolstats/lolstats.css @@ -456,22 +456,26 @@ } .lol-empty { - text-align: center; - padding: 60px 20px; - color: var(--text-faint); + flex: 1; display: flex; flex-direction: column; + align-items: center; justify-content: center; gap: 16px; + padding: 40px; height: 100%; } .lol-empty-icon { - font-size: 48px; - margin-bottom: 12px; + font-size: 64px; line-height: 1; + filter: drop-shadow(0 0 20px rgba(230,126,34,0.5)); + animation: lol-float 3s ease-in-out infinite; +} +@keyframes lol-float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-8px); } } .lol-empty h3 { - margin: 0 0 8px; - color: var(--text-muted); - font-size: 16px; + font-size: 26px; font-weight: 700; color: #f2f3f5; + letter-spacing: -0.5px; margin: 0; } .lol-empty p { - margin: 0; - font-size: 13px; + font-size: 15px; color: #80848e; + text-align: center; max-width: 360px; line-height: 1.5; margin: 0; } /* ── Load more ── */ diff --git a/web/src/plugins/soundboard/soundboard.css b/web/src/plugins/soundboard/soundboard.css index be47e1a..1c69e0a 100644 --- a/web/src/plugins/soundboard/soundboard.css +++ b/web/src/plugins/soundboard/soundboard.css @@ -3,7 +3,7 @@ /* Soundboard Plugin — ported from Jukebox styles */ /* ──────────────────────────────────────────── - Theme Variables — Default (Discord Blurple) + Theme Variables — Default (Orange Accent) ──────────────────────────────────────────── */ .sb-app { --bg-deep: #1a1b1e; @@ -18,10 +18,10 @@ --text-muted: #949ba4; --text-faint: #6d6f78; - --accent: #5865f2; - --accent-rgb: 88, 101, 242; - --accent-hover: #4752c4; - --accent-glow: rgba(88, 101, 242, .45); + --accent: #e67e22; + --accent-rgb: 230, 126, 34; + --accent-hover: #d35400; + --accent-glow: rgba(230, 126, 34, .45); --green: #23a55a; --red: #f23f42; @@ -91,6 +91,18 @@ --accent-glow: rgba(52, 152, 219, .4); } +/* ── Theme: Cherry ── */ +.sb-app[data-theme="cherry"] { + --bg-deep: #1a0f14; + --bg-primary: #221419; + --bg-secondary: #2f1c22; + --bg-tertiary: #3d242c; + --accent: #e74c3c; + --accent-rgb: 231, 76, 60; + --accent-hover: #c0392b; + --accent-glow: rgba(231, 76, 60, .4); +} + /* ──────────────────────────────────────────── App Layout ──────────────────────────────────────────── */ @@ -310,6 +322,33 @@ margin-left: 2px; } +/* ── Disconnect Button ── */ +.disconnect-btn { + display: flex; + align-items: center; + gap: 5px; + padding: 4px 10px; + border-radius: 20px; + background: rgba(242, 63, 66, .1); + border: 1px solid rgba(242, 63, 66, .2); + backdrop-filter: blur(8px); + color: var(--red); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all var(--transition); +} + +.disconnect-btn:hover { + background: rgba(242, 63, 66, .2); + border-color: rgba(242, 63, 66, .4); + box-shadow: 0 0 12px rgba(242, 63, 66, .15); +} + +.disconnect-btn:active { + transform: scale(0.97); +} + /* ── Connection Details Modal ── */ .conn-modal-overlay { position: fixed; @@ -340,9 +379,10 @@ align-items: center; gap: 8px; padding: 14px 16px; - border-bottom: 1px solid var(--border); - font-weight: 700; + border-bottom: 1px solid rgba(255,255,255,.06); font-size: 14px; + font-weight: 700; + color: var(--text-normal); } .conn-modal-close { margin-left: auto; @@ -438,7 +478,9 @@ gap: 6px; padding: 6px 14px; border-radius: 20px; - background: var(--bg-tertiary); + background: rgba(255, 255, 255, .05); + border: 1px solid rgba(255, 255, 255, .08); + backdrop-filter: blur(8px); color: var(--text-muted); font-family: var(--font); font-size: 13px; @@ -449,13 +491,16 @@ } .cat-tab:hover { - background: var(--bg-modifier-selected); + background: rgba(255, 255, 255, .1); + border-color: rgba(255, 255, 255, .15); color: var(--text-normal); } .cat-tab.active { background: var(--accent); color: var(--white); + border-color: var(--accent); + box-shadow: 0 2px 12px rgba(var(--accent-rgb), .35); } .tab-count { @@ -489,9 +534,9 @@ width: 100%; height: 32px; padding: 0 28px 0 32px; - border: 1px solid rgba(255, 255, 255, .06); + border: 1px solid rgba(255, 255, 255, .08); border-radius: 20px; - background: var(--bg-secondary); + background: var(--bg-deep); color: var(--text-normal); font-family: var(--font); font-size: 13px; @@ -572,8 +617,8 @@ height: 24px; padding: 0 10px; border-radius: 14px; - border: 1px solid rgba(var(--accent-rgb, 88, 101, 242), .45); - background: rgba(var(--accent-rgb, 88, 101, 242), .12); + border: 1px solid rgba(var(--accent-rgb), .45); + background: rgba(var(--accent-rgb), .12); color: var(--accent); font-size: 11px; font-weight: 700; @@ -639,7 +684,7 @@ } .tb-btn.random { - border-color: rgba(88, 101, 242, .3); + border-color: rgba(230, 126, 34, .3); color: var(--accent); } @@ -834,7 +879,7 @@ gap: 4px; padding: 3px 8px; border-radius: 999px; - background: rgba(var(--accent-rgb, 88, 101, 242), .15); + background: rgba(var(--accent-rgb), .15); color: var(--accent); font-size: 11px; font-weight: 600; @@ -875,8 +920,9 @@ font-size: 12px; font-weight: 600; color: var(--text-muted); - background: var(--bg-secondary); - border: 1px solid rgba(255, 255, 255, .06); + background: rgba(255, 255, 255, .05); + border: 1px solid rgba(255, 255, 255, .08); + backdrop-filter: blur(8px); white-space: nowrap; cursor: pointer; transition: all var(--transition); @@ -884,13 +930,16 @@ } .cat-chip:hover { - border-color: rgba(255, 255, 255, .12); + border-color: rgba(255, 255, 255, .15); color: var(--text-normal); - background: var(--bg-tertiary); + background: rgba(255, 255, 255, .1); } .cat-chip.active { - background: rgba(88, 101, 242, .1); + background: var(--accent); + color: var(--white); + border-color: var(--accent); + box-shadow: 0 2px 12px rgba(var(--accent-rgb), .35); } .cat-dot { @@ -924,7 +973,7 @@ } /* ──────────────────────────────────────────── - Sound Card + Sound Card — Glass Morphism ──────────────────────────────────────────── */ .sound-card { position: relative; @@ -934,11 +983,13 @@ justify-content: center; gap: 3px; padding: 12px 6px 8px; - background: var(--bg-secondary); + background: rgba(255, 255, 255, .05); + border: 1px solid rgba(255, 255, 255, .08); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); border-radius: var(--radius-lg); cursor: pointer; transition: all var(--transition); - border: 2px solid transparent; user-select: none; overflow: hidden; aspect-ratio: 1; @@ -953,15 +1004,14 @@ border-radius: inherit; opacity: 0; transition: opacity var(--transition); - background: radial-gradient(ellipse at center, var(--accent-glow) 0%, transparent 70%); + background: radial-gradient(circle at 50% 0%, rgba(var(--accent-rgb), .12), transparent 70%); pointer-events: none; } .sound-card:hover { - background: var(--bg-tertiary); - transform: translateY(-3px); - box-shadow: var(--shadow-med), 0 0 20px var(--accent-glow); - border-color: rgba(88, 101, 242, .2); + transform: scale(1.05); + border-color: rgba(var(--accent-rgb), .35); + box-shadow: 0 4px 20px rgba(var(--accent-rgb), .15); } .sound-card:hover::before { @@ -969,7 +1019,7 @@ } .sound-card:active { - transform: translateY(0); + transform: scale(0.97); transition-duration: 50ms; } @@ -992,7 +1042,7 @@ .ripple { position: absolute; border-radius: 50%; - background: rgba(88, 101, 242, .3); + background: rgba(var(--accent-rgb), .3); transform: scale(0); animation: ripple-expand 500ms ease-out forwards; pointer-events: none; @@ -1171,8 +1221,8 @@ gap: 6px; padding: 4px 12px; border-radius: 20px; - background: rgba(var(--accent-rgb, 88, 101, 242), .12); - border: 1px solid rgba(var(--accent-rgb, 88, 101, 242), .2); + background: rgba(var(--accent-rgb), .12); + border: 1px solid rgba(var(--accent-rgb), .2); font-size: 12px; color: var(--text-muted); max-width: none; @@ -1251,23 +1301,26 @@ .vol-slider::-webkit-slider-thumb { -webkit-appearance: none; - width: 12px; - height: 12px; + width: 14px; + height: 14px; border-radius: 50%; background: var(--accent); + box-shadow: 0 0 8px rgba(var(--accent-rgb), .5); cursor: pointer; - transition: transform var(--transition); + transition: transform var(--transition), box-shadow var(--transition); } .vol-slider::-webkit-slider-thumb:hover { transform: scale(1.3); + box-shadow: 0 0 14px rgba(var(--accent-rgb), .7); } .vol-slider::-moz-range-thumb { - width: 12px; - height: 12px; + width: 14px; + height: 14px; border-radius: 50%; background: var(--accent); + box-shadow: 0 0 8px rgba(var(--accent-rgb), .5); border: none; cursor: pointer; } @@ -2042,7 +2095,6 @@ z-index: 300; animation: fade-in 150ms ease; } -@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } .dl-modal { width: 420px; max-width: 92vw; diff --git a/web/src/plugins/streaming/streaming.css b/web/src/plugins/streaming/streaming.css index 9d43c2e..6f8cffa 100644 --- a/web/src/plugins/streaming/streaming.css +++ b/web/src/plugins/streaming/streaming.css @@ -356,23 +356,26 @@ /* ── Empty state ── */ .stream-empty { - text-align: center; - padding: 60px 20px; - color: var(--text-muted); + flex: 1; display: flex; flex-direction: column; + align-items: center; justify-content: center; gap: 16px; + padding: 40px; height: 100%; } .stream-empty-icon { - font-size: 48px; - margin-bottom: 12px; - opacity: 0.4; + font-size: 64px; line-height: 1; + filter: drop-shadow(0 0 20px rgba(230,126,34,0.5)); + animation: stream-float 3s ease-in-out infinite; +} +@keyframes stream-float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-8px); } } .stream-empty h3 { - font-size: 18px; - font-weight: 600; - color: var(--text-normal); - margin-bottom: 6px; + font-size: 26px; font-weight: 700; color: #f2f3f5; + letter-spacing: -0.5px; margin: 0; } .stream-empty p { - font-size: 14px; + font-size: 15px; color: #80848e; + text-align: center; max-width: 360px; line-height: 1.5; margin: 0; } /* ── Error ── */ diff --git a/web/src/plugins/watch-together/watch-together.css b/web/src/plugins/watch-together/watch-together.css index 1873674..c24e283 100644 --- a/web/src/plugins/watch-together/watch-together.css +++ b/web/src/plugins/watch-together/watch-together.css @@ -161,23 +161,26 @@ /* ── Empty state ── */ .wt-empty { - text-align: center; - padding: 60px 20px; - color: var(--text-muted); + flex: 1; display: flex; flex-direction: column; + align-items: center; justify-content: center; gap: 16px; + padding: 40px; height: 100%; } .wt-empty-icon { - font-size: 48px; - margin-bottom: 12px; - opacity: 0.4; + font-size: 64px; line-height: 1; + filter: drop-shadow(0 0 20px rgba(230,126,34,0.5)); + animation: wt-float 3s ease-in-out infinite; +} +@keyframes wt-float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-8px); } } .wt-empty h3 { - font-size: 18px; - font-weight: 600; - color: var(--text-normal); - margin-bottom: 6px; + font-size: 26px; font-weight: 700; color: #f2f3f5; + letter-spacing: -0.5px; margin: 0; } .wt-empty p { - font-size: 14px; + font-size: 15px; color: #80848e; + text-align: center; max-width: 360px; line-height: 1.5; margin: 0; } /* ── Error ── */ diff --git a/web/src/styles.css b/web/src/styles.css index 656b9d5..50e7604 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -4,19 +4,27 @@ --bg-primary: #1e1f22; --bg-secondary: #2b2d31; --bg-tertiary: #313338; - --text-normal: #dbdee1; - --text-muted: #949ba4; - --text-faint: #6d6f78; + --bg-light: #3a3c41; + --bg-lighter: #43464d; + --text-normal: #f2f3f5; + --text-muted: #b5bac1; + --text-faint: #80848e; --accent: #e67e22; --accent-rgb: 230, 126, 34; --accent-hover: #d35400; - --success: #57d28f; + --accent-glow: rgba(230, 126, 34, 0.5); + --accent-dim: rgba(230, 126, 34, 0.35); + --accent-subtle: rgba(230, 126, 34, 0.12); + --glass-bg: rgba(255, 255, 255, 0.05); + --glass-border: rgba(255, 255, 255, 0.08); + --glass-bg-hover: rgba(255, 255, 255, 0.09); + --success: #23a559; --danger: #ed4245; --warning: #fee75c; --border: rgba(255, 255, 255, 0.06); --radius: 8px; --radius-lg: 12px; - --transition: 150ms ease; + --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1); --font: 'Segoe UI', system-ui, -apple-system, sans-serif; --header-height: 56px; } @@ -78,14 +86,18 @@ html, body { .hub-logo { font-size: 24px; line-height: 1; + filter: drop-shadow(0 0 6px var(--accent-glow)); } .hub-title { font-size: 18px; font-weight: 700; - color: var(--text-normal); letter-spacing: -0.02em; white-space: nowrap; + background: linear-gradient(135deg, var(--accent), #f39c12); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } /* ── Connection Status Dot ── */ @@ -155,8 +167,8 @@ html, body { } .hub-tab.active { - color: var(--accent); - background: rgba(var(--accent-rgb), 0.1); + color: var(--text-normal); + background: var(--accent-subtle); } .hub-tab.active::after { @@ -165,10 +177,11 @@ html, body { bottom: -1px; left: 50%; transform: translateX(-50%); - width: calc(100% - 16px); + width: 70%; height: 2px; background: var(--accent); - border-radius: 1px; + border-radius: 2px; + box-shadow: 0 0 8px var(--accent-glow), 0 0 16px rgba(230,126,34,0.2); } .hub-tab-icon { @@ -616,6 +629,213 @@ html, body { color: var(--text-normal); } +/* ── Admin Button ── */ +.hub-admin-btn { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + color: var(--text-muted); + font-size: 16px; + width: 36px; + height: 36px; + border-radius: 50%; + cursor: pointer; + transition: all var(--transition); + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.hub-admin-btn:hover { + background: var(--glass-bg-hover); + border-color: var(--accent-dim); + box-shadow: 0 0 12px rgba(230,126,34,0.15); +} + +.hub-admin-btn.logged-in { + border-color: var(--success); +} + +.hub-admin-green-dot { + position: absolute; + top: 1px; + right: 1px; + width: 8px; + height: 8px; + background: var(--success); + border-radius: 50%; + border: 2px solid var(--bg-primary); +} + +/* ── Avatar ── */ +.hub-avatar { + width: 34px; + height: 34px; + border-radius: 50%; + background: linear-gradient(135deg, var(--accent), #f39c12); + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: 700; + color: #fff; + box-shadow: 0 0 0 2px var(--bg-primary); +} + +/* ── Admin Modal ── */ +.hub-admin-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.7); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + animation: hub-modal-fade-in 0.2s ease; +} + +@keyframes hub-modal-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +.hub-admin-modal { + background: var(--bg-secondary); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + padding: 32px; + width: 360px; + max-width: 90vw; + box-shadow: 0 16px 48px rgba(0,0,0,0.5), 0 0 40px rgba(230,126,34,0.08); + animation: hub-modal-slide-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes hub-modal-slide-in { + from { opacity: 0; transform: scale(0.95) translateY(10px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +.hub-admin-modal-title { + font-size: 20px; + font-weight: 700; + margin-bottom: 6px; + display: flex; + align-items: center; + gap: 8px; +} + +.hub-admin-modal-subtitle { + font-size: 13px; + color: var(--text-faint); + margin-bottom: 24px; +} + +.hub-admin-modal-error { + font-size: 13px; + color: var(--danger); + margin-bottom: 12px; + padding: 8px 12px; + background: rgba(237, 66, 69, 0.1); + border-radius: var(--radius); +} + +.hub-admin-modal-input { + width: 100%; + background: var(--bg-deep); + border: 1px solid var(--glass-border); + border-radius: var(--radius); + color: var(--text-normal); + font-family: var(--font); + font-size: 14px; + padding: 10px 14px; + outline: none; + transition: border-color var(--transition), box-shadow var(--transition); + margin-bottom: 16px; +} + +.hub-admin-modal-input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(230,126,34,0.12); +} + +.hub-admin-modal-login { + width: 100%; + background: var(--accent); + border: none; + border-radius: var(--radius); + color: #fff; + font-family: var(--font); + font-size: 14px; + font-weight: 600; + padding: 10px; + cursor: pointer; + transition: all var(--transition); + box-shadow: 0 2px 10px var(--accent-dim); +} + +.hub-admin-modal-login:hover { + background: var(--accent-hover); + box-shadow: 0 4px 16px var(--accent-glow); +} + +.hub-admin-modal-info { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; +} + +.hub-admin-modal-avatar { + width: 44px; + height: 44px; + border-radius: 50%; + background: linear-gradient(135deg, var(--accent), #f39c12); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + font-weight: 700; + color: #fff; + box-shadow: 0 0 12px var(--accent-dim); +} + +.hub-admin-modal-text { + display: flex; + flex-direction: column; + gap: 2px; +} + +.hub-admin-modal-name { + font-weight: 600; + font-size: 15px; +} + +.hub-admin-modal-role { + font-size: 12px; + color: var(--success); + font-weight: 500; +} + +.hub-admin-modal-logout { + width: 100%; + background: rgba(237,66,69,0.12); + border: 1px solid rgba(237,66,69,0.25); + border-radius: var(--radius); + color: var(--danger); + font-family: var(--font); + font-size: 14px; + font-weight: 500; + padding: 10px; + cursor: pointer; + transition: all var(--transition); +} + +.hub-admin-modal-logout:hover { + background: rgba(237,66,69,0.2); +} + /* ── Focus Styles ── */ :focus-visible { outline: 2px solid var(--accent);