FCS-dev commited on
Commit
ff076f7
·
1 Parent(s): 5a3340c

Se migró la configuración de alertas Telegram desde una variable de entorno global () hacia campos propios del modelo en la base de datos. Cada usuario configura su propio bot token, chat ID y activación de alertas desde el panel web.

Browse files
.env.example CHANGED
@@ -13,9 +13,6 @@ HF_SPACE_QWEN_URL=blackmistcode/polysignal-qwen3-8b
13
  OPENROUTER_API_KEY= # Fallback LLM si HF esta saturado
14
  FINNHUB_API_KEY= # Noticias financieras (finnhub.io)
15
 
16
- # Alertas
17
- TELEGRAM_BOT_TOKEN= # Bot de alertas (@BotFather)
18
-
19
  # Base de datos y auth
20
  DATABASE_URL=file:./backend/prisma/polysignal.db
21
  JWT_SECRET=supersecreto-demasiado-largo-para-la-hackathon-2026
 
13
  OPENROUTER_API_KEY= # Fallback LLM si HF esta saturado
14
  FINNHUB_API_KEY= # Noticias financieras (finnhub.io)
15
 
 
 
 
16
  # Base de datos y auth
17
  DATABASE_URL=file:./backend/prisma/polysignal.db
18
  JWT_SECRET=supersecreto-demasiado-largo-para-la-hackathon-2026
CLAUDE.md CHANGED
@@ -126,7 +126,6 @@ HF_SPACE_QWEN_URL # HuggingFace Space for Qwen3-8B
126
  HF_TOKEN # HF inference API key (fallback)
127
  OPENROUTER_API_KEY # LLM fallback if HF is down
128
  FINNHUB_API_KEY # News source
129
- TELEGRAM_BOT_TOKEN # Alert delivery
130
  JWT_SECRET # Must be ≥32 characters
131
  PORT=7860 # Required by HF Spaces
132
  DATABASE_URL=file:./backend/prisma/polysignal.db
 
126
  HF_TOKEN # HF inference API key (fallback)
127
  OPENROUTER_API_KEY # LLM fallback if HF is down
128
  FINNHUB_API_KEY # News source
 
129
  JWT_SECRET # Must be ≥32 characters
130
  PORT=7860 # Required by HF Spaces
131
  DATABASE_URL=file:./backend/prisma/polysignal.db
README.md CHANGED
@@ -348,9 +348,6 @@ HF_SPACE_QWEN_URL= # URL del Space (ej: usuario/qwen3-8b)
348
  OPENROUTER_API_KEY= # Fallback LLM si HF esta saturado
349
  FINNHUB_API_KEY= # Noticias financieras (finnhub.io)
350
 
351
- # Alertas
352
- TELEGRAM_BOT_TOKEN= # Bot de alertas (@BotFather)
353
-
354
  # Base de datos y auth
355
  DATABASE_URL=file:./backend/prisma/polysignal.db
356
  JWT_SECRET=minimo-32-caracteres # Secreto para firmar JWT
 
348
  OPENROUTER_API_KEY= # Fallback LLM si HF esta saturado
349
  FINNHUB_API_KEY= # Noticias financieras (finnhub.io)
350
 
 
 
 
351
  # Base de datos y auth
352
  DATABASE_URL=file:./backend/prisma/polysignal.db
353
  JWT_SECRET=minimo-32-caracteres # Secreto para firmar JWT
backend/docs/ALERTS.md CHANGED
@@ -93,12 +93,12 @@ socket.on('price_alert', ({ marketId, message }) => {
93
 
94
  Para recibir alertas vía Telegram:
95
 
96
- 1. Crear un bot en [@BotFather](https://t.me/BotFather) y obtener el `TELEGRAM_BOT_TOKEN`.
97
- 2. El usuario debe iniciar conversación con el bot y obtener su `chatId`.
98
- 3. Configurar `telegramChatId` en el registro `User` (actualmente solo vía `prisma studio` o seed).
99
- 4. Añadir `TELEGRAM_BOT_TOKEN` al `.env`.
100
 
101
- Si `TELEGRAM_BOT_TOKEN` no está configurado o el usuario no tiene `telegramChatId`, el envío se omite silenciosamente (la `Alert` se crea igualmente en DB).
102
 
103
  ---
104
 
 
93
 
94
  Para recibir alertas vía Telegram:
95
 
96
+ 1. El usuario crea un bot en [@BotFather](https://t.me/BotFather) y obtiene su **Bot Token**.
97
+ 2. El usuario inicia conversación con el bot y obtiene su **Chat ID**.
98
+ 3. Desde el panel web (botón *Alertas Telegram*), el usuario introduce ambos valores y activa las alertas.
99
+ 4. Los datos se guardan en el modelo `User` (`telegramBotToken`, `telegramChatId`, `telegramAlertsEnabled`).
100
 
101
+ Si el usuario no ha configurado su bot token y chat ID, o tiene las alertas desactivadas, el envío se omite silenciosamente (la `Alert` se crea igualmente en DB).
102
 
103
  ---
104
 
backend/docs/API.md CHANGED
@@ -134,4 +134,3 @@ Ver [`backend/.env.example`](../.env.example) para la plantilla completa.
134
  | `HF_TOKEN` | No | HuggingFace (señales AI reales) |
135
  | `OPENROUTER_API_KEY` | No | Fallback LLM |
136
  | `FINNHUB_API_KEY` | No | Noticias para señales |
137
- | `TELEGRAM_BOT_TOKEN` | No | Alertas Telegram |
 
134
  | `HF_TOKEN` | No | HuggingFace (señales AI reales) |
135
  | `OPENROUTER_API_KEY` | No | Fallback LLM |
136
  | `FINNHUB_API_KEY` | No | Noticias para señales |
 
backend/prisma/migrations/20260518120426_add_telegram_fields/migration.sql ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- RedefineTables
2
+ PRAGMA defer_foreign_keys=ON;
3
+ PRAGMA foreign_keys=OFF;
4
+ CREATE TABLE "new_User" (
5
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
6
+ "email" TEXT NOT NULL,
7
+ "passwordHash" TEXT NOT NULL,
8
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
9
+ "telegramChatId" TEXT,
10
+ "telegramBotToken" TEXT,
11
+ "telegramAlertsEnabled" BOOLEAN NOT NULL DEFAULT false,
12
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
13
+ "updatedAt" DATETIME NOT NULL
14
+ );
15
+ INSERT INTO "new_User" ("createdAt", "email", "id", "isActive", "passwordHash", "telegramChatId", "updatedAt") SELECT "createdAt", "email", "id", "isActive", "passwordHash", "telegramChatId", "updatedAt" FROM "User";
16
+ DROP TABLE "User";
17
+ ALTER TABLE "new_User" RENAME TO "User";
18
+ CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
19
+ PRAGMA foreign_keys=ON;
20
+ PRAGMA defer_foreign_keys=OFF;
backend/prisma/schema.prisma CHANGED
@@ -21,13 +21,15 @@ datasource db {
21
  }
22
 
23
  model User {
24
- id Int @id @default(autoincrement())
25
- email String @unique
26
- passwordHash String
27
- isActive Boolean @default(true)
28
- telegramChatId String? // Configurado manualmente para demo
29
- createdAt DateTime @default(now())
30
- updatedAt DateTime @updatedAt
 
 
31
 
32
  positions Position[]
33
  watchlist Watchlist[]
 
21
  }
22
 
23
  model User {
24
+ id Int @id @default(autoincrement())
25
+ email String @unique
26
+ passwordHash String
27
+ isActive Boolean @default(true)
28
+ telegramChatId String?
29
+ telegramBotToken String?
30
+ telegramAlertsEnabled Boolean @default(false)
31
+ createdAt DateTime @default(now())
32
+ updatedAt DateTime @updatedAt
33
 
34
  positions Position[]
35
  watchlist Watchlist[]
backend/src/alerts/alerts.service.js CHANGED
@@ -52,7 +52,9 @@ export const alertsService = {
52
  const message = formatPriceAlert(market.question, market.yesPrice, alertThreshold);
53
 
54
  await alertsRepository.create({ userId: user.id, marketId: market.id, type: 'price_threshold', message });
55
- await sendMessage(user.telegramChatId, message);
 
 
56
  emitPriceAlert({ marketId: market.id, type: 'price_threshold', message });
57
  logger.info({ marketId: market.id, userId: user.id }, 'price alert sent');
58
  }
 
52
  const message = formatPriceAlert(market.question, market.yesPrice, alertThreshold);
53
 
54
  await alertsRepository.create({ userId: user.id, marketId: market.id, type: 'price_threshold', message });
55
+ if (user.telegramAlertsEnabled) {
56
+ await sendMessage({ botToken: user.telegramBotToken, chatId: user.telegramChatId, text: message });
57
+ }
58
  emitPriceAlert({ marketId: market.id, type: 'price_threshold', message });
59
  logger.info({ marketId: market.id, userId: user.id }, 'price alert sent');
60
  }
backend/src/alerts/telegram.client.js CHANGED
@@ -7,7 +7,7 @@
7
  * escapando entidades del input de usuario.
8
  * - escapeHtml(text) → escapa caracteres HTML sensibles (&, <, >, ").
9
  * - Parse_mode: HTML (permite formato basico: <b>, <i>).
10
- * - Si falta TELEGRAM_BOT_TOKEN o chatId, sendMessage retorna silenciosamente.
11
  * - Errores de red se capturan y loguean como warn (no bloquean el flujo).
12
  * - Respuestas de Telegram con ok: false se loguean como warn y no lanzan excepcion.
13
  *
@@ -15,12 +15,11 @@
15
  * - alerts.service.js → processAll() cuando se dispara una alerta.
16
  *
17
  * Seguridad:
18
- * - El token se lee de variables de entorno (nunca hardcodeado).
19
  * - Todo texto dinamico se escapa via escapeHtml antes de inyectarse en HTML.
20
  */
21
 
22
  import { httpPost } from '../utils/httpClient.js';
23
- import { config } from '../config.js';
24
  import { logger } from '../utils/logger.js';
25
 
26
  /**
@@ -60,22 +59,24 @@ export function formatPriceAlert(question, yesPrice, threshold) {
60
  /**
61
  * Envia un mensaje de texto a un chat de Telegram via Bot API.
62
  *
63
- * Si TELEGRAM_BOT_TOKEN no esta configurado o chatId es falsy, la funcion
64
  * retorna silenciosamente sin realizar peticion.
65
  *
66
  * Ante errores de red o respuestas con ok: false de Telegram, se loguea un
67
  * warning y la funcion retorna sin lanzar excepcion, evitando interrumpir
68
  * el flujo del scheduler.
69
  *
70
- * @param {string|number} chatId - Identificador del chat de Telegram.
71
- * @param {string} text - Texto del mensaje (debe estar ya formateado en HTML).
 
 
72
  * @returns {Promise<void>}
73
  */
74
- export async function sendMessage(chatId, text) {
75
- if (!config.TELEGRAM_BOT_TOKEN || !chatId) return;
76
  try {
77
  const result = await httpPost(
78
- `https://api.telegram.org/bot${config.TELEGRAM_BOT_TOKEN}/sendMessage`,
79
  { chat_id: chatId, text, parse_mode: 'HTML' },
80
  { retries: 1, timeout: 8_000 },
81
  );
 
7
  * escapando entidades del input de usuario.
8
  * - escapeHtml(text) → escapa caracteres HTML sensibles (&, <, >, ").
9
  * - Parse_mode: HTML (permite formato basico: <b>, <i>).
10
+ * - Si falta botToken o chatId, sendMessage retorna silenciosamente.
11
  * - Errores de red se capturan y loguean como warn (no bloquean el flujo).
12
  * - Respuestas de Telegram con ok: false se loguean como warn y no lanzan excepcion.
13
  *
 
15
  * - alerts.service.js → processAll() cuando se dispara una alerta.
16
  *
17
  * Seguridad:
18
+ * - El token se pasa por argumento desde el registro del usuario (nunca hardcodeado).
19
  * - Todo texto dinamico se escapa via escapeHtml antes de inyectarse en HTML.
20
  */
21
 
22
  import { httpPost } from '../utils/httpClient.js';
 
23
  import { logger } from '../utils/logger.js';
24
 
25
  /**
 
59
  /**
60
  * Envia un mensaje de texto a un chat de Telegram via Bot API.
61
  *
62
+ * Si botToken no esta configurado o chatId es falsy, la funcion
63
  * retorna silenciosamente sin realizar peticion.
64
  *
65
  * Ante errores de red o respuestas con ok: false de Telegram, se loguea un
66
  * warning y la funcion retorna sin lanzar excepcion, evitando interrumpir
67
  * el flujo del scheduler.
68
  *
69
+ * @param {Object} params
70
+ * @param {string} params.botToken - Token del bot de Telegram.
71
+ * @param {string|number} params.chatId - Identificador del chat de Telegram.
72
+ * @param {string} params.text - Texto del mensaje (debe estar ya formateado en HTML).
73
  * @returns {Promise<void>}
74
  */
75
+ export async function sendMessage({ botToken, chatId, text }) {
76
+ if (!botToken || !chatId) return;
77
  try {
78
  const result = await httpPost(
79
+ `https://api.telegram.org/bot${botToken}/sendMessage`,
80
  { chat_id: chatId, text, parse_mode: 'HTML' },
81
  { retries: 1, timeout: 8_000 },
82
  );
backend/src/auth/auth.controller.js CHANGED
@@ -37,3 +37,8 @@ export const logout = (req, res) => {
37
  authService.logout({ jti: payload.jti, exp: payload.exp });
38
  ok(res, { message: 'Logged out successfully' });
39
  };
 
 
 
 
 
 
37
  authService.logout({ jti: payload.jti, exp: payload.exp });
38
  ok(res, { message: 'Logged out successfully' });
39
  };
40
+
41
+ export const updateTelegram = async (req, res) => {
42
+ const data = await authService.updateTelegram(req.user.id, req.body);
43
+ ok(res, data); (Se migró la configuración de alertas Telegram desde una variable de entorno global () hacia campos propios del modelo en la base de datos. Cada usuario configura su propio bot token, chat ID y activación de alertas desde el panel web.)
44
+ };
backend/src/auth/auth.routes.js CHANGED
@@ -32,5 +32,6 @@ router.post('/login', rateLimitLogin, validate(loginSchema), ctrl.login);
32
  router.post('/register', validate(registerSchema), ctrl.register);
33
  router.get('/me', requireAuth, ctrl.me);
34
  router.post('/logout', requireAuth, ctrl.logout);
 
35
 
36
  export default router;
 
32
  router.post('/register', validate(registerSchema), ctrl.register);
33
  router.get('/me', requireAuth, ctrl.me);
34
  router.post('/logout', requireAuth, ctrl.logout);
35
+ router.put('/telegram', requireAuth, ctrl.updateTelegram); (Se migró la configuración de alertas Telegram desde una variable de entorno global () hacia campos propios del modelo en la base de datos. Cada usuario configura su propio bot token, chat ID y activación de alertas desde el panel web.)
36
 
37
  export default router;
backend/src/auth/auth.service.js CHANGED
@@ -26,6 +26,16 @@ import { signToken, addToDenylist } from './jwt.js';
26
 
27
  const INVALID_CREDENTIALS = new HttpError(401, 'INVALID_CREDENTIALS', 'Email or password is incorrect');
28
 
 
 
 
 
 
 
 
 
 
 
29
  export const login = async ({ email, password }) => {
30
  const user = await prisma.user.findUnique({ where: { email } });
31
  if (!user || !user.isActive) throw INVALID_CREDENTIALS;
@@ -37,7 +47,7 @@ export const login = async ({ email, password }) => {
37
 
38
  return {
39
  token,
40
- user: { id: user.id, email: user.email },
41
  };
42
  };
43
 
@@ -56,10 +66,24 @@ export const register = async ({ email, password }) => {
56
 
57
  return {
58
  token,
59
- user: { id: user.id, email: user.email },
60
  };
61
  };
62
 
63
  export const logout = ({ jti, exp }) => {
64
  if (jti && exp) addToDenylist(jti, exp);
65
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  const INVALID_CREDENTIALS = new HttpError(401, 'INVALID_CREDENTIALS', 'Email or password is incorrect');
28
 
29
+ function buildUserResponse(user) {
30
+ return {
31
+ id: user.id,
32
+ email: user.email,
33
+ telegramChatId: user.telegramChatId,
34
+ telegramBotToken: user.telegramBotToken,
35
+ telegramAlertsEnabled: user.telegramAlertsEnabled,
36
+ };
37
+ }
38
+
39
  export const login = async ({ email, password }) => {
40
  const user = await prisma.user.findUnique({ where: { email } });
41
  if (!user || !user.isActive) throw INVALID_CREDENTIALS;
 
47
 
48
  return {
49
  token,
50
+ user: buildUserResponse(user),
51
  };
52
  };
53
 
 
66
 
67
  return {
68
  token,
69
+ user: buildUserResponse(user),
70
  };
71
  };
72
 
73
  export const logout = ({ jti, exp }) => {
74
  if (jti && exp) addToDenylist(jti, exp);
75
  };
76
+
77
+ export const updateTelegram = async (userId, { telegramBotToken, telegramChatId, telegramAlertsEnabled }) => {
78
+ const data = {};
79
+ if (telegramBotToken !== undefined) data.telegramBotToken = telegramBotToken || null;
80
+ if (telegramChatId !== undefined) data.telegramChatId = telegramChatId || null;
81
+ if (telegramAlertsEnabled !== undefined) data.telegramAlertsEnabled = !!telegramAlertsEnabled;
82
+
83
+ const user = await prisma.user.update({
84
+ where: { id: userId },
85
+ data,
86
+ });
87
+
88
+ return { user: buildUserResponse(user) }; (Se migró la configuración de alertas Telegram desde una variable de entorno global () hacia campos propios del modelo en la base de datos. Cada usuario configura su propio bot token, chat ID y activación de alertas desde el panel web.)
89
+ };
backend/src/config.js CHANGED
@@ -12,7 +12,6 @@
12
  * - HF_TOKEN / HF_SPACE_*: credenciales para los Spaces de HuggingFace (IA).
13
  * - OPENROUTER_API_KEY: fallback LLM si los Spaces estan saturados.
14
  * - FINNHUB_API_KEY: noticias financieras para el pipeline de senales.
15
- * - TELEGRAM_BOT_TOKEN: bot de alertas (@BotFather).
16
  *
17
  * Si falla la validacion, el proceso termina con error antes de levantar el servidor.
18
  */
@@ -35,7 +34,6 @@ const schema = z.object({
35
  OPENROUTER_API_KEY: z.string().optional(),
36
  DEEPSEEK_API_KEY: z.string().optional(),
37
  FINNHUB_API_KEY: z.string().optional(),
38
- TELEGRAM_BOT_TOKEN: z.string().optional(),
39
  });
40
 
41
  const parsed = schema.safeParse(process.env);
 
12
  * - HF_TOKEN / HF_SPACE_*: credenciales para los Spaces de HuggingFace (IA).
13
  * - OPENROUTER_API_KEY: fallback LLM si los Spaces estan saturados.
14
  * - FINNHUB_API_KEY: noticias financieras para el pipeline de senales.
 
15
  *
16
  * Si falla la validacion, el proceso termina con error antes de levantar el servidor.
17
  */
 
34
  OPENROUTER_API_KEY: z.string().optional(),
35
  DEEPSEEK_API_KEY: z.string().optional(),
36
  FINNHUB_API_KEY: z.string().optional(),
 
37
  });
38
 
39
  const parsed = schema.safeParse(process.env);
backend/src/middlewares/requireAuth.js CHANGED
@@ -19,16 +19,20 @@
19
 
20
  import { verifyToken, isBlocked } from '../auth/jwt.js';
21
  import { prisma } from '../utils/prisma.js';
22
- import { HttpError } from '../utils/apiResponse.js';
23
 
24
- const UNAUTHORIZED = new HttpError(401, 'UNAUTHORIZED', 'Authentication required');
 
 
 
 
25
 
26
  export const requireAuth = async (req, _res, next) => {
27
  try {
28
  const header = req.headers.authorization;
29
- if (!header || !header.startsWith('Bearer ')) throw UNAUTHORIZED;
30
 
31
- const token = header.slice('Bearer '.length).trim();
32
  if (!token) throw UNAUTHORIZED;
33
 
34
  const payload = verifyToken(token);
@@ -37,7 +41,15 @@ export const requireAuth = async (req, _res, next) => {
37
 
38
  const user = await prisma.user.findUnique({
39
  where: { id: payload.sub },
40
- select: { id: true, email: true, isActive: true, createdAt: true },
 
 
 
 
 
 
 
 
41
  });
42
 
43
  if (!user || !user.isActive) throw UNAUTHORIZED;
 
19
 
20
  import { verifyToken, isBlocked } from '../auth/jwt.js';
21
  import { prisma } from '../utils/prisma.js';
22
+ import { HttpError } from '../utils/apiResponse.js'; (Se migró la configuración de alertas Telegram desde una variable de entorno global () hacia campos propios del modelo en la base de datos. Cada usuario configura su propio bot token, chat ID y activación de alertas desde el panel web.)
23
 
24
+ const UNAUTHORIZED = new HttpError(
25
+ 401,
26
+ "UNAUTHORIZED",
27
+ "Authentication required",
28
+ );
29
 
30
  export const requireAuth = async (req, _res, next) => {
31
  try {
32
  const header = req.headers.authorization;
33
+ if (!header || !header.startsWith("Bearer ")) throw UNAUTHORIZED;
34
 
35
+ const token = header.slice("Bearer ".length).trim();
36
  if (!token) throw UNAUTHORIZED;
37
 
38
  const payload = verifyToken(token);
 
41
 
42
  const user = await prisma.user.findUnique({
43
  where: { id: payload.sub },
44
+ //select: { id: true, email: true, isActive: true, createdAt: true },
45
+ select: {
46
+ id: true,
47
+ email: true,
48
+ telegramBotToken: true,
49
+ telegramChatId: true,
50
+ isActive: true,
51
+ createdAt: true,
52
+ },
53
  });
54
 
55
  if (!user || !user.isActive) throw UNAUTHORIZED;
backend/src/watchlist/watchlist.repository.js CHANGED
@@ -38,7 +38,14 @@ export const watchlistRepository = {
38
  return prisma.watchlist.findMany({
39
  where: { alertThreshold: { not: null } },
40
  include: {
41
- user: { select: { id: true, telegramChatId: true } },
 
 
 
 
 
 
 
42
  market: { select: { id: true, question: true, yesPrice: true } },
43
  },
44
  });
 
38
  return prisma.watchlist.findMany({
39
  where: { alertThreshold: { not: null } },
40
  include: {
41
+ user: {
42
+ select: {
43
+ id: true,
44
+ telegramChatId: true,
45
+ telegramBotToken: true,
46
+ telegramAlertsEnabled: true,
47
+ },
48
+ },
49
  market: { select: { id: true, question: true, yesPrice: true } },
50
  },
51
  });
frontend/dist/assets/index-Bq1dV2vj.css CHANGED
@@ -1,2835 +1 @@
1
- @import"https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@400;500;600;700&display=swap";
2
-
3
- :root {
4
- --bg: #0a0c10;
5
- --bg2: #111318;
6
- --bg3: #181c24;
7
- --bg4: #1e232d;
8
- --border: rgba(255, 255, 255, .08);
9
- --border2: rgba(255, 255, 255, .13);
10
- --text: #e8eaf0;
11
- --text2: #8b90a0;
12
- --text3: #7e8494;
13
- --green: #22d37a;
14
- --green2: #0d6e3a;
15
- --green3: #052a17;
16
- --red: #f04040;
17
- --red2: #7a1a1a;
18
- --red3: #2d0808;
19
- --blue: #4a9eff;
20
- --blue2: #1a4a80;
21
- --blue3: #081830;
22
- --amber: #f0a020;
23
- --amber2: #7a4e08;
24
- --amber3: #2d1c02;
25
- --accent: #4a9eff;
26
- --font-sans: "Syne", sans-serif;
27
- --font-mono: "DM Mono", monospace;
28
- --sidebar-width: 240px;
29
- --sidebar-collapsed: 56px;
30
- --topbar-height: 56px;
31
- --panel-gap: 16px;
32
- --radius: 10px;
33
- --radius-sm: 6px;
34
- --fs-p: clamp(1rem, 1.15vw, 1.125rem);
35
- --fs-h1: clamp(1.75rem, 2.8vw, 2.25rem);
36
- --fs-h2: clamp(1.5rem, 2.4vw, 1.875rem);
37
- --fs-h3: clamp(1.25rem, 1.9vw, 1.5rem);
38
- --fs-h4: clamp(1.125rem, 1.6vw, 1.25rem);
39
- --fs-h5: clamp(1rem, 1.3vw, 1.125rem);
40
- --fs-h6: clamp(.875rem, 1.1vw, 1rem)
41
- }
42
-
43
- * {
44
- box-sizing: border-box;
45
- margin: 0;
46
- padding: 0
47
- }
48
-
49
- html {
50
- font-size: 16px
51
- }
52
-
53
- html,
54
- body,
55
- #app {
56
- height: 100%;
57
- width: 100%;
58
- overflow: hidden
59
- }
60
-
61
- body {
62
- background: var(--bg);
63
- color: var(--text);
64
- font-family: var(--font-sans);
65
- font-size: 1rem;
66
- line-height: 1.5;
67
- -webkit-font-smoothing: antialiased
68
- }
69
-
70
- p {
71
- font-size: var(--fs-p);
72
- line-height: 1.55
73
- }
74
-
75
- h1 {
76
- font-size: var(--fs-h1);
77
- line-height: 1.15;
78
- font-weight: 700
79
- }
80
-
81
- h2 {
82
- font-size: var(--fs-h2);
83
- line-height: 1.2;
84
- font-weight: 700
85
- }
86
-
87
- h3 {
88
- font-size: var(--fs-h3);
89
- line-height: 1.25;
90
- font-weight: 600
91
- }
92
-
93
- h4 {
94
- font-size: var(--fs-h4);
95
- line-height: 1.3;
96
- font-weight: 600
97
- }
98
-
99
- h5 {
100
- font-size: var(--fs-h5);
101
- line-height: 1.35;
102
- font-weight: 500
103
- }
104
-
105
- h6 {
106
- font-size: var(--fs-h6);
107
- line-height: 1.4;
108
- font-weight: 500
109
- }
110
-
111
- .layout {
112
- display: grid;
113
- grid-template-areas: "topbar topbar" "sidebar main";
114
- grid-template-columns: var(--sidebar-width) 1fr;
115
- grid-template-rows: var(--topbar-height) 1fr;
116
- height: 100vh;
117
- width: 100vw;
118
- transition: grid-template-columns .25s ease
119
- }
120
-
121
- .layout.collapsed {
122
- grid-template-columns: var(--sidebar-collapsed) 1fr
123
- }
124
-
125
- .sidebar {
126
- grid-area: sidebar;
127
- background: var(--bg2);
128
- border-right: 1px solid var(--border);
129
- display: flex;
130
- flex-direction: column;
131
- overflow: hidden;
132
- transition: width .25s ease;
133
- position: relative
134
- }
135
-
136
- .sidebar-toggle {
137
- position: absolute;
138
- top: 12px;
139
- right: -10px;
140
- width: 24px;
141
- height: 24px;
142
- border-radius: 50%;
143
- background: var(--bg3);
144
- border: 1px solid var(--border2);
145
- color: var(--text2);
146
- display: flex;
147
- align-items: center;
148
- justify-content: center;
149
- cursor: pointer;
150
- font-size: .875rem;
151
- z-index: 10;
152
- transition: transform .2s
153
- }
154
-
155
- .layout.collapsed .sidebar-toggle {
156
- transform: rotate(180deg);
157
- right: -10px
158
- }
159
-
160
- .topbar-logo {
161
- position: fixed;
162
- left: 16px;
163
- top: 0;
164
- height: var(--topbar-height);
165
- display: flex;
166
- align-items: center;
167
- gap: 10px;
168
- z-index: 100
169
- }
170
-
171
- .topbar-logo .logo-dot {
172
- width: 10px;
173
- height: 10px;
174
- border-radius: 50%;
175
- background: var(--blue);
176
- animation: pulse 2s ease-in-out infinite;
177
- flex-shrink: 0
178
- }
179
-
180
- .topbar-logo .logo-text {
181
- font-size: 1.125rem;
182
- font-weight: 700;
183
- color: var(--text);
184
- letter-spacing: -.3px;
185
- white-space: nowrap
186
- }
187
-
188
- .sidebar-nav {
189
- flex: 1;
190
- padding: 14px 8px;
191
- display: flex;
192
- flex-direction: column;
193
- gap: 4px;
194
- overflow-y: auto
195
- }
196
-
197
- .nav-item {
198
- display: flex;
199
- align-items: center;
200
- gap: 14px;
201
- padding: 14px;
202
- border-radius: var(--radius-sm);
203
- cursor: pointer;
204
- color: var(--text2);
205
- font-size: 1rem;
206
- font-weight: 500;
207
- transition: background .15s, color .15s;
208
- white-space: nowrap;
209
- overflow: hidden
210
- }
211
-
212
- .nav-item:hover {
213
- background: var(--bg3);
214
- color: var(--text)
215
- }
216
-
217
- .nav-item.active {
218
- background: var(--blue3);
219
- color: var(--blue);
220
- border: .5px solid var(--blue2)
221
- }
222
-
223
- .nav-icon {
224
- font-size: 1rem;
225
- width: 20px;
226
- text-align: center;
227
- flex-shrink: 0
228
- }
229
-
230
- .nav-label {
231
- opacity: 1;
232
- transition: opacity .15s
233
- }
234
-
235
- .layout.collapsed .nav-label {
236
- opacity: 0;
237
- width: 0
238
- }
239
-
240
- .sidebar-footer {
241
- padding: 14px;
242
- border-top: 1px solid var(--border);
243
- font-size: .875rem;
244
- color: var(--text3);
245
- font-family: var(--font-mono);
246
- text-align: center;
247
- white-space: nowrap;
248
- overflow: hidden
249
- }
250
-
251
- .layout.collapsed .sidebar-footer {
252
- opacity: 0
253
- }
254
-
255
- .topbar {
256
- grid-area: topbar;
257
- background: var(--bg2);
258
- border-bottom: 1px solid var(--border);
259
- display: flex;
260
- align-items: center;
261
- padding: 0 16px;
262
- padding-left: calc(var(--sidebar-width) + var(--panel-gap));
263
- gap: 16px;
264
- overflow: hidden;
265
- position: relative
266
- }
267
-
268
- .layout.collapsed .topbar {
269
- padding-left: calc(var(--sidebar-width) + var(--panel-gap))
270
- }
271
-
272
- .live-badge {
273
- font-family: var(--font-mono);
274
- font-size: .875rem;
275
- background: var(--green3);
276
- color: var(--green);
277
- border: .5px solid var(--green2);
278
- padding: 6px 12px;
279
- border-radius: 4px;
280
- display: flex;
281
- align-items: center;
282
- gap: 4px;
283
- flex-shrink: 0
284
- }
285
-
286
- .live-dot {
287
- width: 8px;
288
- height: 8px;
289
- border-radius: 50%;
290
- background: var(--green);
291
- animation: pulse 1.5s ease-in-out infinite
292
- }
293
-
294
- .topbar-stats {
295
- display: flex;
296
- align-items: center;
297
- gap: 20px;
298
- flex: 1;
299
- overflow: hidden
300
- }
301
-
302
- .stat {
303
- display: flex;
304
- align-items: center;
305
- gap: 14px;
306
- flex-shrink: 0
307
- }
308
-
309
- .stat-label {
310
- font-size: .875rem;
311
- color: var(--text3);
312
- font-family: var(--font-mono);
313
- text-transform: uppercase;
314
- letter-spacing: .06em
315
- }
316
-
317
- .stat-val {
318
- font-size: 1rem;
319
- font-weight: 600;
320
- color: var(--text);
321
- font-family: var(--font-mono)
322
- }
323
-
324
- .stat-delta {
325
- font-size: .875rem;
326
- font-family: var(--font-mono)
327
- }
328
-
329
- .stat-delta.up {
330
- color: var(--green)
331
- }
332
-
333
- .stat-delta.dn {
334
- color: var(--red)
335
- }
336
-
337
- .stat-delta.neutral {
338
- color: var(--text3)
339
- }
340
-
341
- .topbar-filters {
342
- display: flex;
343
- align-items: center;
344
- gap: 10px;
345
- flex-shrink: 0
346
- }
347
-
348
- .filter-select {
349
- background: var(--bg3);
350
- border: .5px solid var(--border2);
351
- border-radius: var(--radius-sm);
352
- padding: 7px 30px 7px 12px;
353
- color: var(--text);
354
- font-size: .875rem;
355
- font-family: var(--font-mono);
356
- outline: none;
357
- cursor: pointer;
358
- min-width: 140px;
359
- max-width: 180px;
360
- appearance: none;
361
- -webkit-appearance: none;
362
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238b90a0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
363
- background-repeat: no-repeat;
364
- background-position: right 10px center;
365
- transition: border-color .15s
366
- }
367
-
368
- .filter-select:hover {
369
- border-color: var(--border2)
370
- }
371
-
372
- .filter-select:focus {
373
- border-color: var(--blue2)
374
- }
375
-
376
- .filter-select option {
377
- background: var(--bg2);
378
- color: var(--text)
379
- }
380
-
381
- .topbar-actions {
382
- margin-left: auto;
383
- display: flex;
384
- align-items: center;
385
- gap: 16px;
386
- flex-shrink: 0
387
- }
388
-
389
- .btn-ghost {
390
- font-size: .9375rem;
391
- font-weight: 500;
392
- color: var(--blue);
393
- border: .5px solid var(--blue2);
394
- background: var(--blue3);
395
- padding: 8px 16px;
396
- border-radius: var(--radius-sm);
397
- cursor: pointer;
398
- font-family: var(--font-sans);
399
- transition: opacity .15s
400
- }
401
-
402
- .btn-ghost:hover {
403
- opacity: .85
404
- }
405
-
406
- .icon-btn {
407
- width: 32px;
408
- height: 32px;
409
- border-radius: 50%;
410
- background: var(--bg3);
411
- border: .5px solid var(--border2);
412
- color: var(--text2);
413
- display: flex;
414
- align-items: center;
415
- justify-content: center;
416
- cursor: pointer;
417
- font-size: 1rem;
418
- transition: color .15s, border-color .15s
419
- }
420
-
421
- .icon-btn:hover {
422
- color: var(--text);
423
- border-color: var(--border2)
424
- }
425
-
426
- .main {
427
- grid-area: main;
428
- overflow: auto;
429
- padding: var(--panel-gap);
430
- background: var(--bg)
431
- }
432
-
433
- .view {
434
- display: none
435
- }
436
-
437
- .view.active {
438
- display: block;
439
- height: 100%
440
- }
441
-
442
- .dashboard-grid {
443
- display: grid;
444
- grid-template-columns: 1fr 336px;
445
- grid-template-rows: minmax(120px, 59%) 1fr;
446
- gap: var(--panel-gap);
447
- height: 100%;
448
- min-height: 0
449
- }
450
-
451
- .panel {
452
- background: var(--bg2);
453
- border: .5px solid var(--border);
454
- border-radius: var(--radius);
455
- display: flex;
456
- flex-direction: column;
457
- overflow: hidden;
458
- min-height: 0
459
- }
460
-
461
- .panel-header {
462
- display: flex;
463
- align-items: center;
464
- justify-content: space-between;
465
- padding: 14px;
466
- border-bottom: .5px solid var(--border);
467
- cursor: pointer;
468
- -webkit-user-select: none;
469
- user-select: none;
470
- transition: background .15s
471
- }
472
-
473
- .panel-header:hover {
474
- background: #ffffff05
475
- }
476
-
477
- .panel-title {
478
- font-size: .875rem;
479
- color: var(--text3);
480
- font-family: var(--font-mono);
481
- text-transform: uppercase;
482
- letter-spacing: .08em;
483
- display: flex;
484
- align-items: center;
485
- gap: 14px
486
- }
487
-
488
- .panel-toggle {
489
- font-size: .875rem;
490
- color: var(--text3);
491
- transition: transform .2s
492
- }
493
-
494
- .panel.collapsed .panel-toggle {
495
- transform: rotate(-90deg)
496
- }
497
-
498
- .panel-body {
499
- flex: 1;
500
- overflow: auto;
501
- padding: 12px;
502
- min-height: 0
503
- }
504
-
505
- .panel.collapsed .panel-body {
506
- display: none
507
- }
508
-
509
- .panel-subtitle {
510
- font-size: .875rem;
511
- color: var(--text3)
512
- }
513
-
514
- .positions-separator {
515
- margin-top: 10px;
516
- padding-top: 10px;
517
- border-top: .5px solid var(--border)
518
- }
519
-
520
- .panel-title.mb-sm {
521
- margin-bottom: 8px
522
- }
523
-
524
- .panel.full-height {
525
- height: 100%
526
- }
527
-
528
- .hidden {
529
- display: none
530
- }
531
-
532
- .desktop-only {
533
- display: initial
534
- }
535
-
536
- .mobile-only,
537
- #btn-notif,
538
- #btn-auth-mobile,
539
- .topbar-stats .legend {
540
- display: none
541
- }
542
-
543
- .map-legend {
544
- display: flex;
545
- align-items: center;
546
- gap: 12px;
547
- margin-left: auto
548
- }
549
-
550
- #panel-map .panel-header {
551
- justify-content: flex-start
552
- }
553
-
554
- .map-panel {
555
- grid-row: 1 / 2;
556
- grid-column: 1 / 2
557
- }
558
-
559
- #map-container {
560
- width: 100%;
561
- height: 100%;
562
- min-height: 300px;
563
- background: var(--bg3);
564
- border-radius: var(--radius-sm);
565
- overflow: hidden
566
- }
567
-
568
- .signals-panel {
569
- grid-row: 1 / 3;
570
- grid-column: 2 / 3
571
- }
572
-
573
- .signals-list {
574
- display: flex;
575
- flex-direction: column;
576
- gap: 16px
577
- }
578
-
579
- .market-card {
580
- background: var(--bg3);
581
- border: .5px solid var(--border);
582
- border-radius: var(--radius-sm);
583
- padding: 14px;
584
- cursor: pointer;
585
- transition: border-color .15s, background .15s
586
- }
587
-
588
- .market-card:hover {
589
- border-color: var(--border2)
590
- }
591
-
592
- .market-card.active {
593
- border-color: var(--blue2);
594
- background: var(--blue3)
595
- }
596
-
597
- .market-cat {
598
- font-size: .875rem;
599
- font-family: var(--font-mono);
600
- color: var(--text3);
601
- text-transform: uppercase;
602
- letter-spacing: .06em;
603
- margin-bottom: 4px
604
- }
605
-
606
- .market-q {
607
- font-size: .9375rem;
608
- color: var(--text);
609
- line-height: 1.4;
610
- font-weight: 500;
611
- margin-bottom: 8px
612
- }
613
-
614
- .market-footer {
615
- display: flex;
616
- align-items: center;
617
- justify-content: space-between;
618
- gap: 16px
619
- }
620
-
621
- .prob-bar-wrap {
622
- flex: 1;
623
- min-width: 0
624
- }
625
-
626
- .prob-bar-bg {
627
- height: 5px;
628
- background: var(--bg4);
629
- border-radius: 2px;
630
- overflow: hidden
631
- }
632
-
633
- .prob-bar-fill {
634
- height: 100%;
635
- border-radius: 2px;
636
- width: var(--prob-width, 0%);
637
- transition: width .8s ease
638
- }
639
-
640
- .prob-val {
641
- font-size: .9375rem;
642
- font-weight: 600;
643
- font-family: var(--font-mono);
644
- flex-shrink: 0
645
- }
646
-
647
- .signal-badge {
648
- font-size: .875rem;
649
- font-weight: 600;
650
- padding: 2px 6px;
651
- border-radius: 4px;
652
- font-family: var(--font-mono);
653
- letter-spacing: .04em;
654
- flex-shrink: 0
655
- }
656
-
657
- .sig-bull {
658
- background: var(--green3);
659
- color: var(--green);
660
- border: .5px solid var(--green2)
661
- }
662
-
663
- .sig-bear {
664
- background: var(--red3);
665
- color: var(--red);
666
- border: .5px solid var(--red2)
667
- }
668
-
669
- .sig-neut {
670
- background: var(--bg4);
671
- color: var(--text2);
672
- border: .5px solid var(--border2)
673
- }
674
-
675
- .badge-abbr {
676
- display: none
677
- }
678
-
679
- .badge-full {
680
- display: inline
681
- }
682
-
683
- .sig-none {
684
- background: transparent;
685
- color: var(--text3, #6e7681);
686
- border: .5px dashed var(--border2);
687
- font-size: .72rem;
688
- letter-spacing: .06em;
689
- font-weight: 500
690
- }
691
-
692
- .model-badge {
693
- font-size: .65rem;
694
- font-weight: 500;
695
- color: var(--text3, #6e7681);
696
- background: var(--bg4);
697
- border: .5px solid var(--border2);
698
- border-radius: 4px;
699
- padding: 1px 5px;
700
- margin-left: 6px;
701
- font-family: var(--font-mono);
702
- letter-spacing: .04em;
703
- text-transform: uppercase;
704
- flex-shrink: 0
705
- }
706
-
707
- .spread-badge {
708
- margin-left: 6px;
709
- font-size: .7rem;
710
- color: var(--text3, #6e7681);
711
- font-family: var(--font-mono)
712
- }
713
-
714
- .spread-badge.illiquid {
715
- color: var(--red, #f04040);
716
- font-weight: 600
717
- }
718
-
719
- .edge-row {
720
- display: flex;
721
- align-items: center;
722
- gap: 6px;
723
- margin-top: 6px;
724
- padding-top: 6px;
725
- border-top: .5px solid var(--border2, #1f242b);
726
- font-size: .72rem;
727
- font-family: var(--font-mono);
728
- letter-spacing: .02em
729
- }
730
-
731
- .edge-implied {
732
- color: var(--text2, #9aa4b2)
733
- }
734
-
735
- .edge-fair {
736
- font-weight: 600
737
- }
738
-
739
- .edge-sep {
740
- color: var(--text3, #4a5260);
741
- opacity: .6
742
- }
743
-
744
- .edge-value {
745
- font-weight: 700;
746
- padding: 1px 5px;
747
- border-radius: 3px
748
- }
749
-
750
- .edge-pos {
751
- color: var(--green, #22d37a);
752
- background: var(--green3, rgba(34, 211, 122, .08))
753
- }
754
-
755
- .edge-neg {
756
- color: var(--red, #f04040);
757
- background: var(--red3, rgba(240, 64, 64, .08))
758
- }
759
-
760
- .edge-zero {
761
- color: var(--text3, #6e7681)
762
- }
763
-
764
- .kelly-note {
765
- flex: 1;
766
- font-size: .78rem;
767
- color: var(--text2, #9aa4b2);
768
- font-family: var(--font-mono);
769
- padding: 4px 8px;
770
- border-left: 2px solid var(--border2, #1f242b);
771
- background: #ffffff05;
772
- border-radius: 0 4px 4px 0
773
- }
774
-
775
- .kelly-warn {
776
- color: var(--red, #f04040);
777
- border-left-color: var(--red, #f04040);
778
- background: #f040400d
779
- }
780
-
781
- .detail-panel {
782
- grid-row: 2 / 3;
783
- grid-column: 1 / 2;
784
- overflow: hidden
785
- }
786
-
787
- .detail-panel .panel-body {
788
- overflow: hidden
789
- }
790
-
791
- .detail-panel .panel-body>div {
792
- height: 100%;
793
- display: flex;
794
- flex-direction: column;
795
- gap: 10px;
796
- min-height: 0
797
- }
798
-
799
- .detail-header {
800
- display: flex;
801
- align-items: flex-start;
802
- justify-content: space-between;
803
- margin-bottom: 12px;
804
- gap: 16px
805
- }
806
-
807
- .detail-tag {
808
- font-size: .875rem;
809
- color: var(--blue);
810
- font-family: var(--font-mono);
811
- text-transform: uppercase;
812
- letter-spacing: .08em;
813
- margin-bottom: 4px
814
- }
815
-
816
- .detail-q {
817
- font-size: 1rem;
818
- font-weight: 600;
819
- color: var(--text);
820
- line-height: 1.5
821
- }
822
-
823
- .detail-meta {
824
- font-size: .875rem;
825
- color: var(--text3);
826
- font-family: var(--font-mono);
827
- margin-top: 2px
828
- }
829
-
830
- .detail-metrics {
831
- display: flex;
832
- gap: 16px;
833
- align-items: center;
834
- flex-shrink: 0
835
- }
836
-
837
- .metric {
838
- text-align: right
839
- }
840
-
841
- .metric-label {
842
- font-size: .875rem;
843
- color: var(--text3);
844
- font-family: var(--font-mono);
845
- margin-bottom: 2px
846
- }
847
-
848
- .metric-value {
849
- font-size: .875rem;
850
- font-weight: 700;
851
- font-family: var(--font-mono)
852
- }
853
-
854
- .metric-sep {
855
- width: 1px;
856
- height: 40px;
857
- background: var(--border)
858
- }
859
-
860
- .outcomes-row {
861
- display: flex;
862
- gap: 16px;
863
- flex: 1;
864
- min-height: 0
865
- }
866
-
867
- .outcome-card {
868
- flex: 1;
869
- background: var(--bg3);
870
- border: .5px solid var(--border);
871
- border-radius: var(--radius-sm);
872
- padding: 14px;
873
- text-align: center
874
- }
875
-
876
- .outcome-name {
877
- font-size: .875rem;
878
- color: var(--text2);
879
- margin-bottom: 4px;
880
- font-family: var(--font-mono)
881
- }
882
-
883
- .outcome-price {
884
- font-size: 1.25rem;
885
- font-weight: 700;
886
- color: var(--text);
887
- font-family: var(--font-mono)
888
- }
889
-
890
- .outcome-delta {
891
- font-size: .875rem;
892
- font-family: var(--font-mono);
893
- margin-top: 2px
894
- }
895
-
896
- .outcome-card.yes .outcome-price {
897
- color: var(--green)
898
- }
899
-
900
- .outcome-card.no .outcome-price {
901
- color: var(--red)
902
- }
903
-
904
- .chart-container {
905
- flex: 2;
906
- background: var(--bg3);
907
- border: .5px solid var(--border);
908
- border-radius: var(--radius-sm);
909
- padding: 14px;
910
- min-height: 80px;
911
- overflow: hidden;
912
- align-self: stretch
913
- }
914
-
915
- .chart-label {
916
- font-size: .875rem;
917
- color: var(--text3);
918
- font-family: var(--font-mono);
919
- margin-bottom: 6px
920
- }
921
-
922
- .outcome-mini {
923
- display: flex;
924
- flex-direction: column;
925
- align-items: center;
926
- gap: 2px;
927
- padding: 0 10px
928
- }
929
-
930
- .outcome-mini-label {
931
- font-size: .72rem;
932
- font-family: var(--font-mono);
933
- color: var(--text3);
934
- text-transform: uppercase;
935
- letter-spacing: .06em
936
- }
937
-
938
- .outcome-mini-price {
939
- font-size: 1rem;
940
- font-weight: 700;
941
- font-family: var(--font-mono)
942
- }
943
-
944
- .outcome-mini-price.yes {
945
- color: var(--green)
946
- }
947
-
948
- .outcome-mini-price.no {
949
- color: var(--red)
950
- }
951
-
952
- .ai-title-group {
953
- display: flex;
954
- align-items: center;
955
- gap: 8px
956
- }
957
-
958
- .ai-title-group .ai-icon {
959
- width: 20px;
960
- height: 20px;
961
- font-size: .75rem
962
- }
963
-
964
- .ai-box {
965
- flex: 1;
966
- background: var(--bg3);
967
- border: .5px solid var(--blue2);
968
- border-radius: var(--radius-sm);
969
- padding: 14px;
970
- display: flex;
971
- flex-direction: column;
972
- gap: 8px;
973
- overflow: hidden;
974
- min-width: 0
975
- }
976
-
977
- .ai-icon {
978
- width: 32px;
979
- height: 32px;
980
- border-radius: 6px;
981
- background: var(--blue3);
982
- display: flex;
983
- align-items: center;
984
- justify-content: center;
985
- flex-shrink: 0;
986
- font-size: .875rem
987
- }
988
-
989
- .ai-label {
990
- font-size: .875rem;
991
- color: var(--blue);
992
- font-family: var(--font-mono);
993
- text-transform: uppercase;
994
- letter-spacing: .06em;
995
- margin-bottom: 3px
996
- }
997
-
998
- .ai-text {
999
- font-size: .9375rem;
1000
- color: var(--text2);
1001
- line-height: 1.5
1002
- }
1003
-
1004
- .sim-row {
1005
- display: flex;
1006
- gap: 16px;
1007
- align-items: center;
1008
- flex-wrap: wrap;
1009
- justify-content: flex-end
1010
- }
1011
-
1012
- .sim-label {
1013
- font-size: .9375rem;
1014
- color: var(--text3);
1015
- font-family: var(--font-mono)
1016
- }
1017
-
1018
- .sim-input {
1019
- background: var(--bg3);
1020
- border: .5px solid var(--border2);
1021
- border-radius: var(--radius-sm);
1022
- padding: 8px 14px;
1023
- color: var(--text);
1024
- font-size: 1rem;
1025
- font-family: var(--font-mono);
1026
- width: 100px;
1027
- outline: none
1028
- }
1029
-
1030
- .sim-input:focus {
1031
- border-color: var(--blue2)
1032
- }
1033
-
1034
- .sim-btn-yes {
1035
- background: var(--green3);
1036
- border: .5px solid var(--green2);
1037
- color: var(--green);
1038
- font-size: .9375rem;
1039
- font-weight: 600;
1040
- padding: 8px 16px;
1041
- border-radius: var(--radius-sm);
1042
- cursor: pointer;
1043
- font-family: var(--font-mono);
1044
- transition: opacity .15s
1045
- }
1046
-
1047
- .sim-btn-no {
1048
- background: var(--red3);
1049
- border: .5px solid var(--red2);
1050
- color: var(--red);
1051
- font-size: .9375rem;
1052
- font-weight: 600;
1053
- padding: 8px 16px;
1054
- border-radius: var(--radius-sm);
1055
- cursor: pointer;
1056
- font-family: var(--font-mono);
1057
- transition: opacity .15s
1058
- }
1059
-
1060
- .sim-btn-yes:hover,
1061
- .sim-btn-no:hover {
1062
- opacity: .85
1063
- }
1064
-
1065
- .sim-disclaimer {
1066
- font-size: .875rem;
1067
- color: var(--text3);
1068
- font-family: var(--font-mono)
1069
- }
1070
-
1071
- .legend {
1072
- display: flex;
1073
- gap: 16px;
1074
- align-items: center
1075
- }
1076
-
1077
- .legend-item {
1078
- display: flex;
1079
- align-items: center;
1080
- gap: 5px;
1081
- font-size: .875rem;
1082
- color: var(--text3);
1083
- font-family: var(--font-mono)
1084
- }
1085
-
1086
- .legend-dot {
1087
- width: 10px;
1088
- height: 10px;
1089
- border-radius: 50%
1090
- }
1091
-
1092
- .legend-dot.green {
1093
- background: var(--green)
1094
- }
1095
-
1096
- .legend-dot.red {
1097
- background: var(--red)
1098
- }
1099
-
1100
- .legend-dot.gray {
1101
- background: var(--text3)
1102
- }
1103
-
1104
- .legend.end {
1105
- margin-left: auto
1106
- }
1107
-
1108
- .table-wrap {
1109
- overflow: auto;
1110
- border: .5px solid var(--border);
1111
- border-radius: var(--radius-sm);
1112
- background: var(--bg3)
1113
- }
1114
-
1115
- table {
1116
- width: 100%;
1117
- border-collapse: collapse;
1118
- font-size: 1rem
1119
- }
1120
-
1121
- th,
1122
- td {
1123
- padding: 14px;
1124
- text-align: left;
1125
- border-bottom: .5px solid var(--border)
1126
- }
1127
-
1128
- th {
1129
- font-family: var(--font-mono);
1130
- font-size: .875rem;
1131
- color: var(--text3);
1132
- text-transform: uppercase;
1133
- letter-spacing: .06em;
1134
- font-weight: 500;
1135
- background: var(--bg4);
1136
- position: sticky;
1137
- top: 0
1138
- }
1139
-
1140
- tr:hover td {
1141
- background: #ffffff05
1142
- }
1143
-
1144
- td {
1145
- color: var(--text)
1146
- }
1147
-
1148
- .td-mono {
1149
- font-family: var(--font-mono)
1150
- }
1151
-
1152
- .td-green {
1153
- color: var(--green)
1154
- }
1155
-
1156
- .td-red {
1157
- color: var(--red)
1158
- }
1159
-
1160
- .td-blue {
1161
- color: var(--blue)
1162
- }
1163
-
1164
- .empty-state {
1165
- padding: 40px;
1166
- text-align: center;
1167
- color: var(--text3);
1168
- font-size: 1rem
1169
- }
1170
-
1171
- #app .leaflet-container {
1172
- background: var(--bg3);
1173
- font-family: var(--font-mono)
1174
- }
1175
-
1176
- #app .leaflet-popup-content-wrapper {
1177
- background: var(--bg2);
1178
- color: var(--text);
1179
- border: .5px solid var(--border);
1180
- border-radius: var(--radius-sm)
1181
- }
1182
-
1183
- #app .leaflet-popup-tip {
1184
- background: var(--bg2)
1185
- }
1186
-
1187
- .sparkline {
1188
- display: flex;
1189
- align-items: flex-end;
1190
- gap: 2px;
1191
- height: 32px;
1192
- margin-top: 6px
1193
- }
1194
-
1195
- .spark-bar {
1196
- width: 3px;
1197
- border-radius: 1px;
1198
- background: var(--blue2);
1199
- transition: height .3s
1200
- }
1201
-
1202
- @keyframes pulse {
1203
-
1204
- 0%,
1205
- to {
1206
- opacity: 1
1207
- }
1208
-
1209
- 50% {
1210
- opacity: .4
1211
- }
1212
- }
1213
-
1214
- @keyframes scroll-stats {
1215
- 0% {
1216
- transform: translate(0)
1217
- }
1218
-
1219
- to {
1220
- transform: translate(-50%)
1221
- }
1222
- }
1223
-
1224
- ::-webkit-scrollbar {
1225
- width: 6px;
1226
- height: 6px
1227
- }
1228
-
1229
- ::-webkit-scrollbar-track {
1230
- background: transparent
1231
- }
1232
-
1233
- ::-webkit-scrollbar-thumb {
1234
- background: var(--bg4);
1235
- border-radius: 3px
1236
- }
1237
-
1238
- ::-webkit-scrollbar-thumb:hover {
1239
- background: var(--text3)
1240
- }
1241
-
1242
- .flex-between {
1243
- display: flex;
1244
- justify-content: space-between;
1245
- align-items: center
1246
- }
1247
-
1248
- .flex-start {
1249
- display: flex;
1250
- align-items: flex-start;
1251
- justify-content: space-between
1252
- }
1253
-
1254
- .flex-row {
1255
- display: flex;
1256
- align-items: center
1257
- }
1258
-
1259
- .flex-wrap {
1260
- flex-wrap: wrap
1261
- }
1262
-
1263
- .gap-6 {
1264
- gap: 14px
1265
- }
1266
-
1267
- .gap-8 {
1268
- gap: 16px
1269
- }
1270
-
1271
- .text-green {
1272
- color: var(--green)
1273
- }
1274
-
1275
- .text-red {
1276
- color: var(--red)
1277
- }
1278
-
1279
- .text-blue {
1280
- color: var(--blue)
1281
- }
1282
-
1283
- .text-amber {
1284
- color: var(--amber)
1285
- }
1286
-
1287
- .text-neutral {
1288
- color: var(--text3)
1289
- }
1290
-
1291
- .bg-green {
1292
- background: var(--green)
1293
- }
1294
-
1295
- .bg-red {
1296
- background: var(--red)
1297
- }
1298
-
1299
- .bg-amber {
1300
- background: var(--amber)
1301
- }
1302
-
1303
- .flex-1 {
1304
- flex: 1
1305
- }
1306
-
1307
- .font-mono {
1308
- font-family: var(--font-mono)
1309
- }
1310
-
1311
- .text-xs,
1312
- .text-sm {
1313
- font-size: .875rem
1314
- }
1315
-
1316
- .text-base {
1317
- font-size: .9375rem
1318
- }
1319
-
1320
- .text-lg {
1321
- font-size: 1rem
1322
- }
1323
-
1324
- .text-xl {
1325
- font-size: 1.125rem
1326
- }
1327
-
1328
- .font-semibold {
1329
- font-weight: 600
1330
- }
1331
-
1332
- .font-bold {
1333
- font-weight: 700
1334
- }
1335
-
1336
- .mb-4 {
1337
- margin-bottom: 4px
1338
- }
1339
-
1340
- .mb-6 {
1341
- margin-bottom: 6px
1342
- }
1343
-
1344
- .mb-8 {
1345
- margin-bottom: 8px
1346
- }
1347
-
1348
- .mt-4 {
1349
- margin-top: 4px
1350
- }
1351
-
1352
- .mt-6 {
1353
- margin-top: 6px
1354
- }
1355
-
1356
- .ml-auto {
1357
- margin-left: auto
1358
- }
1359
-
1360
- .divider {
1361
- height: 1px;
1362
- background: var(--border);
1363
- margin: 8px 0
1364
- }
1365
-
1366
- .empty-state-sm {
1367
- padding: 16px;
1368
- text-align: center;
1369
- color: var(--text3);
1370
- font-size: 1rem
1371
- }
1372
-
1373
- .map-popup {
1374
- font-family: var(--font-sans);
1375
- font-size: .9375rem;
1376
- color: var(--text);
1377
- max-width: 200px
1378
- }
1379
-
1380
- .map-popup-cat {
1381
- font-size: .875rem;
1382
- color: var(--text3);
1383
- font-family: var(--font-mono);
1384
- margin-bottom: 4px;
1385
- text-transform: uppercase
1386
- }
1387
-
1388
- .map-popup-q {
1389
- font-weight: 600;
1390
- margin-bottom: 4px;
1391
- line-height: 1.3
1392
- }
1393
-
1394
- .map-popup-prices {
1395
- display: flex;
1396
- gap: 16px;
1397
- font-family: var(--font-mono);
1398
- font-size: .9375rem
1399
- }
1400
-
1401
- .map-label-text {
1402
- color: var(--text2);
1403
- font-family: var(--font-mono);
1404
- font-size: .875rem;
1405
- text-shadow: 0 1px 2px #000
1406
- }
1407
-
1408
- .modal-overlay {
1409
- position: fixed;
1410
- inset: 0;
1411
- background: #000000b3;
1412
- -webkit-backdrop-filter: blur(4px);
1413
- backdrop-filter: blur(4px);
1414
- display: flex;
1415
- align-items: center;
1416
- justify-content: center;
1417
- z-index: 1000;
1418
- opacity: 1;
1419
- transition: opacity .2s ease
1420
- }
1421
-
1422
- .modal-overlay.hidden {
1423
- display: none
1424
- }
1425
-
1426
- .modal {
1427
- background: var(--bg2);
1428
- border: .5px solid var(--border);
1429
- border-radius: var(--radius);
1430
- width: 100%;
1431
- max-width: 420px;
1432
- overflow: hidden
1433
- }
1434
-
1435
- .modal-header {
1436
- display: flex;
1437
- align-items: center;
1438
- justify-content: space-between;
1439
- padding: 16px 20px;
1440
- border-bottom: .5px solid var(--border)
1441
- }
1442
-
1443
- .modal-tabs {
1444
- display: flex;
1445
- gap: 4px
1446
- }
1447
-
1448
- .modal-tab {
1449
- background: transparent;
1450
- border: none;
1451
- color: var(--text2);
1452
- font-family: var(--font-sans);
1453
- font-size: .9375rem;
1454
- font-weight: 500;
1455
- padding: 8px 16px;
1456
- border-radius: var(--radius-sm);
1457
- cursor: pointer;
1458
- transition: background .15s, color .15s
1459
- }
1460
-
1461
- .modal-tab:hover {
1462
- color: var(--text)
1463
- }
1464
-
1465
- .modal-tab.active {
1466
- background: var(--bg3);
1467
- color: var(--text)
1468
- }
1469
-
1470
- .modal-close {
1471
- background: transparent;
1472
- border: none;
1473
- color: var(--text2);
1474
- font-size: 1.125rem;
1475
- cursor: pointer;
1476
- width: 32px;
1477
- height: 32px;
1478
- display: flex;
1479
- align-items: center;
1480
- justify-content: center;
1481
- border-radius: var(--radius-sm);
1482
- transition: background .15s, color .15s
1483
- }
1484
-
1485
- .modal-close:hover {
1486
- background: var(--bg3);
1487
- color: var(--text)
1488
- }
1489
-
1490
- .modal-body {
1491
- padding: 20px
1492
- }
1493
-
1494
- .modal-form {
1495
- display: none;
1496
- flex-direction: column;
1497
- gap: 16px
1498
- }
1499
-
1500
- .modal-form.active {
1501
- display: flex
1502
- }
1503
-
1504
- .form-group {
1505
- display: flex;
1506
- flex-direction: column;
1507
- gap: 6px
1508
- }
1509
-
1510
- .form-group label {
1511
- font-size: .875rem;
1512
- color: var(--text2);
1513
- font-family: var(--font-mono);
1514
- text-transform: uppercase;
1515
- letter-spacing: .06em
1516
- }
1517
-
1518
- .form-group input {
1519
- background: var(--bg3);
1520
- border: .5px solid var(--border2);
1521
- border-radius: var(--radius-sm);
1522
- padding: 12px 14px;
1523
- color: var(--text);
1524
- font-size: .9375rem;
1525
- font-family: var(--font-sans);
1526
- outline: none;
1527
- transition: border-color .15s
1528
- }
1529
-
1530
- .form-group input:focus {
1531
- border-color: var(--blue2)
1532
- }
1533
-
1534
- .form-group input::placeholder {
1535
- color: var(--text3)
1536
- }
1537
-
1538
- .form-error {
1539
- font-size: .875rem;
1540
- color: var(--red);
1541
- font-family: var(--font-mono);
1542
- min-height: 18px
1543
- }
1544
-
1545
- .modal-submit {
1546
- background: #2563eb;
1547
- border: none;
1548
- color: #fff;
1549
- font-family: var(--font-sans);
1550
- font-size: .9375rem;
1551
- font-weight: 600;
1552
- padding: 12px;
1553
- border-radius: var(--radius-sm);
1554
- cursor: pointer;
1555
- transition: opacity .15s;
1556
- margin-top: 4px
1557
- }
1558
-
1559
- .modal-submit:hover {
1560
- opacity: .9
1561
- }
1562
-
1563
- .modal-title {
1564
- display: flex;
1565
- align-items: center;
1566
- gap: 10px;
1567
- font-size: 1rem;
1568
- font-weight: 600;
1569
- color: var(--text)
1570
- }
1571
-
1572
- .modal-title-icon {
1573
- font-size: 1.125rem
1574
- }
1575
-
1576
- .form-hint {
1577
- font-size: .8125rem;
1578
- color: var(--text3);
1579
- line-height: 1.4
1580
- }
1581
-
1582
- .form-hint a {
1583
- color: var(--blue);
1584
- text-decoration: none
1585
- }
1586
-
1587
- .form-hint a:hover {
1588
- text-decoration: underline
1589
- }
1590
-
1591
- .toggle-row {
1592
- display: flex;
1593
- align-items: center;
1594
- justify-content: space-between
1595
- }
1596
-
1597
- .toggle-switch {
1598
- position: relative;
1599
- display: inline-block;
1600
- width: 44px;
1601
- height: 24px;
1602
- cursor: pointer
1603
- }
1604
-
1605
- .toggle-switch input {
1606
- opacity: 0;
1607
- width: 0;
1608
- height: 0
1609
- }
1610
-
1611
- .toggle-slider {
1612
- position: absolute;
1613
- inset: 0;
1614
- background: var(--bg4);
1615
- border-radius: 24px;
1616
- transition: background .2s;
1617
- border: .5px solid var(--border)
1618
- }
1619
-
1620
- .toggle-slider:before {
1621
- content: "";
1622
- position: absolute;
1623
- height: 18px;
1624
- width: 18px;
1625
- left: 2px;
1626
- bottom: 2px;
1627
- background: var(--text2);
1628
- border-radius: 50%;
1629
- transition: transform .2s, background .2s
1630
- }
1631
-
1632
- .toggle-switch input:checked+.toggle-slider {
1633
- background: var(--blue2);
1634
- border-color: var(--blue)
1635
- }
1636
-
1637
- .toggle-switch input:checked+.toggle-slider:before {
1638
- transform: translate(20px);
1639
- background: var(--text)
1640
- }
1641
-
1642
- .form-status {
1643
- font-size: .875rem;
1644
- font-family: var(--font-mono);
1645
- min-height: 18px;
1646
- color: var(--text2)
1647
- }
1648
-
1649
- .form-status.success {
1650
- color: var(--green)
1651
- }
1652
-
1653
- .form-status.error {
1654
- color: var(--red)
1655
- }
1656
-
1657
- .form-actions {
1658
- display: flex;
1659
- gap: 10px;
1660
- margin-top: 4px
1661
- }
1662
-
1663
- .modal-submit--secondary {
1664
- background: var(--bg3);
1665
- color: var(--text);
1666
- border: .5px solid var(--border2)
1667
- }
1668
-
1669
- .modal-submit--secondary:hover {
1670
- background: var(--bg4);
1671
- opacity: 1
1672
- }
1673
-
1674
- .telegram-instructions {
1675
- display: flex;
1676
- flex-direction: column;
1677
- gap: 12px;
1678
- margin-bottom: 20px;
1679
- padding-bottom: 16px;
1680
- border-bottom: .5px solid var(--border)
1681
- }
1682
-
1683
- .instruction-step {
1684
- display: flex;
1685
- gap: 12px;
1686
- align-items: flex-start
1687
- }
1688
-
1689
- .step-num {
1690
- width: 24px;
1691
- height: 24px;
1692
- border-radius: 50%;
1693
- background: var(--blue3);
1694
- color: var(--blue);
1695
- font-size: .8125rem;
1696
- font-weight: 600;
1697
- display: flex;
1698
- align-items: center;
1699
- justify-content: center;
1700
- flex-shrink: 0;
1701
- margin-top: 2px
1702
- }
1703
-
1704
- .step-body {
1705
- flex: 1
1706
- }
1707
-
1708
- .step-body strong {
1709
- display: block;
1710
- font-size: .875rem;
1711
- font-weight: 600;
1712
- color: var(--text);
1713
- margin-bottom: 2px
1714
- }
1715
-
1716
- .step-body p {
1717
- font-size: .8125rem;
1718
- color: var(--text2);
1719
- line-height: 1.5;
1720
- margin: 0
1721
- }
1722
-
1723
- .step-body a {
1724
- color: var(--blue);
1725
- text-decoration: none
1726
- }
1727
-
1728
- .step-body a:hover {
1729
- text-decoration: underline
1730
- }
1731
-
1732
- .step-body code {
1733
- background: var(--bg3);
1734
- padding: 1px 5px;
1735
- border-radius: 4px;
1736
- font-family: var(--font-mono);
1737
- font-size: .75rem;
1738
- color: var(--text)
1739
- }
1740
-
1741
- .auth-indicator {
1742
- width: 32px;
1743
- height: 32px;
1744
- border-radius: 50%;
1745
- background: var(--bg3);
1746
- border: .5px solid var(--border2);
1747
- color: var(--text2);
1748
- display: flex;
1749
- align-items: center;
1750
- justify-content: center;
1751
- cursor: pointer;
1752
- padding: 0;
1753
- flex-shrink: 0;
1754
- transition: color .15s, border-color .15s, background .2s
1755
- }
1756
-
1757
- .auth-indicator:after {
1758
- content: "";
1759
- width: 8px;
1760
- height: 8px;
1761
- border-radius: 50%;
1762
- background: var(--red);
1763
- display: block;
1764
- transition: background .2s
1765
- }
1766
-
1767
- .auth-indicator.logged-in:after {
1768
- background: var(--green)
1769
- }
1770
-
1771
- .stats-track {
1772
- display: flex;
1773
- align-items: center;
1774
- gap: 20px;
1775
- flex: 1
1776
- }
1777
-
1778
- .stats-track .legend.end~.stat,
1779
- .stats-track .legend.end~.legend {
1780
- display: none
1781
- }
1782
-
1783
- .signals-sentinel {
1784
- padding: 12px;
1785
- text-align: center;
1786
- color: var(--text3);
1787
- font-family: var(--font-mono);
1788
- font-size: .875rem
1789
- }
1790
-
1791
- #filter-trend {
1792
- min-width: 170px;
1793
- background: var(--bg3);
1794
- border: .5px solid var(--amber2);
1795
- color: var(--amber)
1796
- }
1797
-
1798
- #filter-trend:focus {
1799
- border-color: var(--amber)
1800
- }
1801
-
1802
- #filter-trend option {
1803
- background: var(--bg2);
1804
- color: var(--text)
1805
- }
1806
-
1807
- .trend-badge {
1808
- font-size: .75rem;
1809
- font-weight: 600;
1810
- padding: 2px 6px;
1811
- border-radius: 4px;
1812
- font-family: var(--font-mono);
1813
- letter-spacing: .04em;
1814
- margin-left: 8px
1815
- }
1816
-
1817
- .trend-hot {
1818
- background: #f0a02026;
1819
- color: var(--amber);
1820
- border: .5px solid var(--amber2)
1821
- }
1822
-
1823
- .trend-bull {
1824
- background: var(--green3);
1825
- color: var(--green);
1826
- border: .5px solid var(--green2)
1827
- }
1828
-
1829
- .trend-bear {
1830
- background: var(--red3);
1831
- color: var(--red);
1832
- border: .5px solid var(--red2)
1833
- }
1834
-
1835
- .trend-volatile {
1836
- background: #4a9eff26;
1837
- color: var(--blue);
1838
- border: .5px solid var(--blue2)
1839
- }
1840
-
1841
- @media(max-width:1024px) {
1842
- * {
1843
- scrollbar-width: none;
1844
- -ms-overflow-style: none
1845
- }
1846
-
1847
- *::-webkit-scrollbar {
1848
- display: none
1849
- }
1850
-
1851
- .dashboard-grid {
1852
- grid-template-columns: 1fr;
1853
- grid-template-rows: auto auto auto
1854
- }
1855
-
1856
- .signals-panel {
1857
- grid-row: auto;
1858
- grid-column: auto;
1859
- max-height: 400px
1860
- }
1861
-
1862
- .map-panel {
1863
- grid-row: auto;
1864
- grid-column: auto;
1865
- min-height: 300px
1866
- }
1867
-
1868
- .detail-panel {
1869
- grid-row: auto;
1870
- grid-column: auto
1871
- }
1872
- }
1873
-
1874
- @media(max-width:640px) {
1875
- .layout {
1876
- grid-template-columns: 0 1fr;
1877
- grid-template-rows: auto 1fr
1878
- }
1879
-
1880
- .sidebar {
1881
- display: none
1882
- }
1883
-
1884
- .detail-header {
1885
- flex-direction: column;
1886
- gap: 8px;
1887
- margin-bottom: 8px
1888
- }
1889
-
1890
- .detail-metrics {
1891
- width: 100%;
1892
- justify-content: flex-start
1893
- }
1894
-
1895
- .metric {
1896
- text-align: left
1897
- }
1898
-
1899
- .outcomes-row {
1900
- display: grid;
1901
- grid-template-columns: 1fr 1fr;
1902
- gap: 10px;
1903
- margin-bottom: 10px
1904
- }
1905
-
1906
- .outcomes-row .ai-box {
1907
- grid-column: 1 / -1
1908
- }
1909
-
1910
- .outcomes-row .chart-container {
1911
- grid-column: 1 / -1;
1912
- height: 130px;
1913
- max-height: 130px;
1914
- min-height: 0
1915
- }
1916
-
1917
- .sim-row {
1918
- display: grid;
1919
- grid-template-columns: 1fr 1fr;
1920
- gap: 8px;
1921
- align-items: center
1922
- }
1923
-
1924
- .sim-row .kelly-note {
1925
- grid-column: 1 / -1;
1926
- border-left: 2px solid var(--border2);
1927
- padding: 6px 10px
1928
- }
1929
-
1930
- .sim-row .sim-label {
1931
- grid-column: 1 / -1;
1932
- font-size: .875rem
1933
- }
1934
-
1935
- .sim-row .sim-input {
1936
- grid-column: 1 / -1;
1937
- width: 100%;
1938
- min-width: 0
1939
- }
1940
-
1941
- .sim-row .sim-btn-yes,
1942
- .sim-row .sim-btn-no {
1943
- width: 100%;
1944
- padding: 10px 8px;
1945
- font-size: .8125rem;
1946
- white-space: nowrap;
1947
- text-align: center
1948
- }
1949
-
1950
- .sim-row .sim-disclaimer {
1951
- grid-column: 1 / -1;
1952
- font-size: .75rem
1953
- }
1954
-
1955
- .desktop-only {
1956
- display: none !important
1957
- }
1958
-
1959
- .mobile-only {
1960
- display: initial
1961
- }
1962
-
1963
- .model-badge,
1964
- .badge-full {
1965
- display: none
1966
- }
1967
-
1968
- .badge-abbr {
1969
- display: inline
1970
- }
1971
-
1972
- .topbar {
1973
- flex-wrap: wrap;
1974
- padding: 8px 12px;
1975
- gap: 8px;
1976
- min-height: auto;
1977
- height: auto
1978
- }
1979
-
1980
- .topbar-logo {
1981
- position: static;
1982
- height: auto;
1983
- order: 0;
1984
- flex: 1
1985
- }
1986
-
1987
- .topbar-actions {
1988
- order: 0;
1989
- margin-left: 0;
1990
- gap: 10px
1991
- }
1992
-
1993
- .topbar-stats {
1994
- order: 1;
1995
- width: 100%;
1996
- flex: none;
1997
- display: flex;
1998
- align-items: center;
1999
- overflow: hidden
2000
- }
2001
-
2002
- .topbar-stats .live-badge {
2003
- display: none
2004
- }
2005
-
2006
- .stats-track {
2007
- display: flex;
2008
- align-items: center;
2009
- gap: 24px;
2010
- width: max-content;
2011
- animation: scroll-stats 20s linear infinite;
2012
- flex: none;
2013
- mask-image: linear-gradient(90deg, transparent, black 8%, black 92%, transparent);
2014
- -webkit-mask-image: linear-gradient(90deg, transparent, black 8%, black 92%, transparent)
2015
- }
2016
-
2017
- .stats-track:hover {
2018
- animation-play-state: paused
2019
- }
2020
-
2021
- .topbar-stats .stat {
2022
- gap: 8px;
2023
- padding: 4px 0;
2024
- flex-shrink: 0
2025
- }
2026
-
2027
- .topbar-stats .stat-label {
2028
- font-size: .75rem
2029
- }
2030
-
2031
- .topbar-stats .stat-val {
2032
- font-size: .875rem
2033
- }
2034
-
2035
- .topbar-stats .stat-delta {
2036
- font-size: .75rem
2037
- }
2038
-
2039
- .topbar-stats .legend {
2040
- display: none
2041
- }
2042
-
2043
- .stats-track .legend.end~.stat {
2044
- display: flex
2045
- }
2046
-
2047
- .dashboard-grid {
2048
- height: auto
2049
- }
2050
-
2051
- .map-panel {
2052
- min-height: 250px
2053
- }
2054
-
2055
- .signals-panel {
2056
- max-height: none
2057
- }
2058
-
2059
- .panel.collapsed {
2060
- min-height: 0;
2061
- max-height: none
2062
- }
2063
-
2064
- #btn-telegram-mobile {
2065
- color: var(--blue);
2066
- background: var(--blue3);
2067
- border-color: var(--blue2)
2068
- }
2069
-
2070
- #btn-notif,
2071
- #btn-auth-mobile {
2072
- display: flex
2073
- }
2074
-
2075
- .topbar-logo .logo-text {
2076
- font-size: 1rem
2077
- }
2078
-
2079
- #panel-map .panel-header {
2080
- flex-wrap: wrap;
2081
- gap: 6px 0
2082
- }
2083
-
2084
- #panel-map .panel-title {
2085
- flex: 1
2086
- }
2087
-
2088
- .map-legend {
2089
- width: 100%;
2090
- margin: 0;
2091
- justify-content: flex-end;
2092
- order: 1
2093
- }
2094
-
2095
- #panel-signals .panel-header {
2096
- position: sticky;
2097
- top: 0;
2098
- z-index: 10;
2099
- background: var(--bg2)
2100
- }
2101
-
2102
- #panel-detail {
2103
- display: none
2104
- }
2105
-
2106
- .market-card-detail {
2107
- background: var(--bg3);
2108
- border: .5px solid var(--blue2);
2109
- border-radius: var(--radius-sm);
2110
- padding: 14px;
2111
- display: flex;
2112
- flex-direction: column;
2113
- gap: 12px
2114
- }
2115
- }
2116
-
2117
- .leaflet-pane,
2118
- .leaflet-tile,
2119
- .leaflet-marker-icon,
2120
- .leaflet-marker-shadow,
2121
- .leaflet-tile-container,
2122
- .leaflet-pane>svg,
2123
- .leaflet-pane>canvas,
2124
- .leaflet-zoom-box,
2125
- .leaflet-image-layer,
2126
- .leaflet-layer {
2127
- position: absolute;
2128
- left: 0;
2129
- top: 0
2130
- }
2131
-
2132
- .leaflet-container {
2133
- overflow: hidden
2134
- }
2135
-
2136
- .leaflet-tile,
2137
- .leaflet-marker-icon,
2138
- .leaflet-marker-shadow {
2139
- -webkit-user-select: none;
2140
- -moz-user-select: none;
2141
- user-select: none;
2142
- -webkit-user-drag: none
2143
- }
2144
-
2145
- .leaflet-tile::selection {
2146
- background: transparent
2147
- }
2148
-
2149
- .leaflet-safari .leaflet-tile {
2150
- image-rendering: -webkit-optimize-contrast
2151
- }
2152
-
2153
- .leaflet-safari .leaflet-tile-container {
2154
- width: 1600px;
2155
- height: 1600px;
2156
- -webkit-transform-origin: 0 0
2157
- }
2158
-
2159
- .leaflet-marker-icon,
2160
- .leaflet-marker-shadow {
2161
- display: block
2162
- }
2163
-
2164
- .leaflet-container .leaflet-overlay-pane svg {
2165
- max-width: none !important;
2166
- max-height: none !important
2167
- }
2168
-
2169
- .leaflet-container .leaflet-marker-pane img,
2170
- .leaflet-container .leaflet-shadow-pane img,
2171
- .leaflet-container .leaflet-tile-pane img,
2172
- .leaflet-container img.leaflet-image-layer,
2173
- .leaflet-container .leaflet-tile {
2174
- max-width: none !important;
2175
- max-height: none !important;
2176
- width: auto;
2177
- padding: 0
2178
- }
2179
-
2180
- .leaflet-container img.leaflet-tile {
2181
- mix-blend-mode: plus-lighter
2182
- }
2183
-
2184
- .leaflet-container.leaflet-touch-zoom {
2185
- -ms-touch-action: pan-x pan-y;
2186
- touch-action: pan-x pan-y
2187
- }
2188
-
2189
- .leaflet-container.leaflet-touch-drag {
2190
- -ms-touch-action: pinch-zoom;
2191
- touch-action: none;
2192
- touch-action: pinch-zoom
2193
- }
2194
-
2195
- .leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
2196
- -ms-touch-action: none;
2197
- touch-action: none
2198
- }
2199
-
2200
- .leaflet-container {
2201
- -webkit-tap-highlight-color: transparent
2202
- }
2203
-
2204
- .leaflet-container a {
2205
- -webkit-tap-highlight-color: rgba(51, 181, 229, .4)
2206
- }
2207
-
2208
- .leaflet-tile {
2209
- filter: inherit;
2210
- visibility: hidden
2211
- }
2212
-
2213
- .leaflet-tile-loaded {
2214
- visibility: inherit
2215
- }
2216
-
2217
- .leaflet-zoom-box {
2218
- width: 0;
2219
- height: 0;
2220
- -moz-box-sizing: border-box;
2221
- box-sizing: border-box;
2222
- z-index: 800
2223
- }
2224
-
2225
- .leaflet-overlay-pane svg {
2226
- -moz-user-select: none
2227
- }
2228
-
2229
- .leaflet-pane {
2230
- z-index: 400
2231
- }
2232
-
2233
- .leaflet-tile-pane {
2234
- z-index: 200
2235
- }
2236
-
2237
- .leaflet-overlay-pane {
2238
- z-index: 400
2239
- }
2240
-
2241
- .leaflet-shadow-pane {
2242
- z-index: 500
2243
- }
2244
-
2245
- .leaflet-marker-pane {
2246
- z-index: 600
2247
- }
2248
-
2249
- .leaflet-tooltip-pane {
2250
- z-index: 650
2251
- }
2252
-
2253
- .leaflet-popup-pane {
2254
- z-index: 700
2255
- }
2256
-
2257
- .leaflet-map-pane canvas {
2258
- z-index: 100
2259
- }
2260
-
2261
- .leaflet-map-pane svg {
2262
- z-index: 200
2263
- }
2264
-
2265
- .leaflet-vml-shape {
2266
- width: 1px;
2267
- height: 1px
2268
- }
2269
-
2270
- .lvml {
2271
- behavior: url(#default#VML);
2272
- display: inline-block;
2273
- position: absolute
2274
- }
2275
-
2276
- .leaflet-control {
2277
- position: relative;
2278
- z-index: 800;
2279
- pointer-events: visiblePainted;
2280
- pointer-events: auto
2281
- }
2282
-
2283
- .leaflet-top,
2284
- .leaflet-bottom {
2285
- position: absolute;
2286
- z-index: 1000;
2287
- pointer-events: none
2288
- }
2289
-
2290
- .leaflet-top {
2291
- top: 0
2292
- }
2293
-
2294
- .leaflet-right {
2295
- right: 0
2296
- }
2297
-
2298
- .leaflet-bottom {
2299
- bottom: 0
2300
- }
2301
-
2302
- .leaflet-left {
2303
- left: 0
2304
- }
2305
-
2306
- .leaflet-control {
2307
- float: left;
2308
- clear: both
2309
- }
2310
-
2311
- .leaflet-right .leaflet-control {
2312
- float: right
2313
- }
2314
-
2315
- .leaflet-top .leaflet-control {
2316
- margin-top: 10px
2317
- }
2318
-
2319
- .leaflet-bottom .leaflet-control {
2320
- margin-bottom: 10px
2321
- }
2322
-
2323
- .leaflet-left .leaflet-control {
2324
- margin-left: 10px
2325
- }
2326
-
2327
- .leaflet-right .leaflet-control {
2328
- margin-right: 10px
2329
- }
2330
-
2331
- .leaflet-fade-anim .leaflet-popup {
2332
- opacity: 0;
2333
- -webkit-transition: opacity .2s linear;
2334
- -moz-transition: opacity .2s linear;
2335
- transition: opacity .2s linear
2336
- }
2337
-
2338
- .leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
2339
- opacity: 1
2340
- }
2341
-
2342
- .leaflet-zoom-animated {
2343
- -webkit-transform-origin: 0 0;
2344
- -ms-transform-origin: 0 0;
2345
- transform-origin: 0 0
2346
- }
2347
-
2348
- svg.leaflet-zoom-animated {
2349
- will-change: transform
2350
- }
2351
-
2352
- .leaflet-zoom-anim .leaflet-zoom-animated {
2353
- -webkit-transition: -webkit-transform .25s cubic-bezier(0, 0, .25, 1);
2354
- -moz-transition: -moz-transform .25s cubic-bezier(0, 0, .25, 1);
2355
- transition: transform .25s cubic-bezier(0, 0, .25, 1)
2356
- }
2357
-
2358
- .leaflet-zoom-anim .leaflet-tile,
2359
- .leaflet-pan-anim .leaflet-tile {
2360
- -webkit-transition: none;
2361
- -moz-transition: none;
2362
- transition: none
2363
- }
2364
-
2365
- .leaflet-zoom-anim .leaflet-zoom-hide {
2366
- visibility: hidden
2367
- }
2368
-
2369
- .leaflet-interactive {
2370
- cursor: pointer
2371
- }
2372
-
2373
- .leaflet-grab {
2374
- cursor: -webkit-grab;
2375
- cursor: -moz-grab;
2376
- cursor: grab
2377
- }
2378
-
2379
- .leaflet-crosshair,
2380
- .leaflet-crosshair .leaflet-interactive {
2381
- cursor: crosshair
2382
- }
2383
-
2384
- .leaflet-popup-pane,
2385
- .leaflet-control {
2386
- cursor: auto
2387
- }
2388
-
2389
- .leaflet-dragging .leaflet-grab,
2390
- .leaflet-dragging .leaflet-grab .leaflet-interactive,
2391
- .leaflet-dragging .leaflet-marker-draggable {
2392
- cursor: move;
2393
- cursor: -webkit-grabbing;
2394
- cursor: -moz-grabbing;
2395
- cursor: grabbing
2396
- }
2397
-
2398
- .leaflet-marker-icon,
2399
- .leaflet-marker-shadow,
2400
- .leaflet-image-layer,
2401
- .leaflet-pane>svg path,
2402
- .leaflet-tile-container {
2403
- pointer-events: none
2404
- }
2405
-
2406
- .leaflet-marker-icon.leaflet-interactive,
2407
- .leaflet-image-layer.leaflet-interactive,
2408
- .leaflet-pane>svg path.leaflet-interactive,
2409
- svg.leaflet-image-layer.leaflet-interactive path {
2410
- pointer-events: visiblePainted;
2411
- pointer-events: auto
2412
- }
2413
-
2414
- .leaflet-container {
2415
- background: #ddd;
2416
- outline-offset: 1px
2417
- }
2418
-
2419
- .leaflet-container a {
2420
- color: #0078a8
2421
- }
2422
-
2423
- .leaflet-zoom-box {
2424
- border: 2px dotted #38f;
2425
- background: #ffffff80
2426
- }
2427
-
2428
- .leaflet-container {
2429
- font-family: Helvetica Neue, Arial, Helvetica, sans-serif;
2430
- font-size: 12px;
2431
- font-size: .75rem;
2432
- line-height: 1.5
2433
- }
2434
-
2435
- .leaflet-bar {
2436
- box-shadow: 0 1px 5px #000000a6;
2437
- border-radius: 4px
2438
- }
2439
-
2440
- .leaflet-bar a {
2441
- background-color: #fff;
2442
- border-bottom: 1px solid #ccc;
2443
- width: 26px;
2444
- height: 26px;
2445
- line-height: 26px;
2446
- display: block;
2447
- text-align: center;
2448
- text-decoration: none;
2449
- color: #000
2450
- }
2451
-
2452
- .leaflet-bar a,
2453
- .leaflet-control-layers-toggle {
2454
- background-position: 50% 50%;
2455
- background-repeat: no-repeat;
2456
- display: block
2457
- }
2458
-
2459
- .leaflet-bar a:hover,
2460
- .leaflet-bar a:focus {
2461
- background-color: #f4f4f4
2462
- }
2463
-
2464
- .leaflet-bar a:first-child {
2465
- border-top-left-radius: 4px;
2466
- border-top-right-radius: 4px
2467
- }
2468
-
2469
- .leaflet-bar a:last-child {
2470
- border-bottom-left-radius: 4px;
2471
- border-bottom-right-radius: 4px;
2472
- border-bottom: none
2473
- }
2474
-
2475
- .leaflet-bar a.leaflet-disabled {
2476
- cursor: default;
2477
- background-color: #f4f4f4;
2478
- color: #bbb
2479
- }
2480
-
2481
- .leaflet-touch .leaflet-bar a {
2482
- width: 30px;
2483
- height: 30px;
2484
- line-height: 30px
2485
- }
2486
-
2487
- .leaflet-touch .leaflet-bar a:first-child {
2488
- border-top-left-radius: 2px;
2489
- border-top-right-radius: 2px
2490
- }
2491
-
2492
- .leaflet-touch .leaflet-bar a:last-child {
2493
- border-bottom-left-radius: 2px;
2494
- border-bottom-right-radius: 2px
2495
- }
2496
-
2497
- .leaflet-control-zoom-in,
2498
- .leaflet-control-zoom-out {
2499
- font: 700 18px Lucida Console, Monaco, monospace;
2500
- text-indent: 1px
2501
- }
2502
-
2503
- .leaflet-touch .leaflet-control-zoom-in,
2504
- .leaflet-touch .leaflet-control-zoom-out {
2505
- font-size: 22px
2506
- }
2507
-
2508
- .leaflet-control-layers {
2509
- box-shadow: 0 1px 5px #0006;
2510
- background: #fff;
2511
- border-radius: 5px
2512
- }
2513
-
2514
- .leaflet-control-layers-toggle {
2515
- background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAQAAAADQ4RFAAACf0lEQVR4AY1UM3gkARTePdvdoTxXKc+qTl3aU5U6b2Kbkz3Gtq3Zw6ziLGNPzrYx7946Tr6/ee/XeCQ4D3ykPtL5tHno4n0d/h3+xfuWHGLX81cn7r0iTNzjr7LrlxCqPtkbTQEHeqOrTy4Yyt3VCi/IOB0v7rVC7q45Q3Gr5K6jt+3Gl5nCoDD4MtO+j96Wu8atmhGqcNGHObuf8OM/x3AMx38+4Z2sPqzCxRFK2aF2e5Jol56XTLyggAMTL56XOMoS1W4pOyjUcGGQdZxU6qRh7B9Zp+PfpOFlqt0zyDZckPi1ttmIp03jX8gyJ8a/PG2yutpS/Vol7peZIbZcKBAEEheEIAgFbDkz5H6Zrkm2hVWGiXKiF4Ycw0RWKdtC16Q7qe3X4iOMxruonzegJzWaXFrU9utOSsLUmrc0YjeWYjCW4PDMADElpJSSQ0vQvA1Tm6/JlKnqFs1EGyZiFCqnRZTEJJJiKRYzVYzJck2Rm6P4iH+cmSY0YzimYa8l0EtTODFWhcMIMVqdsI2uiTvKmTisIDHJ3od5GILVhBCarCfVRmo4uTjkhrhzkiBV7SsaqS+TzrzM1qpGGUFt28pIySQHR6h7F6KSwGWm97ay+Z+ZqMcEjEWebE7wxCSQwpkhJqoZA5ivCdZDjJepuJ9IQjGGUmuXJdBFUygxVqVsxFsLMbDe8ZbDYVCGKxs+W080max1hFCarCfV+C1KATwcnvE9gRRuMP2prdbWGowm1KB1y+zwMMENkM755cJ2yPDtqhTI6ED1M/82yIDtC/4j4BijjeObflpO9I9MwXTCsSX8jWAFeHr05WoLTJ5G8IQVS/7vwR6ohirYM7f6HzYpogfS3R2OAAAAAElFTkSuQmCC);
2516
- width: 36px;
2517
- height: 36px
2518
- }
2519
-
2520
- .leaflet-retina .leaflet-control-layers-toggle {
2521
- background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAQAAABvcdNgAAAEsklEQVR4AWL4TydIhpZK1kpWOlg0w3ZXP6D2soBtG42jeI6ZmQTHzAxiTbSJsYLjO9HhP+WOmcuhciVnmHVQcJnp7DFvScowZorad/+V/fVzMdMT2g9Cv9guXGv/7pYOrXh2U+RRR3dSd9JRx6bIFc/ekqHI29JC6pJ5ZEh1yWkhkbcFeSjxgx3L2m1cb1C7bceyxA+CNjT/Ifff+/kDk2u/w/33/IeCMOSaWZ4glosqT3DNnNZQ7Cs58/3Ce5HL78iZH/vKVIaYlqzfdLu8Vi7dnvUbEza5Idt36tquZFldl6N5Z/POLof0XLK61mZCmJSWjVF9tEjUluu74IUXvgttuVIHE7YxSkaYhJZam7yiM9Pv82JYfl9nptxZaxMJE4YSPty+vF0+Y2up9d3wwijfjZbabqm/3bZ9ecKHsiGmRflnn1MW4pjHf9oLufyn2z3y1D6n8g8TZhxyzipLNPnAUpsOiuWimg52psrTZYnOWYNDTMuWBWa0tJb4rgq1UvmutpaYEbZlwU3CLJm/ayYjHW5/h7xWLn9Hh1vepDkyf7dE7MtT5LR4e7yYpHrkhOUpEfssBLq2pPhAqoSWKUkk7EDqkmK6RrCEzqDjhNDWNE+XSMvkJRDWlZTmCW0l0PHQGRZY5t1L83kT0Y3l2SItk5JAWHl2dCOBm+fPu3fo5/3v61RMCO9Jx2EEYYhb0rmNQMX/vm7gqOEJLcXTGw3CAuRNeyaPWwjR8PRqKQ1PDA/dpv+on9Shox52WFnx0KY8onHayrJzm87i5h9xGw/tfkev0jGsQizqezUKjk12hBMKJ4kbCqGPVNXudyyrShovGw5CgxsRICxF6aRmSjlBnHRzg7Gx8fKqEubI2rahQYdR1YgDIRQO7JvQyD52hoIQx0mxa0ODtW2Iozn1le2iIRdzwWewedyZzewidueOGqlsn1MvcnQpuVwLGG3/IR1hIKxCjelIDZ8ldqWz25jWAsnldEnK0Zxro19TGVb2ffIZEsIO89EIEDvKMPrzmBOQcKQ+rroye6NgRRxqR4U8EAkz0CL6uSGOm6KQCdWjvjRiSP1BPalCRS5iQYiEIvxuBMJEWgzSoHADcVMuN7IuqqTeyUPq22qFimFtxDyBBJEwNyt6TM88blFHao/6tWWhuuOM4SAK4EI4QmFHA+SEyWlp4EQoJ13cYGzMu7yszEIBOm2rVmHUNqwAIQabISNMRstmdhNWcFLsSm+0tjJH1MdRxO5Nx0WDMhCtgD6OKgZeljJqJKc9po8juskR9XN0Y1lZ3mWjLR9JCO1jRDMd0fpYC2VnvjBSEFg7wBENc0R9HFlb0xvF1+TBEpF68d+DHR6IOWVv2BECtxo46hOFUBd/APU57WIoEwJhIi2CdpyZX0m93BZicktMj1AS9dClteUFAUNUIEygRZCtik5zSxI9MubTBH1GOiHsiLJ3OCoSZkILa9PxiN0EbvhsAo8tdAf9Seepd36lGWHmtNANTv5Jd0z4QYyeo/UEJqxKRpg5LZx6btLPsOaEmdMyxYdlc8LMaJnikDlhclqmPiQnTEpLUIZEwkRagjYkEibQErwhkTAKCLQEbUgkzJQWc/0PstHHcfEdQ+UAAAAASUVORK5CYII=);
2522
- background-size: 26px 26px
2523
- }
2524
-
2525
- .leaflet-touch .leaflet-control-layers-toggle {
2526
- width: 44px;
2527
- height: 44px
2528
- }
2529
-
2530
- .leaflet-control-layers .leaflet-control-layers-list,
2531
- .leaflet-control-layers-expanded .leaflet-control-layers-toggle {
2532
- display: none
2533
- }
2534
-
2535
- .leaflet-control-layers-expanded .leaflet-control-layers-list {
2536
- display: block;
2537
- position: relative
2538
- }
2539
-
2540
- .leaflet-control-layers-expanded {
2541
- padding: 6px 10px 6px 6px;
2542
- color: #333;
2543
- background: #fff
2544
- }
2545
-
2546
- .leaflet-control-layers-scrollbar {
2547
- overflow-y: scroll;
2548
- overflow-x: hidden;
2549
- padding-right: 5px
2550
- }
2551
-
2552
- .leaflet-control-layers-selector {
2553
- margin-top: 2px;
2554
- position: relative;
2555
- top: 1px
2556
- }
2557
-
2558
- .leaflet-control-layers label {
2559
- display: block;
2560
- font-size: 13px;
2561
- font-size: 1.08333em
2562
- }
2563
-
2564
- .leaflet-control-layers-separator {
2565
- height: 0;
2566
- border-top: 1px solid #ddd;
2567
- margin: 5px -10px 5px -6px
2568
- }
2569
-
2570
- .leaflet-default-icon-path {
2571
- background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YaZtr2962HUzbDNpjszW24mRt28p47v7zq/bXZtrp/lWnXr337j3nPCe85NcypgSFdugCpW5YoDAMRaIMqRi6aKq5E3YqDQO3qAwjVWrD8Ncq/RBpykd8oZUb/kaJutow8r1aP9II0WmLKLIsJyv1w/kqw9Ch2MYdB++12Onxee/QMwvf4/Dk/Lfp/i4nxTXtOoQ4pW5Aj7wpici1A9erdAN2OH64x8OSP9j3Ft3b7aWkTg/Fm91siTra0f9on5sQr9INejH6CUUUpavjFNq1B+Oadhxmnfa8RfEmN8VNAsQhPqF55xHkMzz3jSmChWU6f7/XZKNH+9+hBLOHYozuKQPxyMPUKkrX/K0uWnfFaJGS1QPRtZsOPtr3NsW0uyh6NNCOkU3Yz+bXbT3I8G3xE5EXLXtCXbbqwCO9zPQYPRTZ5vIDXD7U+w7rFDEoUUf7ibHIR4y6bLVPXrz8JVZEql13trxwue/uDivd3fkWRbS6/IA2bID4uk0UpF1N8qLlbBlXs4Ee7HLTfV1j54APvODnSfOWBqtKVvjgLKzF5YdEk5ewRkGlK0i33Eofffc7HT56jD7/6U+qH3Cx7SBLNntH5YIPvODnyfIXZYRVDPqgHtLs5ABHD3YzLuespb7t79FY34DjMwrVrcTuwlT55YMPvOBnRrJ4VXTdNnYug5ucHLBjEpt30701A3Ts+HEa73u6dT3FNWwflY86eMHPk+Yu+i6pzUpRrW7SNDg5JHR4KapmM5Wv2E8Tfcb1HoqqHMHU+uWDD7zg54mz5/2BSnizi9T1Dg4QQXLToGNCkb6tb1NU+QAlGr1++eADrzhn/u8Q2YZhQVlZ5+CAOtqfbhmaUCS1ezNFVm2imDbPmPng5wmz+gwh+oHDce0eUtQ6OGDIyR0uUhUsoO3vfDmmgOezH0mZN59x7MBi++WDL1g/eEiU3avlidO671bkLfwbw5XV2P8Pzo0ydy4t2/0eu33xYSOMOD8hTf4CrBtGMSoXfPLchX+J0ruSePw3LZeK0juPJbYzrhkH0io7B3k164hiGvawhOKMLkrQLyVpZg8rHFW7E2uHOL888IBPlNZ1FPzstSJM694fWr6RwpvcJK60+0HCILTBzZLFNdtAzJaohze60T8qBzyh5ZuOg5e7uwQppofEmf2++DYvmySqGBuKaicF1blQjhuHdvCIMvp8whTTfZzI7RldpwtSzL+F1+wkdZ2TBOW2gIF88PBTzD/gpeREAMEbxnJcaJHNHrpzji0gQCS6hdkEeYt9DF/2qPcEC8RM28Hwmr3sdNyht00byAut2k3gufWNtgtOEOFGUwcXWNDbdNbpgBGxEvKkOQsxivJx33iow0Vw5S6SVTrpVq11ysA2Rp7gTfPfktc6zhtXBBC+adRLshf6sG2RfHPZ5EAc4sVZ83yCN00Fk/4kggu40ZTvIEm5g24qtU4KjBrx/BTTH8ifVASAG7gKrnWxJDcU7x8X6Ecczhm3o6YicvsLXWfh3Ch1W0k8x0nXF+0fFxgt4phz8QvypiwCCFKMqXCnqXExjq10beH+UUA7+nG6mdG/Pu0f3LgFcGrl2s0kNNjpmoJ9o4B29CMO8dMT4Q5ox8uitF6fqsrJOr8qnwNbRzv6hSnG5wP+64C7h9lp30hKNtKdWjtdkbuPA19nJ7Tz3zR/ibgARbhb4AlhavcBebmTHcFl2fvYEnW0ox9xMxKBS8btJ+KiEbq9zA4RthQXDhPa0T9TEe69gWupwc6uBUphquXgf+/FrIjweHQS4/pduMe5ERUMHUd9xv8ZR98CxkS4F2n3EUrUZ10EYNw7BWm9x1GiPssi3GgiGRDKWRYZfXlON+dfNbM+GgIwYdwAAAAASUVORK5CYII=)
2572
- }
2573
-
2574
- .leaflet-container .leaflet-control-attribution {
2575
- background: #fff;
2576
- background: #fffc;
2577
- margin: 0
2578
- }
2579
-
2580
- .leaflet-control-attribution,
2581
- .leaflet-control-scale-line {
2582
- padding: 0 5px;
2583
- color: #333;
2584
- line-height: 1.4
2585
- }
2586
-
2587
- .leaflet-control-attribution a {
2588
- text-decoration: none
2589
- }
2590
-
2591
- .leaflet-control-attribution a:hover,
2592
- .leaflet-control-attribution a:focus {
2593
- text-decoration: underline
2594
- }
2595
-
2596
- .leaflet-attribution-flag {
2597
- display: inline !important;
2598
- vertical-align: baseline !important;
2599
- width: 1em;
2600
- height: .6669em
2601
- }
2602
-
2603
- .leaflet-left .leaflet-control-scale {
2604
- margin-left: 5px
2605
- }
2606
-
2607
- .leaflet-bottom .leaflet-control-scale {
2608
- margin-bottom: 5px
2609
- }
2610
-
2611
- .leaflet-control-scale-line {
2612
- border: 2px solid #777;
2613
- border-top: none;
2614
- line-height: 1.1;
2615
- padding: 2px 5px 1px;
2616
- white-space: nowrap;
2617
- -moz-box-sizing: border-box;
2618
- box-sizing: border-box;
2619
- background: #fffc;
2620
- text-shadow: 1px 1px #fff
2621
- }
2622
-
2623
- .leaflet-control-scale-line:not(:first-child) {
2624
- border-top: 2px solid #777;
2625
- border-bottom: none;
2626
- margin-top: -2px
2627
- }
2628
-
2629
- .leaflet-control-scale-line:not(:first-child):not(:last-child) {
2630
- border-bottom: 2px solid #777
2631
- }
2632
-
2633
- .leaflet-touch .leaflet-control-attribution,
2634
- .leaflet-touch .leaflet-control-layers,
2635
- .leaflet-touch .leaflet-bar {
2636
- box-shadow: none
2637
- }
2638
-
2639
- .leaflet-touch .leaflet-control-layers,
2640
- .leaflet-touch .leaflet-bar {
2641
- border: 2px solid rgba(0, 0, 0, .2);
2642
- background-clip: padding-box
2643
- }
2644
-
2645
- .leaflet-popup {
2646
- position: absolute;
2647
- text-align: center;
2648
- margin-bottom: 20px
2649
- }
2650
-
2651
- .leaflet-popup-content-wrapper {
2652
- padding: 1px;
2653
- text-align: left;
2654
- border-radius: 12px
2655
- }
2656
-
2657
- .leaflet-popup-content {
2658
- margin: 13px 24px 13px 20px;
2659
- line-height: 1.3;
2660
- font-size: 13px;
2661
- font-size: 1.08333em;
2662
- min-height: 1px
2663
- }
2664
-
2665
- .leaflet-popup-content p {
2666
- margin: 1.3em 0
2667
- }
2668
-
2669
- .leaflet-popup-tip-container {
2670
- width: 40px;
2671
- height: 20px;
2672
- position: absolute;
2673
- left: 50%;
2674
- margin-top: -1px;
2675
- margin-left: -20px;
2676
- overflow: hidden;
2677
- pointer-events: none
2678
- }
2679
-
2680
- .leaflet-popup-tip {
2681
- width: 17px;
2682
- height: 17px;
2683
- padding: 1px;
2684
- margin: -10px auto 0;
2685
- pointer-events: auto;
2686
- -webkit-transform: rotate(45deg);
2687
- -moz-transform: rotate(45deg);
2688
- -ms-transform: rotate(45deg);
2689
- transform: rotate(45deg)
2690
- }
2691
-
2692
- .leaflet-popup-content-wrapper,
2693
- .leaflet-popup-tip {
2694
- background: #fff;
2695
- color: #333;
2696
- box-shadow: 0 3px 14px #0006
2697
- }
2698
-
2699
- .leaflet-container a.leaflet-popup-close-button {
2700
- position: absolute;
2701
- top: 0;
2702
- right: 0;
2703
- border: none;
2704
- text-align: center;
2705
- width: 24px;
2706
- height: 24px;
2707
- font: 16px/24px Tahoma, Verdana, sans-serif;
2708
- color: #757575;
2709
- text-decoration: none;
2710
- background: transparent
2711
- }
2712
-
2713
- .leaflet-container a.leaflet-popup-close-button:hover,
2714
- .leaflet-container a.leaflet-popup-close-button:focus {
2715
- color: #585858
2716
- }
2717
-
2718
- .leaflet-popup-scrolled {
2719
- overflow: auto
2720
- }
2721
-
2722
- .leaflet-oldie .leaflet-popup-content-wrapper {
2723
- -ms-zoom: 1
2724
- }
2725
-
2726
- .leaflet-oldie .leaflet-popup-tip {
2727
- width: 24px;
2728
- margin: 0 auto;
2729
- -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
2730
- filter: progid:DXImageTransform.Microsoft.Matrix(M11=.70710678, M12=.70710678, M21=-.70710678, M22=.70710678)
2731
- }
2732
-
2733
- .leaflet-oldie .leaflet-control-zoom,
2734
- .leaflet-oldie .leaflet-control-layers,
2735
- .leaflet-oldie .leaflet-popup-content-wrapper,
2736
- .leaflet-oldie .leaflet-popup-tip {
2737
- border: 1px solid #999
2738
- }
2739
-
2740
- .leaflet-div-icon {
2741
- background: #fff;
2742
- border: 1px solid #666
2743
- }
2744
-
2745
- .leaflet-tooltip {
2746
- position: absolute;
2747
- padding: 6px;
2748
- background-color: #fff;
2749
- border: 1px solid #fff;
2750
- border-radius: 3px;
2751
- color: #222;
2752
- white-space: nowrap;
2753
- -webkit-user-select: none;
2754
- -moz-user-select: none;
2755
- -ms-user-select: none;
2756
- user-select: none;
2757
- pointer-events: none;
2758
- box-shadow: 0 1px 3px #0006
2759
- }
2760
-
2761
- .leaflet-tooltip.leaflet-interactive {
2762
- cursor: pointer;
2763
- pointer-events: auto
2764
- }
2765
-
2766
- .leaflet-tooltip-top:before,
2767
- .leaflet-tooltip-bottom:before,
2768
- .leaflet-tooltip-left:before,
2769
- .leaflet-tooltip-right:before {
2770
- position: absolute;
2771
- pointer-events: none;
2772
- border: 6px solid transparent;
2773
- background: transparent;
2774
- content: ""
2775
- }
2776
-
2777
- .leaflet-tooltip-bottom {
2778
- margin-top: 6px
2779
- }
2780
-
2781
- .leaflet-tooltip-top {
2782
- margin-top: -6px
2783
- }
2784
-
2785
- .leaflet-tooltip-bottom:before,
2786
- .leaflet-tooltip-top:before {
2787
- left: 50%;
2788
- margin-left: -6px
2789
- }
2790
-
2791
- .leaflet-tooltip-top:before {
2792
- bottom: 0;
2793
- margin-bottom: -12px;
2794
- border-top-color: #fff
2795
- }
2796
-
2797
- .leaflet-tooltip-bottom:before {
2798
- top: 0;
2799
- margin-top: -12px;
2800
- margin-left: -6px;
2801
- border-bottom-color: #fff
2802
- }
2803
-
2804
- .leaflet-tooltip-left {
2805
- margin-left: -6px
2806
- }
2807
-
2808
- .leaflet-tooltip-right {
2809
- margin-left: 6px
2810
- }
2811
-
2812
- .leaflet-tooltip-left:before,
2813
- .leaflet-tooltip-right:before {
2814
- top: 50%;
2815
- margin-top: -6px
2816
- }
2817
-
2818
- .leaflet-tooltip-left:before {
2819
- right: 0;
2820
- margin-right: -12px;
2821
- border-left-color: #fff
2822
- }
2823
-
2824
- .leaflet-tooltip-right:before {
2825
- left: 0;
2826
- margin-left: -12px;
2827
- border-right-color: #fff
2828
- }
2829
-
2830
- @media print {
2831
- .leaflet-control {
2832
- -webkit-print-color-adjust: exact;
2833
- print-color-adjust: exact
2834
- }
2835
- }
 
1
+ @import"https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@400;500;600;700&display=swap";:root{--bg: #0a0c10;--bg2: #111318;--bg3: #181c24;--bg4: #1e232d;--border: rgba(255,255,255,.08);--border2: rgba(255,255,255,.13);--text: #e8eaf0;--text2: #8b90a0;--text3: #7e8494;--green: #22d37a;--green2: #0d6e3a;--green3: #052a17;--red: #f04040;--red2: #7a1a1a;--red3: #2d0808;--blue: #4a9eff;--blue2: #1a4a80;--blue3: #081830;--amber: #f0a020;--amber2: #7a4e08;--amber3: #2d1c02;--accent: #4a9eff;--font-sans: "Syne", sans-serif;--font-mono: "DM Mono", monospace;--sidebar-width: 240px;--sidebar-collapsed: 56px;--topbar-height: 56px;--panel-gap: 16px;--radius: 10px;--radius-sm: 6px;--fs-p: clamp(1rem, 1.15vw, 1.125rem);--fs-h1: clamp(1.75rem, 2.8vw, 2.25rem);--fs-h2: clamp(1.5rem, 2.4vw, 1.875rem);--fs-h3: clamp(1.25rem, 1.9vw, 1.5rem);--fs-h4: clamp(1.125rem, 1.6vw, 1.25rem);--fs-h5: clamp(1rem, 1.3vw, 1.125rem);--fs-h6: clamp(.875rem, 1.1vw, 1rem)}*{box-sizing:border-box;margin:0;padding:0}html{font-size:16px}html,body,#app{height:100%;width:100%;overflow:hidden}body{background:var(--bg);color:var(--text);font-family:var(--font-sans);font-size:1rem;line-height:1.5;-webkit-font-smoothing:antialiased}p{font-size:var(--fs-p);line-height:1.55}h1{font-size:var(--fs-h1);line-height:1.15;font-weight:700}h2{font-size:var(--fs-h2);line-height:1.2;font-weight:700}h3{font-size:var(--fs-h3);line-height:1.25;font-weight:600}h4{font-size:var(--fs-h4);line-height:1.3;font-weight:600}h5{font-size:var(--fs-h5);line-height:1.35;font-weight:500}h6{font-size:var(--fs-h6);line-height:1.4;font-weight:500}.layout{display:grid;grid-template-areas:"topbar topbar" "sidebar main";grid-template-columns:var(--sidebar-width) 1fr;grid-template-rows:var(--topbar-height) 1fr;height:100vh;width:100vw;transition:grid-template-columns .25s ease}.layout.collapsed{grid-template-columns:var(--sidebar-collapsed) 1fr}.sidebar{grid-area:sidebar;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden;transition:width .25s ease;position:relative}.sidebar-toggle{position:absolute;top:12px;right:-10px;width:24px;height:24px;border-radius:50%;background:var(--bg3);border:1px solid var(--border2);color:var(--text2);display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:.875rem;z-index:10;transition:transform .2s}.layout.collapsed .sidebar-toggle{transform:rotate(180deg);right:-10px}.topbar-logo{position:fixed;left:16px;top:0;height:var(--topbar-height);display:flex;align-items:center;gap:10px;z-index:100}.topbar-logo .logo-dot{width:10px;height:10px;border-radius:50%;background:var(--blue);animation:pulse 2s ease-in-out infinite;flex-shrink:0}.topbar-logo .logo-text{font-size:1.125rem;font-weight:700;color:var(--text);letter-spacing:-.3px;white-space:nowrap}.sidebar-nav{flex:1;padding:14px 8px;display:flex;flex-direction:column;gap:4px;overflow-y:auto}.nav-item{display:flex;align-items:center;gap:14px;padding:14px;border-radius:var(--radius-sm);cursor:pointer;color:var(--text2);font-size:1rem;font-weight:500;transition:background .15s,color .15s;white-space:nowrap;overflow:hidden}.nav-item:hover{background:var(--bg3);color:var(--text)}.nav-item.active{background:var(--blue3);color:var(--blue);border:.5px solid var(--blue2)}.nav-icon{font-size:1rem;width:20px;text-align:center;flex-shrink:0}.nav-label{opacity:1;transition:opacity .15s}.layout.collapsed .nav-label{opacity:0;width:0}.sidebar-footer{padding:14px;border-top:1px solid var(--border);font-size:.875rem;color:var(--text3);font-family:var(--font-mono);text-align:center;white-space:nowrap;overflow:hidden}.layout.collapsed .sidebar-footer{opacity:0}.topbar{grid-area:topbar;background:var(--bg2);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 16px;padding-left:calc(var(--sidebar-width) + var(--panel-gap));gap:16px;overflow:hidden;position:relative}.layout.collapsed .topbar{padding-left:calc(var(--sidebar-width) + var(--panel-gap))}.live-badge{font-family:var(--font-mono);font-size:.875rem;background:var(--green3);color:var(--green);border:.5px solid var(--green2);padding:6px 12px;border-radius:4px;display:flex;align-items:center;gap:4px;flex-shrink:0}.live-dot{width:8px;height:8px;border-radius:50%;background:var(--green);animation:pulse 1.5s ease-in-out infinite}.topbar-stats{display:flex;align-items:center;gap:20px;flex:1;overflow:hidden}.stat{display:flex;align-items:center;gap:14px;flex-shrink:0}.stat-label{font-size:.875rem;color:var(--text3);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em}.stat-val{font-size:1rem;font-weight:600;color:var(--text);font-family:var(--font-mono)}.stat-delta{font-size:.875rem;font-family:var(--font-mono)}.stat-delta.up{color:var(--green)}.stat-delta.dn{color:var(--red)}.stat-delta.neutral{color:var(--text3)}.topbar-filters{display:flex;align-items:center;gap:10px;flex-shrink:0}.filter-select{background:var(--bg3);border:.5px solid var(--border2);border-radius:var(--radius-sm);padding:7px 30px 7px 12px;color:var(--text);font-size:.875rem;font-family:var(--font-mono);outline:none;cursor:pointer;min-width:140px;max-width:180px;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238b90a0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;transition:border-color .15s}.filter-select:hover{border-color:var(--border2)}.filter-select:focus{border-color:var(--blue2)}.filter-select option{background:var(--bg2);color:var(--text)}.topbar-actions{margin-left:auto;display:flex;align-items:center;gap:16px;flex-shrink:0}.btn-ghost{font-size:.9375rem;font-weight:500;color:var(--blue);border:.5px solid var(--blue2);background:var(--blue3);padding:8px 16px;border-radius:var(--radius-sm);cursor:pointer;font-family:var(--font-sans);transition:opacity .15s}.btn-ghost:hover{opacity:.85}.icon-btn{width:32px;height:32px;border-radius:50%;background:var(--bg3);border:.5px solid var(--border2);color:var(--text2);display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:1rem;transition:color .15s,border-color .15s}.icon-btn:hover{color:var(--text);border-color:var(--border2)}.main{grid-area:main;overflow:auto;padding:var(--panel-gap);background:var(--bg)}.view{display:none}.view.active{display:block;height:100%}.dashboard-grid{display:grid;grid-template-columns:1fr 336px;grid-template-rows:minmax(120px,59%) 1fr;gap:var(--panel-gap);height:100%;min-height:0}.panel{background:var(--bg2);border:.5px solid var(--border);border-radius:var(--radius);display:flex;flex-direction:column;overflow:hidden;min-height:0}.panel-header{display:flex;align-items:center;justify-content:space-between;padding:14px;border-bottom:.5px solid var(--border);cursor:pointer;-webkit-user-select:none;user-select:none;transition:background .15s}.panel-header:hover{background:#ffffff05}.panel-title{font-size:.875rem;color:var(--text3);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;display:flex;align-items:center;gap:14px}.panel-toggle{font-size:.875rem;color:var(--text3);transition:transform .2s}.panel.collapsed .panel-toggle{transform:rotate(-90deg)}.panel-body{flex:1;overflow:auto;padding:12px;min-height:0}.panel.collapsed .panel-body{display:none}.panel-subtitle{font-size:.875rem;color:var(--text3)}.positions-separator{margin-top:10px;padding-top:10px;border-top:.5px solid var(--border)}.panel-title.mb-sm{margin-bottom:8px}.panel.full-height{height:100%}.hidden{display:none}.desktop-only{display:initial}.mobile-only,#btn-notif,#btn-auth-mobile,.topbar-stats .legend{display:none}.map-legend{display:flex;align-items:center;gap:12px;margin-left:auto}#panel-map .panel-header{justify-content:flex-start}.map-panel{grid-row:1 / 2;grid-column:1 / 2}#map-container{width:100%;height:100%;min-height:300px;background:var(--bg3);border-radius:var(--radius-sm);overflow:hidden}.signals-panel{grid-row:1 / 3;grid-column:2 / 3}.signals-list{display:flex;flex-direction:column;gap:16px}.market-card{background:var(--bg3);border:.5px solid var(--border);border-radius:var(--radius-sm);padding:14px;cursor:pointer;transition:border-color .15s,background .15s}.market-card:hover{border-color:var(--border2)}.market-card.active{border-color:var(--blue2);background:var(--blue3)}.market-cat{font-size:.875rem;font-family:var(--font-mono);color:var(--text3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px}.market-q{font-size:.9375rem;color:var(--text);line-height:1.4;font-weight:500;margin-bottom:8px}.market-footer{display:flex;align-items:center;justify-content:space-between;gap:16px}.prob-bar-wrap{flex:1;min-width:0}.prob-bar-bg{height:5px;background:var(--bg4);border-radius:2px;overflow:hidden}.prob-bar-fill{height:100%;border-radius:2px;width:var(--prob-width, 0%);transition:width .8s ease}.prob-val{font-size:.9375rem;font-weight:600;font-family:var(--font-mono);flex-shrink:0}.signal-badge{font-size:.875rem;font-weight:600;padding:2px 6px;border-radius:4px;font-family:var(--font-mono);letter-spacing:.04em;flex-shrink:0}.sig-bull{background:var(--green3);color:var(--green);border:.5px solid var(--green2)}.sig-bear{background:var(--red3);color:var(--red);border:.5px solid var(--red2)}.sig-neut{background:var(--bg4);color:var(--text2);border:.5px solid var(--border2)}.badge-abbr{display:none}.badge-full{display:inline}.sig-none{background:transparent;color:var(--text3, #6e7681);border:.5px dashed var(--border2);font-size:.72rem;letter-spacing:.06em;font-weight:500}.model-badge{font-size:.65rem;font-weight:500;color:var(--text3, #6e7681);background:var(--bg4);border:.5px solid var(--border2);border-radius:4px;padding:1px 5px;margin-left:6px;font-family:var(--font-mono);letter-spacing:.04em;text-transform:uppercase;flex-shrink:0}.spread-badge{margin-left:6px;font-size:.7rem;color:var(--text3, #6e7681);font-family:var(--font-mono)}.spread-badge.illiquid{color:var(--red, #f04040);font-weight:600}.edge-row{display:flex;align-items:center;gap:6px;margin-top:6px;padding-top:6px;border-top:.5px solid var(--border2, #1f242b);font-size:.72rem;font-family:var(--font-mono);letter-spacing:.02em}.edge-implied{color:var(--text2, #9aa4b2)}.edge-fair{font-weight:600}.edge-sep{color:var(--text3, #4a5260);opacity:.6}.edge-value{font-weight:700;padding:1px 5px;border-radius:3px}.edge-pos{color:var(--green, #22d37a);background:var(--green3, rgba(34,211,122,.08))}.edge-neg{color:var(--red, #f04040);background:var(--red3, rgba(240,64,64,.08))}.edge-zero{color:var(--text3, #6e7681)}.kelly-note{flex:1;font-size:.78rem;color:var(--text2, #9aa4b2);font-family:var(--font-mono);padding:4px 8px;border-left:2px solid var(--border2, #1f242b);background:#ffffff05;border-radius:0 4px 4px 0}.kelly-warn{color:var(--red, #f04040);border-left-color:var(--red, #f04040);background:#f040400d}.detail-panel{grid-row:2 / 3;grid-column:1 / 2;overflow:hidden}.detail-panel .panel-body{overflow:hidden}.detail-panel .panel-body>div{height:100%;display:flex;flex-direction:column;gap:10px;min-height:0}.detail-header{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:12px;gap:16px}.detail-tag{font-size:.875rem;color:var(--blue);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px}.detail-q{font-size:1rem;font-weight:600;color:var(--text);line-height:1.5}.detail-meta{font-size:.875rem;color:var(--text3);font-family:var(--font-mono);margin-top:2px}.detail-metrics{display:flex;gap:16px;align-items:center;flex-shrink:0}.metric{text-align:right}.metric-label{font-size:.875rem;color:var(--text3);font-family:var(--font-mono);margin-bottom:2px}.metric-value{font-size:.875rem;font-weight:700;font-family:var(--font-mono)}.metric-sep{width:1px;height:40px;background:var(--border)}.outcomes-row{display:flex;gap:16px;flex:1;min-height:0}.outcome-card{flex:1;background:var(--bg3);border:.5px solid var(--border);border-radius:var(--radius-sm);padding:14px;text-align:center}.outcome-name{font-size:.875rem;color:var(--text2);margin-bottom:4px;font-family:var(--font-mono)}.outcome-price{font-size:1.25rem;font-weight:700;color:var(--text);font-family:var(--font-mono)}.outcome-delta{font-size:.875rem;font-family:var(--font-mono);margin-top:2px}.outcome-card.yes .outcome-price{color:var(--green)}.outcome-card.no .outcome-price{color:var(--red)}.chart-container{flex:2;background:var(--bg3);border:.5px solid var(--border);border-radius:var(--radius-sm);padding:14px;min-height:80px;overflow:hidden;align-self:stretch}.chart-label{font-size:.875rem;color:var(--text3);font-family:var(--font-mono);margin-bottom:6px}.outcome-mini{display:flex;flex-direction:column;align-items:center;gap:2px;padding:0 10px}.outcome-mini-label{font-size:.72rem;font-family:var(--font-mono);color:var(--text3);text-transform:uppercase;letter-spacing:.06em}.outcome-mini-price{font-size:1rem;font-weight:700;font-family:var(--font-mono)}.outcome-mini-price.yes{color:var(--green)}.outcome-mini-price.no{color:var(--red)}.ai-title-group{display:flex;align-items:center;gap:8px}.ai-title-group .ai-icon{width:20px;height:20px;font-size:.75rem}.ai-box{flex:1;background:var(--bg3);border:.5px solid var(--blue2);border-radius:var(--radius-sm);padding:14px;display:flex;flex-direction:column;gap:8px;overflow:hidden;min-width:0}.ai-icon{width:32px;height:32px;border-radius:6px;background:var(--blue3);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:.875rem}.ai-label{font-size:.875rem;color:var(--blue);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;margin-bottom:3px}.ai-text{font-size:.9375rem;color:var(--text2);line-height:1.5}.sim-row{display:flex;gap:16px;align-items:center;flex-wrap:wrap;justify-content:flex-end}.sim-label{font-size:.9375rem;color:var(--text3);font-family:var(--font-mono)}.sim-input{background:var(--bg3);border:.5px solid var(--border2);border-radius:var(--radius-sm);padding:8px 14px;color:var(--text);font-size:1rem;font-family:var(--font-mono);width:100px;outline:none}.sim-input:focus{border-color:var(--blue2)}.sim-btn-yes{background:var(--green3);border:.5px solid var(--green2);color:var(--green);font-size:.9375rem;font-weight:600;padding:8px 16px;border-radius:var(--radius-sm);cursor:pointer;font-family:var(--font-mono);transition:opacity .15s}.sim-btn-no{background:var(--red3);border:.5px solid var(--red2);color:var(--red);font-size:.9375rem;font-weight:600;padding:8px 16px;border-radius:var(--radius-sm);cursor:pointer;font-family:var(--font-mono);transition:opacity .15s}.sim-btn-yes:hover,.sim-btn-no:hover{opacity:.85}.sim-disclaimer{font-size:.875rem;color:var(--text3);font-family:var(--font-mono)}.legend{display:flex;gap:16px;align-items:center}.legend-item{display:flex;align-items:center;gap:5px;font-size:.875rem;color:var(--text3);font-family:var(--font-mono)}.legend-dot{width:10px;height:10px;border-radius:50%}.legend-dot.green{background:var(--green)}.legend-dot.red{background:var(--red)}.legend-dot.gray{background:var(--text3)}.legend.end{margin-left:auto}.table-wrap{overflow:auto;border:.5px solid var(--border);border-radius:var(--radius-sm);background:var(--bg3)}table{width:100%;border-collapse:collapse;font-size:1rem}th,td{padding:14px;text-align:left;border-bottom:.5px solid var(--border)}th{font-family:var(--font-mono);font-size:.875rem;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;font-weight:500;background:var(--bg4);position:sticky;top:0}tr:hover td{background:#ffffff05}td{color:var(--text)}.td-mono{font-family:var(--font-mono)}.td-green{color:var(--green)}.td-red{color:var(--red)}.td-blue{color:var(--blue)}.empty-state{padding:40px;text-align:center;color:var(--text3);font-size:1rem}#app .leaflet-container{background:var(--bg3);font-family:var(--font-mono)}#app .leaflet-popup-content-wrapper{background:var(--bg2);color:var(--text);border:.5px solid var(--border);border-radius:var(--radius-sm)}#app .leaflet-popup-tip{background:var(--bg2)}.sparkline{display:flex;align-items:flex-end;gap:2px;height:32px;margin-top:6px}.spark-bar{width:3px;border-radius:1px;background:var(--blue2);transition:height .3s}@keyframes pulse{0%,to{opacity:1}50%{opacity:.4}}@keyframes scroll-stats{0%{transform:translate(0)}to{transform:translate(-50%)}}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--bg4);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--text3)}.flex-between{display:flex;justify-content:space-between;align-items:center}.flex-start{display:flex;align-items:flex-start;justify-content:space-between}.flex-row{display:flex;align-items:center}.flex-wrap{flex-wrap:wrap}.gap-6{gap:14px}.gap-8{gap:16px}.text-green{color:var(--green)}.text-red{color:var(--red)}.text-blue{color:var(--blue)}.text-amber{color:var(--amber)}.text-neutral{color:var(--text3)}.bg-green{background:var(--green)}.bg-red{background:var(--red)}.bg-amber{background:var(--amber)}.flex-1{flex:1}.font-mono{font-family:var(--font-mono)}.text-xs,.text-sm{font-size:.875rem}.text-base{font-size:.9375rem}.text-lg{font-size:1rem}.text-xl{font-size:1.125rem}.font-semibold{font-weight:600}.font-bold{font-weight:700}.mb-4{margin-bottom:4px}.mb-6{margin-bottom:6px}.mb-8{margin-bottom:8px}.mt-4{margin-top:4px}.mt-6{margin-top:6px}.ml-auto{margin-left:auto}.divider{height:1px;background:var(--border);margin:8px 0}.empty-state-sm{padding:16px;text-align:center;color:var(--text3);font-size:1rem}.map-popup{font-family:var(--font-sans);font-size:.9375rem;color:var(--text);max-width:200px}.map-popup-cat{font-size:.875rem;color:var(--text3);font-family:var(--font-mono);margin-bottom:4px;text-transform:uppercase}.map-popup-q{font-weight:600;margin-bottom:4px;line-height:1.3}.map-popup-prices{display:flex;gap:16px;font-family:var(--font-mono);font-size:.9375rem}.map-label-text{color:var(--text2);font-family:var(--font-mono);font-size:.875rem;text-shadow:0 1px 2px #000}.modal-overlay{position:fixed;inset:0;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:1000;opacity:1;transition:opacity .2s ease}.modal-overlay.hidden{display:none}.modal{background:var(--bg2);border:.5px solid var(--border);border-radius:var(--radius);width:100%;max-width:420px;overflow:hidden}.modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:.5px solid var(--border)}.modal-tabs{display:flex;gap:4px}.modal-tab{background:transparent;border:none;color:var(--text2);font-family:var(--font-sans);font-size:.9375rem;font-weight:500;padding:8px 16px;border-radius:var(--radius-sm);cursor:pointer;transition:background .15s,color .15s}.modal-tab:hover{color:var(--text)}.modal-tab.active{background:var(--bg3);color:var(--text)}.modal-close{background:transparent;border:none;color:var(--text2);font-size:1.125rem;cursor:pointer;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:var(--radius-sm);transition:background .15s,color .15s}.modal-close:hover{background:var(--bg3);color:var(--text)}.modal-body{padding:20px}.modal-form{display:none;flex-direction:column;gap:16px}.modal-form.active{display:flex}.form-group{display:flex;flex-direction:column;gap:6px}.form-group label{font-size:.875rem;color:var(--text2);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em}.form-group input{background:var(--bg3);border:.5px solid var(--border2);border-radius:var(--radius-sm);padding:12px 14px;color:var(--text);font-size:.9375rem;font-family:var(--font-sans);outline:none;transition:border-color .15s}.form-group input:focus{border-color:var(--blue2)}.form-group input::placeholder{color:var(--text3)}.form-error{font-size:.875rem;color:var(--red);font-family:var(--font-mono);min-height:18px}.modal-submit{background:#2563eb;border:none;color:#fff;font-family:var(--font-sans);font-size:.9375rem;font-weight:600;padding:12px;border-radius:var(--radius-sm);cursor:pointer;transition:opacity .15s;margin-top:4px}.modal-submit:hover{opacity:.9}.modal-title{display:flex;align-items:center;gap:10px;font-size:1rem;font-weight:600;color:var(--text)}.modal-title-icon{font-size:1.125rem}.form-hint{font-size:.8125rem;color:var(--text3);line-height:1.4}.form-hint a{color:var(--blue);text-decoration:none}.form-hint a:hover{text-decoration:underline}.toggle-row{display:flex;align-items:center;justify-content:space-between}.toggle-switch{position:relative;display:inline-block;width:44px;height:24px;cursor:pointer}.toggle-switch input{opacity:0;width:0;height:0}.toggle-slider{position:absolute;inset:0;background:var(--bg4);border-radius:24px;transition:background .2s;border:.5px solid var(--border)}.toggle-slider:before{content:"";position:absolute;height:18px;width:18px;left:2px;bottom:2px;background:var(--text2);border-radius:50%;transition:transform .2s,background .2s}.toggle-switch input:checked+.toggle-slider{background:var(--blue2);border-color:var(--blue)}.toggle-switch input:checked+.toggle-slider:before{transform:translate(20px);background:var(--text)}.form-status{font-size:.875rem;font-family:var(--font-mono);min-height:18px;color:var(--text2)}.form-status.success{color:var(--green)}.form-status.error{color:var(--red)}.form-actions{display:flex;gap:10px;margin-top:4px}.modal-submit--secondary{background:var(--bg3);color:var(--text);border:.5px solid var(--border2)}.modal-submit--secondary:hover{background:var(--bg4);opacity:1}.telegram-instructions{display:flex;flex-direction:column;gap:12px;margin-bottom:20px;padding-bottom:16px;border-bottom:.5px solid var(--border)}.instruction-step{display:flex;gap:12px;align-items:flex-start}.step-num{width:24px;height:24px;border-radius:50%;background:var(--blue3);color:var(--blue);font-size:.8125rem;font-weight:600;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:2px}.step-body{flex:1}.step-body strong{display:block;font-size:.875rem;font-weight:600;color:var(--text);margin-bottom:2px}.step-body p{font-size:.8125rem;color:var(--text2);line-height:1.5;margin:0}.step-body a{color:var(--blue);text-decoration:none}.step-body a:hover{text-decoration:underline}.step-body code{background:var(--bg3);padding:1px 5px;border-radius:4px;font-family:var(--font-mono);font-size:.75rem;color:var(--text)}.auth-indicator{width:32px;height:32px;border-radius:50%;background:var(--bg3);border:.5px solid var(--border2);color:var(--text2);display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;flex-shrink:0;transition:color .15s,border-color .15s,background .2s}.auth-indicator:after{content:"";width:8px;height:8px;border-radius:50%;background:var(--red);display:block;transition:background .2s}.auth-indicator.logged-in:after{background:var(--green)}.stats-track{display:flex;align-items:center;gap:20px;flex:1}.stats-track .legend.end~.stat,.stats-track .legend.end~.legend{display:none}.signals-sentinel{padding:12px;text-align:center;color:var(--text3);font-family:var(--font-mono);font-size:.875rem}#filter-trend{min-width:170px;background:var(--bg3);border:.5px solid var(--amber2);color:var(--amber)}#filter-trend:focus{border-color:var(--amber)}#filter-trend option{background:var(--bg2);color:var(--text)}.trend-badge{font-size:.75rem;font-weight:600;padding:2px 6px;border-radius:4px;font-family:var(--font-mono);letter-spacing:.04em;margin-left:8px}.trend-hot{background:#f0a02026;color:var(--amber);border:.5px solid var(--amber2)}.trend-bull{background:var(--green3);color:var(--green);border:.5px solid var(--green2)}.trend-bear{background:var(--red3);color:var(--red);border:.5px solid var(--red2)}.trend-volatile{background:#4a9eff26;color:var(--blue);border:.5px solid var(--blue2)}@media(max-width:1024px){*{scrollbar-width:none;-ms-overflow-style:none}*::-webkit-scrollbar{display:none}.dashboard-grid{grid-template-columns:1fr;grid-template-rows:auto auto auto}.signals-panel{grid-row:auto;grid-column:auto;max-height:400px}.map-panel{grid-row:auto;grid-column:auto;min-height:300px}.detail-panel{grid-row:auto;grid-column:auto}}@media(max-width:640px){.layout{grid-template-columns:0 1fr;grid-template-rows:auto 1fr}.sidebar{display:none}.detail-header{flex-direction:column;gap:8px;margin-bottom:8px}.detail-metrics{width:100%;justify-content:flex-start}.metric{text-align:left}.outcomes-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}.outcomes-row .ai-box{grid-column:1 / -1}.outcomes-row .chart-container{grid-column:1 / -1;height:130px;max-height:130px;min-height:0}.sim-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;align-items:center}.sim-row .kelly-note{grid-column:1 / -1;border-left:2px solid var(--border2);padding:6px 10px}.sim-row .sim-label{grid-column:1 / -1;font-size:.875rem}.sim-row .sim-input{grid-column:1 / -1;width:100%;min-width:0}.sim-row .sim-btn-yes,.sim-row .sim-btn-no{width:100%;padding:10px 8px;font-size:.8125rem;white-space:nowrap;text-align:center}.sim-row .sim-disclaimer{grid-column:1 / -1;font-size:.75rem}.desktop-only{display:none!important}.mobile-only{display:initial}.model-badge,.badge-full{display:none}.badge-abbr{display:inline}.topbar{flex-wrap:wrap;padding:8px 12px;gap:8px;min-height:auto;height:auto}.topbar-logo{position:static;height:auto;order:0;flex:1}.topbar-actions{order:0;margin-left:0;gap:10px}.topbar-stats{order:1;width:100%;flex:none;display:flex;align-items:center;overflow:hidden}.topbar-stats .live-badge{display:none}.stats-track{display:flex;align-items:center;gap:24px;width:max-content;animation:scroll-stats 20s linear infinite;flex:none;mask-image:linear-gradient(90deg,transparent,black 8%,black 92%,transparent);-webkit-mask-image:linear-gradient(90deg,transparent,black 8%,black 92%,transparent)}.stats-track:hover{animation-play-state:paused}.topbar-stats .stat{gap:8px;padding:4px 0;flex-shrink:0}.topbar-stats .stat-label{font-size:.75rem}.topbar-stats .stat-val{font-size:.875rem}.topbar-stats .stat-delta{font-size:.75rem}.topbar-stats .legend{display:none}.stats-track .legend.end~.stat{display:flex}.dashboard-grid{height:auto}.map-panel{min-height:250px}.signals-panel{max-height:none}.panel.collapsed{min-height:0;max-height:none}#btn-telegram-mobile{color:var(--blue);background:var(--blue3);border-color:var(--blue2)}#btn-notif,#btn-auth-mobile{display:flex}.topbar-logo .logo-text{font-size:1rem}#panel-map .panel-header{flex-wrap:wrap;gap:6px 0}#panel-map .panel-title{flex:1}.map-legend{width:100%;margin:0;justify-content:flex-end;order:1}#panel-signals .panel-header{position:sticky;top:0;z-index:10;background:var(--bg2)}#panel-detail{display:none}.market-card-detail{background:var(--bg3);border:.5px solid var(--blue2);border-radius:var(--radius-sm);padding:14px;display:flex;flex-direction:column;gap:12px}}.leaflet-pane,.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile-container,.leaflet-pane>svg,.leaflet-pane>canvas,.leaflet-zoom-box,.leaflet-image-layer,.leaflet-layer{position:absolute;left:0;top:0}.leaflet-container{overflow:hidden}.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow{-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-user-drag:none}.leaflet-tile::selection{background:transparent}.leaflet-safari .leaflet-tile{image-rendering:-webkit-optimize-contrast}.leaflet-safari .leaflet-tile-container{width:1600px;height:1600px;-webkit-transform-origin:0 0}.leaflet-marker-icon,.leaflet-marker-shadow{display:block}.leaflet-container .leaflet-overlay-pane svg{max-width:none!important;max-height:none!important}.leaflet-container .leaflet-marker-pane img,.leaflet-container .leaflet-shadow-pane img,.leaflet-container .leaflet-tile-pane img,.leaflet-container img.leaflet-image-layer,.leaflet-container .leaflet-tile{max-width:none!important;max-height:none!important;width:auto;padding:0}.leaflet-container img.leaflet-tile{mix-blend-mode:plus-lighter}.leaflet-container.leaflet-touch-zoom{-ms-touch-action:pan-x pan-y;touch-action:pan-x pan-y}.leaflet-container.leaflet-touch-drag{-ms-touch-action:pinch-zoom;touch-action:none;touch-action:pinch-zoom}.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom{-ms-touch-action:none;touch-action:none}.leaflet-container{-webkit-tap-highlight-color:transparent}.leaflet-container a{-webkit-tap-highlight-color:rgba(51,181,229,.4)}.leaflet-tile{filter:inherit;visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-box{width:0;height:0;-moz-box-sizing:border-box;box-sizing:border-box;z-index:800}.leaflet-overlay-pane svg{-moz-user-select:none}.leaflet-pane{z-index:400}.leaflet-tile-pane{z-index:200}.leaflet-overlay-pane{z-index:400}.leaflet-shadow-pane{z-index:500}.leaflet-marker-pane{z-index:600}.leaflet-tooltip-pane{z-index:650}.leaflet-popup-pane{z-index:700}.leaflet-map-pane canvas{z-index:100}.leaflet-map-pane svg{z-index:200}.leaflet-vml-shape{width:1px;height:1px}.lvml{behavior:url(#default#VML);display:inline-block;position:absolute}.leaflet-control{position:relative;z-index:800;pointer-events:visiblePainted;pointer-events:auto}.leaflet-top,.leaflet-bottom{position:absolute;z-index:1000;pointer-events:none}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-control{float:left;clear:both}.leaflet-right .leaflet-control{float:right}.leaflet-top .leaflet-control{margin-top:10px}.leaflet-bottom .leaflet-control{margin-bottom:10px}.leaflet-left .leaflet-control{margin-left:10px}.leaflet-right .leaflet-control{margin-right:10px}.leaflet-fade-anim .leaflet-popup{opacity:0;-webkit-transition:opacity .2s linear;-moz-transition:opacity .2s linear;transition:opacity .2s linear}.leaflet-fade-anim .leaflet-map-pane .leaflet-popup{opacity:1}.leaflet-zoom-animated{-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}svg.leaflet-zoom-animated{will-change:transform}.leaflet-zoom-anim .leaflet-zoom-animated{-webkit-transition:-webkit-transform .25s cubic-bezier(0,0,.25,1);-moz-transition:-moz-transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1)}.leaflet-zoom-anim .leaflet-tile,.leaflet-pan-anim .leaflet-tile{-webkit-transition:none;-moz-transition:none;transition:none}.leaflet-zoom-anim .leaflet-zoom-hide{visibility:hidden}.leaflet-interactive{cursor:pointer}.leaflet-grab{cursor:-webkit-grab;cursor:-moz-grab;cursor:grab}.leaflet-crosshair,.leaflet-crosshair .leaflet-interactive{cursor:crosshair}.leaflet-popup-pane,.leaflet-control{cursor:auto}.leaflet-dragging .leaflet-grab,.leaflet-dragging .leaflet-grab .leaflet-interactive,.leaflet-dragging .leaflet-marker-draggable{cursor:move;cursor:-webkit-grabbing;cursor:-moz-grabbing;cursor:grabbing}.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-image-layer,.leaflet-pane>svg path,.leaflet-tile-container{pointer-events:none}.leaflet-marker-icon.leaflet-interactive,.leaflet-image-layer.leaflet-interactive,.leaflet-pane>svg path.leaflet-interactive,svg.leaflet-image-layer.leaflet-interactive path{pointer-events:visiblePainted;pointer-events:auto}.leaflet-container{background:#ddd;outline-offset:1px}.leaflet-container a{color:#0078a8}.leaflet-zoom-box{border:2px dotted #38f;background:#ffffff80}.leaflet-container{font-family:Helvetica Neue,Arial,Helvetica,sans-serif;font-size:12px;font-size:.75rem;line-height:1.5}.leaflet-bar{box-shadow:0 1px 5px #000000a6;border-radius:4px}.leaflet-bar a{background-color:#fff;border-bottom:1px solid #ccc;width:26px;height:26px;line-height:26px;display:block;text-align:center;text-decoration:none;color:#000}.leaflet-bar a,.leaflet-control-layers-toggle{background-position:50% 50%;background-repeat:no-repeat;display:block}.leaflet-bar a:hover,.leaflet-bar a:focus{background-color:#f4f4f4}.leaflet-bar a:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-bar a:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom:none}.leaflet-bar a.leaflet-disabled{cursor:default;background-color:#f4f4f4;color:#bbb}.leaflet-touch .leaflet-bar a{width:30px;height:30px;line-height:30px}.leaflet-touch .leaflet-bar a:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.leaflet-touch .leaflet-bar a:last-child{border-bottom-left-radius:2px;border-bottom-right-radius:2px}.leaflet-control-zoom-in,.leaflet-control-zoom-out{font:700 18px Lucida Console,Monaco,monospace;text-indent:1px}.leaflet-touch .leaflet-control-zoom-in,.leaflet-touch .leaflet-control-zoom-out{font-size:22px}.leaflet-control-layers{box-shadow:0 1px 5px #0006;background:#fff;border-radius:5px}.leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAQAAAADQ4RFAAACf0lEQVR4AY1UM3gkARTePdvdoTxXKc+qTl3aU5U6b2Kbkz3Gtq3Zw6ziLGNPzrYx7946Tr6/ee/XeCQ4D3ykPtL5tHno4n0d/h3+xfuWHGLX81cn7r0iTNzjr7LrlxCqPtkbTQEHeqOrTy4Yyt3VCi/IOB0v7rVC7q45Q3Gr5K6jt+3Gl5nCoDD4MtO+j96Wu8atmhGqcNGHObuf8OM/x3AMx38+4Z2sPqzCxRFK2aF2e5Jol56XTLyggAMTL56XOMoS1W4pOyjUcGGQdZxU6qRh7B9Zp+PfpOFlqt0zyDZckPi1ttmIp03jX8gyJ8a/PG2yutpS/Vol7peZIbZcKBAEEheEIAgFbDkz5H6Zrkm2hVWGiXKiF4Ycw0RWKdtC16Q7qe3X4iOMxruonzegJzWaXFrU9utOSsLUmrc0YjeWYjCW4PDMADElpJSSQ0vQvA1Tm6/JlKnqFs1EGyZiFCqnRZTEJJJiKRYzVYzJck2Rm6P4iH+cmSY0YzimYa8l0EtTODFWhcMIMVqdsI2uiTvKmTisIDHJ3od5GILVhBCarCfVRmo4uTjkhrhzkiBV7SsaqS+TzrzM1qpGGUFt28pIySQHR6h7F6KSwGWm97ay+Z+ZqMcEjEWebE7wxCSQwpkhJqoZA5ivCdZDjJepuJ9IQjGGUmuXJdBFUygxVqVsxFsLMbDe8ZbDYVCGKxs+W080max1hFCarCfV+C1KATwcnvE9gRRuMP2prdbWGowm1KB1y+zwMMENkM755cJ2yPDtqhTI6ED1M/82yIDtC/4j4BijjeObflpO9I9MwXTCsSX8jWAFeHr05WoLTJ5G8IQVS/7vwR6ohirYM7f6HzYpogfS3R2OAAAAAElFTkSuQmCC);width:36px;height:36px}.leaflet-retina .leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAQAAABvcdNgAAAEsklEQVR4AWL4TydIhpZK1kpWOlg0w3ZXP6D2soBtG42jeI6ZmQTHzAxiTbSJsYLjO9HhP+WOmcuhciVnmHVQcJnp7DFvScowZorad/+V/fVzMdMT2g9Cv9guXGv/7pYOrXh2U+RRR3dSd9JRx6bIFc/ekqHI29JC6pJ5ZEh1yWkhkbcFeSjxgx3L2m1cb1C7bceyxA+CNjT/Ifff+/kDk2u/w/33/IeCMOSaWZ4glosqT3DNnNZQ7Cs58/3Ce5HL78iZH/vKVIaYlqzfdLu8Vi7dnvUbEza5Idt36tquZFldl6N5Z/POLof0XLK61mZCmJSWjVF9tEjUluu74IUXvgttuVIHE7YxSkaYhJZam7yiM9Pv82JYfl9nptxZaxMJE4YSPty+vF0+Y2up9d3wwijfjZbabqm/3bZ9ecKHsiGmRflnn1MW4pjHf9oLufyn2z3y1D6n8g8TZhxyzipLNPnAUpsOiuWimg52psrTZYnOWYNDTMuWBWa0tJb4rgq1UvmutpaYEbZlwU3CLJm/ayYjHW5/h7xWLn9Hh1vepDkyf7dE7MtT5LR4e7yYpHrkhOUpEfssBLq2pPhAqoSWKUkk7EDqkmK6RrCEzqDjhNDWNE+XSMvkJRDWlZTmCW0l0PHQGRZY5t1L83kT0Y3l2SItk5JAWHl2dCOBm+fPu3fo5/3v61RMCO9Jx2EEYYhb0rmNQMX/vm7gqOEJLcXTGw3CAuRNeyaPWwjR8PRqKQ1PDA/dpv+on9Shox52WFnx0KY8onHayrJzm87i5h9xGw/tfkev0jGsQizqezUKjk12hBMKJ4kbCqGPVNXudyyrShovGw5CgxsRICxF6aRmSjlBnHRzg7Gx8fKqEubI2rahQYdR1YgDIRQO7JvQyD52hoIQx0mxa0ODtW2Iozn1le2iIRdzwWewedyZzewidueOGqlsn1MvcnQpuVwLGG3/IR1hIKxCjelIDZ8ldqWz25jWAsnldEnK0Zxro19TGVb2ffIZEsIO89EIEDvKMPrzmBOQcKQ+rroye6NgRRxqR4U8EAkz0CL6uSGOm6KQCdWjvjRiSP1BPalCRS5iQYiEIvxuBMJEWgzSoHADcVMuN7IuqqTeyUPq22qFimFtxDyBBJEwNyt6TM88blFHao/6tWWhuuOM4SAK4EI4QmFHA+SEyWlp4EQoJ13cYGzMu7yszEIBOm2rVmHUNqwAIQabISNMRstmdhNWcFLsSm+0tjJH1MdRxO5Nx0WDMhCtgD6OKgZeljJqJKc9po8juskR9XN0Y1lZ3mWjLR9JCO1jRDMd0fpYC2VnvjBSEFg7wBENc0R9HFlb0xvF1+TBEpF68d+DHR6IOWVv2BECtxo46hOFUBd/APU57WIoEwJhIi2CdpyZX0m93BZicktMj1AS9dClteUFAUNUIEygRZCtik5zSxI9MubTBH1GOiHsiLJ3OCoSZkILa9PxiN0EbvhsAo8tdAf9Seepd36lGWHmtNANTv5Jd0z4QYyeo/UEJqxKRpg5LZx6btLPsOaEmdMyxYdlc8LMaJnikDlhclqmPiQnTEpLUIZEwkRagjYkEibQErwhkTAKCLQEbUgkzJQWc/0PstHHcfEdQ+UAAAAASUVORK5CYII=);background-size:26px 26px}.leaflet-touch .leaflet-control-layers-toggle{width:44px;height:44px}.leaflet-control-layers .leaflet-control-layers-list,.leaflet-control-layers-expanded .leaflet-control-layers-toggle{display:none}.leaflet-control-layers-expanded .leaflet-control-layers-list{display:block;position:relative}.leaflet-control-layers-expanded{padding:6px 10px 6px 6px;color:#333;background:#fff}.leaflet-control-layers-scrollbar{overflow-y:scroll;overflow-x:hidden;padding-right:5px}.leaflet-control-layers-selector{margin-top:2px;position:relative;top:1px}.leaflet-control-layers label{display:block;font-size:13px;font-size:1.08333em}.leaflet-control-layers-separator{height:0;border-top:1px solid #ddd;margin:5px -10px 5px -6px}.leaflet-default-icon-path{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YaZtr2962HUzbDNpjszW24mRt28p47v7zq/bXZtrp/lWnXr337j3nPCe85NcypgSFdugCpW5YoDAMRaIMqRi6aKq5E3YqDQO3qAwjVWrD8Ncq/RBpykd8oZUb/kaJutow8r1aP9II0WmLKLIsJyv1w/kqw9Ch2MYdB++12Onxee/QMwvf4/Dk/Lfp/i4nxTXtOoQ4pW5Aj7wpici1A9erdAN2OH64x8OSP9j3Ft3b7aWkTg/Fm91siTra0f9on5sQr9INejH6CUUUpavjFNq1B+Oadhxmnfa8RfEmN8VNAsQhPqF55xHkMzz3jSmChWU6f7/XZKNH+9+hBLOHYozuKQPxyMPUKkrX/K0uWnfFaJGS1QPRtZsOPtr3NsW0uyh6NNCOkU3Yz+bXbT3I8G3xE5EXLXtCXbbqwCO9zPQYPRTZ5vIDXD7U+w7rFDEoUUf7ibHIR4y6bLVPXrz8JVZEql13trxwue/uDivd3fkWRbS6/IA2bID4uk0UpF1N8qLlbBlXs4Ee7HLTfV1j54APvODnSfOWBqtKVvjgLKzF5YdEk5ewRkGlK0i33Eofffc7HT56jD7/6U+qH3Cx7SBLNntH5YIPvODnyfIXZYRVDPqgHtLs5ABHD3YzLuespb7t79FY34DjMwrVrcTuwlT55YMPvOBnRrJ4VXTdNnYug5ucHLBjEpt30701A3Ts+HEa73u6dT3FNWwflY86eMHPk+Yu+i6pzUpRrW7SNDg5JHR4KapmM5Wv2E8Tfcb1HoqqHMHU+uWDD7zg54mz5/2BSnizi9T1Dg4QQXLToGNCkb6tb1NU+QAlGr1++eADrzhn/u8Q2YZhQVlZ5+CAOtqfbhmaUCS1ezNFVm2imDbPmPng5wmz+gwh+oHDce0eUtQ6OGDIyR0uUhUsoO3vfDmmgOezH0mZN59x7MBi++WDL1g/eEiU3avlidO671bkLfwbw5XV2P8Pzo0ydy4t2/0eu33xYSOMOD8hTf4CrBtGMSoXfPLchX+J0ruSePw3LZeK0juPJbYzrhkH0io7B3k164hiGvawhOKMLkrQLyVpZg8rHFW7E2uHOL888IBPlNZ1FPzstSJM694fWr6RwpvcJK60+0HCILTBzZLFNdtAzJaohze60T8qBzyh5ZuOg5e7uwQppofEmf2++DYvmySqGBuKaicF1blQjhuHdvCIMvp8whTTfZzI7RldpwtSzL+F1+wkdZ2TBOW2gIF88PBTzD/gpeREAMEbxnJcaJHNHrpzji0gQCS6hdkEeYt9DF/2qPcEC8RM28Hwmr3sdNyht00byAut2k3gufWNtgtOEOFGUwcXWNDbdNbpgBGxEvKkOQsxivJx33iow0Vw5S6SVTrpVq11ysA2Rp7gTfPfktc6zhtXBBC+adRLshf6sG2RfHPZ5EAc4sVZ83yCN00Fk/4kggu40ZTvIEm5g24qtU4KjBrx/BTTH8ifVASAG7gKrnWxJDcU7x8X6Ecczhm3o6YicvsLXWfh3Ch1W0k8x0nXF+0fFxgt4phz8QvypiwCCFKMqXCnqXExjq10beH+UUA7+nG6mdG/Pu0f3LgFcGrl2s0kNNjpmoJ9o4B29CMO8dMT4Q5ox8uitF6fqsrJOr8qnwNbRzv6hSnG5wP+64C7h9lp30hKNtKdWjtdkbuPA19nJ7Tz3zR/ibgARbhb4AlhavcBebmTHcFl2fvYEnW0ox9xMxKBS8btJ+KiEbq9zA4RthQXDhPa0T9TEe69gWupwc6uBUphquXgf+/FrIjweHQS4/pduMe5ERUMHUd9xv8ZR98CxkS4F2n3EUrUZ10EYNw7BWm9x1GiPssi3GgiGRDKWRYZfXlON+dfNbM+GgIwYdwAAAAASUVORK5CYII=)}.leaflet-container .leaflet-control-attribution{background:#fff;background:#fffc;margin:0}.leaflet-control-attribution,.leaflet-control-scale-line{padding:0 5px;color:#333;line-height:1.4}.leaflet-control-attribution a{text-decoration:none}.leaflet-control-attribution a:hover,.leaflet-control-attribution a:focus{text-decoration:underline}.leaflet-attribution-flag{display:inline!important;vertical-align:baseline!important;width:1em;height:.6669em}.leaflet-left .leaflet-control-scale{margin-left:5px}.leaflet-bottom .leaflet-control-scale{margin-bottom:5px}.leaflet-control-scale-line{border:2px solid #777;border-top:none;line-height:1.1;padding:2px 5px 1px;white-space:nowrap;-moz-box-sizing:border-box;box-sizing:border-box;background:#fffc;text-shadow:1px 1px #fff}.leaflet-control-scale-line:not(:first-child){border-top:2px solid #777;border-bottom:none;margin-top:-2px}.leaflet-control-scale-line:not(:first-child):not(:last-child){border-bottom:2px solid #777}.leaflet-touch .leaflet-control-attribution,.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{box-shadow:none}.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{border:2px solid rgba(0,0,0,.2);background-clip:padding-box}.leaflet-popup{position:absolute;text-align:center;margin-bottom:20px}.leaflet-popup-content-wrapper{padding:1px;text-align:left;border-radius:12px}.leaflet-popup-content{margin:13px 24px 13px 20px;line-height:1.3;font-size:13px;font-size:1.08333em;min-height:1px}.leaflet-popup-content p{margin:1.3em 0}.leaflet-popup-tip-container{width:40px;height:20px;position:absolute;left:50%;margin-top:-1px;margin-left:-20px;overflow:hidden;pointer-events:none}.leaflet-popup-tip{width:17px;height:17px;padding:1px;margin:-10px auto 0;pointer-events:auto;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.leaflet-popup-content-wrapper,.leaflet-popup-tip{background:#fff;color:#333;box-shadow:0 3px 14px #0006}.leaflet-container a.leaflet-popup-close-button{position:absolute;top:0;right:0;border:none;text-align:center;width:24px;height:24px;font:16px/24px Tahoma,Verdana,sans-serif;color:#757575;text-decoration:none;background:transparent}.leaflet-container a.leaflet-popup-close-button:hover,.leaflet-container a.leaflet-popup-close-button:focus{color:#585858}.leaflet-popup-scrolled{overflow:auto}.leaflet-oldie .leaflet-popup-content-wrapper{-ms-zoom:1}.leaflet-oldie .leaflet-popup-tip{width:24px;margin:0 auto;-ms-filter:"progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";filter:progid:DXImageTransform.Microsoft.Matrix(M11=.70710678,M12=.70710678,M21=-.70710678,M22=.70710678)}.leaflet-oldie .leaflet-control-zoom,.leaflet-oldie .leaflet-control-layers,.leaflet-oldie .leaflet-popup-content-wrapper,.leaflet-oldie .leaflet-popup-tip{border:1px solid #999}.leaflet-div-icon{background:#fff;border:1px solid #666}.leaflet-tooltip{position:absolute;padding:6px;background-color:#fff;border:1px solid #fff;border-radius:3px;color:#222;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;box-shadow:0 1px 3px #0006}.leaflet-tooltip.leaflet-interactive{cursor:pointer;pointer-events:auto}.leaflet-tooltip-top:before,.leaflet-tooltip-bottom:before,.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{position:absolute;pointer-events:none;border:6px solid transparent;background:transparent;content:""}.leaflet-tooltip-bottom{margin-top:6px}.leaflet-tooltip-top{margin-top:-6px}.leaflet-tooltip-bottom:before,.leaflet-tooltip-top:before{left:50%;margin-left:-6px}.leaflet-tooltip-top:before{bottom:0;margin-bottom:-12px;border-top-color:#fff}.leaflet-tooltip-bottom:before{top:0;margin-top:-12px;margin-left:-6px;border-bottom-color:#fff}.leaflet-tooltip-left{margin-left:-6px}.leaflet-tooltip-right{margin-left:6px}.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{top:50%;margin-top:-6px}.leaflet-tooltip-left:before{right:0;margin-right:-12px;border-left-color:#fff}.leaflet-tooltip-right:before{left:0;margin-left:-12px;border-right-color:#fff}@media print{.leaflet-control{-webkit-print-color-adjust:exact;print-color-adjust:exact}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/dist/assets/index-CjgKC2Lf.js ADDED
The diff for this file is too large to render. See raw diff
 
frontend/dist/assets/index-DkbPlw3S.js DELETED
The diff for this file is too large to render. See raw diff
 
frontend/src/api.js CHANGED
@@ -41,6 +41,18 @@ export function isAuthenticated() {
41
  }
42
 
43
  /* ─── Auth ─── */
 
 
 
 
 
 
 
 
 
 
 
 
44
  export async function login(email, password) {
45
  const body = await fetchJson(`${BASE}/auth/login`, {
46
  method: 'POST',
@@ -50,6 +62,9 @@ export async function login(email, password) {
50
  if (body.token) {
51
  setToken(body.token)
52
  }
 
 
 
53
  return body
54
  }
55
 
@@ -62,6 +77,9 @@ export async function register(email, password) {
62
  if (body.token) {
63
  setToken(body.token)
64
  }
 
 
 
65
  return body
66
  }
67
 
@@ -83,7 +101,26 @@ export async function logout() {
83
  }
84
 
85
  export async function getMe() {
86
- return fetchJson(`${BASE}/auth/me`)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
  /* ─── Core fetch ─── */
 
41
  }
42
 
43
  /* ─── Auth ─── */
44
+ function saveTelegramConfig(user) {
45
+ if (!user) return
46
+ localStorage.setItem(
47
+ 'telegramConfig',
48
+ JSON.stringify({
49
+ botToken: user.telegramBotToken || '',
50
+ chatId: user.telegramChatId || '',
51
+ enabled: user.telegramAlertsEnabled || false,
52
+ }),
53
+ )
54
+ }
55
+
56
  export async function login(email, password) {
57
  const body = await fetchJson(`${BASE}/auth/login`, {
58
  method: 'POST',
 
62
  if (body.token) {
63
  setToken(body.token)
64
  }
65
+ if (body.user) {
66
+ saveTelegramConfig(body.user)
67
+ }
68
  return body
69
  }
70
 
 
77
  if (body.token) {
78
  setToken(body.token)
79
  }
80
+ if (body.user) {
81
+ saveTelegramConfig(body.user)
82
+ }
83
  return body
84
  }
85
 
 
101
  }
102
 
103
  export async function getMe() {
104
+ const data = await fetchJson(`${BASE}/auth/me`)
105
+ if (data && data.user) {
106
+ saveTelegramConfig(data.user)
107
+ }
108
+ return data
109
+ }
110
+
111
+ export async function updateTelegramConfig({ botToken, chatId, enabled }) {
112
+ const body = await fetchJson(`${BASE}/auth/telegram`, {
113
+ method: 'PUT',
114
+ body: JSON.stringify({
115
+ telegramBotToken: botToken,
116
+ telegramChatId: chatId,
117
+ telegramAlertsEnabled: enabled,
118
+ }),
119
+ })
120
+ if (body && body.user) {
121
+ saveTelegramConfig(body.user)
122
+ }
123
+ return body
124
  }
125
 
126
  /* ─── Core fetch ─── */
frontend/src/app.js CHANGED
@@ -406,7 +406,7 @@ function closeTelegramModal() {
406
  }
407
  }
408
 
409
- function handleTelegramSave(e) {
410
  e.preventDefault()
411
  const botToken = document.getElementById('telegram-bot-token').value.trim()
412
  const chatId = document.getElementById('telegram-chat-id').value.trim()
@@ -419,11 +419,15 @@ function handleTelegramSave(e) {
419
  return
420
  }
421
 
422
- localStorage.setItem('telegramConfig', JSON.stringify({ botToken, chatId, enabled }))
423
- statusEl.textContent = 'Configuración guardada correctamente.'
424
- statusEl.className = 'form-status success'
425
-
426
- setTimeout(() => closeTelegramModal(), 1200)
 
 
 
 
427
  }
428
 
429
  function handleTelegramTest() {
 
406
  }
407
  }
408
 
409
+ async function handleTelegramSave(e) {
410
  e.preventDefault()
411
  const botToken = document.getElementById('telegram-bot-token').value.trim()
412
  const chatId = document.getElementById('telegram-chat-id').value.trim()
 
419
  return
420
  }
421
 
422
+ try {
423
+ await api.updateTelegramConfig({ botToken, chatId, enabled })
424
+ statusEl.textContent = 'Configuración guardada correctamente.'
425
+ statusEl.className = 'form-status success'
426
+ setTimeout(() => closeTelegramModal(), 1200)
427
+ } catch (err) {
428
+ statusEl.textContent = 'Error al guardar. Inténtalo de nuevo.'
429
+ statusEl.className = 'form-status error'
430
+ }
431
  }
432
 
433
  function handleTelegramTest() {