tokenrace 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>tokenrace</title>
7
- <script type="module" crossorigin src="/assets/index-DQk3XNu9.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-B0dy8dWP.css">
7
+ <script type="module" crossorigin src="/assets/index-B-htjFDg.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-52-EmhaW.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenrace",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Monitor en tiempo real para Claude Code",
5
5
  "bin": {
6
6
  "tokenrace": "bin/cli.js"
package/src/api-routes.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  getStatus, getSummary, getTimeseries, getProjects,
15
15
  getSessions, getUnlabeledSessions, getSessionEvents,
16
16
  getEvents, getTools, getAgents, getModels,
17
- labelSession, ignoreSession, reset
17
+ labelSession, ignoreSession, reset, resetProject
18
18
  } from './store.js'
19
19
 
20
20
  // ─── Clientes SSE activos ────────────────────────────────────────────────────
@@ -194,6 +194,14 @@ export function createRouter({ port = 1337 } = {}) {
194
194
  res.json({ ok: true })
195
195
  })
196
196
 
197
+ /** POST /api/projects/:project/reset — resetea los datos de un proyecto */
198
+ router.post('/api/projects/:project/reset', requireSafeOrigin, (req, res) => {
199
+ const projectName = decodeURIComponent(req.params.project)
200
+ resetProject(projectName)
201
+ broadcast('project_reset', { project: projectName })
202
+ res.json({ ok: true })
203
+ })
204
+
197
205
  return router
198
206
  }
199
207
 
package/src/prices.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * prices.js — Tabla de precios de modelos Claude (USD por millón de tokens).
3
+ */
4
+
5
+ export const MODEL_PRICES = {
6
+ 'claude-opus-4-8': { input: 15.00, output: 75.00, cacheWrite: 18.75, cacheRead: 1.50 },
7
+ 'claude-sonnet-4-6': { input: 3.00, output: 15.00, cacheWrite: 3.75, cacheRead: 0.30 },
8
+ 'claude-haiku-4-5': { input: 0.80, output: 4.00, cacheWrite: 1.00, cacheRead: 0.08 },
9
+ }
10
+
11
+ const DEFAULT_PRICES = MODEL_PRICES['claude-sonnet-4-6']
12
+
13
+ /**
14
+ * Devuelve los precios para un nombre de modelo dado.
15
+ * Usa pattern matching por si el nombre incluye fecha (ej: claude-sonnet-4-6-20251022).
16
+ */
17
+ export function getPrices(modelName) {
18
+ if (!modelName) return DEFAULT_PRICES
19
+ const lower = modelName.toLowerCase()
20
+ if (lower.includes('opus')) return MODEL_PRICES['claude-opus-4-8']
21
+ if (lower.includes('haiku')) return MODEL_PRICES['claude-haiku-4-5']
22
+ if (lower.includes('sonnet')) return MODEL_PRICES['claude-sonnet-4-6']
23
+ return DEFAULT_PRICES
24
+ }
25
+
26
+ /**
27
+ * Estima el coste a partir de tokens para un modelo dado.
28
+ * @param {string|null} model
29
+ * @param {number} tokensInput
30
+ * @param {number} tokensOutput
31
+ * @param {number} tokensCacheRead
32
+ * @param {number} tokensCacheWrite
33
+ * @returns {number} Coste estimado en USD
34
+ */
35
+ export function estimateCost(model, tokensInput, tokensOutput, tokensCacheRead = 0, tokensCacheWrite = 0) {
36
+ const p = getPrices(model)
37
+ return (
38
+ tokensInput * p.input / 1_000_000 +
39
+ tokensOutput * p.output / 1_000_000 +
40
+ tokensCacheRead * p.cacheRead / 1_000_000 +
41
+ tokensCacheWrite * p.cacheWrite / 1_000_000
42
+ )
43
+ }
package/src/store.js CHANGED
@@ -12,6 +12,7 @@
12
12
  import fs from 'node:fs'
13
13
  import os from 'node:os'
14
14
  import path from 'node:path'
15
+ import { estimateCost } from './prices.js'
15
16
 
16
17
  // ─── Ruta de persistencia ───────────────────────────────────────────────────
17
18
 
@@ -477,6 +478,49 @@ export function reset() {
477
478
  saveSync()
478
479
  }
479
480
 
481
+ /**
482
+ * Resetea los datos de un proyecto concreto eliminando sus sesiones y sus
483
+ * puntos de timeseries, eventos y entradas cumulativas asociadas.
484
+ * @param {string} projectName
485
+ */
486
+ export function resetProject(projectName) {
487
+ // Recoger los sessionIds del proyecto
488
+ const sessionIds = new Set()
489
+ for (const session of state.sessions.values()) {
490
+ if (resolveProject(session.sessionId, session.project) === projectName) {
491
+ sessionIds.add(session.sessionId)
492
+ }
493
+ }
494
+
495
+ // Eliminar sesiones y sus entradas auxiliares
496
+ for (const sid of sessionIds) {
497
+ state.sessions.delete(sid)
498
+ state.sessionMappings.delete(sid)
499
+ state.ignoredSessions.delete(sid)
500
+
501
+ // Limpiar baselines de métricas cumulativas para esta sesión
502
+ for (const key of state.cumulativeValues.keys()) {
503
+ if (key.includes(`:${sid}:`)) state.cumulativeValues.delete(key)
504
+ }
505
+ }
506
+
507
+ // Filtrar timeseries: eliminar puntos de las sesiones del proyecto
508
+ for (const [metric, points] of state.timeseries.entries()) {
509
+ const filtered = points.filter(p => !sessionIds.has(p.labels['session.id']))
510
+ if (filtered.length !== points.length) state.timeseries.set(metric, filtered)
511
+ }
512
+
513
+ // Filtrar buffer de eventos
514
+ const remaining = state.events.filter(e => !sessionIds.has(e.sessionId))
515
+ state.events.length = 0
516
+ state.events.push(...remaining)
517
+
518
+ // Eliminar el proyecto del mapa
519
+ state.projects.delete(projectName)
520
+
521
+ scheduleSave()
522
+ }
523
+
480
524
  // ─── Getters ─────────────────────────────────────────────────────────────────
481
525
 
482
526
  /**
@@ -516,7 +560,9 @@ export function getSummary(from) {
516
560
  tokensInput += session.tokensInput
517
561
  tokensOutput += session.tokensOutput
518
562
  tokensCache += session.tokensCache
519
- cost += session.cost
563
+ cost += session.cost > 0
564
+ ? session.cost
565
+ : estimateCost(session.model, session.tokensInput, session.tokensOutput, session.tokensCache)
520
566
  activeTimeMs += session.durationActiveMs
521
567
  sessionSet.add(session.sessionId)
522
568
  }
@@ -612,7 +658,9 @@ export function getProjects(from) {
612
658
  const session = state.sessions.get(sessionId)
613
659
  if (!session || session.lastSeen < minTs) continue
614
660
  activeSessions.add(sessionId)
615
- cost += session.cost
661
+ cost += session.cost > 0
662
+ ? session.cost
663
+ : estimateCost(session.model, session.tokensInput, session.tokensOutput, session.tokensCache)
616
664
  tokensInput += session.tokensInput
617
665
  tokensOutput += session.tokensOutput
618
666
  tokensCache += session.tokensCache
@@ -655,7 +703,10 @@ export function getSessions({ limit = 50, project = null } = {}) {
655
703
 
656
704
  return sessions.slice(0, limit).map(s => ({
657
705
  ...s,
658
- project: resolveProject(s.sessionId, s.project)
706
+ project: resolveProject(s.sessionId, s.project),
707
+ cost: s.cost > 0
708
+ ? s.cost
709
+ : estimateCost(s.model, s.tokensInput, s.tokensOutput, s.tokensCache)
659
710
  }))
660
711
  }
661
712
 
@@ -749,15 +800,20 @@ export function getAgents() {
749
800
  */
750
801
  export function getModels(from) {
751
802
  return Array.from(state.models.entries())
752
- .map(([model, stats]) => ({
753
- model,
754
- requests: stats.requests,
755
- tokensInput: stats.tokensInput,
756
- tokensOutput: stats.tokensOutput,
757
- cost: stats.cost,
758
- avgLatencyMs: 0,
759
- avgTtftMs: 0
760
- }))
803
+ .map(([model, stats]) => {
804
+ const cost = stats.cost > 0
805
+ ? stats.cost
806
+ : estimateCost(model, stats.tokensInput, stats.tokensOutput)
807
+ return {
808
+ model,
809
+ requests: stats.requests,
810
+ tokensInput: stats.tokensInput,
811
+ tokensOutput: stats.tokensOutput,
812
+ cost,
813
+ avgLatencyMs: 0,
814
+ avgTtftMs: 0
815
+ }
816
+ })
761
817
  .sort((a, b) => b.cost - a.cost)
762
818
  }
763
819
 
@@ -1 +0,0 @@
1
- *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,Fira Code,Menlo,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.right-2{right:.5rem}.top-0{top:0}.top-10{top:2.5rem}.top-2{top:.5rem}.z-40{z-index:40}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-2{height:.5rem}.h-48{height:12rem}.h-\[600px\]{height:600px}.h-full{height:100%}.max-h-40{max-height:10rem}.min-h-\[60vh\]{min-height:60vh}.min-h-screen{min-height:100vh}.w-2{width:.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-32{width:8rem}.w-6{width:1.5rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-screen-2xl{max-width:1536px}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-accent-green{--tw-border-opacity: 1;border-color:rgb(255 107 53 / var(--tw-border-opacity, 1))}.border-bg-border{--tw-border-opacity: 1;border-color:rgb(26 26 26 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.bg-accent-green{--tw-bg-opacity: 1;background-color:rgb(255 107 53 / var(--tw-bg-opacity, 1))}.bg-accent-purple{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity, 1))}.bg-accent-teal{--tw-bg-opacity: 1;background-color:rgb(0 212 170 / var(--tw-bg-opacity, 1))}.bg-accent-yellow{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-bg-base{--tw-bg-opacity: 1;background-color:rgb(20 25 31 / var(--tw-bg-opacity, 1))}.bg-bg-card,.bg-bg-subtle{--tw-bg-opacity: 1;background-color:rgb(33 38 43 / var(--tw-bg-opacity, 1))}.bg-text-muted{--tw-bg-opacity: 1;background-color:rgb(68 68 68 / var(--tw-bg-opacity, 1))}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,Fira Code,Menlo,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-wider{letter-spacing:.05em}.text-accent-blue{--tw-text-opacity: 1;color:rgb(77 166 255 / var(--tw-text-opacity, 1))}.text-accent-green,.text-accent-orange{--tw-text-opacity: 1;color:rgb(255 107 53 / var(--tw-text-opacity, 1))}.text-accent-purple{--tw-text-opacity: 1;color:rgb(168 85 247 / var(--tw-text-opacity, 1))}.text-accent-teal{--tw-text-opacity: 1;color:rgb(0 212 170 / var(--tw-text-opacity, 1))}.text-accent-yellow{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-text-muted{--tw-text-opacity: 1;color:rgb(68 68 68 / var(--tw-text-opacity, 1))}.text-text-primary{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-text-secondary{--tw-text-opacity: 1;color:rgb(136 136 136 / var(--tw-text-opacity, 1))}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{--bg-base: #14191F;--bg-card: #21262B;--bg-card-hover: #272d33;--bg-border: #1a1a1a;--bg-subtle: #21262B;--text-primary: #ffffff;--text-secondary: #888888;--text-muted: #444444;--accent-green: #ff6b35;--accent-blue: #4da6ff;--accent-purple: #a855f7;--accent-orange: #ff6b35;--accent-teal: #00d4aa;--accent-yellow: #fbbf24}*{box-sizing:border-box}html,body,#root{height:100%;margin:0;padding:0;background-color:#14191f;color:#fff}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;-webkit-font-smoothing:antialiased}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:#0d0d0d}::-webkit-scrollbar-thumb{background:#333;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#444}.hover\:border-accent-green:hover{--tw-border-opacity: 1;border-color:rgb(255 107 53 / var(--tw-border-opacity, 1))}.hover\:bg-bg-base:hover{--tw-bg-opacity: 1;background-color:rgb(20 25 31 / var(--tw-bg-opacity, 1))}.hover\:bg-bg-card:hover{--tw-bg-opacity: 1;background-color:rgb(33 38 43 / var(--tw-bg-opacity, 1))}.hover\:bg-bg-card-hover:hover{--tw-bg-opacity: 1;background-color:rgb(39 45 51 / var(--tw-bg-opacity, 1))}.hover\:text-accent-orange:hover{--tw-text-opacity: 1;color:rgb(255 107 53 / var(--tw-text-opacity, 1))}.hover\:text-text-primary:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.focus\:border-accent-green:focus{--tw-border-opacity: 1;border-color:rgb(255 107 53 / var(--tw-border-opacity, 1))}@media(min-width:768px){.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}