Files
Learn_System/frontend/css/ls.css

1803 lines
62 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ═══════════════════════════════════════════════════════
LearnSpace — Shared Design System /css/ls.css
═══════════════════════════════════════════════════════ */
/* ── Reset ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* ── Design tokens ── */
:root {
--bg: #EEF2FF;
--surface: rgba(255,255,255,0.82);
--border: rgba(15,23,42,0.10);
--border-h: rgba(15,23,42,0.20);
--text: #0F172A;
--text-2: #3D4F6B;
--text-3: #56687A; /* WCAG AA: ~5.1:1 on white, ~4.6:1 on --bg */
--violet: #9B5DE5;
--cyan: #06D6E0;
--green: #06D664;
--pink: #F15BB5;
--amber: #FFB347;
--grad-1: linear-gradient(135deg, #06D6E0, #9B5DE5);
/* Spacing scale (4px base) */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
/* Radius ladder */
--r-xs: 4px;
--r-sm: 8px;
--r-md: 12px;
--r-lg: 20px;
--r-xl: 24px;
--r-pill: 999px;
/* Semantic color aliases */
--success: var(--green);
--warning: var(--amber);
--danger: var(--pink);
--info: var(--cyan);
/* Typography scale */
--text-xs: 0.72rem;
--text-sm: 0.82rem;
--text-base: 0.92rem;
--text-md: 1.02rem;
--text-lg: 1.18rem;
--text-xl: 1.5rem;
--text-2xl: 2rem;
--text-3xl: 2.6rem;
/* Font-weight scale */
--fw-regular: 400;
--fw-medium: 500;
--fw-semibold: 600;
--fw-bold: 700;
--fw-extrabold: 800;
/* Motion */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--duration-fast: 0.12s;
--duration-base: 0.22s;
--duration-slow: 0.40s;
/* Layout breakpoints (documentation only — CSS @media doesn't support var()) */
--bp-mobile: 640px;
--bp-tablet: 1024px;
--bp-desktop: 1280px;
--blur: blur(20px);
/* two-layer shadow: crisp + ambient */
--shadow: 0 2px 8px rgba(15,23,42,0.08), 0 8px 40px rgba(15,23,42,0.10);
--shadow-h: 0 4px 16px rgba(15,23,42,0.12), 0 16px 56px rgba(15,23,42,0.13);
--tr: 0.22s ease;
}
/* ── Body + dot-grid ── */
body {
font-family: 'Manrope', sans-serif;
background: var(--bg);
background-image: radial-gradient(circle, rgba(15,23,42,0.055) 1px, transparent 1px);
background-size: 22px 22px;
min-height: 100vh;
color: var(--text);
}
/* ── Custom scrollbar ── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(155,93,229,0.35); border-radius: 99px; }
::-webkit-scrollbar-thumb:hover { background: rgba(155,93,229,0.60); }
/* ── Focus ring ── */
:focus-visible { outline: 2px solid var(--violet); outline-offset: 3px; }
/* ── Icon-only button (WCAG 2.5.5: 44×44 tap area) ── */
.icon-btn {
display: inline-flex; align-items: center; justify-content: center;
min-width: 44px; min-height: 44px;
border: none; border-radius: 10px;
background: transparent;
cursor: pointer;
transition: background var(--tr), color var(--tr);
color: var(--text-2);
flex-shrink: 0;
}
.icon-btn:hover { background: rgba(155,93,229,0.08); color: var(--violet); }
.icon-btn svg { width: 20px; height: 20px; }
/* ── Navbar ── */
.nav {
position: sticky; top: 0; z-index: 100;
padding: 10px 24px;
display: flex; align-items: center; justify-content: space-between;
background: rgba(238,242,255,0.90);
backdrop-filter: var(--blur);
border-bottom: 1px solid var(--border);
}
.nav-logo {
font-family: 'Unbounded', sans-serif;
font-size: 1rem; font-weight: 800;
color: var(--text); text-decoration: none;
letter-spacing: -0.01em;
}
.nav-logo span {
background: var(--grad-1);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.nav-right { display: flex; align-items: center; gap: 10px; }
/* nav user avatar chip */
.nav-user-chip {
display: flex; align-items: center; gap: 8px;
padding: 4px 12px 4px 4px;
background: rgba(155,93,229,0.07);
border: 1.5px solid rgba(155,93,229,0.18);
border-radius: var(--r-pill);
}
.nav-avatar {
width: 28px; height: 28px;
border-radius: 50%;
background: var(--grad-1);
display: flex; align-items: center; justify-content: center;
font-family: 'Unbounded', sans-serif;
font-size: 0.62rem; font-weight: 800; color: #fff;
flex-shrink: 0;
}
.nav-user-name {
font-size: 0.78rem; font-weight: 600; color: var(--text-2);
max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* legacy .nav-user (plain text) */
.nav-user { font-size: 0.85rem; color: var(--text-2); }
.btn-nav {
padding: 6px 16px;
border: 1.5px solid var(--border-h);
border-radius: var(--r-pill);
background: transparent;
font-family: 'Manrope', sans-serif; font-size: 0.8rem; font-weight: 600;
color: var(--text-3);
cursor: pointer; text-decoration: none; display: inline-block;
transition: all var(--tr);
}
.btn-nav:hover { border-color: var(--violet); color: var(--violet); }
.nav-active {
background: rgba(155,93,229,0.08) !important;
border-color: var(--violet) !important;
color: var(--violet) !important;
cursor: default; pointer-events: none;
}
/* ── Shimmer btn-primary ── */
.btn-primary {
position: relative; overflow: hidden;
padding: 10px 26px;
min-height: 44px; /* WCAG 2.5.5 touch target */
border: none; border-radius: var(--r-pill);
background: var(--grad-1);
color: #fff;
font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 700;
cursor: pointer;
transition: transform var(--tr), box-shadow var(--tr);
box-shadow: 0 2px 14px rgba(155,93,229,0.30);
}
.btn-primary::after {
content: '';
position: absolute; top: 0; left: -120%;
width: 80%; height: 100%;
background: linear-gradient(100deg, transparent, rgba(255,255,255,0.28), transparent);
transform: skewX(-15deg);
transition: left 0.55s ease;
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 22px rgba(155,93,229,0.40); }
.btn-primary:hover::after { left: 160%; }
.btn-primary:active { transform: translateY(0); }
/* ── Ghost & danger buttons ── */
.btn-ghost {
padding: 8px 18px;
min-height: 44px; /* WCAG 2.5.5 touch target */
border: 1.5px solid var(--border-h); border-radius: var(--r-pill);
background: transparent;
font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 600;
color: var(--text-2); cursor: pointer;
transition: all var(--tr);
}
.btn-ghost:hover { border-color: var(--violet); color: var(--violet); }
.btn-danger {
padding: 6px 14px;
border: 1.5px solid rgba(241,91,181,0.35); border-radius: var(--r-pill);
background: rgba(241,91,181,0.06);
font-family: 'Manrope', sans-serif; font-size: 0.76rem; font-weight: 600;
color: #c0306a; cursor: pointer;
transition: all var(--tr);
}
.btn-danger:hover { border-color: var(--pink); background: rgba(241,91,181,0.12); color: #a0204a; }
/* ── Form inputs ── */
.form-input {
width: 100%;
padding: 10px 14px;
border: 1.5px solid var(--border-h); border-radius: 12px;
background: rgba(255,255,255,0.70);
font-family: 'Manrope', sans-serif; font-size: 0.88rem; color: var(--text);
transition: border-color var(--tr), box-shadow var(--tr);
}
.form-input:focus {
outline: none;
border-color: var(--violet);
box-shadow: 0 0 0 3px rgba(155,93,229,0.15);
}
/* ── Badges ── */
.badge {
display: inline-flex; align-items: center;
padding: 2px 9px;
border-radius: var(--r-pill);
font-size: 0.72rem; font-weight: 700;
}
.badge-violet { background: rgba(155,93,229,0.12); color: var(--violet); }
.badge-cyan { background: rgba(6,214,224,0.12); color: #05aab3; }
.badge-green { background: rgba(6,214,100,0.12); color: #059950; }
.badge-pink { background: rgba(241,91,181,0.12); color: #c0306a; }
.badge-amber { background: rgba(255,179,71,0.15); color: #b06a00; }
/* ── Modal ── */
.modal-overlay {
position: fixed; inset: 0; z-index: 1000;
background: rgba(15,23,42,0.48);
backdrop-filter: blur(6px);
display: flex; align-items: center; justify-content: center;
padding: 20px;
animation: fadeIn 0.18s ease;
}
.modal {
background: #fff;
border-radius: 24px;
padding: 32px 28px;
max-width: 520px; width: 100%;
box-shadow: var(--shadow-h);
animation: fadeUp 0.22s ease;
}
.modal-title {
font-family: 'Unbounded', sans-serif;
font-size: 1.05rem; font-weight: 800;
margin-bottom: 20px;
}
.modal-footer { display: flex; gap: 10px; justify-content: flex-end; margin-top: 24px; }
/* ── Spinner ── */
.spinner {
width: 32px; height: 32px;
border: 3px solid var(--border);
border-top-color: var(--violet);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 30px auto; display: block;
}
/* ── Empty state ── */
.empty {
text-align: center; padding: 40px 20px;
color: var(--text-3); font-size: 0.88rem;
}
.empty-icon { font-size: 2.4rem; margin-bottom: 12px; }
/* ── Error text ── */
.error { color: var(--pink); font-size: 0.85rem; padding: 12px 0; }
/* ── Keyframes ── */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeUp { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes shimmer {
0% { left: -120%; }
100% { left: 160%; }
}
/* ══════════════════════════════════════════
SIDEBAR LAYOUT
══════════════════════════════════════════ */
.app-layout {
display: flex;
min-height: 100vh;
}
.app-layout > .sidebar {
width: 230px;
flex-shrink: 0;
position: sticky;
top: 0;
height: 100vh;
overflow: hidden; /* sidebar itself does NOT scroll */
display: flex;
flex-direction: column;
background: rgba(238,242,255,0.94);
backdrop-filter: blur(28px);
border-right: 1.5px solid var(--border);
padding: 0 10px 0;
z-index: 50;
}
.sb-brand {
display: flex;
align-items: center;
padding: 20px 12px 14px;
margin-bottom: 4px;
}
.sb-logo {
font-family: 'Unbounded', sans-serif;
font-size: 1.05rem;
font-weight: 800;
color: var(--text);
text-decoration: none;
}
.sb-logo span {
background: var(--grad-1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.sb-nav {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
min-height: 0; /* allows flex child to shrink below content size */
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
}
.sb-nav::-webkit-scrollbar { display: none; }
.sb-link, a.sb-link, button.sb-link {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
min-height: 44px; /* WCAG 2.5.5 touch target */
border-radius: 12px;
text-decoration: none;
font-size: 0.875rem;
font-weight: 600;
color: var(--text-3);
transition: all var(--tr);
cursor: pointer;
border: none;
background: transparent;
width: 100%;
text-align: left;
font-family: 'Manrope', sans-serif;
position: relative;
white-space: nowrap;
}
.sb-link:hover {
background: rgba(155,93,229,0.07);
color: var(--text);
}
.sb-link.active {
background: rgba(155,93,229,0.10);
color: var(--violet);
font-weight: 700;
}
.sb-link.active::before {
content: '';
position: absolute;
left: 0; top: 5px; bottom: 5px;
width: 3px;
border-radius: 0 3px 3px 0;
background: var(--grad-1);
}
.sb-icon {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: inherit;
}
.sb-icon svg {
width: 18px;
height: 18px;
stroke-width: 1.85;
}
/* tab-icon: inline svg в табах и кнопках */
.tab-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
flex-shrink: 0;
}
.tab-icon svg {
width: 16px;
height: 16px;
stroke-width: 2;
}
/* stat-icon: иконки в карточках KPI */
.stat-icon {
display: inline-flex;
align-items: center;
justify-content: center;
}
.stat-icon svg {
width: 22px;
height: 22px;
stroke-width: 1.75;
}
.sb-divider {
height: 1px;
background: var(--border);
margin: 8px 2px;
}
.sb-badge {
min-width: 18px;
height: 18px;
border-radius: 99px;
background: var(--pink);
color: #fff;
font-size: 0.6rem;
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 4px;
margin-left: auto;
}
.sb-foot {
padding: 10px 0 16px;
border-top: 1px solid var(--border);
margin-top: 6px;
flex-shrink: 0; /* never shrink — always visible at bottom */
}
.sb-user-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 12px;
transition: background var(--tr);
}
.sb-user-row:hover { background: rgba(155,93,229,0.05); }
.sb-avatar {
width: 32px; height: 32px;
border-radius: 10px;
background: var(--grad-1);
display: flex; align-items: center; justify-content: center;
font-family: 'Unbounded', sans-serif;
font-size: 0.6rem; font-weight: 800; color: #fff;
flex-shrink: 0;
overflow: hidden;
}
.sb-user-info { flex: 1; min-width: 0; }
.sb-user-name {
font-size: 0.78rem; font-weight: 700; color: var(--text);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
margin-bottom: 1px;
}
.sb-logout {
font-size: 0.68rem; color: var(--text-3);
background: none; border: none; cursor: pointer; padding: 0;
font-family: 'Manrope', sans-serif; font-weight: 600;
transition: color var(--tr);
}
.sb-logout:hover { color: var(--pink); }
.sb-content {
flex: 1;
min-width: 0;
overflow-x: hidden;
}
/* notif wrap — visually separate from nav items */
#notif-wrap {
border-top: 1.5px solid var(--border);
margin-top: 6px;
padding-top: 6px;
}
/* notif drop anchored relative to sidebar */
.notif-drop {
position: fixed;
left: 246px;
width: 340px;
max-height: 460px;
overflow-y: auto;
background: #fff;
border: 1.5px solid var(--border-h);
border-radius: 18px;
box-shadow: 0 16px 56px rgba(15,23,42,.16), 0 4px 16px rgba(15,23,42,.06);
z-index: 1000;
display: none;
font-size: .82rem;
color: var(--text-2);
}
.notif-drop.open { display: block; }
.notif-drop-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 16px 11px; border-bottom: 1px solid var(--border);
position: sticky; top: 0; background: #fff; z-index: 1;
border-radius: 18px 18px 0 0;
}
.notif-drop-header-left { display: flex; align-items: center; gap: 8px; }
.notif-drop-title { font-family: 'Unbounded', sans-serif; font-size: .77rem; font-weight: 800; color: var(--text); }
.notif-unread-badge { background: var(--violet); color: #fff; font-size: .62rem; font-weight: 800;
padding: 2px 7px; border-radius: 20px; font-family: 'Unbounded', sans-serif; line-height: 1.4; }
.notif-read-all { background: none; border: none; font-size: .72rem; color: var(--violet);
cursor: pointer; font-family: 'Manrope', sans-serif; font-weight: 600;
padding: 0; white-space: nowrap; }
.notif-read-all:hover { text-decoration: underline; }
.notif-item { display: flex; gap: 11px; padding: 11px 16px; cursor: pointer;
text-decoration: none; color: inherit; transition: background .15s;
position: relative; align-items: flex-start; }
.notif-item + .notif-item { border-top: 1px solid var(--border); }
.notif-item:hover { background: rgba(155,93,229,.04); }
.notif-item.unread { background: rgba(155,93,229,.05); }
.notif-item.unread::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0;
width: 3px; background: var(--violet); border-radius: 0 2px 2px 0; }
.notif-icon { width: 34px; height: 34px; border-radius: 10px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
background: var(--ni, rgba(155,93,229,.1)); color: var(--nc, #9B5DE5);
margin-top: 1px; }
.notif-icon .ic { width: 15px; height: 15px; }
.notif-body { flex: 1; min-width: 0; }
.notif-msg { font-size: .81rem; line-height: 1.45; color: var(--text); }
.notif-time { font-size: .67rem; color: var(--text-3); margin-top: 3px; font-weight: 500; }
.notif-empty { padding: 28px 16px; text-align: center; color: var(--text-3); font-size: .83rem; }
/* ══════════════════════════════════════════
PAGE TRANSITIONS (View Transitions API)
══════════════════════════════════════════ */
@view-transition { navigation: auto; }
/* Shared element: sidebar stays fixed, only main content transitions */
.sb-content { view-transition-name: main-content; }
.sidebar { view-transition-name: sidebar; }
/* Sidebar: no animation — stays in place */
::view-transition-old(sidebar),
::view-transition-new(sidebar) { animation: none; }
/* Main content: slide + fade */
::view-transition-old(main-content) {
animation: 180ms cubic-bezier(.4,0,1,1) both vt-slide-out;
}
::view-transition-new(main-content) {
animation: 260ms cubic-bezier(0,.5,.3,1) both vt-slide-in;
}
/* Fallback for pages without sidebar (login, test-run, test-result) */
::view-transition-old(root) {
animation: 160ms cubic-bezier(.4,0,1,1) both vt-fade-out;
}
::view-transition-new(root) {
animation: 240ms cubic-bezier(0,.5,.3,1) both vt-fade-in;
}
@keyframes vt-slide-out {
from { opacity: 1; transform: translateX(0) scale(1); }
to { opacity: 0; transform: translateX(-16px) scale(.98); }
}
@keyframes vt-slide-in {
from { opacity: 0; transform: translateX(20px) scale(.98); }
to { opacity: 1; transform: translateX(0) scale(1); }
}
@keyframes vt-fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes vt-fade-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* ══════════════════════════════════════════
LOGIN PAGE SPLIT LAYOUT
══════════════════════════════════════════ */
.login-page {
display: block !important;
padding: 0 !important;
}
.login-layout {
display: flex;
min-height: 100vh;
}
.login-left {
flex: 1;
background: linear-gradient(145deg, #0f0c29 0%, #1a1547 40%, #24243e 100%);
position: relative;
overflow: hidden;
display: flex;
align-items: center;
padding: 60px 56px;
}
.login-left::before {
content: '';
position: absolute; inset: 0;
background-image: radial-gradient(circle, rgba(255,255,255,0.055) 1px, transparent 1px);
background-size: 22px 22px;
pointer-events: none;
}
.ll-blob1, .ll-blob2 { position: absolute; border-radius: 50%; pointer-events: none; }
.ll-blob1 { width: 520px; height: 520px; background: radial-gradient(circle, rgba(155,93,229,0.32), transparent 70%); top: -160px; right: -100px; }
.ll-blob2 { width: 380px; height: 380px; background: radial-gradient(circle, rgba(6,214,224,0.22), transparent 70%); bottom: -100px; left: 15%; }
.ll-inner { position: relative; z-index: 1; max-width: 440px; }
.ll-logo {
font-family: 'Unbounded', sans-serif;
font-size: 1.35rem; font-weight: 800;
color: #fff;
margin-bottom: 52px;
}
.ll-logo span {
background: linear-gradient(135deg, #06D6E0, #9B5DE5);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.ll-headline {
font-family: 'Unbounded', sans-serif;
font-size: 1.75rem; font-weight: 800; line-height: 1.3;
color: #fff;
margin-bottom: 14px;
text-shadow: 0 2px 20px rgba(0,0,0,0.25);
}
.ll-tagline {
font-size: 0.88rem; color: rgba(255,255,255,0.58); font-weight: 500;
margin-bottom: 40px; line-height: 1.65;
}
.ll-features { display: flex; flex-direction: column; gap: 10px; }
.ll-feat {
display: flex; align-items: center; gap: 12px;
font-size: 0.85rem; color: rgba(255,255,255,0.82); font-weight: 600;
padding: 10px 16px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.09);
border-radius: 12px;
}
.ll-feat-icon { font-size: 1rem; flex-shrink: 0; }
.login-right {
width: 450px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 44px 40px;
background: #f4f6ff;
}
.login-right .auth-wrap {
width: 100%;
max-width: 100%;
}
@media (max-width: 840px) {
.login-layout { flex-direction: column; }
.login-left { padding: 40px 28px; min-height: 300px; }
.login-right { width: 100%; padding: 36px 24px; }
.ll-headline { font-size: 1.35rem; }
}
/* ══════════════════════════════════════════
A1: STAGGERED ENTRANCE ANIMATION
══════════════════════════════════════════ */
@keyframes cardIn {
from { opacity: 0; transform: translateY(18px) scale(0.97); }
to { opacity: 1; transform: none; }
}
.stagger-item {
animation: cardIn 0.42s cubic-bezier(0.22, 0.61, 0.36, 1) both;
animation-delay: calc(var(--i, 0) * 50ms);
}
/* ══════════════════════════════════════════
D1: SKELETON 2.0 — VIOLET SHIMMER
══════════════════════════════════════════ */
@keyframes ls-shimmer {
from { background-position: -600px 0; }
to { background-position: 600px 0; }
}
.ls-sk {
background: linear-gradient(
90deg,
rgba(155,93,229,0.06) 20%,
rgba(155,93,229,0.16) 40%,
rgba(255,255,255,0.7) 50%,
rgba(155,93,229,0.16) 60%,
rgba(155,93,229,0.06) 80%
) !important;
background-size: 1200px 100% !important;
animation: ls-shimmer 1.8s infinite ease-in-out !important;
}
/* ══════════════════════════════════════════
C2: RICH EMPTY STATES
══════════════════════════════════════════ */
.rich-empty {
display: flex; flex-direction: column; align-items: center;
gap: 10px; padding: 52px 24px; text-align: center;
background: rgba(255,255,255,0.7);
border: 1.5px dashed rgba(155,93,229,0.22);
border-radius: 20px;
backdrop-filter: blur(12px);
animation: cardIn 0.4s ease both;
}
.rich-empty-svg { opacity: 0.7; }
.rich-empty-title {
font-family: 'Unbounded', sans-serif;
font-size: 0.95rem; font-weight: 800; color: var(--text);
margin-top: 4px;
}
.rich-empty-sub {
font-size: 0.83rem; color: var(--text-3);
max-width: 290px; line-height: 1.62; margin-bottom: 2px;
}
.rich-empty-btn {
padding: 10px 26px;
background: var(--grad-1); color: #fff;
border: none; border-radius: 999px;
font-family: 'Manrope', sans-serif;
font-size: 0.84rem; font-weight: 700;
cursor: pointer; transition: all var(--tr);
}
.rich-empty-btn:hover { opacity: 0.88; transform: translateY(-2px); box-shadow: 0 8px 24px rgba(155,93,229,0.3); }
/* ══════════════════════════════════════════
C1: COLLAPSIBLE SIDEBAR
══════════════════════════════════════════ */
/* Smooth transition for sidebar width */
.app-layout > .sidebar { transition: width 0.28s cubic-bezier(0.4,0,0.2,1); }
/* Label text inside nav links — fades/collapses */
.sb-lbl {
overflow: hidden;
max-width: 160px;
transition: max-width 0.25s ease, opacity 0.18s ease, margin 0.18s ease;
white-space: nowrap;
flex-shrink: 1;
min-width: 0;
}
/* Toggle button (chevron) */
.sb-toggle {
width: 26px; height: 26px; border-radius: 50%;
background: rgba(255,255,255,0.85);
border: 1.5px solid var(--border-h);
display: inline-flex; align-items: center; justify-content: center;
cursor: pointer; flex-shrink: 0;
box-shadow: 0 1px 6px rgba(15,23,42,0.10);
transition: background var(--tr), border-color var(--tr), transform 0.28s ease;
padding: 0; margin-left: auto;
}
.sb-toggle:hover { background: var(--violet); color: #fff; border-color: var(--violet); }
.sb-toggle svg { width: 13px; height: 13px; stroke-width: 2.5; }
/* ── Collapsed state ── */
.app-layout.sb-collapsed > .sidebar { width: 62px; padding: 0 6px 16px; }
.app-layout.sb-collapsed .sb-brand { padding: 20px 4px 14px; justify-content: center; }
.app-layout.sb-collapsed .sb-logo { display: none; }
.app-layout.sb-collapsed .sb-lbl { max-width: 0; opacity: 0; }
.app-layout.sb-collapsed .sb-link { justify-content: center; padding: 10px 0; gap: 0; }
.app-layout.sb-collapsed .sb-badge { display: none !important; }
.app-layout.sb-collapsed .sb-user-info { display: none; }
.app-layout.sb-collapsed .sb-user-row { justify-content: center; padding: 8px 0; }
.app-layout.sb-collapsed .sb-toggle { margin: 0; transform: rotate(180deg); }
/* ══════════════════════════════════════════
GLOBAL SEARCH MODAL
══════════════════════════════════════════ */
.gs-overlay {
position: fixed; inset: 0; z-index: 9500;
background: rgba(15,23,42,0.45); backdrop-filter: blur(12px);
display: flex; align-items: flex-start; justify-content: center;
padding: 10vh 20px 20px; opacity: 0; pointer-events: none;
transition: opacity .18s ease;
}
.gs-overlay.open { opacity: 1; pointer-events: auto; }
.gs-box {
width: 100%; max-width: 560px; background: #fff;
border-radius: 20px; box-shadow: 0 40px 100px rgba(15,23,42,0.28);
overflow: hidden; transform: scale(.96) translateY(-8px);
transition: transform .18s ease;
}
.gs-overlay.open .gs-box { transform: scale(1) translateY(0); }
.gs-input-wrap {
display: flex; align-items: center; gap: 10px;
padding: 16px 20px; border-bottom: 1px solid rgba(15,23,42,0.08);
}
.gs-input-wrap svg { flex-shrink: 0; color: var(--text-3); }
.gs-input {
flex: 1; border: none; outline: none; font-family: 'Manrope', sans-serif;
font-size: 0.95rem; font-weight: 500; color: #0F172A; background: transparent;
}
.gs-input::placeholder { color: #B0BEC5; }
.gs-kbd {
font-size: 0.65rem; font-weight: 700; color: var(--text-3); background: rgba(15,23,42,0.06);
padding: 3px 7px; border-radius: 5px; line-height: 1;
}
.gs-results {
max-height: 380px; overflow-y: auto; padding: 8px;
}
.gs-empty {
text-align: center; padding: 40px 20px; color: var(--text-3); font-size: 0.85rem;
}
.gs-group-label {
font-size: 0.65rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.06em; color: var(--text-3); padding: 10px 12px 4px;
}
.gs-item {
display: flex; align-items: center; gap: 12px;
padding: 10px 12px; border-radius: 12px; cursor: pointer;
transition: background .12s;
}
.gs-item:hover, .gs-item.active { background: rgba(155,93,229,0.06); }
.gs-item-icon {
width: 34px; height: 34px; border-radius: 9px;
display: flex; align-items: center; justify-content: center;
font-size: 0.85rem; flex-shrink: 0;
}
.gs-icon-lesson { background: rgba(155,93,229,0.1); color: #9B5DE5; }
.gs-icon-course { background: rgba(6,214,160,0.1); color: #06D6A0; }
.gs-icon-file { background: rgba(6,214,224,0.1); color: #06D6E0; }
.gs-icon-question { background: rgba(245,158,11,0.1); color: #F59E0B; }
.gs-item-body { flex: 1; min-width: 0; }
.gs-item-title {
font-size: 0.85rem; font-weight: 600; color: #0F172A;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gs-item-sub {
font-size: 0.7rem; color: var(--text-3); margin-top: 1px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gs-item-arrow { color: #ccc; flex-shrink: 0; }
/* ══════════════════════════════════════════
MOBILE TOPBAR
══════════════════════════════════════════ */
.mob-bar { display: none; }
/* ══════════════════════════════════════════
RESPONSIVE — TABLET & MOBILE (≤ 768px)
══════════════════════════════════════════ */
@media (max-width: 768px) {
/* ── Mobile top bar ── */
.mob-bar {
display: flex;
align-items: center;
justify-content: space-between;
position: fixed;
top: 0; left: 0; right: 0;
height: 56px;
padding: 0 14px;
background: rgba(238,242,255,0.95);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border);
z-index: 150;
gap: 10px;
}
.mob-bar-logo {
font-family: 'Unbounded', sans-serif;
font-size: 0.9rem; font-weight: 800;
color: var(--text); text-decoration: none;
flex: 1;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mob-bar-logo span {
background: var(--grad-1);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.mob-bar-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.mob-icon-btn {
width: 36px; height: 36px;
border-radius: 10px;
background: transparent;
border: 1.5px solid var(--border-h);
display: flex; align-items: center; justify-content: center;
cursor: pointer;
transition: all var(--tr);
position: relative;
flex-shrink: 0;
}
.mob-icon-btn:hover { background: rgba(155,93,229,0.08); border-color: var(--violet); }
.mob-icon-btn svg { width: 17px; height: 17px; stroke: var(--text-2); stroke-width: 2; pointer-events: none; }
/* ── Sidebar becomes fixed drawer ── */
.app-layout { flex-direction: column; }
.app-layout > .sidebar {
position: fixed !important;
top: 0; left: 0;
height: 100vh !important;
width: 272px !important;
transform: translateX(-100%);
z-index: 200;
transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.28s ease !important;
box-shadow: none !important;
padding-top: 0 !important;
}
.app-layout.sb-open > .sidebar {
transform: translateX(0) !important;
box-shadow: 8px 0 40px rgba(15,23,42,0.22) !important;
}
/* Backdrop overlay when drawer is open */
.sb-backdrop {
display: none;
position: fixed; inset: 0;
background: rgba(15,23,42,0.40);
z-index: 190;
backdrop-filter: blur(2px);
animation: fadeIn 0.2s ease;
}
.sb-backdrop.open { display: block; }
/* Hide desktop collapse toggle on mobile */
.sb-toggle { display: none !important; }
/* Content: full width, offset for topbar */
.sb-content { width: 100%; padding-top: 56px; }
/* Restore labels inside drawer on mobile (ignore collapsed state) */
.app-layout > .sidebar .sb-lbl { max-width: 160px !important; opacity: 1 !important; }
.app-layout > .sidebar .sb-logo { display: block !important; }
.app-layout > .sidebar .sb-link { justify-content: flex-start !important; padding: 9px 12px !important; gap: 10px !important; }
.app-layout > .sidebar .sb-user-info { display: block !important; }
.app-layout > .sidebar .sb-user-row { justify-content: flex-start !important; padding: 8px 10px !important; }
.app-layout > .sidebar .sb-badge { display: inline-flex !important; }
.app-layout > .sidebar .sb-brand { padding: 20px 12px 14px !important; justify-content: flex-start !important; }
/* ── Notification dropdown ── */
.notif-drop {
position: fixed !important;
left: auto !important;
right: 14px !important;
top: 60px !important;
width: min(360px, calc(100vw - 28px)) !important;
}
/* ── Modal: bottom sheet on mobile ── */
.modal-overlay {
align-items: flex-end;
padding: 0;
}
.modal {
max-width: 100% !important;
width: 100%;
border-radius: 22px 22px 0 0;
padding: 24px 20px calc(32px + env(safe-area-inset-bottom, 0px));
max-height: 90vh;
overflow-y: auto;
}
/* ── Safe area for fixed bottom elements ── */
.mob-bar {
padding-bottom: env(safe-area-inset-bottom, 0px);
}
/* ── Login page: handled by login.html inline styles ── */
}
/* ══════════════════════════════════════════
INLINE SVG ICONS (.ic)
══════════════════════════════════════════ */
/* Replaces emoji / Unicode symbols throughout the UI */
.ic {
display: inline-block;
width: 1em; height: 1em;
vertical-align: -0.15em;
fill: none;
stroke: currentColor;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
flex-shrink: 0;
overflow: visible;
}
/* Larger display icons (page headers, empty-state hints) */
.page-header-icon .ic,
.hint-icon .ic,
.complete-icon .ic {
width: 1.1em; height: 1.1em;
stroke-width: 1.5;
}
/* ══════════════════════════════════════════
FEATURE FLAGS
══════════════════════════════════════════ */
/* Student without a class: hide leaderboard */
body.no-class #lb-section { display: none !important; }
/* Gamification kill-switch.
When admin turns off the feature, body.no-gamification is set by
api.js/hideDisabledFeatures and EVERY XP / coin / streak / shop /
achievement / frame element must vanish — across the whole app,
not just the dashboard. The rules below cover:
• dashboard widgets (.gam-bar, .lb-widget)
• profile tabs (achievements, shop, frames)
• textbook XP badges and progress cards (.hero-xp-badge, .po-xp,
.xp-bar, .xp-card)
• a catch-all [data-gamified] hook that wraps any future block —
authors of new pages should wrap XP UI in a <div data-gamified>
instead of inventing new classes. */
body.no-gamification .gam-bar,
body.no-gamification .lb-widget,
body.no-gamification .achievements-section,
body.no-gamification #tab-btn-achievements,
body.no-gamification #tab-btn-shop,
body.no-gamification #tab-achievements,
body.no-gamification #tab-shop,
body.no-gamification #frames-section,
body.no-gamification .hero-xp-badge,
body.no-gamification .po-xp,
body.no-gamification .xp-card,
body.no-gamification .xp-bar,
body.no-gamification .xp-pill,
body.no-gamification .xp-badge,
body.no-gamification [data-gamified] { display: none !important; }
/* ══════════════════════════════════════════
RESPONSIVE — SMALL PHONES (≤ 480px)
══════════════════════════════════════════ */
@media (max-width: 480px) {
.mob-bar { padding: 0 12px; }
.modal { padding: 20px 16px 28px; }
.btn-primary { padding: 10px 20px; }
.filter-tabs { gap: 4px; }
.filter-tab { padding: 6px 12px; font-size: 0.78rem; }
}
/* ══════════════════════════════════════════
SKELETON SHIMMER
══════════════════════════════════════════ */
@keyframes ls-shimmer-util {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.ls-skeleton {
background: linear-gradient(90deg,
rgba(15,23,42,0.04) 0%,
rgba(15,23,42,0.10) 50%,
rgba(15,23,42,0.04) 100%
);
background-size: 200% 100%;
animation: ls-shimmer-util 1.6s infinite;
border-radius: var(--r-sm);
}
.ls-skeleton-line { height: 12px; margin: var(--space-2) 0; }
.ls-skeleton-card { aspect-ratio: 1; }
.ls-skeleton-row { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3); }
/* ── Utility Classes ── */
/* Visibility */
.hidden { display: none !important; }
.invisible { visibility: hidden; }
/* Padding */
.p-0 { padding: 0; }
.p-1 { padding: var(--space-1); }
.p-2 { padding: var(--space-2); }
.p-3 { padding: var(--space-3); }
.p-4 { padding: var(--space-4); }
.p-5 { padding: var(--space-5); }
.p-6 { padding: var(--space-6); }
.px-1 { padding-left: var(--space-1); padding-right: var(--space-1); }
.px-2 { padding-left: var(--space-2); padding-right: var(--space-2); }
.px-3 { padding-left: var(--space-3); padding-right: var(--space-3); }
.px-4 { padding-left: var(--space-4); padding-right: var(--space-4); }
.py-1 { padding-top: var(--space-1); padding-bottom: var(--space-1); }
.py-2 { padding-top: var(--space-2); padding-bottom: var(--space-2); }
.py-3 { padding-top: var(--space-3); padding-bottom: var(--space-3); }
.py-4 { padding-top: var(--space-4); padding-bottom: var(--space-4); }
/* Margin */
.m-0 { margin: 0; }
.m-1 { margin: var(--space-1); }
.m-2 { margin: var(--space-2); }
.m-3 { margin: var(--space-3); }
.m-4 { margin: var(--space-4); }
.m-5 { margin: var(--space-5); }
.m-6 { margin: var(--space-6); }
.mt-1 { margin-top: var(--space-1); }
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.mt-5 { margin-top: var(--space-5); }
.mt-6 { margin-top: var(--space-6); }
.mb-1 { margin-bottom: var(--space-1); }
.mb-2 { margin-bottom: var(--space-2); }
.mb-3 { margin-bottom: var(--space-3); }
.mb-4 { margin-bottom: var(--space-4); }
.mb-5 { margin-bottom: var(--space-5); }
.mb-6 { margin-bottom: var(--space-6); }
.ml-1 { margin-left: var(--space-1); }
.ml-2 { margin-left: var(--space-2); }
.ml-3 { margin-left: var(--space-3); }
.ml-4 { margin-left: var(--space-4); }
.ml-5 { margin-left: var(--space-5); }
.ml-6 { margin-left: var(--space-6); }
.mr-1 { margin-right: var(--space-1); }
.mr-2 { margin-right: var(--space-2); }
.mr-3 { margin-right: var(--space-3); }
.mr-4 { margin-right: var(--space-4); }
.mr-5 { margin-right: var(--space-5); }
.mr-6 { margin-right: var(--space-6); }
.mx-1 { margin-left: var(--space-1); margin-right: var(--space-1); }
.mx-2 { margin-left: var(--space-2); margin-right: var(--space-2); }
.mx-3 { margin-left: var(--space-3); margin-right: var(--space-3); }
.mx-4 { margin-left: var(--space-4); margin-right: var(--space-4); }
.mx-5 { margin-left: var(--space-5); margin-right: var(--space-5); }
.mx-6 { margin-left: var(--space-6); margin-right: var(--space-6); }
.mx-auto { margin-left: auto; margin-right: auto; }
.my-1 { margin-top: var(--space-1); margin-bottom: var(--space-1); }
.my-2 { margin-top: var(--space-2); margin-bottom: var(--space-2); }
.my-3 { margin-top: var(--space-3); margin-bottom: var(--space-3); }
.my-4 { margin-top: var(--space-4); margin-bottom: var(--space-4); }
.my-5 { margin-top: var(--space-5); margin-bottom: var(--space-5); }
.my-6 { margin-top: var(--space-6); margin-bottom: var(--space-6); }
/* Gap */
.gap-1 { gap: var(--space-1); }
.gap-2 { gap: var(--space-2); }
.gap-3 { gap: var(--space-3); }
.gap-4 { gap: var(--space-4); }
.gap-6 { gap: var(--space-6); }
/* Flex / Grid */
.flex { display: flex; }
.inline-flex { display: inline-flex; }
.grid { display: grid; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.flex-col { flex-direction: column; }
.flex-1 { flex: 1; }
.flex-wrap { flex-wrap: wrap; }
/* Text */
.text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); }
.text-base { font-size: var(--text-base); }
.text-lg { font-size: var(--text-lg); }
.text-xl { font-size: var(--text-xl); }
.font-bold { font-weight: var(--fw-bold); }
.font-semibold{ font-weight: var(--fw-semibold); }
.text-muted { color: var(--text-3); }
.text-secondary { color: var(--text-2); }
.text-violet { color: var(--violet); }
.text-success { color: var(--success); }
.text-danger { color: var(--danger); }
.text-center { text-align: center; }
.text-right { text-align: right; }
/* Radius */
.rounded-sm { border-radius: var(--r-sm); }
.rounded-md { border-radius: var(--r-md); }
.rounded-lg { border-radius: var(--r-lg); }
.rounded-full { border-radius: var(--r-pill); }
/* ══════════════════════════════════════════
ANIMATED BACKGROUNDS (Phase 6)
Painted by a single fixed div appended to <body> by api.js
applyCosmetics: <div id="ls-bg-fx" class="bg-<slug>"></div>.
The container is always position:fixed inset:0 z-index:-1, never
intercepts pointer events, and sits BEHIND every page chrome.
Each slug has a static fallback first (no animation), then opts
into motion outside the prefers-reduced-motion overlay below.
Picker preview swatches reuse `.bg-preview.<slug>` with the same
visuals at smaller scale.
══════════════════════════════════════════ */
#ls-bg-fx {
position: fixed;
inset: 0;
z-index: -2;
pointer-events: none;
overflow: hidden;
will-change: opacity, transform, background-position;
transition: opacity .4s ease;
}
/* The veil: a translucent white sheet that sits between the animated
bg layer and page content. Without it, vibrant backgrounds bleed
straight through every UI element that uses an rgba() fill — chips,
cards, panel backdrops. With it, the animation reads as a subtle
accent rather than a takeover. */
#ls-bg-veil {
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background: rgba(245, 247, 251, 0.78);
transition: background .4s ease;
}
/* On the 'dark' preset we want a darker veil so light content still
pops. body[data-bg-tone="dark"] is set by api.js for dark slugs. */
body[data-bg-tone="dark"] #ls-bg-veil {
background: rgba(11, 17, 32, 0.78);
}
/* ── Dark-tone readability overrides ──
When a dark background is active the page chrome (headings, filter
tabs, section titles, breadcrumbs, balance counters) sits directly
on the dark veil and inherits the light-theme text colors — which
become invisible. Override only elements that don't have an opaque
container of their own. White cards (`.shop-item`, `.ach-item`,
`.ep-card`, etc.) keep their dark text unchanged. */
body[data-bg-tone="dark"] {
color: #e6e9f2;
}
body[data-bg-tone="dark"] .shop-title,
body[data-bg-tone="dark"] .ach-group-title,
body[data-bg-tone="dark"] .ach-group-count,
body[data-bg-tone="dark"] .ach-sum-chip,
body[data-bg-tone="dark"] .shop-balance,
body[data-bg-tone="dark"] .filter-bar,
body[data-bg-tone="dark"] .filter-tab,
body[data-bg-tone="dark"] .p-stat-label,
body[data-bg-tone="dark"] .p-stat-val,
body[data-bg-tone="dark"] .profile-name,
body[data-bg-tone="dark"] .frames-section > h4,
body[data-bg-tone="dark"] .p-pane > h2,
body[data-bg-tone="dark"] .p-pane > h3,
body[data-bg-tone="dark"] .p-section > h2,
body[data-bg-tone="dark"] .p-section > h3 {
color: #f3f4f6;
}
body[data-bg-tone="dark"] .shop-filter:not(.active),
body[data-bg-tone="dark"] .p-tab:not(.active) {
color: rgba(243, 244, 246, 0.78);
}
body[data-bg-tone="dark"] .shop-filter:not(.active):hover,
body[data-bg-tone="dark"] .p-tab:not(.active):hover {
color: #fff;
background: rgba(255, 255, 255, 0.08);
}
/* Container pills (`.p-tabs`, `.shop-filters`) used a 6% dark fill that
vanishes on the dark veil — restore the rounded outline with a
white-tinted wash + subtle border so the button group still reads
as a contained pill. */
body[data-bg-tone="dark"] .p-tabs,
body[data-bg-tone="dark"] .shop-filters {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.10);
}
/* Active tab/filter — keep the white pill but add a contrast halo so
it doesn't look like a separate floating chip. */
body[data-bg-tone="dark"] .p-tab.active,
body[data-bg-tone="dark"] .shop-filter.active {
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.35);
}
/* Thin progress bars (xp, daily-goal) used a 7% dark fill that's
invisible on dark — switch to a faint light track. */
body[data-bg-tone="dark"] .ach-xp-progress,
body[data-bg-tone="dark"] .ep-bar,
body[data-bg-tone="dark"] .po-bar {
background: rgba(255, 255, 255, 0.10);
}
body[data-bg-tone="dark"] .ach-group-head {
border-bottom-color: rgba(255, 255, 255, 0.15);
}
body[data-bg-tone="dark"] .ach-group-count,
body[data-bg-tone="dark"] .shop-balance,
body[data-bg-tone="dark"] .frame-unlock-hint {
color: rgba(230, 233, 242, 0.72);
}
/* Sidebar already has its own opaque background — leave it alone. */
.bg-preview {
position: relative;
width: 100%;
aspect-ratio: 16 / 10;
border-radius: 10px;
overflow: hidden;
background: #f5f7fb;
}
/* ── 1. none (no overlay) ─────────────────────────────────────── */
.bg-none, .bg-preview.bg-none { background: transparent; }
/* ── 2. gradient-soft (free, static) ──────────────────────────── */
.bg-gradient-soft,
.bg-preview.bg-gradient-soft {
background: linear-gradient(135deg, #e0e7ff 0%, #fce7f3 100%);
}
/* ── 3. dots (free, static) ───────────────────────────────────── */
.bg-dots,
.bg-preview.bg-dots {
background-color: #fafbff;
background-image: radial-gradient(rgba(99,102,241,0.18) 1px, transparent 1px);
background-size: 22px 22px;
}
/* ── 4. dark (free, static) ───────────────────────────────────── */
.bg-dark,
.bg-preview.bg-dark {
background: radial-gradient(ellipse at top, #1e1b4b 0%, #0f172a 100%);
}
/* ── 5. gradient-flow (paid, animated) ────────────────────────── */
@keyframes ls-bg-flow {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.bg-gradient-flow,
.bg-preview.bg-gradient-flow {
background: linear-gradient(120deg, #9B5DE5, #06D6E0, #FFD166, #9B5DE5);
background-size: 300% 300%;
animation: ls-bg-flow 24s ease-in-out infinite;
}
/* ── 6. grid (paid, animated) ─────────────────────────────────── */
@keyframes ls-bg-grid-scan {
0% { background-position: 0 0, 0 0; }
100% { background-position: 0 60px, 60px 0; }
}
.bg-grid,
.bg-preview.bg-grid {
background-color: #0f172a;
background-image:
linear-gradient(rgba(6,214,224,0.18) 1px, transparent 1px),
linear-gradient(90deg, rgba(6,214,224,0.18) 1px, transparent 1px);
background-size: 60px 60px;
animation: ls-bg-grid-scan 18s linear infinite;
}
/* ── 7. bubbles (paid, animated) ──────────────────────────────── */
@keyframes ls-bg-bubble-rise {
0% { transform: translateY(0) scale(0.9); opacity: 0.55; }
50% { transform: translateY(-50vh) scale(1.05); opacity: 0.85; }
100% { transform: translateY(-100vh) scale(0.9); opacity: 0; }
}
.bg-bubbles,
.bg-preview.bg-bubbles {
background: linear-gradient(180deg, #cbeaff 0%, #e9f5ff 100%);
position: relative;
}
.bg-bubbles::before,
.bg-bubbles::after,
.bg-preview.bg-bubbles::before,
.bg-preview.bg-bubbles::after {
content: '';
position: absolute;
inset: 0;
background-image:
radial-gradient(circle at 12% 80%, rgba(255,255,255,0.7) 6px, transparent 7px),
radial-gradient(circle at 38% 90%, rgba(255,255,255,0.5) 9px, transparent 10px),
radial-gradient(circle at 65% 95%, rgba(255,255,255,0.6) 5px, transparent 6px),
radial-gradient(circle at 85% 75%, rgba(255,255,255,0.55) 11px, transparent 12px);
animation: ls-bg-bubble-rise 14s linear infinite;
}
.bg-bubbles::after,
.bg-preview.bg-bubbles::after {
background-image:
radial-gradient(circle at 22% 70%, rgba(255,255,255,0.5) 8px, transparent 9px),
radial-gradient(circle at 50% 88%, rgba(255,255,255,0.6) 4px, transparent 5px),
radial-gradient(circle at 78% 60%, rgba(255,255,255,0.6) 7px, transparent 8px);
animation-duration: 19s;
animation-delay: -6s;
}
/* ── 8. stars (paid, animated) ────────────────────────────────── */
@keyframes ls-bg-stars-twinkle {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.bg-stars,
.bg-preview.bg-stars {
background-color: #0a0e27;
background-image:
radial-gradient(2px 2px at 20% 30%, #ffffff, transparent),
radial-gradient(1px 1px at 40% 70%, #ffffff, transparent),
radial-gradient(2px 2px at 60% 20%, #ffffff, transparent),
radial-gradient(1px 1px at 80% 50%, #ffffff, transparent),
radial-gradient(2px 2px at 90% 85%, #ffffff, transparent),
radial-gradient(1px 1px at 10% 90%, #ffffff, transparent),
radial-gradient(2px 2px at 50% 10%, #ffffff, transparent),
radial-gradient(1px 1px at 30% 50%, #ffffff, transparent);
background-size: 200px 200px;
animation: ls-bg-stars-twinkle 6s ease-in-out infinite;
}
/* ── 9. aurora (premium, animated) ────────────────────────────── */
@keyframes ls-bg-aurora-spin {
0% { transform: rotate(0deg) scale(1.2); }
50% { transform: rotate(180deg) scale(1.4); }
100% { transform: rotate(360deg) scale(1.2); }
}
.bg-aurora,
.bg-preview.bg-aurora {
background: #050b1a;
position: relative;
overflow: hidden;
}
.bg-aurora::before,
.bg-preview.bg-aurora::before {
content: '';
position: absolute;
inset: -40%;
background: conic-gradient(
from 0deg at 50% 50%,
transparent 0%,
rgba(6,214,224,0.45) 12%,
rgba(155,93,229,0.55) 24%,
transparent 36%,
rgba(6,214,160,0.35) 60%,
transparent 78%,
rgba(255,107,53,0.30) 92%,
transparent 100%
);
filter: blur(46px);
animation: ls-bg-aurora-spin 40s linear infinite;
}
/* ── 10. nebula (premium, animated) ───────────────────────────── */
@keyframes ls-bg-nebula-pan {
0% { background-position: 0% 0%, 100% 100%; }
50% { background-position: 50% 50%, 50% 50%; }
100% { background-position: 0% 0%, 100% 100%; }
}
.bg-nebula,
.bg-preview.bg-nebula {
background:
radial-gradient(ellipse at 30% 30%, rgba(155,93,229,0.6), transparent 55%),
radial-gradient(ellipse at 70% 70%, rgba(6,214,224,0.5), transparent 55%),
radial-gradient(circle at 50% 50%, rgba(255,107,53,0.25), transparent 60%),
#050214;
background-size: 150% 150%, 150% 150%, 100% 100%, 100% 100%;
animation: ls-bg-nebula-pan 30s ease-in-out infinite;
}
/* ══════════════════════════════════════════
PREMIUM ANIMATED BACKGROUNDS (migration 036)
══════════════════════════════════════════ */
/* ── 11. sunset (paid, animated) ────────────────────────────────
A warm three-stop gradient that slowly hue-rotates through a
sunset cycle — same palette, different ambient temperature. */
@keyframes ls-bg-sunset-cycle {
0%, 100% { filter: hue-rotate(0deg) saturate(1.05); }
50% { filter: hue-rotate(35deg) saturate(1.20); }
}
.bg-sunset,
.bg-preview.bg-sunset {
background: linear-gradient(180deg, #1a1240 0%, #ff6b6b 55%, #ffa07a 100%);
animation: ls-bg-sunset-cycle 60s linear infinite;
}
/* ── 12. rain (paid, animated) ──────────────────────────────────
Two pseudo-element layers of elongated radial 'drops' tiled at
different sizes/speeds so they read as actual rain — not striped
curtains. Drops are 1.5px wide × 12-18px tall ellipses scattered
across a 160-200px tile. */
@keyframes ls-bg-rain-front {
from { background-position: 0 0; }
to { background-position: 0 180px; }
}
@keyframes ls-bg-rain-back {
from { background-position: 0 0; }
to { background-position: 0 130px; }
}
.bg-rain,
.bg-preview.bg-rain {
background-color: #0a1828;
position: relative;
overflow: hidden;
}
.bg-rain::before,
.bg-preview.bg-rain::before {
content: '';
position: absolute;
inset: 0;
background-image:
radial-gradient(ellipse 1.5px 14px at 12% 18%, rgba(255,255,255,0.70), transparent 80%),
radial-gradient(ellipse 1.5px 12px at 32% 58%, rgba(255,255,255,0.65), transparent 80%),
radial-gradient(ellipse 1.5px 16px at 55% 12%, rgba(255,255,255,0.60), transparent 80%),
radial-gradient(ellipse 1.5px 14px at 76% 70%, rgba(255,255,255,0.70), transparent 80%),
radial-gradient(ellipse 1.5px 12px at 90% 32%, rgba(255,255,255,0.55), transparent 80%),
radial-gradient(ellipse 1.5px 18px at 22% 86%, rgba(255,255,255,0.60), transparent 80%),
radial-gradient(ellipse 1.5px 13px at 48% 92%, rgba(255,255,255,0.55), transparent 80%);
background-size: 180px 180px;
background-repeat: repeat;
animation: ls-bg-rain-front 0.9s linear infinite;
}
.bg-rain::after,
.bg-preview.bg-rain::after {
content: '';
position: absolute;
inset: 0;
background-image:
radial-gradient(ellipse 1px 10px at 18% 28%, rgba(255,255,255,0.45), transparent 80%),
radial-gradient(ellipse 1px 9px at 45% 72%, rgba(255,255,255,0.40), transparent 80%),
radial-gradient(ellipse 1px 11px at 68% 14%, rgba(255,255,255,0.45), transparent 80%),
radial-gradient(ellipse 1px 10px at 85% 50%, rgba(255,255,255,0.40), transparent 80%),
radial-gradient(ellipse 1px 8px at 30% 95%, rgba(255,255,255,0.35), transparent 80%);
background-size: 130px 130px;
background-repeat: repeat;
animation: ls-bg-rain-back 0.55s linear infinite;
}
/* ── 13. snow (paid, animated) ──────────────────────────────────
Pattern of small white circles drifting downward; multi-size
layers create depth. */
@keyframes ls-bg-snow-fall {
from { background-position: 0 0, 0 0, 0 0; }
to { background-position: 0 200px, 0 300px, 0 250px; }
}
.bg-snow,
.bg-preview.bg-snow {
background-color: #0f1729;
background-image:
radial-gradient(circle 1.5px at 20% 30%, white 100%, transparent 100%),
radial-gradient(circle 2.5px at 60% 70%, white 100%, transparent 100%),
radial-gradient(circle 2px at 80% 20%, white 100%, transparent 100%);
background-size: 200px 200px, 300px 300px, 250px 250px;
animation: ls-bg-snow-fall 22s linear infinite;
}
/* ── 14. clouds (paid, animated, LIGHT) ─────────────────────────
Drifting white blobs on a sky gradient. Two layers cross the
screen at different speeds for parallax. */
@keyframes ls-bg-clouds-drift {
from { transform: translateX(-30%); }
to { transform: translateX(30%); }
}
.bg-clouds,
.bg-preview.bg-clouds {
background: linear-gradient(180deg, #87ceeb 0%, #e0f6ff 60%, #ffffff 100%);
position: relative;
overflow: hidden;
}
.bg-clouds::before,
.bg-clouds::after,
.bg-preview.bg-clouds::before,
.bg-preview.bg-clouds::after {
content: '';
position: absolute;
inset: -10%;
background-image:
radial-gradient(ellipse 18% 8% at 15% 25%, rgba(255,255,255,0.9), transparent 60%),
radial-gradient(ellipse 14% 7% at 45% 35%, rgba(255,255,255,0.8), transparent 60%),
radial-gradient(ellipse 22% 9% at 75% 22%, rgba(255,255,255,0.85), transparent 60%);
animation: ls-bg-clouds-drift 80s linear infinite alternate;
}
.bg-clouds::after,
.bg-preview.bg-clouds::after {
background-image:
radial-gradient(ellipse 16% 7% at 25% 60%, rgba(255,255,255,0.55), transparent 60%),
radial-gradient(ellipse 20% 8% at 65% 75%, rgba(255,255,255,0.5), transparent 60%);
animation-duration: 120s;
animation-delay: -20s;
opacity: 0.7;
}
/* ── 15. fireflies (paid, animated) ─────────────────────────────
Pulsing glowing dots that drift in opposing directions. Two
pseudo-element layers for variety; soft blur + opacity pulse. */
@keyframes ls-bg-firefly-drift {
0% { transform: translate(0, 0); }
25% { transform: translate(3%, -4%); }
50% { transform: translate(-2%, -2%); }
75% { transform: translate(2%, 3%); }
100% { transform: translate(0, 0); }
}
@keyframes ls-bg-firefly-pulse {
0%, 100% { opacity: 0.35; }
50% { opacity: 1; }
}
.bg-fireflies,
.bg-preview.bg-fireflies {
background: radial-gradient(ellipse at center, #1a2942 0%, #0a1828 70%, #050b14 100%);
position: relative;
overflow: hidden;
}
.bg-fireflies::before,
.bg-fireflies::after,
.bg-preview.bg-fireflies::before,
.bg-preview.bg-fireflies::after {
content: '';
position: absolute;
inset: 0;
background-image:
radial-gradient(circle 4px at 12% 22%, #ffd166 100%, transparent 100%),
radial-gradient(circle 3px at 38% 48%, #ffd166 100%, transparent 100%),
radial-gradient(circle 5px at 62% 18%, #ffd166 100%, transparent 100%),
radial-gradient(circle 3px at 88% 55%, #ffd166 100%, transparent 100%),
radial-gradient(circle 4px at 22% 78%, #ffd166 100%, transparent 100%),
radial-gradient(circle 3px at 68% 85%, #ffd166 100%, transparent 100%);
filter: blur(2px);
animation: ls-bg-firefly-drift 9s ease-in-out infinite, ls-bg-firefly-pulse 3.6s ease-in-out infinite;
}
.bg-fireflies::after,
.bg-preview.bg-fireflies::after {
background-image:
radial-gradient(circle 3px at 28% 38%, #ffe599 100%, transparent 100%),
radial-gradient(circle 4px at 52% 68%, #ffe599 100%, transparent 100%),
radial-gradient(circle 3px at 78% 30%, #ffe599 100%, transparent 100%);
animation-duration: 13s, 4.8s;
animation-delay: -2s, -1s;
opacity: 0.7;
}
/* ── 16. cyber-grid (paid, animated) ────────────────────────────
Neon-blue grid scrolling downward; perspective is faked via a
vertical gradient overlay that fades into darkness. */
@keyframes ls-bg-cyber-scan {
from { background-position: 0 0, 0 0, 0 0; }
to { background-position: 0 100%, 0 50px, 0 50px; }
}
.bg-cyber-grid,
.bg-preview.bg-cyber-grid {
background-color: #060a1f;
background-image:
linear-gradient(180deg, rgba(6,214,224,0.10) 0%, transparent 60%),
linear-gradient(0deg, rgba(6,214,224,0.30) 1px, transparent 1px),
linear-gradient(90deg, rgba(6,214,224,0.30) 1px, transparent 1px);
background-size: 100% 100%, 50px 50px, 50px 50px;
animation: ls-bg-cyber-scan 6s linear infinite;
}
/* ── 17. kaleidoscope (paid, animated) ──────────────────────────
Two heavily-blurred conic-gradients rotating in opposite
directions, blended via mix-blend-mode. */
@keyframes ls-bg-kal-cw { to { transform: rotate(360deg); } }
@keyframes ls-bg-kal-ccw { to { transform: rotate(-360deg); } }
.bg-kaleidoscope,
.bg-preview.bg-kaleidoscope {
background: #0a0a1f;
position: relative;
overflow: hidden;
}
.bg-kaleidoscope::before,
.bg-preview.bg-kaleidoscope::before {
content: '';
position: absolute;
inset: -50%;
background: conic-gradient(
from 0deg,
transparent 0%, rgba(155,93,229,0.45) 20%,
transparent 33%, rgba(6,214,224,0.45) 53%,
transparent 66%, rgba(255,107,53,0.45) 86%,
transparent 100%
);
filter: blur(40px);
animation: ls-bg-kal-cw 28s linear infinite;
}
.bg-kaleidoscope::after,
.bg-preview.bg-kaleidoscope::after {
content: '';
position: absolute;
inset: -50%;
background: conic-gradient(
from 90deg,
transparent 0%, rgba(6,214,160,0.35) 25%,
transparent 40%, rgba(255,209,102,0.35) 65%,
transparent 100%
);
filter: blur(55px);
mix-blend-mode: screen;
animation: ls-bg-kal-ccw 44s linear infinite;
}
/* ── 18. ocean (paid, animated) ─────────────────────────────────
Layered radial blobs slowly drift to fake undulating waves of
color near the bottom of the screen. */
@keyframes ls-bg-ocean-flow {
0% { background-position: 0% 100%, 0% 100%, 100% 100%; }
50% { background-position: 50% 60%, 50% 80%, 50% 90%; }
100% { background-position: 0% 100%, 0% 100%, 100% 100%; }
}
.bg-ocean,
.bg-preview.bg-ocean {
background-color: #0d2a4a;
background-image:
radial-gradient(ellipse 60% 40% at 50% 100%, rgba(6,214,224,0.55), transparent 60%),
radial-gradient(ellipse 50% 35% at 30% 100%, rgba(6,214,160,0.50), transparent 65%),
radial-gradient(ellipse 70% 35% at 70% 100%, rgba(155,93,229,0.40), transparent 65%);
background-size: 220% 220%, 260% 260%, 240% 240%;
background-repeat: no-repeat;
animation: ls-bg-ocean-flow 22s ease-in-out infinite;
}
/* ── 19. aurora-dance (premium top, animated) ───────────────────
Multi-layered aurora — two huge skewed bands waving in opposite
phases, plus a slow conic underlay for depth. */
@keyframes ls-bg-aurora-band-a {
0%, 100% { transform: translateX(-12%) skewY(-4deg); }
50% { transform: translateX( 12%) skewY( 4deg); }
}
@keyframes ls-bg-aurora-band-b {
0%, 100% { transform: translateX( 14%) skewY( 6deg); }
50% { transform: translateX(-14%) skewY(-6deg); }
}
.bg-aurora-dance,
.bg-preview.bg-aurora-dance {
background: radial-gradient(ellipse at top, #1a0a4a 0%, #050214 70%);
position: relative;
overflow: hidden;
}
.bg-aurora-dance::before,
.bg-preview.bg-aurora-dance::before {
content: '';
position: absolute;
inset: -20%;
background: linear-gradient(180deg,
transparent 0%,
rgba(6,214,224,0.55) 30%,
rgba(155,93,229,0.65) 50%,
rgba(6,214,160,0.55) 72%,
transparent 100%
);
filter: blur(34px);
animation: ls-bg-aurora-band-a 9s ease-in-out infinite;
}
.bg-aurora-dance::after,
.bg-preview.bg-aurora-dance::after {
content: '';
position: absolute;
inset: -20%;
background: linear-gradient(180deg,
transparent 0%,
rgba(255,107,53,0.40) 28%,
rgba(255,209,102,0.35) 48%,
rgba(155,93,229,0.50) 70%,
transparent 100%
);
filter: blur(40px);
mix-blend-mode: screen;
animation: ls-bg-aurora-band-b 13s ease-in-out infinite;
}
/* ── Reduced-motion: kill all bg animations, keep static palette ─ */
@media (prefers-reduced-motion: reduce) {
#ls-bg-fx,
#ls-bg-fx::before,
#ls-bg-fx::after,
.bg-preview,
.bg-preview::before,
.bg-preview::after {
animation: none !important;
}
}