samarthya-bot 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +340 -353
  3. package/backend/bin/samarthya.js +29 -10
  4. package/backend/config/constants.js +4 -4
  5. package/backend/controllers/chatController.js +72 -0
  6. package/backend/controllers/telegramController.js +8 -0
  7. package/backend/package.json +1 -1
  8. package/backend/public/assets/index-DiMx9ERJ.css +1 -0
  9. package/backend/public/assets/index-Dn5WYZTH.js +32 -0
  10. package/backend/public/index.html +186 -16
  11. package/backend/public/robots.txt +32 -0
  12. package/backend/public/sitemap.xml +39 -0
  13. package/backend/services/agent/commandService.js +168 -0
  14. package/backend/services/llm/llmService.js +8 -0
  15. package/backend/services/planner/plannerService.js +17 -0
  16. package/backend/services/security/sandboxService.js +11 -4
  17. package/backend/services/system/platform.js +150 -0
  18. package/backend/services/tools/toolRegistry.js +521 -36
  19. package/backend/services/worker/workerClient.js +141 -29
  20. package/package.json +1 -1
  21. package/backend/public/assets/index-6PCzI3K2.js +0 -40
  22. package/backend/public/assets/index-6TF5jVRQ.js +0 -149
  23. package/backend/public/assets/index-B0U7rt6f.js +0 -46
  24. package/backend/public/assets/index-BF0RZh9i.js +0 -149
  25. package/backend/public/assets/index-BFRAq8Y1.js +0 -149
  26. package/backend/public/assets/index-CGw8cc8z.js +0 -149
  27. package/backend/public/assets/index-Ckf0GO1B.css +0 -1
  28. package/backend/public/assets/index-Cx0Ei-z7.js +0 -149
  29. package/backend/public/assets/index-DIPdcLv-.js +0 -25
  30. package/backend/public/assets/index-Da1E-MYB.js +0 -53
  31. package/backend/public/assets/index-DdCKkq38.js +0 -149
  32. package/backend/public/assets/index-Do4jNsZS.js +0 -19
  33. package/backend/public/assets/index-DyjpBYmS.js +0 -51
  34. package/backend/public/assets/index-DzlXcaXT.js +0 -149
  35. package/backend/public/assets/index-J7XSVHCz.css +0 -1
  36. package/backend/public/assets/index-Ui-pyZvK.js +0 -25
  37. package/backend/public/assets/index-kzffNwzo.js +0 -149
@@ -4,6 +4,24 @@ const path = require('path');
4
4
  const fs = require('fs');
5
5
  const readline = require('readline');
6
6
 
7
+ let platform;
8
+ try {
9
+ platform = require('../services/system/platform');
10
+ } catch (_) {
11
+ // Minimal inline fallback so the CLI still works if the module is missing.
12
+ const isWin = process.platform === 'win32';
13
+ platform = {
14
+ isWindows: isWin,
15
+ killPortCommand: (p) => isWin
16
+ ? `for /f "tokens=5" %a in ('netstat -ano ^| findstr :${p} ^| findstr LISTENING') do taskkill /PID %a /F`
17
+ : `lsof -t -i:${p} | xargs kill -9 2>/dev/null || fuser -k ${p}/tcp 2>/dev/null || true`,
18
+ };
19
+ }
20
+
21
+ const isWin = process.platform === 'win32';
22
+ // `npm` is a .cmd shim on Windows and must be run through a shell.
23
+ const NPM = isWin ? 'npm.cmd' : 'npm';
24
+
7
25
  const args = process.argv.slice(2);
8
26
  const command = args[0];
9
27
  const backendDir = path.join(__dirname, '..');
@@ -370,7 +388,10 @@ switch (command) {
370
388
  const spin2 = new Spinner('Installing dependencies...');
371
389
  spin2.start();
372
390
  try {
373
- execSync('npm install --production 2>/dev/null', { cwd: backendDir, stdio: 'ignore' });
391
+ // stdio:'ignore' already silences output no shell-specific redirection
392
+ // (`2>/dev/null` is invalid on Windows cmd). `--omit=dev` replaces the
393
+ // deprecated `--production` flag and works on all platforms.
394
+ execSync(`${NPM} install --omit=dev`, { cwd: backendDir, stdio: 'ignore', shell: true });
374
395
  spin2.stop('Dependencies installed');
375
396
  } catch {
376
397
  spin2.fail('Dependencies install failed (non-critical)');
@@ -409,7 +430,7 @@ switch (command) {
409
430
  // Load .env and spawn the server
410
431
  try { require('dotenv').config({ path: envPath }); } catch (e) { }
411
432
 
412
- const gatewayChild = spawn('node', ['server.js'], {
433
+ const gatewayChild = spawn(process.execPath, ['server.js'], {
413
434
  cwd: backendDir,
414
435
  stdio: 'inherit'
415
436
  });
@@ -422,9 +443,8 @@ switch (command) {
422
443
  setTimeout(() => {
423
444
  if (envVars['TELEGRAM_BOT_TOKEN'] && envVars['TELEGRAM_BOT_TOKEN'] !== 'dummy') {
424
445
  info('Auto-starting tunnel for Telegram webhook...');
425
- const isWin = process.platform === 'win32';
426
- const tunnelChild = spawn('npm', ['exec', 'localtunnel', '--', '--port', '5000'], {
427
- stdio: 'pipe', shell: isWin
446
+ const tunnelChild = spawn(NPM, ['exec', 'localtunnel', '--', '--port', '5000'], {
447
+ stdio: 'pipe', shell: true
428
448
  });
429
449
  tunnelChild.stdout.on('data', async (data) => {
430
450
  const output = data.toString();
@@ -651,7 +671,7 @@ switch (command) {
651
671
  console.log(` ${c.dim}Provider: ${c.saffron}${prov}${c.reset} ${c.dim}(${mod})${c.reset}`);
652
672
  console.log(` ${c.dim}Dashboard: ${c.cyan}${c.under}http://localhost:5000${c.reset}\n`);
653
673
 
654
- const child = spawn('node', ['server.js'], {
674
+ const child = spawn(process.execPath, ['server.js'], {
655
675
  cwd: backendDir,
656
676
  stdio: 'inherit'
657
677
  });
@@ -698,9 +718,8 @@ switch (command) {
698
718
  const spin = new Spinner('Connecting to tunnel service...');
699
719
  spin.start();
700
720
 
701
- const isWin = process.platform === 'win32';
702
- const tunnelProcess = spawn('npm', ['exec', 'localtunnel', '--', '--port', '5000'], {
703
- stdio: 'pipe', shell: isWin
721
+ const tunnelProcess = spawn(NPM, ['exec', 'localtunnel', '--', '--port', '5000'], {
722
+ stdio: 'pipe', shell: true
704
723
  });
705
724
 
706
725
  tunnelProcess.stdout.on('data', async (data) => {
@@ -801,7 +820,7 @@ switch (command) {
801
820
 
802
821
  setTimeout(() => {
803
822
  spin.stop('Restarted');
804
- const restartChild = spawn('node', ['server.js'], {
823
+ const restartChild = spawn(process.execPath, ['server.js'], {
805
824
  cwd: backendDir,
806
825
  stdio: 'inherit'
807
826
  });
@@ -5,25 +5,25 @@ module.exports = {
5
5
  student: {
6
6
  name: '🎓 Student Pack',
7
7
  nameHi: '🎓 स्टूडेंट पैक',
8
- tools: ['web_search', 'summarize_text', 'note_take', 'calculate', 'reminder_set', 'file_read', 'file_write', 'file_list', 'weather_info', 'run_command', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
8
+ tools: ['web_search', 'summarize_text', 'note_take', 'calculate', 'reminder_set', 'file_read', 'file_write', 'file_list', 'weather_info', 'run_command', 'open_path', 'http_request', 'translate_text', 'currency_convert', 'timezone_now', 'qr_generate', 'base64_tool', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
9
9
  description: 'Assignment summary, notes, exam reminders, calculations'
10
10
  },
11
11
  business: {
12
12
  name: '🏢 Small Business Pack',
13
13
  nameHi: '🏢 बिज़नेस पैक',
14
- tools: ['web_search', 'send_email', 'calendar_schedule', 'gst_reminder', 'calculate', 'file_read', 'file_write', 'file_list', 'upi_generate', 'note_take', 'reminder_set', 'weather_info', 'system_info', 'run_command', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
14
+ tools: ['web_search', 'send_email', 'calendar_schedule', 'gst_reminder', 'calculate', 'file_read', 'file_write', 'file_list', 'upi_generate', 'qr_generate', 'currency_convert', 'note_take', 'reminder_set', 'weather_info', 'system_info', 'run_command', 'open_path', 'http_request', 'password_generate', 'url_shorten', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
15
15
  description: 'GST reminders, email, UPI links, file management'
16
16
  },
17
17
  developer: {
18
18
  name: '👨‍💻 Developer Pack',
19
19
  nameHi: '👨‍💻 डेवलपर पैक',
20
- tools: ['web_search', 'file_read', 'file_write', 'file_list', 'calculate', 'send_email', 'run_command', 'system_info', 'browser_action', 'ssh_deploy', 'note_take', 'reminder_set', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
20
+ tools: ['web_search', 'file_read', 'file_write', 'file_list', 'calculate', 'send_email', 'run_command', 'devops_execute_stream', 'open_path', 'http_request', 'password_generate', 'hash_text', 'base64_tool', 'url_shorten', 'ip_geolocate', 'crypto_price', 'system_info', 'browser_action', 'ssh_deploy', 'note_take', 'reminder_set', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
21
21
  description: 'Shell commands, file ops, system info, browser control'
22
22
  },
23
23
  personal: {
24
24
  name: '🏠 Personal Pack',
25
25
  nameHi: '🏠 पर्सनल पैक',
26
- tools: ['web_search', 'reminder_set', 'note_take', 'calculate', 'weather_info', 'file_read', 'file_write', 'file_list', 'calendar_schedule', 'upi_generate', 'send_email', 'system_info', 'browser_action', 'run_command', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
26
+ tools: ['web_search', 'reminder_set', 'note_take', 'calculate', 'weather_info', 'file_read', 'file_write', 'file_list', 'calendar_schedule', 'upi_generate', 'qr_generate', 'send_email', 'system_info', 'browser_action', 'run_command', 'open_path', 'http_request', 'password_generate', 'translate_text', 'currency_convert', 'crypto_price', 'url_shorten', 'ip_geolocate', 'timezone_now', 'clipboard_copy', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
27
27
  description: 'Weather, notes, reminders, UPI, email, files'
28
28
  }
29
29
  },
@@ -3,6 +3,71 @@ const User = require('../models/User');
3
3
  const plannerService = require('../services/planner/plannerService');
4
4
  const securityService = require('../services/security/securityService');
5
5
 
6
+ /**
7
+ * Handle a message arriving from an external channel (Discord, etc.).
8
+ * Resolves/creates a per-channel user, routes through the agentic planner
9
+ * (so slash-commands like /help, /status, /new all work), and persists the
10
+ * conversation. Returns the assistant's text reply.
11
+ */
12
+ exports.handleExternalMessage = async (message, externalUserId, channel = 'external') => {
13
+ try {
14
+ if (!message || !message.trim()) return '🤔 Empty message.';
15
+
16
+ const email = `${channel}_${externalUserId}@samarthya.local`;
17
+ let user = await User.findOne({ email });
18
+ if (!user) {
19
+ user = await User.create({
20
+ name: `${channel} user`,
21
+ email,
22
+ password: `${channel}_user`,
23
+ language: 'hinglish',
24
+ workType: 'personal',
25
+ activePack: 'personal',
26
+ source: channel
27
+ });
28
+ }
29
+
30
+ // Most recent active conversation for this channel (last 24h)
31
+ let conversation = await Conversation.findOne({
32
+ userId: user._id,
33
+ source: channel,
34
+ isActive: { $ne: false },
35
+ updatedAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
36
+ }).sort({ updatedAt: -1 });
37
+
38
+ const previousMessages = conversation?.messages?.slice(-6)?.map(m => ({
39
+ role: m.role, content: m.content
40
+ })) || [];
41
+
42
+ const result = await plannerService.processMessage(user, previousMessages, message);
43
+
44
+ // Honour a /new (reset) slash-command: deactivate the old thread so the
45
+ // next message starts a fresh conversation.
46
+ if (result.command?.action === 'new_conversation') {
47
+ if (conversation) { conversation.isActive = false; await conversation.save(); }
48
+ return result.response;
49
+ }
50
+
51
+ if (!conversation) {
52
+ conversation = new Conversation({ userId: user._id, title: message.substring(0, 40), source: channel, messages: [] });
53
+ }
54
+ conversation.messages.push({ role: 'user', content: message });
55
+ conversation.messages.push({
56
+ role: 'assistant',
57
+ content: result.response,
58
+ toolCalls: result.toolCalls,
59
+ language: result.language,
60
+ metadata: { tokensUsed: result.tokensUsed, model: result.model }
61
+ });
62
+ await conversation.save();
63
+
64
+ return result.response;
65
+ } catch (error) {
66
+ console.error(`External message (${channel}) error:`, error.message);
67
+ return '❌ Kuch error aa gaya processing mein. Please try again.';
68
+ }
69
+ };
70
+
6
71
  // Send message and get AI response
7
72
  exports.sendMessage = async (req, res) => {
8
73
  try {
@@ -63,6 +128,12 @@ exports.sendMessage = async (req, res) => {
63
128
  }
64
129
  });
65
130
 
131
+ // Honour a /new (reset) slash-command on the web channel: deactivate the
132
+ // current conversation so the UI knows to open a fresh one.
133
+ if (result.command?.action === 'new_conversation') {
134
+ conversation.isActive = false;
135
+ }
136
+
66
137
  await conversation.save();
67
138
 
68
139
  // Emit via socket if available
@@ -82,6 +153,7 @@ exports.sendMessage = async (req, res) => {
82
153
  res.json({
83
154
  success: true,
84
155
  conversationId: conversation._id,
156
+ command: result.command || null,
85
157
  message: {
86
158
  role: 'assistant',
87
159
  content: result.response,
@@ -70,6 +70,7 @@ exports.handleMessage = async (req, res) => {
70
70
  let conversation = await Conversation.findOne({
71
71
  userId: user._id,
72
72
  source: 'telegram',
73
+ isActive: { $ne: false },
73
74
  updatedAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
74
75
  }).sort({ updatedAt: -1 });
75
76
 
@@ -97,6 +98,13 @@ exports.handleMessage = async (req, res) => {
97
98
 
98
99
  await telegramService.sendMessage(chatId, replyText);
99
100
 
101
+ // Honour a /new (reset) slash-command: deactivate the current thread so
102
+ // the next message starts fresh, and skip saving into the old one.
103
+ if (result.command?.action === 'new_conversation') {
104
+ if (conversation) { conversation.isActive = false; await conversation.save(); }
105
+ return;
106
+ }
107
+
100
108
  // Save conversation
101
109
  if (!conversation) {
102
110
  conversation = new Conversation({ userId: user._id, title: text.substring(0, 20), source: 'telegram', messages: [] });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "samarthya-agent",
3
- "version": "2.1.1",
3
+ "version": "2.3.0",
4
4
  "main": "server.js",
5
5
  "bin": {
6
6
  "samarthya": "./bin/samarthya.js",
@@ -0,0 +1 @@
1
+ :root{--bg-primary: #06060a;--bg-secondary: #0e0e14;--bg-tertiary: #12121c;--bg-card: #0e0e16;--bg-hover: #14141e;--bg-input: #0a0a12;--accent-saffron: #FF9933;--accent-saffron-glow: rgba(255, 153, 51, .25);--accent-navy: #000080;--accent-green: #138808;--accent-green-glow: rgba(19, 136, 8, .2);--accent-primary: #FF9933;--accent-secondary: #e68a2e;--accent-white: #ffffff;--accent-gold: #f5c842;--accent-teal: #14b8a6;--accent-rose: #f43f5e;--accent-indigo: #6366f1;--gradient-primary: linear-gradient(135deg, #FF9933 0%, #e68a2e 50%, #f5c842 100%);--gradient-indian: linear-gradient(135deg, #FF9933 0%, #ffffff 50%, #138808 100%);--gradient-navy: linear-gradient(135deg, #000080 0%, #1a1a6e 100%);--gradient-glass: linear-gradient(135deg, rgba(255, 153, 51, .06) 0%, rgba(19, 136, 8, .03) 100%);--gradient-dark: linear-gradient(180deg, #0D0D0D 0%, #111118 100%);--gradient-hero: linear-gradient(135deg, rgba(255, 153, 51, .08) 0%, rgba(0, 0, 128, .05) 50%, rgba(19, 136, 8, .04) 100%);--gradient-card-hover: linear-gradient(135deg, rgba(255, 153, 51, .1) 0%, rgba(255, 153, 51, .02) 100%);--text-primary: #f0f0ff;--text-secondary: #8888a8;--text-muted: #3a3a4e;--text-accent: #FF9933;--border-primary: rgba(255, 153, 51, .18);--border-subtle: #1e1e28;--border-hover: rgba(255, 153, 51, .4);--shadow-sm: 0 2px 8px rgba(0, 0, 0, .4);--shadow-md: 0 4px 20px rgba(0, 0, 0, .5);--shadow-lg: 0 8px 40px rgba(0, 0, 0, .6);--shadow-glow: 0 0 24px rgba(255, 153, 51, .12);--shadow-glow-strong: 0 0 48px rgba(255, 153, 51, .22);--shadow-green-glow: 0 0 20px rgba(19, 136, 8, .15);--font-primary: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;--font-hindi: "Noto Sans Devanagari", "Inter", sans-serif;--radius-sm: 8px;--radius-md: 12px;--radius-lg: 16px;--radius-xl: 24px;--radius-full: 9999px;--transition-fast: .15s cubic-bezier(.4, 0, .2, 1);--transition-normal: .25s cubic-bezier(.4, 0, .2, 1);--transition-slow: .4s cubic-bezier(.4, 0, .2, 1);--transition-spring: .5s cubic-bezier(.34, 1.56, .64, 1)}*,*:before,*:after{margin:0;padding:0;box-sizing:border-box}html{font-size:16px;scroll-behavior:smooth;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{font-family:var(--font-primary);background:var(--bg-primary);color:var(--text-primary);min-height:100vh;overflow-x:hidden;line-height:1.6;position:relative}body:before{content:"";position:fixed;inset:0;z-index:-2;pointer-events:none;background:radial-gradient(40% 50% at 12% 8%,rgba(255,153,51,.1),transparent 70%),radial-gradient(45% 55% at 88% 12%,rgba(99,102,241,.08),transparent 70%),radial-gradient(50% 60% at 50% 100%,rgba(19,136,8,.08),transparent 70%);background-size:200% 200%;animation:auroraDrift 24s ease-in-out infinite}body:after{content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;background-image:linear-gradient(rgba(255,255,255,.015) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.015) 1px,transparent 1px);background-size:44px 44px;mask-image:radial-gradient(ellipse 80% 60% at 50% 0%,#000 30%,transparent 80%);-webkit-mask-image:radial-gradient(ellipse 80% 60% at 50% 0%,#000 30%,transparent 80%)}@keyframes auroraDrift{0%,to{background-position:0% 0%,100% 0%,50% 100%}50%{background-position:20% 30%,70% 40%,40% 70%}}@media(prefers-reduced-motion:reduce){body:before{animation:none}}#root{min-height:100vh;display:flex;flex-direction:column;position:relative;z-index:0}::-webkit-scrollbar{width:5px;height:5px}::-webkit-scrollbar-track{background:var(--bg-primary)}::-webkit-scrollbar-thumb{background:#f933;border-radius:var(--radius-full)}::-webkit-scrollbar-thumb:hover{background:var(--accent-saffron)}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeInDown{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}@keyframes slideInLeft{0%{opacity:0;transform:translate(-24px)}to{opacity:1;transform:translate(0)}}@keyframes slideInRight{0%{opacity:0;transform:translate(24px)}to{opacity:1;transform:translate(0)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@keyframes shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}@keyframes float{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes glow{0%,to{box-shadow:0 0 5px #ff993326}50%{box-shadow:0 0 25px #ff993359}}@keyframes typing{0%,60%,to{opacity:.3;transform:scale(.8)}30%{opacity:1;transform:scale(1)}}@keyframes ripple{0%{transform:scale(0);opacity:.5}to{transform:scale(4);opacity:0}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes bounceIn{0%{transform:scale(.3);opacity:0}50%{transform:scale(1.05)}70%{transform:scale(.9)}to{transform:scale(1);opacity:1}}@keyframes typewriter{0%{width:0}to{width:100%}}@keyframes blink{50%{border-color:transparent}}@keyframes gradientShift{0%{background-position:0% 50%}50%{background-position:100% 50%}to{background-position:0% 50%}}@keyframes scaleIn{0%{transform:scale(.9);opacity:0}to{transform:scale(1);opacity:1}}@keyframes slideUp{0%{transform:translateY(40px);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes voicePulse{0%{box-shadow:0 0 #ff993380}70%{box-shadow:0 0 0 20px #f930}to{box-shadow:0 0 #f930}}@keyframes pulseAlarm{0%{transform:scale(1);box-shadow:0 0 20px #ef444466}to{transform:scale(1.02);box-shadow:0 0 40px #f939}}@keyframes ringAlarm{0%{transform:rotate(0)}25%{transform:rotate(15deg)}50%{transform:rotate(-15deg)}75%{transform:rotate(10deg)}to{transform:rotate(-10deg)}}.animate-fade-in{animation:fadeIn .4s ease-out}.animate-fade-in-up{animation:fadeInUp .5s ease-out}.animate-slide-left{animation:slideInLeft .4s ease-out}.animate-slide-right{animation:slideInRight .4s ease-out}.animate-bounce-in{animation:bounceIn .6s ease-out}.animate-scale-in{animation:scaleIn .3s ease-out}.glass{background:#1a1a2e8c;backdrop-filter:blur(24px) saturate(140%);-webkit-backdrop-filter:blur(24px) saturate(140%);border:1px solid var(--border-subtle);box-shadow:inset 0 1px #ffffff0a,var(--shadow-md)}.glass-strong{background:#0d0d0dc7;backdrop-filter:blur(40px) saturate(150%);-webkit-backdrop-filter:blur(40px) saturate(150%);border:1px solid var(--border-primary);box-shadow:inset 0 1px #ffffff0d,var(--shadow-lg)}.gradient-text-animated{background:linear-gradient(90deg,#f93,#f5c842,#138808,#f93);background-size:300% 100%;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;animation:gradientShift 6s ease infinite}.lift{transition:transform var(--transition-normal),box-shadow var(--transition-normal),border-color var(--transition-normal)}.lift:hover{transform:translateY(-3px);box-shadow:var(--shadow-lg),var(--shadow-glow);border-color:var(--border-hover)}.gradient-text{background:var(--gradient-primary);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}.gradient-text-indian{background:var(--gradient-indian);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:12px 24px;border:none;border-radius:var(--radius-md);font-family:var(--font-primary);font-size:.9rem;font-weight:600;cursor:pointer;transition:all var(--transition-normal);position:relative;overflow:hidden;letter-spacing:.01em}.btn:after{content:"";position:absolute;width:100%;height:100%;top:0;left:0;background:radial-gradient(circle,rgba(255,255,255,.12) 10%,transparent 60%);transform:scale(0);opacity:0;transition:all .5s}.btn:active:after{transform:scale(4);opacity:0;transition:0s}.btn:before{content:"";position:absolute;top:0;left:-75%;width:50%;height:100%;background:linear-gradient(120deg,transparent,rgba(255,255,255,.25),transparent);transform:skew(-20deg);transition:left .6s ease;pointer-events:none}.btn:hover:before{left:130%}.btn-primary{background:var(--gradient-primary);background-size:150% 150%;color:#0d0d0d;box-shadow:var(--shadow-glow)}.btn-primary:hover{box-shadow:var(--shadow-glow-strong);transform:translateY(-2px);background-position:100% 0}.btn-secondary{background:var(--bg-card);color:var(--text-primary);border:1px solid var(--border-primary)}.btn-secondary:hover{background:var(--bg-hover);border-color:var(--border-hover);transform:translateY(-1px)}.btn-ghost{background:transparent;color:var(--text-secondary)}.btn-ghost:hover{background:var(--bg-hover);color:var(--text-primary)}.input{width:100%;padding:12px 16px;background:var(--bg-input);border:1px solid var(--border-subtle);border-radius:var(--radius-md);color:var(--text-primary);font-family:var(--font-primary);font-size:.9375rem;transition:all var(--transition-normal);outline:none}.input:focus{border-color:var(--accent-saffron);box-shadow:0 0 0 3px var(--accent-saffron-glow)}.input::placeholder{color:var(--text-muted)}.badge{display:inline-flex;align-items:center;gap:4px;padding:4px 12px;border-radius:var(--radius-full);font-size:.75rem;font-weight:600;letter-spacing:.02em}.badge-low{background:#1388081f;color:#138808;border:1px solid rgba(19,136,8,.25)}.badge-medium{background:#ff99331f;color:#f93;border:1px solid rgba(255,153,51,.25)}.badge-high{background:#ef44441f;color:#ef4444;border:1px solid rgba(239,68,68,.25)}.badge-critical{background:#f43f5e1f;color:#f43f5e;border:1px solid rgba(244,63,94,.25);animation:glow 2s infinite}@media(max-width:768px){html{font-size:14px}}@media(max-width:480px){html{font-size:13px}}