morpheus-cli 0.1.6 → 0.1.8

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.
package/README.md CHANGED
@@ -115,6 +115,9 @@ When enabled:
115
115
  ### 🧩 MCP Support (Model Context Protocol)
116
116
  Full integration with [Model Context Protocol](https://modelcontextprotocol.io/), allowing Morpheus to use standardized tools from any MCP-compatible server.
117
117
 
118
+ ### 📊 Usage Analytics
119
+ Track your token usage across different providers and models directly from the Web UI. View detailed breakdowns of input/output tokens and message counts to monitor costs and activity.
120
+
118
121
  ### 🎙️ Audio Transcription (Telegram)
119
122
  Send voice messages directly to the Telegram bot. Morpheus will:
120
123
  1. Transcribe the audio using **Google Gemini**.
@@ -213,6 +216,10 @@ Morpheus supports external tools via **MCP (Model Context Protocol)**. Configure
213
216
  "COOLIFY_URL": "https://app.coolify.io",
214
217
  "COOLIFY_TOKEN": "your-token"
215
218
  }
219
+ },
220
+ "coingecko": {
221
+ "transport": "http",
222
+ "url": "https://mcps.mnunes.xyz/coingecko/mcp"
216
223
  }
217
224
  }
218
225
  ```
@@ -7,6 +7,7 @@ import os from 'os';
7
7
  import { ConfigManager } from '../config/manager.js';
8
8
  import { DisplayManager } from '../runtime/display.js';
9
9
  import { AudioAgent } from '../runtime/audio-agent.js';
10
+ import { convert } from 'telegram-markdown-v2';
10
11
  export class TelegramAdapter {
11
12
  bot = null;
12
13
  isConnected = false;
@@ -22,6 +23,10 @@ export class TelegramAdapter {
22
23
  this.display.log('Telegram adapter already connected.', { source: 'Telegram', level: 'warning' });
23
24
  return;
24
25
  }
26
+ const escapeMarkdownV2 = (text) => {
27
+ // Regex matches all special characters requiring escaping
28
+ return convert(text);
29
+ };
25
30
  try {
26
31
  this.display.log('Connecting to Telegram...', { source: 'Telegram' });
27
32
  this.bot = new Telegraf(token);
package/dist/cli/index.js CHANGED
@@ -40,3 +40,7 @@ export async function cli() {
40
40
  program.addCommand(doctorCommand);
41
41
  program.parse(process.argv);
42
42
  }
43
+ // Support direct execution via tsx
44
+ if (import.meta.url.startsWith('file:') && (process.argv[1]?.endsWith('index.ts') || process.argv[1]?.endsWith('cli/index.js'))) {
45
+ cli();
46
+ }
@@ -45,13 +45,22 @@ export const ConfigSchema = z.object({
45
45
  retention: z.string().default(DEFAULT_CONFIG.logging.retention),
46
46
  }).default(DEFAULT_CONFIG.logging),
47
47
  });
48
- export const MCPServerConfigSchema = z.object({
49
- transport: z.enum(['stdio', 'http']),
50
- command: z.string().min(1, 'Command is required'),
51
- args: z.array(z.string()).optional().default([]),
52
- env: z.record(z.string(), z.string()).optional().default({}),
53
- _comment: z.string().optional(),
54
- });
48
+ export const MCPServerConfigSchema = z.discriminatedUnion('transport', [
49
+ z.object({
50
+ transport: z.literal('stdio'),
51
+ command: z.string().min(1, 'Command is required for stdio transport'),
52
+ args: z.array(z.string()).optional().default([]),
53
+ env: z.record(z.string(), z.string()).optional().default({}),
54
+ _comment: z.string().optional(),
55
+ }),
56
+ z.object({
57
+ transport: z.literal('http'),
58
+ url: z.string().url('Valid URL is required for http transport'),
59
+ args: z.array(z.string()).optional().default([]),
60
+ env: z.record(z.string(), z.string()).optional().default({}),
61
+ _comment: z.string().optional(),
62
+ }),
63
+ ]);
55
64
  export const MCPConfigFileSchema = z.record(z.string(), z.union([
56
65
  MCPServerConfigSchema,
57
66
  z.string(),
package/dist/http/api.js CHANGED
@@ -51,6 +51,17 @@ export function createApiRouter() {
51
51
  res.status(500).json({ error: error.message });
52
52
  }
53
53
  });
54
+ router.get('/stats/usage/grouped', async (req, res) => {
55
+ try {
56
+ const history = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
57
+ const stats = await history.getUsageStatsByProviderAndModel();
58
+ history.close();
59
+ res.json(stats);
60
+ }
61
+ catch (error) {
62
+ res.status(500).json({ error: error.message });
63
+ }
64
+ });
54
65
  // Calculate diff between two objects
55
66
  const getDiff = (obj1, obj2, prefix = '') => {
56
67
  const changes = [];
@@ -6,51 +6,45 @@ import { AIMessage } from "@langchain/core/messages";
6
6
  import * as fs from "fs-extra";
7
7
  import * as path from "path";
8
8
  import { tmpdir } from "os";
9
- import { homedir } from "os";
9
+ import { ToolsFactory } from "../tools/factory.js";
10
10
  vi.mock("../providers/factory.js");
11
+ vi.mock("../tools/factory.js");
11
12
  describe("Agent Persistence Integration", () => {
12
13
  let agent;
13
14
  let testDbPath;
15
+ let tempDir;
14
16
  const mockProvider = {
15
17
  invoke: vi.fn(),
16
18
  };
17
19
  beforeEach(async () => {
18
20
  vi.resetAllMocks();
19
- // Clean up by clearing all data from the database (safer than deleting the locked file)
20
- const defaultDbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
21
- if (fs.existsSync(defaultDbPath)) {
22
- try {
23
- const Database = (await import("better-sqlite3")).default;
24
- const db = new Database(defaultDbPath);
25
- db.exec("DELETE FROM messages");
26
- db.close();
27
- }
28
- catch (err) {
29
- // Ignore errors if database doesn't exist or is corrupted
30
- }
31
- }
32
- // Create a temporary test database path
33
- const tempDir = path.join(tmpdir(), "morpheus-test-agent", Date.now().toString());
21
+ // Create a unique temporary test database path for each test to avoid interference
22
+ tempDir = path.join(tmpdir(), "morpheus-test-agent", Date.now().toString() + Math.random().toString(36).substring(7));
34
23
  testDbPath = path.join(tempDir, "short-memory.db");
35
- // Mock the SQLiteChatMessageHistory to use test path
36
- // We'll use the default ~/.morpheus path for this test
37
- mockProvider.invoke.mockResolvedValue(new AIMessage("Test response"));
24
+ mockProvider.invoke.mockImplementation(async ({ messages }) => {
25
+ return {
26
+ messages: [...messages, new AIMessage("Test response")]
27
+ };
28
+ });
38
29
  vi.mocked(ProviderFactory.create).mockResolvedValue(mockProvider);
39
- agent = new Agent(DEFAULT_CONFIG);
30
+ vi.mocked(ToolsFactory.create).mockResolvedValue([]);
31
+ agent = new Agent(DEFAULT_CONFIG, { databasePath: testDbPath });
40
32
  });
41
33
  afterEach(async () => {
42
- // Clean up test database
43
- const defaultDbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
44
- if (fs.existsSync(defaultDbPath)) {
45
- // We can't delete it if it's open, so we'll just clear it
46
- // The agent should have closed the connection
34
+ // Clean up temporary test directory
35
+ if (fs.existsSync(tempDir)) {
36
+ try {
37
+ fs.removeSync(tempDir);
38
+ }
39
+ catch (err) {
40
+ // Ignore removal errors if file is locked
41
+ }
47
42
  }
48
43
  });
49
44
  describe("Database File Creation", () => {
50
45
  it("should create database file on initialization", async () => {
51
46
  await agent.initialize();
52
- const defaultDbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
53
- expect(fs.existsSync(defaultDbPath)).toBe(true);
47
+ expect(fs.existsSync(testDbPath)).toBe(true);
54
48
  });
55
49
  });
56
50
  describe("Message Persistence", () => {
@@ -70,8 +64,8 @@ describe("Agent Persistence Integration", () => {
70
64
  await agent.chat("Remember this message");
71
65
  const firstHistory = await agent.getHistory();
72
66
  expect(firstHistory).toHaveLength(2);
73
- // Simulate restart: create new agent instance
74
- const agent2 = new Agent(DEFAULT_CONFIG);
67
+ // Simulate restart: create new agent instance with SAME database path
68
+ const agent2 = new Agent(DEFAULT_CONFIG, { databasePath: testDbPath });
75
69
  await agent2.initialize();
76
70
  // Verify history was restored
77
71
  const restoredHistory = await agent2.getHistory();
@@ -86,7 +80,11 @@ describe("Agent Persistence Integration", () => {
86
80
  // First conversation
87
81
  await agent.chat("First message");
88
82
  // Second conversation
89
- mockProvider.invoke.mockResolvedValue(new AIMessage("Second response"));
83
+ mockProvider.invoke.mockImplementation(async ({ messages }) => {
84
+ return {
85
+ messages: [...messages, new AIMessage("Second response")]
86
+ };
87
+ });
90
88
  await agent.chat("Second message");
91
89
  // Verify all messages are persisted
92
90
  const history = await agent.getHistory();
@@ -118,7 +116,11 @@ describe("Agent Persistence Integration", () => {
118
116
  await agent.chat("Old message");
119
117
  await agent.clearMemory();
120
118
  // Add new message
121
- mockProvider.invoke.mockResolvedValue(new AIMessage("New response"));
119
+ mockProvider.invoke.mockImplementation(async ({ messages }) => {
120
+ return {
121
+ messages: [...messages, new AIMessage("New response")]
122
+ };
123
+ });
122
124
  await agent.chat("New message");
123
125
  // Verify only new messages exist
124
126
  const history = await agent.getHistory();
@@ -133,11 +135,15 @@ describe("Agent Persistence Integration", () => {
133
135
  // First message
134
136
  await agent.chat("My name is Alice");
135
137
  // Second message (should have context)
136
- mockProvider.invoke.mockResolvedValue(new AIMessage("Hello Alice!"));
138
+ mockProvider.invoke.mockImplementation(async ({ messages }) => {
139
+ return {
140
+ messages: [...messages, new AIMessage("Hello Alice!")]
141
+ };
142
+ });
137
143
  await agent.chat("What is my name?");
138
144
  // Verify the provider was called with full history
139
145
  const lastCall = mockProvider.invoke.mock.calls[1];
140
- const messagesPassedToLLM = lastCall[0];
146
+ const messagesPassedToLLM = lastCall[0].messages;
141
147
  // Should include: system message, previous user message, previous AI response, new user message
142
148
  expect(messagesPassedToLLM.length).toBeGreaterThanOrEqual(4);
143
149
  // Check that context includes the original message
@@ -10,8 +10,10 @@ export class Agent {
10
10
  config;
11
11
  history;
12
12
  display = DisplayManager.getInstance();
13
- constructor(config) {
13
+ databasePath;
14
+ constructor(config, overrides) {
14
15
  this.config = config || ConfigManager.getInstance().get();
16
+ this.databasePath = overrides?.databasePath;
15
17
  }
16
18
  async initialize() {
17
19
  if (!this.config.llm) {
@@ -32,6 +34,7 @@ export class Agent {
32
34
  // Initialize persistent memory with SQLite
33
35
  this.history = new SQLiteChatMessageHistory({
34
36
  sessionId: "default",
37
+ databasePath: this.databasePath,
35
38
  limit: this.config.memory?.limit || 100, // Fallback purely defensive if config type allows optional
36
39
  });
37
40
  }
@@ -52,6 +55,11 @@ export class Agent {
52
55
  try {
53
56
  this.display.log('Processing message...', { source: 'Agent' });
54
57
  const userMessage = new HumanMessage(message);
58
+ // Inject provider/model metadata for persistence
59
+ userMessage.provider_metadata = {
60
+ provider: this.config.llm.provider,
61
+ model: this.config.llm.model
62
+ };
55
63
  // Attach extra usage (e.g. from Audio) to the user message to be persisted
56
64
  if (extraUsage) {
57
65
  userMessage.usage_metadata = extraUsage;
@@ -134,6 +142,11 @@ export class Agent {
134
142
  await this.history.addMessage(userMessage);
135
143
  // Persist all new intermediate tool calls and responses
136
144
  for (const msg of newGeneratedMessages) {
145
+ // Inject provider/model metadata search interactors
146
+ msg.provider_metadata = {
147
+ provider: this.config.llm.provider,
148
+ model: this.config.llm.model
149
+ };
137
150
  await this.history.addMessage(msg);
138
151
  }
139
152
  this.display.log('Response generated.', { source: 'Agent' });
@@ -86,7 +86,9 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
86
86
  input_tokens INTEGER,
87
87
  output_tokens INTEGER,
88
88
  total_tokens INTEGER,
89
- cache_read_tokens INTEGER
89
+ cache_read_tokens INTEGER,
90
+ provider TEXT,
91
+ model TEXT
90
92
  );
91
93
 
92
94
  CREATE INDEX IF NOT EXISTS idx_messages_session_id
@@ -109,12 +111,16 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
109
111
  'input_tokens',
110
112
  'output_tokens',
111
113
  'total_tokens',
112
- 'cache_read_tokens'
114
+ 'cache_read_tokens',
115
+ 'provider',
116
+ 'model'
113
117
  ];
118
+ const integerColumns = new Set(['input_tokens', 'output_tokens', 'total_tokens', 'cache_read_tokens']);
114
119
  for (const col of newColumns) {
115
120
  if (!columns.has(col)) {
116
121
  try {
117
- this.db.exec(`ALTER TABLE messages ADD COLUMN ${col} INTEGER`);
122
+ const type = integerColumns.has(col) ? 'INTEGER' : 'TEXT';
123
+ this.db.exec(`ALTER TABLE messages ADD COLUMN ${col} ${type}`);
118
124
  }
119
125
  catch (e) {
120
126
  // Ignore error if column already exists (race condition or check failed)
@@ -134,7 +140,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
134
140
  async getMessages() {
135
141
  try {
136
142
  // Fetch new columns
137
- const stmt = this.db.prepare("SELECT type, content, input_tokens, output_tokens, total_tokens, cache_read_tokens FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ?");
143
+ const stmt = this.db.prepare("SELECT type, content, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ?");
138
144
  const rows = stmt.all(this.sessionId, this.limit);
139
145
  return rows.map((row) => {
140
146
  let msg;
@@ -145,6 +151,11 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
145
151
  total_tokens: row.total_tokens || 0,
146
152
  input_token_details: row.cache_read_tokens ? { cache_read: row.cache_read_tokens } : undefined
147
153
  } : undefined;
154
+ // Reconstruct provider metadata
155
+ const provider_metadata = row.provider ? {
156
+ provider: row.provider,
157
+ model: row.model || "unknown"
158
+ } : undefined;
148
159
  switch (row.type) {
149
160
  case "human":
150
161
  msg = new HumanMessage(row.content);
@@ -190,6 +201,9 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
190
201
  if (usage_metadata) {
191
202
  msg.usage_metadata = usage_metadata;
192
203
  }
204
+ if (provider_metadata) {
205
+ msg.provider_metadata = provider_metadata;
206
+ }
193
207
  return msg;
194
208
  });
195
209
  }
@@ -237,6 +251,9 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
237
251
  const outputTokens = usage?.output_tokens ?? null;
238
252
  const totalTokens = usage?.total_tokens ?? null;
239
253
  const cacheReadTokens = usage?.input_token_details?.cache_read ?? usage?.cache_read_tokens ?? null;
254
+ // Extract provider metadata
255
+ const provider = anyMsg.provider_metadata?.provider ?? null;
256
+ const model = anyMsg.provider_metadata?.model ?? null;
240
257
  // Handle special content serialization for Tools
241
258
  let finalContent = "";
242
259
  if (type === 'ai' && (message.tool_calls?.length ?? 0) > 0) {
@@ -259,8 +276,8 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
259
276
  ? message.content
260
277
  : JSON.stringify(message.content);
261
278
  }
262
- const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
263
- stmt.run(this.sessionId, type, finalContent, Date.now(), inputTokens, outputTokens, totalTokens, cacheReadTokens);
279
+ const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
280
+ stmt.run(this.sessionId, type, finalContent, Date.now(), inputTokens, outputTokens, totalTokens, cacheReadTokens, provider, model);
264
281
  }
265
282
  catch (error) {
266
283
  // Check for specific SQLite errors
@@ -294,6 +311,36 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
294
311
  throw new Error(`Failed to get usage stats: ${error}`);
295
312
  }
296
313
  }
314
+ /**
315
+ * Retrieves aggregated usage statistics grouped by provider and model.
316
+ */
317
+ async getUsageStatsByProviderAndModel() {
318
+ try {
319
+ const stmt = this.db.prepare(`SELECT
320
+ provider,
321
+ COALESCE(model, 'unknown') as model,
322
+ SUM(input_tokens) as totalInputTokens,
323
+ SUM(output_tokens) as totalOutputTokens,
324
+ SUM(total_tokens) as totalTokens,
325
+ COUNT(*) as messageCount
326
+ FROM messages
327
+ WHERE provider IS NOT NULL
328
+ GROUP BY provider, COALESCE(model, 'unknown')
329
+ ORDER BY provider, model`);
330
+ const rows = stmt.all();
331
+ return rows.map((row) => ({
332
+ provider: row.provider,
333
+ model: row.model,
334
+ totalInputTokens: row.totalInputTokens || 0,
335
+ totalOutputTokens: row.totalOutputTokens || 0,
336
+ totalTokens: row.totalTokens || 0,
337
+ messageCount: row.messageCount || 0
338
+ }));
339
+ }
340
+ catch (error) {
341
+ throw new Error(`Failed to get grouped usage stats: ${error}`);
342
+ }
343
+ }
297
344
  /**
298
345
  * Clears all messages for the current session from the database.
299
346
  */
@@ -7,7 +7,7 @@ import { createAgent, createMiddleware } from "langchain";
7
7
  // import { MultiServerMCPClient, } from "@langchain/mcp-adapters"; // REMOVED
8
8
  import { z } from "zod";
9
9
  import { DisplayManager } from "../display.js";
10
- import { ConfigQueryTool, ConfigUpdateTool, DiagnosticTool, MessageCountTool, TokenUsageTool } from "../tools/index.js";
10
+ import { ConfigQueryTool, ConfigUpdateTool, DiagnosticTool, MessageCountTool, TokenUsageTool, ProviderModelUsageTool } from "../tools/index.js";
11
11
  export class ProviderFactory {
12
12
  static async create(config, tools = []) {
13
13
  let display = DisplayManager.getInstance();
@@ -72,7 +72,8 @@ export class ProviderFactory {
72
72
  ConfigUpdateTool,
73
73
  DiagnosticTool,
74
74
  MessageCountTool,
75
- TokenUsageTool
75
+ TokenUsageTool,
76
+ ProviderModelUsageTool
76
77
  ];
77
78
  return createAgent({
78
79
  model: model,
@@ -34,6 +34,36 @@ export const MessageCountTool = tool(async ({ timeRange }) => {
34
34
  }).optional(),
35
35
  }),
36
36
  });
37
+ // Tool for querying token usage grouped by provider and model
38
+ export const ProviderModelUsageTool = tool(async () => {
39
+ try {
40
+ const db = new Database(dbPath);
41
+ const query = `
42
+ SELECT
43
+ provider,
44
+ COALESCE(model, 'unknown') as model,
45
+ SUM(input_tokens) as totalInputTokens,
46
+ SUM(output_tokens) as totalOutputTokens,
47
+ SUM(total_tokens) as totalTokens,
48
+ COUNT(*) as messageCount
49
+ FROM messages
50
+ WHERE provider IS NOT NULL
51
+ GROUP BY provider, COALESCE(model, 'unknown')
52
+ ORDER BY provider, model
53
+ `;
54
+ const results = db.prepare(query).all();
55
+ db.close();
56
+ return JSON.stringify(results);
57
+ }
58
+ catch (error) {
59
+ console.error("Error in ProviderModelUsageTool:", error);
60
+ return JSON.stringify({ error: `Failed to get provider usage stats: ${error.message}` });
61
+ }
62
+ }, {
63
+ name: "provider_model_usage",
64
+ description: "Returns token usage statistics grouped by provider and model.",
65
+ schema: z.object({}),
66
+ });
37
67
  // Tool for querying token usage statistics from the database
38
68
  export const TokenUsageTool = tool(async ({ timeRange }) => {
39
69
  try {
@@ -53,15 +53,15 @@ export class ToolsFactory {
53
53
  mcpServers: mcpServers,
54
54
  onConnectionError: "ignore",
55
55
  // log the MCP client's internal events
56
- beforeToolCall: ({ serverName, name, args }) => {
57
- display.log(`MCP Tool Call - Server: ${serverName}, Tool: ${name}, Args: ${JSON.stringify(args)}`, { source: 'MCPServer' });
58
- return;
59
- },
60
- // log the results of tool calls
61
- afterToolCall: (res) => {
62
- display.log(`MCP Tool Result - ${JSON.stringify(res)}`, { source: 'MCPServer' });
63
- return;
64
- }
56
+ // beforeToolCall: ({ serverName, name, args }) => {
57
+ // display.log(`MCP Tool Call - Server: ${serverName}, Tool: ${name}, Args: ${JSON.stringify(args)}`, { source: 'MCPServer' });
58
+ // return;
59
+ // },
60
+ // // log the results of tool calls
61
+ // afterToolCall: (res) => {
62
+ // display.log(`MCP Tool Result - ${JSON.stringify(res)}`, { source: 'MCPServer' });
63
+ // return;
64
+ // }
65
65
  });
66
66
  try {
67
67
  const tools = await client.getTools();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:Courier New,Courier,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));font-family:Courier New,Courier,monospace;--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}body:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(0 143 17 / var(--tw-text-opacity, 1))}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}:is(.dark *)::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb{--tw-bg-opacity: 1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}:is(.dark *)::-webkit-scrollbar-thumb{--tw-bg-opacity: 1;background-color:rgb(0 59 0 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}:is(.dark *)::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(0 143 17 / var(--tw-bg-opacity, 1))}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.static{position:static}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.top-0{top:0}.z-10{z-index:10}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mt-1{margin-top:.25rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-11{width:2.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-full{width:100%}.max-w-4xl{max-width:56rem}.max-w-6xl{max-width:72rem}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.translate-x-1{--tw-translate-x: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-6{--tw-translate-x: 1.5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity, 1))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-t-md{border-top-left-radius:.375rem;border-top-right-radius:.375rem}.border{border-width:1px}.border-x{border-left-width:1px;border-right-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-green-500{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.border-green-800{--tw-border-opacity: 1;border-color:rgb(22 101 52 / var(--tw-border-opacity, 1))}.border-matrix-highlight{--tw-border-opacity: 1;border-color:rgb(0 255 65 / var(--tw-border-opacity, 1))}.border-matrix-primary{--tw-border-opacity: 1;border-color:rgb(0 59 0 / var(--tw-border-opacity, 1))}.border-matrix-primary\/50{border-color:#003b0080}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.border-red-500\/50{border-color:#ef444480}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-900{--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.bg-green-900\/20{background-color:#14532d33}.bg-matrix-base{--tw-bg-opacity: 1;background-color:rgb(13 2 8 / var(--tw-bg-opacity, 1))}.bg-matrix-base\/50{background-color:#0d020880}.bg-matrix-highlight{--tw-bg-opacity: 1;background-color:rgb(0 255 65 / var(--tw-bg-opacity, 1))}.bg-matrix-highlight\/10{background-color:#00ff411a}.bg-matrix-primary{--tw-bg-opacity: 1;background-color:rgb(0 59 0 / var(--tw-bg-opacity, 1))}.bg-matrix-primary\/20{background-color:#003b0033}.bg-matrix-primary\/50{background-color:#003b0080}.bg-red-900\/10{background-color:#7f1d1d1a}.bg-red-900\/20{background-color:#7f1d1d33}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-zinc-900{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity, 1))}.bg-zinc-950{--tw-bg-opacity: 1;background-color:rgb(12 12 12 / var(--tw-bg-opacity, 1))}.bg-zinc-950\/50{background-color:#0c0c0c80}.bg-\[linear-gradient\(rgba\(18\,16\,16\,0\)_50\%\,rgba\(0\,0\,0\,0\.1\)_50\%\)\,linear-gradient\(90deg\,rgba\(0\,255\,0\,0\.03\)\,rgba\(0\,255\,0\,0\.01\)\)\]{background-image:linear-gradient(#12101000 50%,#0000001a 50%),linear-gradient(90deg,#00ff0008,#00ff0003)}.bg-\[length\:100\%_2px\,3px_100\%\]{background-size:100% 2px,3px 100%}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-px{padding-bottom:1px}.pl-4{padding-left:1rem}.pr-4{padding-right:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:Courier New,Courier,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[10px\]{font-size:10px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tracking-tighter{letter-spacing:-.05em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-matrix-highlight{--tw-text-opacity: 1;color:rgb(0 255 65 / var(--tw-text-opacity, 1))}.text-matrix-highlight\/50{color:#00ff4180}.text-matrix-highlight\/80{color:#00ff41cc}.text-matrix-primary{--tw-text-opacity: 1;color:rgb(0 59 0 / var(--tw-text-opacity, 1))}.text-matrix-secondary{--tw-text-opacity: 1;color:rgb(0 143 17 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.placeholder-green-900::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(20 83 45 / var(--tw-placeholder-opacity, 1))}.placeholder-green-900::placeholder{--tw-placeholder-opacity: 1;color:rgb(20 83 45 / var(--tw-placeholder-opacity, 1))}.placeholder-matrix-secondary\/50::-moz-placeholder{color:#008f1180}.placeholder-matrix-secondary\/50::placeholder{color:#008f1180}.opacity-0{opacity:0}.opacity-30{opacity:.3}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.shadow-\[0_0_15px_rgba\(34\,197\,94\,0\.3\)\]{--tw-shadow: 0 0 15px rgba(34,197,94,.3);--tw-shadow-colored: 0 0 15px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.hover\:border-matrix-highlight:hover{--tw-border-opacity: 1;border-color:rgb(0 255 65 / var(--tw-border-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-green-700:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.hover\:bg-matrix-highlight\/90:hover{background-color:#00ff41e6}.hover\:bg-matrix-secondary:hover{--tw-bg-opacity: 1;background-color:rgb(0 143 17 / var(--tw-bg-opacity, 1))}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:bg-zinc-900:hover{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity, 1))}.hover\:text-matrix-highlight:hover{--tw-text-opacity: 1;color:rgb(0 255 65 / var(--tw-text-opacity, 1))}.hover\:text-red-600:hover{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.focus\:border-green-500:focus{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.focus\:border-matrix-highlight:focus{--tw-border-opacity: 1;border-color:rgb(0 255 65 / var(--tw-border-opacity, 1))}.focus\:border-red-500:focus{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-matrix-highlight:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(0 255 65 / var(--tw-ring-opacity, 1))}.focus\:ring-red-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity, 1))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus\:ring-offset-black:focus{--tw-ring-offset-color: #000}.group:hover .group-hover\:text-matrix-highlight{--tw-text-opacity: 1;color:rgb(0 255 65 / var(--tw-text-opacity, 1))}.dark\:divide-matrix-primary\/30:is(.dark *)>:not([hidden])~:not([hidden]){border-color:#003b004d}.dark\:border-matrix-primary:is(.dark *){--tw-border-opacity: 1;border-color:rgb(0 59 0 / var(--tw-border-opacity, 1))}.dark\:bg-black:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.dark\:bg-black\/40:is(.dark *){background-color:#0006}.dark\:bg-matrix-primary:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(0 59 0 / var(--tw-bg-opacity, 1))}.dark\:bg-zinc-900\/50:is(.dark *){background-color:#18181b80}.dark\:bg-zinc-950:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(12 12 12 / var(--tw-bg-opacity, 1))}.dark\:text-gray-300:is(.dark *){--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.dark\:text-matrix-highlight:is(.dark *){--tw-text-opacity: 1;color:rgb(0 255 65 / var(--tw-text-opacity, 1))}.dark\:text-matrix-secondary:is(.dark *){--tw-text-opacity: 1;color:rgb(0 143 17 / var(--tw-text-opacity, 1))}.dark\:opacity-20:is(.dark *){opacity:.2}.dark\:hover\:bg-matrix-primary\/10:hover:is(.dark *){background-color:#003b001a}.dark\:hover\:bg-matrix-primary\/50:hover:is(.dark *){background-color:#003b0080}.dark\:hover\:bg-red-900\/20:hover:is(.dark *){background-color:#7f1d1d33}@media(min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}