068e7e9534
scrub-watermark-text.py crashed on every run (missing import os). strip-watermark-art.py was starved every Sunday by lock contention from strip-mb-tags' growing runtime -- moved it to 01:00 so the rest of the weekly chain has room. enrich-buy-url.py's summary line counted matches found, not successful writes, so a run where every metaflac write failed (root-owned files) still reported "N tagged". pipeline-status.sh now tells a 403 from Qobuz (Akamai/CDN block) apart from a 401 (actually expired token) -- re-exporting the token does nothing for the former. Also fixes the dedup review table's "Caught by" badge overflowing into the Keep column's format pill.
852 lines
27 KiB
CSS
852 lines
27 KiB
CSS
/* ==========================================================================
|
|
Alembic — holographic/chrome design system
|
|
Late-2000s iridescent-digital mood: dark glass, oil-slick gradients,
|
|
soft bloom, sparkle accents. Built to stay legible for a real utility
|
|
app (tables, forms, statuses) — the shimmer is trim, not wallpaper.
|
|
========================================================================== */
|
|
|
|
:root {
|
|
color-scheme: dark;
|
|
|
|
--bg: #08090d;
|
|
--bg-soft: #0c0e14;
|
|
--panel: rgba(255, 255, 255, 0.035);
|
|
--panel-strong: rgba(255, 255, 255, 0.06);
|
|
--border: rgba(255, 255, 255, 0.09);
|
|
--border-strong: rgba(255, 255, 255, 0.16);
|
|
|
|
--fg: #eef0f7;
|
|
--muted: #9799ac;
|
|
--muted-2: #6d6f83;
|
|
|
|
--iris-blue: #7dd3fc;
|
|
--iris-violet: #a78bfa;
|
|
--iris-pink: #f9a8d4;
|
|
--iris-cyan: #67e8f9;
|
|
|
|
--gradient-holo: linear-gradient(115deg, var(--iris-blue) 0%, var(--iris-violet) 32%, var(--iris-pink) 58%, var(--iris-cyan) 82%, var(--iris-blue) 100%);
|
|
--gradient-holo-soft: linear-gradient(115deg, rgba(125,211,252,0.5), rgba(167,139,250,0.5), rgba(249,168,212,0.5));
|
|
|
|
--status-success: #4ade80;
|
|
--status-warning: #fb923c;
|
|
--status-info: #38bdf8;
|
|
--status-danger: #fb7185;
|
|
--status-muted: #8285a0;
|
|
|
|
--font-display: "Space Grotesk", "Segoe UI", system-ui, sans-serif;
|
|
--font-body: "Manrope", "Segoe UI", system-ui, sans-serif;
|
|
--font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
|
|
|
--radius: 14px;
|
|
--radius-sm: 9px;
|
|
--shadow-panel: 0 1px 0 rgba(255,255,255,0.05) inset, 0 24px 60px -28px rgba(0,0,0,0.75);
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
[x-cloak] { display: none !important; }
|
|
|
|
html { color-scheme: dark; }
|
|
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
font-family: var(--font-body);
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
line-height: 1.55;
|
|
position: relative;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* ---- ambient bloom + grain ---- */
|
|
|
|
.ambient-blob {
|
|
position: fixed;
|
|
border-radius: 50%;
|
|
filter: blur(110px);
|
|
opacity: 0.28;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
mix-blend-mode: screen;
|
|
animation: drift 26s ease-in-out infinite alternate;
|
|
}
|
|
.ambient-blob.one { width: 42vw; height: 42vw; top: -14vw; left: -10vw; background: radial-gradient(circle, var(--iris-violet), transparent 70%); animation-delay: 0s; }
|
|
.ambient-blob.two { width: 38vw; height: 38vw; bottom: -16vw; right: -8vw; background: radial-gradient(circle, var(--iris-cyan), transparent 70%); animation-delay: -8s; }
|
|
.ambient-blob.three { width: 30vw; height: 30vw; top: 30vh; right: 20vw; background: radial-gradient(circle, var(--iris-pink), transparent 70%); opacity: 0.16; animation-delay: -16s; }
|
|
|
|
@keyframes drift {
|
|
from { transform: translate(0, 0) scale(1); }
|
|
to { transform: translate(3vw, 4vw) scale(1.08); }
|
|
}
|
|
|
|
body::before {
|
|
content: "";
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 999;
|
|
pointer-events: none;
|
|
opacity: 0.035;
|
|
mix-blend-mode: overlay;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
|
}
|
|
|
|
/* ---- sparkles ---- */
|
|
|
|
.sparkle {
|
|
position: absolute;
|
|
width: 14px;
|
|
height: 14px;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23c9c4fa' d='M12 0c.6 5.7 1.8 8.6 4.2 10.6 2.4 2 5.3 2.6 7.8 3.4-2.5.8-5.4 1.4-7.8 3.4-2.4 2-3.6 4.9-4.2 10.6-.6-5.7-1.8-8.6-4.2-10.6C5.4 15.4 2.5 14.8 0 14c2.5-.8 5.4-1.4 7.8-3.4C10.2 8.6 11.4 5.7 12 0Z'/%3E%3C/svg%3E");
|
|
background-size: contain;
|
|
background-repeat: no-repeat;
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
animation: twinkle 3.2s ease-in-out infinite;
|
|
}
|
|
@keyframes twinkle {
|
|
0%, 100% { opacity: 0.15; transform: scale(0.8) rotate(0deg); }
|
|
50% { opacity: 0.7; transform: scale(1.05) rotate(8deg); }
|
|
}
|
|
|
|
/* ---- chrome / holo text ---- */
|
|
|
|
.chrome-text {
|
|
background: var(--gradient-holo);
|
|
background-size: 220% auto;
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
color: transparent;
|
|
animation: shimmer 9s ease-in-out infinite alternate;
|
|
}
|
|
@keyframes shimmer {
|
|
from { background-position: 0% 50%; }
|
|
to { background-position: 100% 50%; }
|
|
}
|
|
|
|
h1, h2, h3 {
|
|
font-family: var(--font-display);
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
margin: 0 0 0.4rem;
|
|
color: var(--fg);
|
|
}
|
|
|
|
h1 { font-size: clamp(1.7rem, 2.4vw, 2.15rem); }
|
|
h2 { font-size: 1.15rem; margin-top: 2.25rem; color: #d7d8e6; }
|
|
h2:first-of-type { margin-top: 0; }
|
|
|
|
p { margin: 0 0 1rem; }
|
|
a { color: var(--iris-cyan); text-decoration: none; }
|
|
a:hover { color: var(--iris-pink); text-decoration: underline; }
|
|
|
|
.muted { color: var(--muted); }
|
|
.mono { font-family: var(--font-mono); font-size: 0.85em; }
|
|
.truncate { max-width: 42ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: bottom; }
|
|
|
|
/* ---- topbar ---- */
|
|
|
|
.topbar {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 50;
|
|
background: rgba(8, 9, 13, 0.72);
|
|
backdrop-filter: blur(18px) saturate(140%);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.topbar-inner {
|
|
max-width: 1180px;
|
|
margin: 0 auto;
|
|
padding: 0.85rem 1.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1.75rem;
|
|
}
|
|
|
|
.brand {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.55rem;
|
|
font-family: var(--font-display);
|
|
font-weight: 700;
|
|
font-size: 1.2rem;
|
|
color: var(--fg) !important;
|
|
text-decoration: none !important;
|
|
position: relative;
|
|
flex-shrink: 0;
|
|
}
|
|
.brand:hover .chrome-text { animation-duration: 2.5s; }
|
|
.brand svg { flex-shrink: 0; filter: drop-shadow(0 0 10px rgba(167,139,250,0.45)); }
|
|
|
|
.nav-pills {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.15rem;
|
|
flex: 1;
|
|
overflow-x: auto;
|
|
scrollbar-width: none;
|
|
}
|
|
.nav-pills::-webkit-scrollbar { display: none; }
|
|
|
|
.nav-pills a {
|
|
color: var(--muted);
|
|
font-size: 0.88rem;
|
|
font-weight: 600;
|
|
padding: 0.45rem 0.85rem;
|
|
border-radius: 999px;
|
|
white-space: nowrap;
|
|
text-decoration: none !important;
|
|
transition: color 0.15s ease, background 0.15s ease;
|
|
}
|
|
.nav-pills a:hover { color: var(--fg); background: var(--panel); }
|
|
.nav-pills a.active {
|
|
color: #08090d;
|
|
background: var(--gradient-holo);
|
|
background-size: 200% auto;
|
|
}
|
|
|
|
.user-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: 0.85rem;
|
|
color: var(--muted);
|
|
flex-shrink: 0;
|
|
}
|
|
.user-chip .user-dot {
|
|
width: 7px; height: 7px; border-radius: 50%;
|
|
background: var(--status-success);
|
|
box-shadow: 0 0 8px var(--status-success);
|
|
}
|
|
.user-chip a.logout,
|
|
.user-chip button.logout {
|
|
color: var(--muted-2);
|
|
font-size: 0.95rem;
|
|
}
|
|
.user-chip button.logout {
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
line-height: 1;
|
|
}
|
|
.user-chip a.logout:hover,
|
|
.user-chip button.logout:hover { color: var(--status-danger); text-decoration: none; }
|
|
|
|
/* ---- layout ---- */
|
|
|
|
main {
|
|
max-width: 1180px;
|
|
margin: 0 auto;
|
|
padding: 2.5rem 1.5rem 5rem;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: space-between;
|
|
gap: 1.5rem;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 2rem;
|
|
}
|
|
.page-header .subtitle { margin-top: 0.35rem; }
|
|
.page-header .actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }
|
|
|
|
.back-link { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.85rem; margin-bottom: 1rem; color: var(--muted); }
|
|
.back-link:hover { color: var(--iris-cyan); }
|
|
|
|
/* ---- panels / cards ---- */
|
|
|
|
.panel {
|
|
background: linear-gradient(180deg, var(--panel-strong), var(--panel));
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-panel);
|
|
padding: 1.5rem 1.75rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.stat-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
.stat-card {
|
|
background: linear-gradient(180deg, var(--panel-strong), var(--panel));
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-panel);
|
|
padding: 1.35rem 1.5rem;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.stat-card::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: -1px;
|
|
border-radius: inherit;
|
|
padding: 1px;
|
|
background: var(--gradient-holo-soft);
|
|
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
-webkit-mask-composite: xor;
|
|
mask-composite: exclude;
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
.stat-value { font-family: var(--font-display); font-size: 2.1rem; font-weight: 700; line-height: 1; margin-bottom: 0.5rem; }
|
|
.stat-label { font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); font-weight: 700; }
|
|
|
|
/* ---- buttons ---- */
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.4rem;
|
|
font: inherit;
|
|
font-weight: 700;
|
|
font-size: 0.86rem;
|
|
padding: 0.6rem 1.15rem;
|
|
border-radius: 999px;
|
|
border: 1px solid var(--border-strong);
|
|
background: var(--panel);
|
|
color: var(--fg);
|
|
cursor: pointer;
|
|
text-decoration: none !important;
|
|
transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
|
|
white-space: nowrap;
|
|
}
|
|
.btn:hover { background: var(--panel-strong); border-color: var(--border-strong); transform: translateY(-1px); }
|
|
.btn:active { transform: translateY(0); }
|
|
.btn:focus-visible { outline: 2px solid var(--iris-cyan); outline-offset: 2px; }
|
|
|
|
.btn-primary {
|
|
border: none;
|
|
color: #08090d;
|
|
background: var(--gradient-holo);
|
|
background-size: 220% auto;
|
|
box-shadow: 0 10px 30px -12px rgba(167, 139, 250, 0.6);
|
|
}
|
|
/* Re-declare the full `background` shorthand here, not just
|
|
background-position: .btn:hover above sets `background` (shorthand) to
|
|
a flat color at equal specificity, and since it comes first in source
|
|
order .btn-primary:hover must repeat the shorthand to still win --
|
|
otherwise it silently wipes out the gradient image/size, leaving the
|
|
near-black .btn-primary text on a near-black background ("the glow
|
|
makes the button black"). */
|
|
.btn-primary:hover {
|
|
background: var(--gradient-holo);
|
|
background-size: 220% auto;
|
|
background-position: 100% 50%;
|
|
box-shadow: 0 10px 34px -8px rgba(167, 139, 250, 0.75);
|
|
}
|
|
|
|
.btn-danger {
|
|
background: rgba(251, 113, 133, 0.1);
|
|
border-color: rgba(251, 113, 133, 0.35);
|
|
color: #fda4af;
|
|
}
|
|
.btn-danger:hover { background: rgba(251, 113, 133, 0.18); }
|
|
|
|
.btn-sm { padding: 0.4rem 0.85rem; font-size: 0.78rem; }
|
|
/* A fully transparent border left the pill outline invisible, so a ghost
|
|
button sitting next to a filled one (Keep both / Delete, Edit / Delete)
|
|
read as unaligned text rather than a matching pair of buttons. A
|
|
visible-but-quiet border keeps the lower-emphasis look while still
|
|
giving it the same pill shape as everything else. */
|
|
.btn-ghost { background: transparent; border-color: var(--border); color: var(--muted); }
|
|
.btn-ghost:hover { background: var(--panel); color: var(--fg); border-color: var(--border-strong); }
|
|
|
|
form.inline { display: inline-block; }
|
|
.btn-row { display: flex; gap: 0.6rem; flex-wrap: wrap; align-items: center; }
|
|
|
|
/* ---- forms ---- */
|
|
|
|
.field { display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 1.1rem; max-width: 480px; }
|
|
.field.wide { max-width: none; }
|
|
.field label {
|
|
font-size: 0.74rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--muted);
|
|
}
|
|
.field .hint { font-size: 0.78rem; color: var(--muted-2); margin-top: -0.15rem; }
|
|
|
|
/* A row of .field's next to a bare button, aligned to the bottom (e.g. the
|
|
library page's search/filter bar). .field's margin-bottom is meant for
|
|
fields stacked vertically -- with align-items: flex-end it pushes the
|
|
field's box lower than the button next to it (which has no such margin),
|
|
making the button look higher/misaligned. Zero it out in this context. */
|
|
.filter-bar {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
flex-wrap: wrap;
|
|
align-items: flex-end;
|
|
max-width: none;
|
|
}
|
|
.filter-bar .field { margin-bottom: 0; }
|
|
|
|
input[type="text"], input[type="password"], input[type="file"], input[type="search"], textarea, select {
|
|
font: inherit;
|
|
color: var(--fg);
|
|
background: rgba(255, 255, 255, 0.035);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 0.65rem 0.85rem;
|
|
width: 100%;
|
|
}
|
|
textarea { resize: vertical; font-family: var(--font-mono); font-size: 0.85rem; }
|
|
input::placeholder, textarea::placeholder { color: var(--muted-2); }
|
|
input:focus, textarea:focus, select:focus {
|
|
outline: none;
|
|
border-color: var(--iris-violet);
|
|
box-shadow: 0 0 0 3px rgba(167, 139, 250, 0.18);
|
|
}
|
|
/* No intrinsic width/height on the inline SVG means browsers fall back to
|
|
an oversized default replaced-element size for the background-image
|
|
(no background-size was set here) -- that's what was rendering the
|
|
arrow huge enough to sit on top of the "(any)" text. Pin it to a small
|
|
fixed size explicitly. */
|
|
select {
|
|
appearance: none;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M5 7.5 10 12.5 15 7.5' stroke='%239799ac' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
background-repeat: no-repeat;
|
|
background-position: right 0.75rem center;
|
|
background-size: 0.85rem 0.85rem;
|
|
padding-right: 2.25rem;
|
|
}
|
|
/* The closed <select> box uses a translucent background (fine, since it
|
|
sits on top of the page). The open option list is a separate native
|
|
popup with no dark surface behind it, so that same translucent value
|
|
renders as plain white -- it needs an explicit opaque background. */
|
|
select option {
|
|
background: var(--bg-soft);
|
|
color: var(--fg);
|
|
}
|
|
|
|
.checkbox-field {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.55rem;
|
|
font-size: 0.9rem;
|
|
color: var(--fg);
|
|
cursor: pointer;
|
|
margin-bottom: 1.1rem;
|
|
}
|
|
input[type="checkbox"] { width: 1.05em; height: 1.05em; accent-color: var(--iris-violet); cursor: pointer; }
|
|
|
|
form { margin: 0; }
|
|
|
|
/* ---- tables ---- */
|
|
|
|
.table-wrap {
|
|
border-radius: var(--radius);
|
|
border: 1px solid var(--border);
|
|
background: linear-gradient(180deg, var(--panel-strong), var(--panel));
|
|
box-shadow: var(--shadow-panel);
|
|
overflow: hidden;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.table-wrap.scroll { overflow-x: auto; }
|
|
|
|
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
|
|
|
thead th {
|
|
text-align: left;
|
|
padding: 0.8rem 1.1rem;
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--muted);
|
|
font-weight: 700;
|
|
border-bottom: 1px solid var(--border-strong);
|
|
background: rgba(255, 255, 255, 0.02);
|
|
white-space: nowrap;
|
|
}
|
|
tbody td { padding: 0.7rem 1.1rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
|
|
tbody tr:last-child td { border-bottom: none; }
|
|
tbody tr:hover { background: rgba(167, 139, 250, 0.055); }
|
|
tbody td.actions-cell { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
|
|
|
.dedup-table { table-layout: fixed; }
|
|
|
|
/* Each candidate is its own <tbody> (title row + detail row) so the pair
|
|
highlights together on hover, and so :hover can bubble from either row
|
|
up to the shared group without JS. That means every detail row is now
|
|
a tbody's last-child, which would otherwise strip the separator between
|
|
one candidate and the next (the generic `tbody tr:last-child` rule above
|
|
assumed one shared tbody) -- restore it here, and only drop it for the
|
|
actual last group in the table. */
|
|
.dedup-table .dedup-row-group:hover td { background: rgba(167, 139, 250, 0.06); }
|
|
.dedup-table .dedup-detail-row td { border-bottom: 1px solid var(--border); }
|
|
.dedup-table .dedup-row-group:last-of-type .dedup-detail-row td { border-bottom: none; }
|
|
|
|
.dedup-title-row td { padding: 0.75rem 1.1rem 0.2rem; border-bottom: none; }
|
|
.dedup-detail-row td { padding-top: 0.15rem; }
|
|
|
|
.dedup-title {
|
|
font-family: var(--font-display);
|
|
font-size: 1.02rem;
|
|
font-weight: 700;
|
|
color: var(--fg);
|
|
line-height: 1.35;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.dedup-title-alt { font-weight: 400; color: var(--muted); }
|
|
|
|
.dedup-context { font-size: 0.8rem; margin-top: 0.15rem; }
|
|
|
|
/* A thin colored rail on each side echoes the keep/delete verdict itself --
|
|
green for the copy that survives, muted-pink for the one on the chopping
|
|
block -- so which side is which reads before you've even read the words. */
|
|
.dedup-side {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.55rem;
|
|
padding-left: 0.65rem;
|
|
border-left: 2px solid transparent;
|
|
min-height: 1.6rem;
|
|
}
|
|
.dedup-side-keep { border-left-color: var(--status-success); }
|
|
.dedup-side-delete { border-left-color: var(--status-danger); }
|
|
|
|
.dedup-side-size { font-size: 0.78rem; white-space: nowrap; flex-shrink: 0; }
|
|
|
|
/* The actual filename/path -- always visible, never truncated. Tags can be
|
|
wrong; this is the ground truth for judging whether two files are really
|
|
the same recording, so it can't be hover-only the way it was in the first
|
|
pass of this redesign. */
|
|
.dedup-side-path {
|
|
padding-left: 0.65rem;
|
|
margin-top: 0.2rem;
|
|
font-size: 0.78rem;
|
|
color: var(--muted-2);
|
|
overflow-wrap: anywhere;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.empty-row td { padding: 2.5rem 1rem; text-align: center; }
|
|
|
|
/* ---- badges / pills ---- */
|
|
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
padding: 0.28rem 0.7rem;
|
|
border-radius: 999px;
|
|
font-size: 0.68rem;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
white-space: nowrap;
|
|
}
|
|
.badge::before { content: ""; width: 0.4em; height: 0.4em; border-radius: 50%; background: currentColor; box-shadow: 0 0 7px currentColor; flex-shrink: 0; }
|
|
|
|
.badge-success { background: rgba(74, 222, 128, 0.12); color: var(--status-success); }
|
|
.badge-warning { background: rgba(251, 146, 60, 0.12); color: var(--status-warning); }
|
|
.badge-info { background: rgba(56, 189, 248, 0.12); color: var(--status-info); }
|
|
.badge-danger { background: rgba(251, 113, 133, 0.12); color: var(--status-danger); }
|
|
.badge-muted { background: rgba(130, 133, 160, 0.14); color: var(--status-muted); }
|
|
|
|
.badge-pulse::before { animation: pulse-dot 1.4s ease-in-out infinite; }
|
|
@keyframes pulse-dot { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
|
|
|
|
/* The dedup "Caught by" column is only 10.75rem wide -- "Acoustically
|
|
Similar" at the default badge size overflowed past it and rendered on
|
|
top of the keep-side format pill. Smaller size/padding/tracking keeps it
|
|
inside the column instead of shrinking the column itself, which would
|
|
just crowd the Keep/Delete path columns next to it. */
|
|
.badge-caughtby { font-size: 0.6rem; padding: 0.24rem 0.55rem; letter-spacing: 0.02em; }
|
|
|
|
/* ---- stacked status bar (playlist track reconciliation) ---- */
|
|
|
|
.status-bar {
|
|
display: flex;
|
|
height: 11px;
|
|
border-radius: 999px;
|
|
overflow: hidden;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
margin-bottom: 1rem;
|
|
border: 1px solid var(--border);
|
|
}
|
|
.status-bar span { height: 100%; }
|
|
|
|
.status-legend { display: flex; flex-wrap: wrap; gap: 0.5rem 1.4rem; margin-bottom: 1.75rem; }
|
|
.status-legend .item { display: flex; align-items: center; gap: 0.5rem; font-size: 0.84rem; color: var(--muted); }
|
|
.status-legend .item strong { color: var(--fg); }
|
|
.status-legend .dot { width: 0.6em; height: 0.6em; border-radius: 50%; box-shadow: 0 0 7px currentColor; }
|
|
|
|
/* ---- playlist compare (original vs. downloaded/waiting) ---- */
|
|
|
|
.playlist-compare {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 1.5rem;
|
|
align-items: start;
|
|
}
|
|
@media (max-width: 860px) {
|
|
.playlist-compare { grid-template-columns: 1fr; }
|
|
}
|
|
|
|
.compare-col-header {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 0.75rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
.compare-col-header h3 { margin: 0; font-size: 1rem; }
|
|
|
|
/* Fixed-height scroll area so a 150+ track playlist doesn't make the two
|
|
columns drift out of vertical sync -- the point of side-by-side columns
|
|
is comparing them at a glance, which only works if both stay on screen
|
|
together. Sticky header keeps column labels visible while scrolling. */
|
|
.compare-scroll { max-height: 30rem; overflow-y: auto; }
|
|
.compare-scroll thead th { position: sticky; top: 0; background: var(--bg-soft); z-index: 1; }
|
|
|
|
.seg-tabs {
|
|
display: inline-flex;
|
|
gap: 0.2rem;
|
|
padding: 0.2rem;
|
|
border-radius: 999px;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
margin-bottom: 0.9rem;
|
|
}
|
|
.seg-tabs button {
|
|
font: inherit;
|
|
font-weight: 700;
|
|
font-size: 0.8rem;
|
|
padding: 0.4rem 0.9rem;
|
|
border: none;
|
|
border-radius: 999px;
|
|
background: transparent;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
transition: color 0.15s ease, background 0.15s ease;
|
|
white-space: nowrap;
|
|
}
|
|
.seg-tabs button:hover { color: var(--fg); }
|
|
.seg-tabs button.active { color: #08090d; background: var(--gradient-holo); background-size: 200% auto; }
|
|
|
|
.status-dot-info { background: var(--status-info); box-shadow: 0 0 6px var(--status-info); }
|
|
|
|
/* ---- misc ---- */
|
|
|
|
.notice {
|
|
padding: 0.9rem 1.15rem;
|
|
border-radius: var(--radius-sm);
|
|
border: 1px solid var(--border);
|
|
background: var(--panel);
|
|
color: var(--muted);
|
|
font-size: 0.88rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.notice.success { border-color: rgba(74, 222, 128, 0.35); background: rgba(74, 222, 128, 0.07); color: #b3f0c4; }
|
|
.notice.warning { border-color: rgba(251, 146, 60, 0.3); background: rgba(251, 146, 60, 0.06); color: #fdd0a8; }
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 3rem 1.5rem;
|
|
color: var(--muted);
|
|
}
|
|
.empty-state .sparkle-inline { display: inline-block; margin-bottom: 0.5rem; width: 22px; height: 22px; position: relative; opacity: 1; animation: none; }
|
|
.empty-state .sparkle-inline .sparkle { position: relative; opacity: 0.85; animation: twinkle 3.2s ease-in-out infinite; }
|
|
|
|
.chip-list { display: flex; flex-wrap: wrap; gap: 0.5rem; list-style: none; margin: 0 0 1.5rem; padding: 0; }
|
|
.chip-list li {
|
|
padding: 0.4rem 0.85rem;
|
|
border-radius: 999px;
|
|
border: 1px solid var(--border);
|
|
background: var(--panel);
|
|
font-size: 0.82rem;
|
|
font-family: var(--font-mono);
|
|
color: var(--muted);
|
|
}
|
|
|
|
.scope-header { display: flex; align-items: center; gap: 0.6rem; }
|
|
|
|
/* Compact variant for the dashboard's credential-status row -- a dot
|
|
indicator instead of a full badge pill so all 7 scopes fit on one line
|
|
at normal desktop widths instead of the last one wrapping to its own row. */
|
|
.auth-chip-list li {
|
|
padding: 0.3rem 0.7rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
white-space: nowrap;
|
|
}
|
|
.status-dot {
|
|
display: inline-block;
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
background: var(--status-muted);
|
|
}
|
|
.status-dot-success { background: var(--status-success); box-shadow: 0 0 6px var(--status-success); }
|
|
.status-dot-muted { background: var(--status-muted); }
|
|
.status-dot-warning { background: var(--status-warning); box-shadow: 0 0 6px var(--status-warning); }
|
|
|
|
/* Credentials page: two columns on wide viewports instead of one long
|
|
single-column stack of 7 scopes. align-items: start so a taller form
|
|
(bandcamp's cookie textarea) doesn't stretch its row-mate. */
|
|
.cred-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 0 2rem;
|
|
align-items: start;
|
|
}
|
|
.cred-scope .panel { max-width: none; }
|
|
@media (max-width: 720px) {
|
|
.cred-grid { grid-template-columns: 1fr; }
|
|
}
|
|
|
|
.cred-scope-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
margin-bottom: 1.1rem;
|
|
}
|
|
.cred-scope-header h2 { margin: 0 0 0.35rem; text-transform: capitalize; }
|
|
.cred-desc { font-size: 0.85rem; margin: 0; max-width: 46ch; }
|
|
.cred-scope.cred-disabled .panel { opacity: 0.55; }
|
|
.cred-disabled-note { font-size: 0.78rem; color: var(--status-warning); margin: -0.5rem 0 1.1rem; }
|
|
|
|
/* ---- toggle switch ---- */
|
|
|
|
.switch { position: relative; display: inline-flex; width: 38px; height: 21px; flex-shrink: 0; cursor: pointer; }
|
|
.switch input {
|
|
position: absolute;
|
|
inset: 0;
|
|
opacity: 0;
|
|
margin: 0;
|
|
cursor: pointer;
|
|
}
|
|
.switch-track {
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: 999px;
|
|
background: var(--panel-strong);
|
|
border: 1px solid var(--border-strong);
|
|
transition: background 0.15s ease, border-color 0.15s ease;
|
|
}
|
|
.switch-thumb {
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
width: 15px;
|
|
height: 15px;
|
|
border-radius: 50%;
|
|
background: var(--muted);
|
|
transition: transform 0.15s ease, background 0.15s ease;
|
|
}
|
|
.switch input:checked ~ .switch-track { background: rgba(167, 139, 250, 0.22); border-color: var(--iris-violet); }
|
|
.switch input:checked ~ .switch-track .switch-thumb {
|
|
transform: translateX(17px);
|
|
background: var(--iris-violet);
|
|
box-shadow: 0 0 8px rgba(167, 139, 250, 0.6);
|
|
}
|
|
.switch input:focus-visible ~ .switch-track { outline: 2px solid var(--iris-cyan); outline-offset: 2px; }
|
|
|
|
/* ---- settings layout (sidebar sub-nav) ---- */
|
|
|
|
.settings-layout {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 2.25rem;
|
|
}
|
|
.settings-nav {
|
|
flex: 0 0 180px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.2rem;
|
|
position: sticky;
|
|
top: 5.5rem;
|
|
}
|
|
.settings-nav a {
|
|
padding: 0.65rem 1rem;
|
|
border-radius: var(--radius-sm);
|
|
border-left: 2px solid transparent;
|
|
color: var(--muted);
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
text-decoration: none !important;
|
|
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
|
|
}
|
|
.settings-nav a:hover { color: var(--fg); background: var(--panel); }
|
|
.settings-nav a.active {
|
|
color: var(--fg);
|
|
background: var(--panel-strong);
|
|
border-left-color: var(--iris-violet);
|
|
}
|
|
.settings-content { flex: 1; min-width: 0; }
|
|
|
|
@media (max-width: 780px) {
|
|
.settings-layout { flex-direction: column; gap: 1.25rem; }
|
|
.settings-nav {
|
|
position: static;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
width: 100%;
|
|
gap: 0.4rem;
|
|
}
|
|
.settings-nav a { border-left: none; border-radius: 999px; }
|
|
.settings-nav a.active { background: var(--gradient-holo); color: #08090d; }
|
|
}
|
|
|
|
/* ---- import dropzone ---- */
|
|
|
|
.dropzone-panel { padding: 0; overflow: hidden; }
|
|
.dropzone {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
padding: 3rem 1.5rem;
|
|
border: 1.5px dashed var(--border-strong);
|
|
border-radius: var(--radius);
|
|
margin: 0;
|
|
cursor: pointer;
|
|
text-align: center;
|
|
transition: border-color 0.15s ease, background 0.15s ease;
|
|
}
|
|
.dropzone:hover, .dropzone.dragover {
|
|
border-color: var(--iris-violet);
|
|
background: rgba(167, 139, 250, 0.06);
|
|
}
|
|
.dropzone .dropzone-icon { color: var(--iris-violet); filter: drop-shadow(0 0 10px rgba(167, 139, 250, 0.4)); }
|
|
.dropzone-title { font-weight: 700; font-size: 0.98rem; color: var(--fg); }
|
|
.dropzone-hint { font-size: 0.8rem; color: var(--muted); }
|
|
.dropzone input[type="file"] {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
opacity: 0;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
border: none;
|
|
}
|
|
|
|
@media (max-width: 1040px) {
|
|
.topbar-inner { flex-wrap: wrap; }
|
|
.nav-pills { order: 3; width: 100%; flex-wrap: wrap; overflow: visible; row-gap: 0.3rem; }
|
|
}
|
|
|
|
@media (max-width: 720px) {
|
|
.topbar-inner { padding: 0.75rem 1.1rem; }
|
|
main { padding: 1.75rem 1.1rem 4rem; }
|
|
.page-header { align-items: flex-start; }
|
|
}
|