Mirrored playlist modal: revamp + fix header clipping on long playlists

The preview modal looked amateur and its header/footer clipped on long
playlists (wolf39's 316-track "Road trip" showed neither title nor buttons).

Root cause of the clip: .mm-list (the scroll area) was a flex child with
flex:1 but no min-height:0. Flex items default to min-height:auto, so the
list refused to shrink below its content, the modal blew past max-height,
and overflow:hidden + vertical centering pushed the header off the top and
the footer off the bottom. Now the list has min-height:0 and the hero +
action bar are flex-shrink:0, so they stay pinned and the list scrolls.

Visual revamp to match the rest of the app, using data already returned by
/api/mirrored-playlists/<id> (image_url on the playlist and each track):
- Hero uses real artwork (playlist cover -> first track art -> gradient
  fallback) with a blurred art backdrop + darkening overlay, replacing the
  emoji-in-a-box. Eyebrow + large title + meta line (source pill, owner,
  track count, total runtime, mirrored-ago).
- Track rows gain per-track album thumbnails, two-line title/artist, album,
  duration, and a sticky column header. Missing art falls back to a gradient
  tile via onerror (no broken-image icons).
- Cleaner action bar: primary Discover, secondary Auto-Sync, ghost Edit/
  Close, quiet danger-outline Delete.

Old .mirrored-modal-* / .mirrored-track-* / .mirrored-btn-* classes removed
from style.css and replaced with the new .mm-* set; the _escJs escaping in
the footer buttons (apostrophe fix) is preserved.
This commit is contained in:
BoulderBadgeDad 2026-06-01 09:31:20 -07:00
parent ffb9249ded
commit 6e7948b642
2 changed files with 288 additions and 218 deletions

View file

@ -829,52 +829,74 @@ async function openMirroredPlaylistModal(playlistId) {
const tracks = data.tracks || []; const tracks = data.tracks || [];
const source = data.source || 'unknown'; const source = data.source || 'unknown';
const sourceRef = getMirroredSourceRef(data); const sourceRef = getMirroredSourceRef(data);
const sourceIcons = { spotify: '🎵', tidal: '🌊', youtube: '▶', beatport: '🎛' }; const sourceIcons = { spotify: '🎵', spotify_public: '🎵', tidal: '🌊', youtube: '▶', beatport: '🎛', deezer: '🎧', qobuz: '♫' };
const sourceLabels = { spotify: 'Spotify', spotify_public: 'Spotify', tidal: 'Tidal', youtube: 'YouTube', beatport: 'Beatport', deezer: 'Deezer', qobuz: 'Qobuz' };
const sourceIcon = sourceIcons[source] || '📋'; const sourceIcon = sourceIcons[source] || '📋';
const srcLabel = sourceLabels[source] || source;
// Hero artwork: playlist cover → first track with art → gradient fallback.
const heroArt = data.image_url || (tracks.find(t => t.image_url) || {}).image_url || '';
// Total runtime for the meta line.
const totalMs = tracks.reduce((sum, t) => sum + (t.duration_ms || 0), 0);
const totalMin = Math.round(totalMs / 60000);
const totalLabel = totalMin >= 60 ? `${Math.floor(totalMin / 60)} hr ${totalMin % 60} min` : `${totalMin} min`;
const trackRows = tracks.map(t => { const trackRows = tracks.map(t => {
const dur = t.duration_ms ? `${Math.floor(t.duration_ms / 60000)}:${String(Math.floor((t.duration_ms % 60000) / 1000)).padStart(2, '0')}` : ''; const dur = t.duration_ms ? `${Math.floor(t.duration_ms / 60000)}:${String(Math.floor((t.duration_ms % 60000) / 1000)).padStart(2, '0')}` : '';
return `<div class="mirrored-track-row"> const art = t.image_url
<span class="track-pos">${t.position}</span> ? `<img class="mm-row-art" src="${_escAttr(t.image_url)}" loading="lazy" onerror="this.onerror=null;this.classList.add('mm-art-empty');this.removeAttribute('src');">`
<span class="track-title">${_esc(t.track_name)}</span> : `<span class="mm-row-art mm-art-empty"></span>`;
<span class="track-artist">${_esc(t.artist_name)}</span> return `<div class="mm-row">
<span class="track-album">${_esc(t.album_name)}</span> <span class="mm-row-pos">${t.position}</span>
<span class="track-duration">${dur}</span> ${art}
<div class="mm-row-main">
<span class="mm-row-title">${_esc(t.track_name)}</span>
<span class="mm-row-artist">${_esc(t.artist_name)}</span>
</div>
<span class="mm-row-album">${_esc(t.album_name || '')}</span>
<span class="mm-row-dur">${dur}</span>
</div>`; </div>`;
}).join(''); }).join('');
const heroCover = heroArt
? `<div class="mm-cover" style="background-image:url('${_escAttr(heroArt)}')"></div>`
: `<div class="mm-cover mm-cover-empty ${_escAttr(source)}">${sourceIcon}</div>`;
overlay.innerHTML = ` overlay.innerHTML = `
<div class="mirrored-modal"> <div class="mirrored-modal">
<div class="mirrored-modal-header"> <div class="mm-hero">
<div class="mirrored-modal-hero"> ${heroArt ? `<div class="mm-hero-bg" style="background-image:url('${_escAttr(heroArt)}')"></div>` : ''}
<div class="mirrored-modal-hero-icon ${_escAttr(source)}">${sourceIcon}</div> <div class="mm-hero-content">
<div class="mirrored-modal-hero-info"> ${heroCover}
<h2 class="mirrored-modal-hero-title">${_esc(data.name)}</h2> <div class="mm-hero-info">
<div class="mirrored-modal-hero-subtitle"> <span class="mm-eyebrow">Mirrored Playlist</span>
<span class="mirrored-modal-hero-badge">${_esc(source)}</span> <h2 class="mm-title">${_esc(data.name)}</h2>
<span>${tracks.length} tracks</span> <div class="mm-meta">
<span>&middot;</span> <span class="mm-source-pill ${_escAttr(source)}">${_esc(srcLabel)}</span>
<span>Mirrored ${timeAgo(data.updated_at || data.mirrored_at)}</span> ${data.owner ? `<span class="mm-meta-item">${_esc(data.owner)}</span><span class="mm-dot">&middot;</span>` : ''}
<span class="mm-meta-item">${tracks.length} tracks</span>
${totalMs ? `<span class="mm-dot">&middot;</span><span class="mm-meta-item">${totalLabel}</span>` : ''}
<span class="mm-dot">&middot;</span>
<span class="mm-meta-item">Mirrored ${timeAgo(data.updated_at || data.mirrored_at)}</span>
</div> </div>
</div> </div>
</div> </div>
<span class="mirrored-modal-close" onclick="closeMirroredModal()">&times;</span> <button class="mm-close" onclick="closeMirroredModal()" aria-label="Close">&times;</button>
</div> </div>
<div class="mirrored-modal-tracks"> <div class="mm-list">
<div class="mirrored-track-header"> <div class="mm-list-head">
<span>#</span><span>Track</span><span>Artist</span><span>Album</span><span style="text-align:right">Time</span> <span>#</span><span></span><span>Title</span><span>Album</span><span class="mm-col-dur">Time</span>
</div> </div>
${trackRows} ${trackRows || '<div class="mm-empty">No tracks in this mirror yet.</div>'}
</div> </div>
<div class="mirrored-modal-footer"> <div class="mm-actions">
<div class="mirrored-modal-footer-left"> <button class="mm-btn mm-btn-danger" onclick="closeMirroredModal(); deleteMirroredPlaylist(${playlistId}, '${_escJs(data.name)}')">Delete Mirror</button>
<button class="mirrored-btn-delete" onclick="closeMirroredModal(); deleteMirroredPlaylist(${playlistId}, '${_escJs(data.name)}')">Delete Mirror</button> <div class="mm-actions-right">
</div> <button class="mm-btn mm-btn-ghost" onclick="editMirroredSourceRef(${playlistId}, '${_escJs(data.name)}', '${_escJs(source)}', '${_escJs(sourceRef)}')">Edit Source</button>
<div class="mirrored-modal-footer-right" style="display:flex;gap:10px;"> <button class="mm-btn mm-btn-secondary" onclick="runMirroredPlaylistPipeline(${playlistId}, '${_escJs(data.name)}')">Auto-Sync</button>
<button class="mirrored-btn-close" onclick="editMirroredSourceRef(${playlistId}, '${_escJs(data.name)}', '${_escJs(source)}', '${_escJs(sourceRef)}')">Edit Source</button> <button class="mm-btn mm-btn-ghost" onclick="closeMirroredModal()">Close</button>
<button class="mirrored-btn-pipeline" onclick="runMirroredPlaylistPipeline(${playlistId}, '${_escJs(data.name)}')">Auto-Sync</button> <button class="mm-btn mm-btn-primary" onclick="discoverMirroredPlaylist(${playlistId})">Discover</button>
<button class="mirrored-btn-close" onclick="closeMirroredModal()">Close</button>
<button class="mirrored-btn-discover" onclick="discoverMirroredPlaylist(${playlistId})">Discover</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -15244,315 +15244,363 @@ body.helper-mode-active #dashboard-activity-feed:hover {
.mirrored-modal-overlay { .mirrored-modal-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(18, 18, 18, 0.85); background: rgba(8, 8, 10, 0.78);
z-index: 10000; z-index: 10000;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
backdrop-filter: blur(12px); padding: 24px;
animation: mirroredOverlayFadeIn 0.3s ease forwards; backdrop-filter: blur(14px) saturate(1.1);
animation: mirroredOverlayFadeIn 0.25s ease forwards;
} }
@keyframes mirroredOverlayFadeIn { @keyframes mirroredOverlayFadeIn {
from { opacity: 0; backdrop-filter: blur(0px); } from { opacity: 0; }
to { opacity: 1; backdrop-filter: blur(12px); } to { opacity: 1; }
} }
@keyframes mirroredModalSlideIn { @keyframes mirroredModalSlideIn {
from { opacity: 0; transform: scale(0.92) translateY(20px); } from { opacity: 0; transform: scale(0.96) translateY(16px); }
to { opacity: 1; transform: scale(1) translateY(0); } to { opacity: 1; transform: scale(1) translateY(0); }
} }
.mirrored-modal { .mirrored-modal {
background: linear-gradient(135deg, rgba(20, 20, 20, 0.95) 0%, rgba(12, 12, 12, 0.98) 100%); position: relative;
backdrop-filter: blur(20px) saturate(1.2); background: #161616;
border-radius: 20px; border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid rgba(255, 255, 255, 0.10);
border-top: 1px solid rgba(255, 255, 255, 0.18); width: 100%;
width: 90%; max-width: 900px;
max-width: 950px; max-height: 88vh;
max-height: 85vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
box-shadow: box-shadow: 0 30px 90px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 0, 0, 0.4);
0 25px 80px rgba(0, 0, 0, 0.7), animation: mirroredModalSlideIn 0.3s cubic-bezier(0.22, 1, 0.36, 1) forwards;
0 8px 32px rgba(0, 0, 0, 0.4),
0 0 40px rgba(var(--accent-rgb), 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
animation: mirroredModalSlideIn 0.35s cubic-bezier(0.4, 0, 0.2, 1) forwards;
} }
.mirrored-modal-header { /* ---------- Hero ---------- */
background: linear-gradient(135deg, rgba(45, 45, 45, 0.8) 0%, rgba(26, 26, 26, 0.9) 100%); .mm-hero {
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
padding: 0;
position: relative; position: relative;
flex-shrink: 0; /* header is pinned, never clipped */
padding: 30px 32px;
overflow: hidden; overflow: hidden;
border-top-left-radius: 20px; border-bottom: 1px solid rgba(255, 255, 255, 0.07);
border-top-right-radius: 20px;
} }
.mirrored-modal-hero { .mm-hero-bg {
display: flex; position: absolute;
align-items: center; inset: 0;
padding: 28px 32px; background-size: cover;
padding-right: 60px; background-position: center;
gap: 20px; filter: blur(44px) saturate(1.5);
opacity: 0.4;
transform: scale(1.25);
z-index: 0;
}
.mm-hero::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(22, 22, 22, 0.55) 0%, rgba(22, 22, 22, 0.88) 100%);
z-index: 0;
}
.mm-hero-content {
position: relative; position: relative;
z-index: 2; z-index: 1;
min-height: 110px; display: flex;
align-items: flex-end;
gap: 22px;
padding-right: 44px;
} }
.mirrored-modal-hero-icon { .mm-cover {
width: 72px; width: 128px;
height: 72px; height: 128px;
flex-shrink: 0;
border-radius: 12px;
background-size: cover;
background-position: center;
background-color: #242424;
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.55);
border: 1px solid rgba(255, 255, 255, 0.10);
}
.mm-cover-empty {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 32px; font-size: 52px;
border-radius: 14px; background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.22), rgba(var(--accent-rgb), 0.06));
background: linear-gradient(135deg, rgba(167, 139, 250, 0.2) 0%, rgba(167, 139, 250, 0.08) 100%); color: rgb(var(--accent-light-rgb));
border: 1px solid rgba(167, 139, 250, 0.3);
box-shadow: 0 8px 24px rgba(167, 139, 250, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
flex-shrink: 0;
color: #a78bfa;
} }
.mirrored-modal-hero-icon.spotify { background: linear-gradient(135deg, rgba(29, 185, 84, 0.2) 0%, rgba(29, 185, 84, 0.08) 100%); border-color: rgba(29, 185, 84, 0.3); box-shadow: 0 8px 24px rgba(29, 185, 84, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1); color: #1db954; } .mm-cover-empty.spotify,
.mirrored-modal-hero-icon.tidal { background: linear-gradient(135deg, rgba(255, 102, 0, 0.2) 0%, rgba(255, 102, 0, 0.08) 100%); border-color: rgba(255, 102, 0, 0.3); box-shadow: 0 8px 24px rgba(255, 102, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1); color: #ff6600; } .mm-cover-empty.spotify_public { background: linear-gradient(135deg, rgba(29, 185, 84, 0.25), rgba(29, 185, 84, 0.06)); color: #1db954; }
.mirrored-modal-hero-icon.youtube { background: linear-gradient(135deg, rgba(255, 0, 0, 0.2) 0%, rgba(255, 0, 0, 0.08) 100%); border-color: rgba(255, 0, 0, 0.3); box-shadow: 0 8px 24px rgba(255, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1); color: #ff4444; } .mm-cover-empty.tidal { background: linear-gradient(135deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.04)); color: #fff; }
.mirrored-modal-hero-icon.beatport { background: linear-gradient(135deg, rgba(1, 255, 149, 0.2) 0%, rgba(1, 255, 149, 0.08) 100%); border-color: rgba(1, 255, 149, 0.3); box-shadow: 0 8px 24px rgba(1, 255, 149, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1); color: #01ff95; } .mm-cover-empty.youtube { background: linear-gradient(135deg, rgba(255, 0, 0, 0.22), rgba(255, 0, 0, 0.06)); color: #ff4d4d; }
.mirrored-modal-hero-info { .mm-hero-info {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.mirrored-modal-hero-title { .mm-eyebrow {
color: #ffffff; display: block;
font-size: 22px; text-transform: uppercase;
letter-spacing: 1.2px;
font-size: 11px;
font-weight: 700; font-weight: 700;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 8px;
}
.mm-title {
margin: 0; margin: 0;
line-height: 1.3; color: #fff;
font-weight: 800;
font-size: clamp(22px, 3vw, 34px);
line-height: 1.12;
letter-spacing: -0.5px; letter-spacing: -0.5px;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }
.mirrored-modal-hero-subtitle { .mm-meta {
color: #b3b3b3;
font-size: 14px;
margin-top: 6px;
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
gap: 10px; gap: 8px;
margin-top: 13px;
font-size: 13px;
color: rgba(255, 255, 255, 0.62);
} }
.mirrored-modal-hero-badge { .mm-dot { color: rgba(255, 255, 255, 0.3); }
.mm-source-pill {
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
padding: 3px 10px; padding: 3px 10px;
border-radius: 6px; border-radius: 20px;
color: rgb(var(--accent-light-rgb)); color: rgb(var(--accent-light-rgb));
background: rgba(var(--accent-rgb), 0.1); background: rgba(var(--accent-rgb), 0.14);
border: 1px solid rgba(var(--accent-rgb), 0.2); border: 1px solid rgba(var(--accent-rgb), 0.25);
} }
.mirrored-modal-close { .mm-source-pill.spotify,
.mm-source-pill.spotify_public { color: #1db954; background: rgba(29, 185, 84, 0.14); border-color: rgba(29, 185, 84, 0.3); }
.mm-close {
position: absolute; position: absolute;
top: 24px; top: 18px;
right: 24px; right: 18px;
z-index: 3; z-index: 2;
color: #b3b3b3; width: 34px;
font-size: 18px; height: 34px;
font-weight: 300; border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
width: 32px;
height: 32px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 50%; font-size: 22px;
background: rgba(0, 0, 0, 0.3); line-height: 1;
border: 1px solid rgba(255, 255, 255, 0.1); cursor: pointer;
backdrop-filter: blur(10px); color: rgba(255, 255, 255, 0.7);
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.12);
transition: all 0.18s ease;
} }
.mirrored-modal-close:hover { .mm-close:hover {
color: #ffffff; color: #fff;
background: rgba(255, 255, 255, 0.15); background: rgba(0, 0, 0, 0.55);
transform: scale(1.1); transform: scale(1.08);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
} }
.mirrored-modal-tracks { /* ---------- Track list ---------- */
flex: 1; .mm-list {
flex: 1 1 auto;
min-height: 0; /* clip fix: list scrolls instead of growing the modal */
overflow-y: auto; overflow-y: auto;
padding: 0;
background: #121212; background: #121212;
padding: 6px 0 10px;
} }
.mirrored-modal-tracks::-webkit-scrollbar { width: 8px; } .mm-list::-webkit-scrollbar { width: 10px; }
.mirrored-modal-tracks::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); } .mm-list::-webkit-scrollbar-track { background: transparent; }
.mirrored-modal-tracks::-webkit-scrollbar-thumb { background: rgba(var(--accent-rgb), 0.3); border-radius: 4px; } .mm-list::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.14); border-radius: 5px; border: 2px solid #121212; }
.mirrored-modal-tracks::-webkit-scrollbar-thumb:hover { background: rgba(var(--accent-rgb), 0.5); } .mm-list::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.24); }
.mirrored-track-header { .mm-list-head,
.mm-row {
display: grid; display: grid;
grid-template-columns: 40px 1.6fr 1.4fr 1fr 56px; grid-template-columns: 34px 40px minmax(0, 1.6fr) minmax(0, 1.1fr) 56px;
align-items: center; align-items: center;
padding: 10px 28px; gap: 14px;
font-size: 11px; padding: 0 26px;
font-weight: 600; }
color: #666;
text-transform: uppercase; .mm-list-head {
letter-spacing: 0.8px; height: 34px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
position: sticky; position: sticky;
top: 0; top: 0;
background: #121212;
z-index: 1; z-index: 1;
background: #121212;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: rgba(255, 255, 255, 0.4);
} }
.mirrored-track-row { .mm-col-dur { text-align: right; }
display: grid;
grid-template-columns: 40px 1.6fr 1.4fr 1fr 56px; .mm-row {
align-items: center; height: 56px;
padding: 11px 28px; margin: 1px 8px;
padding: 0 18px;
border-radius: 8px;
transition: background 0.15s ease;
}
.mm-row:hover { background: rgba(255, 255, 255, 0.06); }
.mm-row-pos {
color: rgba(255, 255, 255, 0.4);
font-size: 13px; font-size: 13px;
color: #b3b3b3; font-variant-numeric: tabular-nums;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
}
.mirrored-track-row:hover {
background: linear-gradient(90deg, rgba(var(--accent-rgb), 0.06) 0%, rgba(var(--accent-rgb), 0.02) 100%);
transform: translateX(2px);
}
.mirrored-track-row .track-pos {
color: #555;
text-align: center; text-align: center;
font-size: 12px;
font-weight: 500;
} }
.mirrored-track-row:hover .track-pos { .mm-row-art {
color: rgb(var(--accent-light-rgb)); width: 40px;
height: 40px;
border-radius: 5px;
object-fit: cover;
background: #242424;
flex-shrink: 0;
} }
.mirrored-track-row .track-title { .mm-art-empty {
background: linear-gradient(135deg, #2c2c2c, #1b1b1b);
display: inline-block;
}
.mm-row-main {
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.mm-row-title {
color: #fff; color: #fff;
font-size: 14px;
font-weight: 500; font-weight: 500;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
padding-right: 12px;
} }
.mirrored-track-row .track-artist { .mm-row-artist {
width: auto; color: rgba(255, 255, 255, 0.55);
font-size: 12.5px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
padding-right: 12px;
} }
.mirrored-track-row .track-album { .mm-row-album {
color: rgba(255, 255, 255, 0.45);
font-size: 13px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
padding-right: 12px;
color: #777;
} }
.mirrored-track-row .track-duration { .mm-row-dur {
text-align: right; text-align: right;
color: #666; color: rgba(255, 255, 255, 0.4);
font-size: 12px; font-size: 13px;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.mirrored-modal-footer { .mm-empty {
background: linear-gradient(135deg, rgba(42, 42, 42, 0.9) 0%, rgba(30, 30, 30, 0.95) 100%); padding: 48px;
border-top: 1px solid rgba(255, 255, 255, 0.08); text-align: center;
padding: 18px 32px; color: rgba(255, 255, 255, 0.4);
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
} }
.mirrored-modal-footer button { /* ---------- Action bar ---------- */
padding: 10px 20px; .mm-actions {
border-radius: 10px; flex-shrink: 0; /* footer is pinned, never clipped */
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 26px;
background: #181818;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.mm-actions-right {
display: flex;
align-items: center;
gap: 10px;
}
.mm-btn {
padding: 9px 18px;
border-radius: 9px;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
border: none; border: 1px solid transparent;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.18s ease;
min-width: 100px; white-space: nowrap;
} }
.mirrored-btn-close { .mm-btn-primary {
background: rgba(255, 255, 255, 0.1); background: rgb(var(--accent-rgb));
color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(255, 255, 255, 0.15) !important;
}
.mirrored-btn-close:hover {
background: rgba(255, 255, 255, 0.18);
color: #ffffff;
border-color: rgba(255, 255, 255, 0.25) !important;
transform: translateY(-1px);
}
.mirrored-btn-discover {
background: linear-gradient(135deg, rgb(var(--accent-rgb)) 0%, rgb(var(--accent-light-rgb)) 100%);
color: #000; color: #000;
box-shadow: 0 4px 16px rgba(var(--accent-rgb), 0.3); box-shadow: 0 4px 16px rgba(var(--accent-rgb), 0.28);
border: 1px solid rgba(var(--accent-rgb), 0.3) !important;
} }
.mm-btn-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 22px rgba(var(--accent-rgb), 0.42); }
.mirrored-btn-discover:hover { .mm-btn-secondary {
transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(var(--accent-rgb), 0.4);
}
.mirrored-btn-pipeline {
background: rgba(56, 189, 248, 0.12); background: rgba(56, 189, 248, 0.12);
color: #7dd3fc; color: #7dd3fc;
border: 1px solid rgba(56, 189, 248, 0.28) !important; border-color: rgba(56, 189, 248, 0.28);
} }
.mm-btn-secondary:hover { background: rgba(56, 189, 248, 0.2); color: #e0f2fe; border-color: rgba(56, 189, 248, 0.45); }
.mirrored-btn-pipeline:hover { .mm-btn-ghost {
background: rgba(56, 189, 248, 0.2); background: rgba(255, 255, 255, 0.07);
border-color: rgba(56, 189, 248, 0.45) !important; color: rgba(255, 255, 255, 0.8);
color: #e0f2fe; border-color: rgba(255, 255, 255, 0.12);
transform: translateY(-1px);
} }
.mm-btn-ghost:hover { background: rgba(255, 255, 255, 0.13); color: #fff; }
.mirrored-btn-delete { .mm-btn-danger {
background: rgba(244, 67, 54, 0.12); background: transparent;
color: #ef5350; color: #ef5350;
border: 1px solid rgba(244, 67, 54, 0.2) !important; border-color: rgba(239, 83, 80, 0.35);
} }
.mm-btn-danger:hover { background: rgba(239, 83, 80, 0.14); color: #ff6b66; border-color: rgba(239, 83, 80, 0.55); }
.mirrored-btn-delete:hover { @media (max-width: 640px) {
background: rgba(244, 67, 54, 0.2); .mm-hero-content { flex-direction: column; align-items: flex-start; gap: 14px; }
color: #ff6659; .mm-cover { width: 92px; height: 92px; }
border-color: rgba(244, 67, 54, 0.35) !important; .mm-actions { flex-direction: column; align-items: stretch; }
transform: translateY(-1px); .mm-actions-right { justify-content: space-between; }
.mm-btn { flex: 1; text-align: center; }
} }
.sync-tab-content { .sync-tab-content {