Files
alembic/app/static/style.css
T
andrew 9a116f3da4 Credential enable/disable + descriptions, artist casing settings page, playlist compare view
Credentials page now shows a fact-checked one-line description per
service and marks Spotify/Soulseek/Navidrome as Required (no toggle --
they're load-bearing for every playlist sync). The four optional
integrations (Bandcamp, AzuraCast, Qobuz, Telegram) get a real on/off
switch: Bandcamp pauses its scheduled job (same mechanism as the Jobs
page), the other three just remove their rendered credential file, which
the scripts that read them already treat as "not configured, skip" --
no script changes needed. Disabled optional scopes also drop off the
dashboard's credential status row.

New Settings > Artist Casing page to view/add/remove entries in
artist-canonical.list without shelling in -- adding a name that already
matches case-insensitively replaces its casing in place instead of
duplicating.

Playlist detail page's status section is now two side-by-side columns:
the original Spotify tracklist on the left, and a tabbed Downloaded /
Waiting-to-download view on the right (client-side tabs via Alpine),
both with an internal scroll area so long playlists don't pull the
columns out of sync. Uses the existing status_service reconciliation
(sldl index + beets + quarantine) -- no backend changes needed there.
2026-07-09 10:52:08 -06:00

786 lines
24 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 {
color: var(--muted-2);
font-size: 0.95rem;
}
.user-chip a.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; }
.dedup-table .path-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 0; /* forces the cell to respect the colgroup width instead of the content's natural width */
}
.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; } }
/* ---- 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; }
}