UI improvements: glow effects, deck colors, listener count, remove black bar, mobile header fix
This commit is contained in:
parent
6027f2e973
commit
2e64870daa
11
index.html
11
index.html
|
|
@ -424,10 +424,6 @@
|
||||||
|
|
||||||
<!-- Settings Panel -->
|
<!-- Settings Panel -->
|
||||||
<div class="settings-panel" id="settings-panel">
|
<div class="settings-panel" id="settings-panel">
|
||||||
<div class="settings-header">
|
|
||||||
<span>SETTINGS</span>
|
|
||||||
<button class="close-settings" onclick="toggleSettings()">X</button>
|
|
||||||
</div>
|
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<div class="setting-item"><label><input type="checkbox" id="repeat-A"
|
<div class="setting-item"><label><input type="checkbox" id="repeat-A"
|
||||||
onchange="toggleRepeat('A', this.checked)">Repeat Deck A</label></div>
|
onchange="toggleRepeat('A', this.checked)">Repeat Deck A</label></div>
|
||||||
|
|
@ -446,10 +442,15 @@
|
||||||
<div class="setting-item"><label><input type="checkbox" id="glow-B"
|
<div class="setting-item"><label><input type="checkbox" id="glow-B"
|
||||||
onchange="updateManualGlow('B', this.checked)">Glow Deck B (Magenta)</label></div>
|
onchange="updateManualGlow('B', this.checked)">Glow Deck B (Magenta)</label></div>
|
||||||
<div class="setting-item" style="flex-direction: column; align-items: flex-start;">
|
<div class="setting-item" style="flex-direction: column; align-items: flex-start;">
|
||||||
<label>Glow Intensity</label>
|
<label>Glow Intensity (DJ Panel)</label>
|
||||||
<input type="range" id="glow-intensity" min="1" max="100" value="30" style="width: 100%;"
|
<input type="range" id="glow-intensity" min="1" max="100" value="30" style="width: 100%;"
|
||||||
oninput="updateGlowIntensity(this.value)">
|
oninput="updateGlowIntensity(this.value)">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-item" style="flex-direction: column; align-items: flex-start;">
|
||||||
|
<label>Listener Page Glow</label>
|
||||||
|
<input type="range" id="listener-glow-intensity" min="0" max="100" value="30" style="width: 100%;"
|
||||||
|
oninput="updateListenerGlow(this.value)">
|
||||||
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<button class="btn-primary" onclick="openKeyboardSettings()"
|
<button class="btn-primary" onclick="openKeyboardSettings()"
|
||||||
style="width: 100%; padding: 12px; margin-top: 10px;">
|
style="width: 100%; padding: 12px; margin-top: 10px;">
|
||||||
|
|
|
||||||
43
listener.css
43
listener.css
|
|
@ -45,34 +45,33 @@ body::before {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1;
|
z-index: 999;
|
||||||
opacity: var(--glow-opacity, 0.3);
|
opacity: var(--glow-opacity, 0.3);
|
||||||
transition: all 1s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 1s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Listener atmospheric glow */
|
/* Listener atmospheric glow — static, intensity driven by --glow-opacity/--glow-spread */
|
||||||
body.listener-glow::before {
|
body.listener-glow::before {
|
||||||
animation: pulse-listener 4s ease-in-out infinite;
|
box-shadow:
|
||||||
|
0 0 var(--glow-spread) rgba(0, 80, 255, calc(var(--glow-opacity) * 1.5)),
|
||||||
|
0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 1.5)),
|
||||||
|
inset 0 0 var(--glow-spread) rgba(0, 80, 255, calc(var(--glow-opacity) * 1)),
|
||||||
|
inset 0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-listener {
|
/* Deck-driven glow — mirrors the DJ panel colours */
|
||||||
0%, 100% {
|
body.listener-glow.playing-A::before {
|
||||||
filter: hue-rotate(0deg) brightness(1.2);
|
box-shadow: inset 0 0 var(--glow-spread) var(--primary-cyan);
|
||||||
box-shadow:
|
}
|
||||||
0 0 var(--glow-spread) rgba(0, 80, 255, calc(var(--glow-opacity) * 1.5)),
|
|
||||||
0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 1.5)),
|
body.listener-glow.playing-B::before {
|
||||||
inset 0 0 var(--glow-spread) rgba(0, 80, 255, calc(var(--glow-opacity) * 1)),
|
box-shadow: inset 0 0 var(--glow-spread) var(--secondary-magenta);
|
||||||
inset 0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 1));
|
}
|
||||||
}
|
|
||||||
50% {
|
body.listener-glow.playing-A.playing-B::before {
|
||||||
filter: hue-rotate(15deg) brightness(1.8);
|
box-shadow:
|
||||||
box-shadow:
|
inset 0 0 var(--glow-spread) var(--primary-cyan),
|
||||||
0 0 calc(var(--glow-spread) * 1.5) rgba(0, 120, 255, calc(var(--glow-opacity) * 2.2)),
|
inset 0 0 calc(var(--glow-spread) * 1.5) var(--secondary-magenta);
|
||||||
0 0 calc(var(--glow-spread) * 2) rgba(220, 50, 255, calc(var(--glow-opacity) * 2.2)),
|
|
||||||
0 0 calc(var(--glow-spread) * 4) rgba(0, 243, 255, calc(var(--glow-opacity) * 1)),
|
|
||||||
inset 0 0 calc(var(--glow-spread) * 1.5) rgba(0, 120, 255, calc(var(--glow-opacity) * 1.5)),
|
|
||||||
inset 0 0 calc(var(--glow-spread) * 2) rgba(220, 50, 255, calc(var(--glow-opacity) * 1.5));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== Listener Mode Layout ========== */
|
/* ========== Listener Mode Layout ========== */
|
||||||
|
|
@ -80,7 +79,7 @@ body.listener-glow::before {
|
||||||
.listener-mode {
|
.listener-mode {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: linear-gradient(135deg, #0a0a12 0%, #1a0a1a 100%);
|
background: transparent;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<!-- Listener UI -->
|
<!-- Listener UI -->
|
||||||
<div class="listener-mode" id="listener-mode">
|
<div class="listener-mode" id="listener-mode">
|
||||||
<div class="listener-header">
|
<div class="listener-header">
|
||||||
<h1>TECHDJ LIVE</h1>
|
<h1>TECHY.MUSIC</h1>
|
||||||
<div class="live-indicator">
|
<div class="live-indicator">
|
||||||
<span class="pulse-dot"></span>
|
<span class="pulse-dot"></span>
|
||||||
<span>LIVE</span>
|
<span>LIVE</span>
|
||||||
|
|
|
||||||
|
|
@ -308,6 +308,15 @@ function initSocket() {
|
||||||
handleBroadcastOffline();
|
handleBroadcastOffline();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('listener_glow', (data) => {
|
||||||
|
updateGlowIntensity(data.intensity ?? 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('deck_glow', (data) => {
|
||||||
|
document.body.classList.toggle('playing-A', !!data.A);
|
||||||
|
document.body.classList.toggle('playing-B', !!data.B);
|
||||||
|
});
|
||||||
|
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
19
script.js
19
script.js
|
|
@ -773,6 +773,12 @@ function formatTime(seconds) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Playback Logic
|
// Playback Logic
|
||||||
|
function _notifyListenerDeckGlow() {
|
||||||
|
// Emit the current playing state of both decks to the listener page
|
||||||
|
if (!socket) return;
|
||||||
|
socket.emit('deck_glow', { A: !!decks.A.playing, B: !!decks.B.playing });
|
||||||
|
}
|
||||||
|
|
||||||
function playDeck(id) {
|
function playDeck(id) {
|
||||||
vibrate(15);
|
vibrate(15);
|
||||||
// Server-side audio mode
|
// Server-side audio mode
|
||||||
|
|
@ -782,8 +788,7 @@ function playDeck(id) {
|
||||||
const deckEl = document.getElementById('deck-' + id);
|
const deckEl = document.getElementById('deck-' + id);
|
||||||
if (deckEl) deckEl.classList.add('playing');
|
if (deckEl) deckEl.classList.add('playing');
|
||||||
document.body.classList.add('playing-' + id);
|
document.body.classList.add('playing-' + id);
|
||||||
|
_notifyListenerDeckGlow();
|
||||||
|
|
||||||
console.log(`[Deck ${id}] Play command sent to server`);
|
console.log(`[Deck ${id}] Play command sent to server`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -800,6 +805,7 @@ function playDeck(id) {
|
||||||
const deckEl = document.getElementById('deck-' + id);
|
const deckEl = document.getElementById('deck-' + id);
|
||||||
if (deckEl) deckEl.classList.add('playing');
|
if (deckEl) deckEl.classList.add('playing');
|
||||||
document.body.classList.add('playing-' + id);
|
document.body.classList.add('playing-' + id);
|
||||||
|
_notifyListenerDeckGlow();
|
||||||
|
|
||||||
if (audioCtx.state === 'suspended') {
|
if (audioCtx.state === 'suspended') {
|
||||||
console.log(`[Deck ${id}] Resuming suspended AudioContext`);
|
console.log(`[Deck ${id}] Resuming suspended AudioContext`);
|
||||||
|
|
@ -836,7 +842,7 @@ function pauseDeck(id) {
|
||||||
const deckEl = document.getElementById('deck-' + id);
|
const deckEl = document.getElementById('deck-' + id);
|
||||||
if (deckEl) deckEl.classList.remove('playing');
|
if (deckEl) deckEl.classList.remove('playing');
|
||||||
document.body.classList.remove('playing-' + id);
|
document.body.classList.remove('playing-' + id);
|
||||||
|
_notifyListenerDeckGlow();
|
||||||
console.log(`[Deck ${id}] Pause command sent to server`);
|
console.log(`[Deck ${id}] Pause command sent to server`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -863,6 +869,7 @@ function pauseDeck(id) {
|
||||||
const deckEl = document.getElementById('deck-' + id);
|
const deckEl = document.getElementById('deck-' + id);
|
||||||
if (deckEl) deckEl.classList.remove('playing');
|
if (deckEl) deckEl.classList.remove('playing');
|
||||||
document.body.classList.remove('playing-' + id);
|
document.body.classList.remove('playing-' + id);
|
||||||
|
_notifyListenerDeckGlow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1774,6 +1781,12 @@ function updateGlowIntensity(val) {
|
||||||
document.documentElement.style.setProperty('--glow-spread', `${spread}px`);
|
document.documentElement.style.setProperty('--glow-spread', `${spread}px`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateListenerGlow(val) {
|
||||||
|
settings.listenerGlowIntensity = parseInt(val);
|
||||||
|
if (!socket) initSocket();
|
||||||
|
socket.emit('listener_glow', { intensity: settings.listenerGlowIntensity });
|
||||||
|
}
|
||||||
|
|
||||||
// Dismiss landscape prompt
|
// Dismiss landscape prompt
|
||||||
function dismissLandscapePrompt() {
|
function dismissLandscapePrompt() {
|
||||||
const prompt = document.getElementById('landscape-prompt');
|
const prompt = document.getElementById('landscape-prompt');
|
||||||
|
|
|
||||||
70
server.py
70
server.py
|
|
@ -615,7 +615,6 @@ def dj_login():
|
||||||
<input id=\"password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" autofocus />
|
<input id=\"password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" autofocus />
|
||||||
<button type=\"submit\">Unlock DJ Panel</button>
|
<button type=\"submit\">Unlock DJ Panel</button>
|
||||||
{f"<div class='err'>{error}</div>" if error else ""}
|
{f"<div class='err'>{error}</div>" if error else ""}
|
||||||
<div class=\"hint\">Set/disable this in config.json (dj_panel_password).</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -736,6 +735,23 @@ def dj_start(data=None):
|
||||||
def dj_get_listener_count():
|
def dj_get_listener_count():
|
||||||
emit('listener_count', {'count': len(listener_sids)})
|
emit('listener_count', {'count': len(listener_sids)})
|
||||||
|
|
||||||
|
@dj_socketio.on('listener_glow')
|
||||||
|
def dj_listener_glow(data):
|
||||||
|
"""DJ sets the glow intensity on the listener page."""
|
||||||
|
intensity = int(data.get('intensity', 30)) if isinstance(data, dict) else 30
|
||||||
|
intensity = max(0, min(100, intensity))
|
||||||
|
listener_socketio.emit('listener_glow', {'intensity': intensity}, namespace='/')
|
||||||
|
|
||||||
|
@dj_socketio.on('deck_glow')
|
||||||
|
def dj_deck_glow(data):
|
||||||
|
"""Relay which decks are playing so the listener page can mirror the glow colour."""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return
|
||||||
|
listener_socketio.emit('deck_glow', {
|
||||||
|
'A': bool(data.get('A', False)),
|
||||||
|
'B': bool(data.get('B', False)),
|
||||||
|
}, namespace='/')
|
||||||
|
|
||||||
@dj_socketio.on('stop_broadcast')
|
@dj_socketio.on('stop_broadcast')
|
||||||
def dj_stop():
|
def dj_stop():
|
||||||
broadcast_state['active'] = False
|
broadcast_state['active'] = False
|
||||||
|
|
@ -784,34 +800,49 @@ listener_socketio = SocketIO(
|
||||||
cors_allowed_origins=CONFIG_CORS,
|
cors_allowed_origins=CONFIG_CORS,
|
||||||
async_mode='eventlet',
|
async_mode='eventlet',
|
||||||
max_http_buffer_size=CONFIG_MAX_UPLOAD_MB * 1024 * 1024,
|
max_http_buffer_size=CONFIG_MAX_UPLOAD_MB * 1024 * 1024,
|
||||||
ping_timeout=60,
|
# Lower timeouts: stale connections detected in ~25s instead of ~85s
|
||||||
ping_interval=25,
|
# ping_interval: how often to probe (seconds)
|
||||||
|
# ping_timeout: how long to wait for pong before declaring dead
|
||||||
|
ping_timeout=15,
|
||||||
|
ping_interval=10,
|
||||||
logger=CONFIG_DEBUG,
|
logger=CONFIG_DEBUG,
|
||||||
engineio_logger=CONFIG_DEBUG
|
engineio_logger=CONFIG_DEBUG
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _broadcast_listener_count():
|
||||||
|
"""Compute the most accurate listener count and broadcast to both panels.
|
||||||
|
|
||||||
|
Uses the larger of:
|
||||||
|
- listener_sids: Socket.IO connections (people with the page open)
|
||||||
|
- _mp3_clients: active /stream.mp3 HTTP connections (people actually hearing audio)
|
||||||
|
|
||||||
|
Taking the max avoids undercounting when someone hasn't clicked Enable Audio
|
||||||
|
yet, and also avoids undercounting direct stream URL listeners (e.g. VLC).
|
||||||
|
"""
|
||||||
|
with _mp3_lock:
|
||||||
|
stream_count = len(_mp3_clients)
|
||||||
|
count = max(len(listener_sids), stream_count)
|
||||||
|
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
|
||||||
|
dj_socketio.emit('listener_count', {'count': count}, namespace='/')
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
@listener_socketio.on('connect')
|
@listener_socketio.on('connect')
|
||||||
def listener_connect():
|
def listener_connect():
|
||||||
print(f"LISTENER: Listener Socket Connected: {request.sid}")
|
# Count immediately on connect — don't wait for join_listener
|
||||||
|
listener_sids.add(request.sid)
|
||||||
|
count = _broadcast_listener_count()
|
||||||
|
print(f"LISTENER: Connected {request.sid}. Total: {count}")
|
||||||
|
|
||||||
@listener_socketio.on('disconnect')
|
@listener_socketio.on('disconnect')
|
||||||
def listener_disconnect():
|
def listener_disconnect():
|
||||||
listener_sids.discard(request.sid)
|
listener_sids.discard(request.sid)
|
||||||
count = len(listener_sids)
|
count = _broadcast_listener_count()
|
||||||
print(f"REMOVED: Listener left. Total: {count}")
|
print(f"REMOVED: Listener left {request.sid}. Total: {count}")
|
||||||
# Notify BOTH namespaces
|
|
||||||
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
|
|
||||||
dj_socketio.emit('listener_count', {'count': count}, namespace='/')
|
|
||||||
|
|
||||||
@listener_socketio.on('join_listener')
|
@listener_socketio.on('join_listener')
|
||||||
def listener_join():
|
def listener_join():
|
||||||
if request.sid not in listener_sids:
|
# SID already added in listener_connect(); just send stream status back
|
||||||
listener_sids.add(request.sid)
|
|
||||||
count = len(listener_sids)
|
|
||||||
print(f"LISTENER: New listener joined. Total: {count}")
|
|
||||||
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
|
|
||||||
dj_socketio.emit('listener_count', {'count': count}, namespace='/')
|
|
||||||
|
|
||||||
emit('stream_status', {'active': broadcast_state['active']})
|
emit('stream_status', {'active': broadcast_state['active']})
|
||||||
|
|
||||||
@listener_socketio.on('get_listener_count')
|
@listener_socketio.on('get_listener_count')
|
||||||
|
|
@ -833,12 +864,11 @@ def _transcoder_watchdog():
|
||||||
|
|
||||||
|
|
||||||
def _listener_count_sync_loop():
|
def _listener_count_sync_loop():
|
||||||
"""Periodic background sync to ensure listener count is always accurate."""
|
"""Periodic reconciliation — catches any edge cases where connect/disconnect
|
||||||
|
events were missed (e.g. server under load, eventlet greenlet delays)."""
|
||||||
while True:
|
while True:
|
||||||
count = len(listener_sids)
|
|
||||||
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
|
|
||||||
dj_socketio.emit('listener_count', {'count': count}, namespace='/')
|
|
||||||
eventlet.sleep(5)
|
eventlet.sleep(5)
|
||||||
|
_broadcast_listener_count()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
27
style.css
27
style.css
|
|
@ -24,6 +24,12 @@
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
overflow: hidden;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome / Edge / Safari */
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
@ -218,7 +224,7 @@ header h1 {
|
||||||
grid-template-rows: 1fr 80px;
|
grid-template-rows: 1fr 80px;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: calc(100vh - 60px);
|
height: 100vh;
|
||||||
/* Adjust based on header height */
|
/* Adjust based on header height */
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -1706,6 +1712,9 @@ input[type=range] {
|
||||||
/* Less intense on mobile */
|
/* Less intense on mobile */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-top-bar {
|
.mobile-top-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1916,8 +1925,8 @@ input[type=range] {
|
||||||
/* Streaming Button */
|
/* Streaming Button */
|
||||||
.streaming-btn {
|
.streaming-btn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 25px;
|
bottom: 175px;
|
||||||
right: 175px;
|
right: 25px;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
@ -1947,8 +1956,8 @@ input[type=range] {
|
||||||
|
|
||||||
.upload-btn {
|
.upload-btn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 25px;
|
bottom: 100px;
|
||||||
right: 100px;
|
right: 25px;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
@ -1980,7 +1989,7 @@ input[type=range] {
|
||||||
.streaming-panel {
|
.streaming-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: -400px;
|
right: -460px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 380px;
|
width: 380px;
|
||||||
background: rgba(10, 10, 20, 0.98);
|
background: rgba(10, 10, 20, 0.98);
|
||||||
|
|
@ -2587,8 +2596,8 @@ body.listening-active .landscape-prompt {
|
||||||
/* Base Settings Button Fix */
|
/* Base Settings Button Fix */
|
||||||
.keyboard-btn {
|
.keyboard-btn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 25px;
|
bottom: 250px;
|
||||||
right: 250px;
|
right: 25px;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
@ -2637,7 +2646,7 @@ body.listening-active .landscape-prompt {
|
||||||
.settings-panel {
|
.settings-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: -350px;
|
right: -400px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
background: rgba(10, 10, 20, 0.98);
|
background: rgba(10, 10, 20, 0.98);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue