Files
Learn_System/frontend/css/ls.css
T
Maxim Dolgolyov 98ec1ed478 feat(shop): animated backgrounds — system-wide cosmetic + picker
A new cosmetic family: a fixed-position overlay painted behind every
page of the app, switchable from the profile shop. 4 free presets + 6
paid (250-1200 coins) so the new economy has another sink. Every
animation respects prefers-reduced-motion and falls back to its static
gradient.

Catalogue (migration 035):
  free:   none, gradient-soft, dots, dark
  paid:   gradient-flow, grid, bubbles, stars (mid)
          aurora, nebula                       (premium)

Backend:
  • migration 035 adds users.active_background + rebuilds shop_items
    CHECK to include 'background' (standard SQLite 'new + copy + swap')
    and seeds 10 items
  • shopController.getMyActive returns { background: { slug } } and
    activateItem handles type='background' (stores bare slug in
    active_background) + skips the user_purchases check for price=0
    so free presets work for everyone without per-user rows
  • routes/shop validate schema lets 'background' through

Frontend:
  • api.js applyCosmetics injects <div id='ls-bg-fx'> at body start
    and toggles class to bg-<slug>. Cleared backgrounds remove the
    element so dark→light transitions don't leave artifacts.
  • ls.css gains a self-contained 'ANIMATED BACKGROUNDS' block:
    keyframes per animated slug (ls-bg-flow, ls-bg-grid-scan,
    ls-bg-bubble-rise, ls-bg-stars-twinkle, ls-bg-aurora-spin,
    ls-bg-nebula-pan) wrapped in a prefers-reduced-motion kill-switch.
    Same .bg-<slug> classes are reused for the .bg-preview swatches.
  • profile.html shop:
    - new 'Фоны' filter button between Рамки and Титулы
    - _renderItemPreview type='background' draws a real 56-aspect swatch
      (same CSS as the page bg — what you see is what you apply)
    - _isItemActive matches by slug for background type
    - free items (price===0) treated as auto-owned in render so users
      can apply them without a fake 'purchase' step

Verified: getMyActive returns { background: { slug: 'nebula' } } after
flipping users.active_background; activate path updates the row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 21:13:53 +03:00

1396 lines
47 KiB
CSS
Raw 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: 320px;
max-height: 420px;
overflow-y: auto;
background: #fff;
border: 1.5px solid var(--border-h);
border-radius: 16px;
box-shadow: 0 12px 48px rgba(15,23,42,0.14);
z-index: 1000;
display: none;
font-size: 0.82rem;
color: var(--text-2);
}
.notif-drop.open { display: block; }
.notif-drop-header { display: flex; align-items: center; justify-content: space-between; padding: 13px 16px 9px; border-bottom: 1px solid var(--border); }
.notif-drop-title { font-family: 'Unbounded', sans-serif; font-size: 0.77rem; font-weight: 800; }
.notif-read-all { background: none; border: none; font-size: 0.72rem; color: var(--violet); cursor: pointer; font-family: 'Manrope', sans-serif; font-weight: 600; padding: 0; }
.notif-read-all:hover { text-decoration: underline; }
.notif-item { display: flex; gap: 9px; padding: 10px 14px; border-bottom: 1px solid var(--border); cursor: pointer; text-decoration: none; color: inherit; transition: background var(--tr); }
.notif-item:last-child { border-bottom: none; }
.notif-item:hover { background: rgba(155,93,229,0.04); }
.notif-item.unread { background: rgba(155,93,229,0.06); }
.notif-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--violet); flex-shrink: 0; margin-top: 5px; }
.notif-dot.read { background: transparent; border: 1.5px solid var(--border-h); }
.notif-msg { font-size: 0.79rem; line-height: 1.4; flex: 1; }
.notif-time { font-size: 0.68rem; color: var(--text-3); margin-top: 2px; }
.notif-empty { padding: 26px 16px; text-align: center; color: var(--text-3); font-size: 0.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: -1;
pointer-events: none;
overflow: hidden;
will-change: opacity, transform, background-position;
transition: opacity .4s ease;
}
.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;
}
/* ── 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;
}
}