Files
Learn_System/frontend/css/ls.css
T
Maxim Dolgolyov 660e7e2747 feat(gamification): Phase 1 — full kill-switch + textbook XP wrapping
Until now the 'gamification' feature flag did nothing: it had no row in
app_settings, the admin couldn't toggle it, awardXP/awardCoins ignored
it, and the CSS only hid three dashboard widgets — XP bars in textbooks
stayed visible regardless.

Phase 1 closes every hole.

Backend (source of truth):
  • migration 029 seeds feature_gamification_enabled=1
  • new isGamificationEnabled() helper in gamification/_shared.js with a
    30s cache + invalidateGamificationCache() for instant admin toggles
  • awardXP / awardCoins / updateStreak / unlockAchievement /
    checkAchievements all bail out when the flag is off
  • /api/gamification/* and /api/shop/* (user routes) return 404 when
    disabled; admin routes remain open so the switch itself is reachable
  • adminController.updateFeatures gains 'gamification' in the allow-list
    and invalidates the cache on flip

Frontend:
  • LS.isGamificationEnabled() (synchronous, populated by loadFeatures)
    so xp.js + applyCosmetics can bail without a round-trip
  • xp.js load/add/flush become no-ops when the flag is off
  • applyCosmetics skips the round-trip when off
  • CSS .no-gamification rule expanded to cover .hero-xp-badge, .po-xp,
    .xp-card, .xp-bar, #frames-section, and a universal [data-gamified]
    hook for future blocks

Textbooks (Variant 2 of the plan):
  • backend/scripts/wrap_textbook_xp.py — idempotent script that adds
    data-gamified to 167 XP tags across 63 textbook files (chapters +
    hubs, all subjects/grades). Single CSS rule now hides everything.

Verified end-to-end: with the flag off, awardXP/awardCoins write nothing;
flipping back restores normal behavior.

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

1197 lines
40 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); }