loki-mode 6.63.0 → 6.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/loki +37 -5
  4. package/dashboard/__init__.py +1 -1
  5. package/docs/INSTALLATION.md +1 -1
  6. package/mcp/__init__.py +1 -1
  7. package/package.json +1 -1
  8. package/web-app/dist/assets/{Badge-Daan3gu4.js → Badge-8l0OZCRe.js} +1 -1
  9. package/web-app/dist/assets/{Button-BfeQWtXn.js → Button-6k_tnJgc.js} +1 -1
  10. package/web-app/dist/assets/{Card-JqwSaE0I.js → Card-DwkzVihG.js} +1 -1
  11. package/web-app/dist/assets/{HomePage-ZrDPLDGe.js → HomePage-C0-_6Avk.js} +1 -1
  12. package/web-app/dist/assets/{LoginPage-lJUDQIlI.js → LoginPage-BlJm-Tzr.js} +1 -1
  13. package/web-app/dist/assets/{NotFoundPage-kZTYx4v_.js → NotFoundPage-CsRjjzWq.js} +1 -1
  14. package/web-app/dist/assets/{ProjectPage-DayJk_FX.js → ProjectPage-DQG_ZYM7.js} +51 -46
  15. package/web-app/dist/assets/{ProjectsPage-4_PqKgaD.js → ProjectsPage-BAQOc1tx.js} +1 -1
  16. package/web-app/dist/assets/{SettingsPage-DmjFCI0F.js → SettingsPage-DiKaBtvg.js} +1 -1
  17. package/web-app/dist/assets/{TemplatesPage-BOX60wWf.js → TemplatesPage-CyxNji74.js} +1 -1
  18. package/web-app/dist/assets/{TerminalOutput-B9rfXUCC.js → TerminalOutput-BLPNvDc5.js} +1 -1
  19. package/web-app/dist/assets/{arrow-left-Rh7PJrlD.js → arrow-left-dP_J0CkC.js} +1 -1
  20. package/web-app/dist/assets/{clock-CDe-IBc9.js → clock-CGZn7bQ1.js} +1 -1
  21. package/web-app/dist/assets/{external-link-BviPLjiY.js → external-link-ypPFWwc1.js} +1 -1
  22. package/web-app/dist/assets/index-CQcaFLVo.css +1 -0
  23. package/web-app/dist/assets/{index--VmvfdEx.js → index-tGQw_JnU.js} +22 -22
  24. package/web-app/dist/index.html +2 -2
  25. package/web-app/server.py +468 -13
  26. package/web-app/dist/assets/index-DzYIpBt0.css +0 -1
@@ -8,8 +8,8 @@
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
10
  <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
11
- <script type="module" crossorigin src="/assets/index--VmvfdEx.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-DzYIpBt0.css">
11
+ <script type="module" crossorigin src="/assets/index-tGQw_JnU.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-CQcaFLVo.css">
13
13
  </head>
14
14
  <body class="bg-background text-ink font-sans antialiased">
15
15
  <div id="root"></div>
package/web-app/server.py CHANGED
@@ -649,11 +649,14 @@ class DevServerManager:
649
649
  "image": svc.get("image"),
650
650
  "has_build": "build" in svc,
651
651
  })
652
- # Use the first exposed host port as the primary port
653
- for svc_entry in services_info:
654
- if svc_entry["ports"]:
655
- port = svc_entry["ports"][0]
656
- break
652
+ # Use smart resolution to pick the user-facing service
653
+ _primary_name, port = self._resolve_primary_service(services_info)
654
+ if _primary_name is None:
655
+ # Fallback: first service with any port
656
+ for svc_entry in services_info:
657
+ if svc_entry["ports"]:
658
+ port = svc_entry["ports"][0]
659
+ break
657
660
  except ImportError:
658
661
  # yaml not available -- fall back to regex parsing
659
662
  try:
@@ -912,6 +915,36 @@ class DevServerManager:
912
915
  return port
913
916
  return None
914
917
 
918
+ def _resolve_primary_service(self, services_info: list[dict]) -> tuple[Optional[str], int]:
919
+ """Determine the primary user-facing service from Docker Compose services."""
920
+ frontend_names = {"frontend", "web", "client", "app", "ui", "next", "vite", "nginx"}
921
+ frontend_ports = {3000, 5173, 8080, 4200, 5000, 4000}
922
+
923
+ # Priority 1: Service name matches frontend patterns
924
+ for svc in services_info:
925
+ if svc["name"].lower() in frontend_names and svc.get("ports"):
926
+ return svc["name"], svc["ports"][0]
927
+
928
+ # Priority 2: Service has a frontend-typical port
929
+ for svc in services_info:
930
+ for p in svc.get("ports", []):
931
+ if p in frontend_ports:
932
+ return svc["name"], p
933
+
934
+ # Priority 3: Custom-built service (has build, no standard image like postgres/redis)
935
+ infra_images = {"postgres", "redis", "mongo", "mysql", "rabbitmq", "memcached", "elasticsearch"}
936
+ for svc in services_info:
937
+ img = (svc.get("image") or "").split(":")[0].split("/")[-1].lower()
938
+ if svc.get("has_build") and img not in infra_images and svc.get("ports"):
939
+ return svc["name"], svc["ports"][0]
940
+
941
+ # Fallback: first service with any port
942
+ for svc in services_info:
943
+ if svc.get("ports"):
944
+ return svc["name"], svc["ports"][0]
945
+
946
+ return None, 3000
947
+
915
948
  def _install_pip_deps(self, project_path: Path, build_env: dict) -> None:
916
949
  """Install pip dependencies into a project venv (creates one if needed)."""
917
950
  if not (project_path / "requirements.txt").exists():
@@ -1203,6 +1236,10 @@ class DevServerManager:
1203
1236
 
1204
1237
  asyncio.create_task(self._monitor_output(session_id))
1205
1238
 
1239
+ # For Docker projects, also start the service health monitor
1240
+ if framework == "docker":
1241
+ asyncio.create_task(self._monitor_docker_services(session_id))
1242
+
1206
1243
  # Wait for port detection (up to 30s)
1207
1244
  for _ in range(60):
1208
1245
  await asyncio.sleep(0.5)
@@ -1387,6 +1424,24 @@ class DevServerManager:
1387
1424
  f"DEV SERVER ERROR OUTPUT:\n{error_context}"
1388
1425
  )
1389
1426
 
1427
+ # Enrich with Docker context if applicable
1428
+ if info.get("framework") == "docker":
1429
+ try:
1430
+ docker_ctx = await _gather_docker_context(target)
1431
+ if docker_ctx.get("failing_services"):
1432
+ fix_message += "\n\nDOCKER SERVICE STATUS:\n" + json.dumps(docker_ctx["service_status"], indent=2)
1433
+ for svc_name, svc_logs in docker_ctx.get("service_logs", {}).items():
1434
+ if svc_name != "_combined":
1435
+ fix_message += f"\n\nFAILING SERVICE '{svc_name}' LOGS:\n{svc_logs}"
1436
+ diagnoses = _diagnose_errors("\n".join(docker_ctx.get("service_logs", {}).values()))
1437
+ if diagnoses:
1438
+ fix_message += "\n\nAUTO-DIAGNOSIS:\n" + "\n".join(
1439
+ f"- {d['diagnosis']}: {d['suggestion']}" for d in diagnoses)
1440
+ if docker_ctx.get("project_structure"):
1441
+ fix_message += "\n\nPROJECT FILES:\n" + docker_ctx["project_structure"]
1442
+ except Exception:
1443
+ logger.debug("Docker context gathering for auto-fix failed", exc_info=True)
1444
+
1390
1445
  # Save original command before stop() removes the info dict
1391
1446
  cmd = info.get("original_command")
1392
1447
 
@@ -1604,6 +1659,114 @@ class DevServerManager:
1604
1659
  result["port"] = 1355
1605
1660
  return result
1606
1661
 
1662
+ async def _monitor_docker_services(self, session_id: str) -> None:
1663
+ """Background loop: poll Docker Compose services, auto-fix failures."""
1664
+ info = self.servers.get(session_id)
1665
+ if not info:
1666
+ return
1667
+
1668
+ project_dir = Path(info.get("project_dir", "."))
1669
+ info["docker_service_health"] = {}
1670
+
1671
+ while info.get("process") and info["process"].poll() is None:
1672
+ await asyncio.sleep(10)
1673
+
1674
+ try:
1675
+ docker_ctx = await _gather_docker_context(project_dir)
1676
+ except Exception:
1677
+ continue
1678
+
1679
+ # Update service health
1680
+ for svc in docker_ctx.get("service_status", []):
1681
+ name = svc["name"]
1682
+ prev = info["docker_service_health"].get(name, {})
1683
+ svc_health: dict = {
1684
+ "name": name,
1685
+ "status": svc["state"],
1686
+ "exit_code": svc.get("exit_code", 0),
1687
+ "restarts": prev.get("restarts", 0),
1688
+ "fix_attempts": prev.get("fix_attempts", 0),
1689
+ "fix_timestamps": prev.get("fix_timestamps", []),
1690
+ }
1691
+
1692
+ # Detect new failure
1693
+ was_running = prev.get("status") in ("running", None)
1694
+ now_failed = svc["state"] in ("exited", "dead")
1695
+
1696
+ if was_running and now_failed:
1697
+ svc_health["restarts"] = prev.get("restarts", 0) + 1
1698
+ logger.warning("Docker service '%s' failed (exit %s)", name, svc.get("exit_code"))
1699
+
1700
+ # Circuit breaker: max 3 fixes per service per 10 min
1701
+ now = time.time()
1702
+ recent_fixes = [t for t in svc_health["fix_timestamps"] if now - t < 600]
1703
+
1704
+ if len(recent_fixes) < 3:
1705
+ svc_logs = docker_ctx.get("service_logs", {}).get(name, "")
1706
+ diagnoses = _diagnose_errors(svc_logs)
1707
+ diag_text = "\n".join(f"- {d['diagnosis']}: {d['suggestion']}" for d in diagnoses)
1708
+
1709
+ fix_prompt = f"The '{name}' Docker service crashed (exit code {svc.get('exit_code', 1)}).\n"
1710
+ if diag_text:
1711
+ fix_prompt += f"\nDiagnosis:\n{diag_text}\n"
1712
+ fix_prompt += f"\nService logs:\n{svc_logs[:2000]}\n"
1713
+ fix_prompt += "\nFix the issue and ensure the service starts correctly."
1714
+
1715
+ svc_health["fix_attempts"] += 1
1716
+ svc_health["fix_timestamps"] = recent_fixes + [now]
1717
+ svc_health["fix_status"] = "fixing"
1718
+
1719
+ # Trigger fix asynchronously
1720
+ asyncio.ensure_future(self._auto_fix_service(session_id, name, fix_prompt))
1721
+ else:
1722
+ svc_health["fix_status"] = "circuit_breaker_open"
1723
+
1724
+ info["docker_service_health"][name] = svc_health
1725
+
1726
+ # Broadcast service health via WebSocket
1727
+ try:
1728
+ await _broadcast({
1729
+ "type": "service_health",
1730
+ "data": {
1731
+ "session_id": session_id,
1732
+ "services": list(info["docker_service_health"].values()),
1733
+ }
1734
+ })
1735
+ except Exception:
1736
+ pass
1737
+
1738
+ async def _auto_fix_service(self, session_id: str, service_name: str, fix_prompt: str) -> None:
1739
+ """Run targeted fix for a specific Docker service."""
1740
+ info = self.servers.get(session_id)
1741
+ if not info:
1742
+ return
1743
+ project_dir = info.get("project_dir", ".")
1744
+ loki = _find_loki_cli()
1745
+ if not loki:
1746
+ return
1747
+
1748
+ try:
1749
+ proc = await asyncio.to_thread(
1750
+ subprocess.run,
1751
+ [loki, "quick", fix_prompt],
1752
+ capture_output=True, text=True, cwd=project_dir, timeout=300,
1753
+ env={**os.environ, **_load_secrets()}
1754
+ )
1755
+
1756
+ # After fix, restart the specific service
1757
+ await asyncio.to_thread(
1758
+ subprocess.run,
1759
+ ["docker", "compose", "restart", service_name],
1760
+ capture_output=True, cwd=project_dir, timeout=30
1761
+ )
1762
+
1763
+ svc_health = info.get("docker_service_health", {}).get(service_name, {})
1764
+ svc_health["fix_status"] = "fixed" if proc.returncode == 0 else "fix_failed"
1765
+ except Exception as exc:
1766
+ logger.error("Auto-fix for service '%s' failed: %s", service_name, exc)
1767
+ svc_health = info.get("docker_service_health", {}).get(service_name, {})
1768
+ svc_health["fix_status"] = "fix_failed"
1769
+
1607
1770
  async def stop_all(self) -> None:
1608
1771
  """Stop all dev servers (used on shutdown)."""
1609
1772
  for sid in list(self.servers.keys()):
@@ -1613,6 +1776,135 @@ class DevServerManager:
1613
1776
  dev_server_manager = DevServerManager()
1614
1777
 
1615
1778
 
1779
+ # ---------------------------------------------------------------------------
1780
+ # Docker context gathering and error diagnosis
1781
+ # ---------------------------------------------------------------------------
1782
+
1783
+
1784
+ async def _gather_docker_context(project_dir: Path) -> dict:
1785
+ """Gather Docker Compose service status, logs, and project context."""
1786
+ loop = asyncio.get_running_loop()
1787
+ result: dict = {"service_status": [], "failing_services": [], "service_logs": {},
1788
+ "project_structure": "", "env_keys": []}
1789
+
1790
+ # Get service status via docker compose ps
1791
+ try:
1792
+ ps_proc = await loop.run_in_executor(None, lambda: subprocess.run(
1793
+ ["docker", "compose", "ps", "--format", "json"],
1794
+ capture_output=True, text=True, cwd=str(project_dir), timeout=10
1795
+ ))
1796
+ if ps_proc.returncode == 0:
1797
+ for line in ps_proc.stdout.strip().split("\n"):
1798
+ if line.strip():
1799
+ try:
1800
+ svc = json.loads(line)
1801
+ status_entry = {
1802
+ "name": svc.get("Service", svc.get("Name", "unknown")),
1803
+ "state": svc.get("State", "unknown"),
1804
+ "status": svc.get("Status", ""),
1805
+ "exit_code": svc.get("ExitCode", 0),
1806
+ }
1807
+ result["service_status"].append(status_entry)
1808
+ if status_entry["state"] in ("exited", "dead", "restarting"):
1809
+ result["failing_services"].append(status_entry["name"])
1810
+ except json.JSONDecodeError:
1811
+ pass
1812
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
1813
+ pass
1814
+
1815
+ # Get logs for failing services
1816
+ for svc_name in result["failing_services"]:
1817
+ try:
1818
+ log_proc = await loop.run_in_executor(None, lambda sn=svc_name: subprocess.run(
1819
+ ["docker", "compose", "logs", "--tail", "50", sn],
1820
+ capture_output=True, text=True, cwd=str(project_dir), timeout=15
1821
+ ))
1822
+ if log_proc.stdout:
1823
+ result["service_logs"][svc_name] = log_proc.stdout[-3000:] # Cap at 3KB
1824
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
1825
+ pass
1826
+
1827
+ # If no specific failures, get combined logs tail
1828
+ if not result["failing_services"]:
1829
+ try:
1830
+ log_proc = await loop.run_in_executor(None, lambda: subprocess.run(
1831
+ ["docker", "compose", "logs", "--tail", "30"],
1832
+ capture_output=True, text=True, cwd=str(project_dir), timeout=15
1833
+ ))
1834
+ if log_proc.stdout:
1835
+ result["service_logs"]["_combined"] = log_proc.stdout[-3000:]
1836
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
1837
+ pass
1838
+
1839
+ # Project structure
1840
+ try:
1841
+ ls_proc = await loop.run_in_executor(None, lambda: subprocess.run(
1842
+ ["find", ".", "-maxdepth", "2", "-type", "f",
1843
+ "-name", "*.py", "-o", "-name", "*.ts",
1844
+ "-o", "-name", "*.tsx", "-o", "-name", "*.js", "-o", "-name", "package.json",
1845
+ "-o", "-name", "requirements.txt", "-o", "-name", "Dockerfile",
1846
+ "-o", "-name", "docker-compose.yml", "-o", "-name", "*.env"],
1847
+ capture_output=True, text=True, cwd=str(project_dir), timeout=5
1848
+ ))
1849
+ result["project_structure"] = ls_proc.stdout[:2000] if ls_proc.stdout else ""
1850
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
1851
+ pass
1852
+
1853
+ # Env variable names (not values)
1854
+ env_file = project_dir / ".env"
1855
+ if env_file.exists():
1856
+ try:
1857
+ for line in env_file.read_text(errors="replace").split("\n"):
1858
+ line = line.strip()
1859
+ if line and not line.startswith("#") and "=" in line:
1860
+ result["env_keys"].append(line.split("=", 1)[0])
1861
+ except OSError:
1862
+ pass
1863
+
1864
+ return result
1865
+
1866
+
1867
+ def _diagnose_errors(logs: str) -> list[dict]:
1868
+ """Pattern-match common errors in Docker/service logs and return diagnoses."""
1869
+ diagnoses: list[dict] = []
1870
+ patterns = [
1871
+ (r"ModuleNotFoundError: No module named ['\"](\w+)['\"]",
1872
+ lambda m: {"pattern": "missing_python_dep", "diagnosis": f"Missing Python dependency: {m.group(1)}",
1873
+ "suggestion": f"Add '{m.group(1)}' to requirements.txt and rebuild"}),
1874
+ (r"Cannot find module ['\"]([^'\"]+)['\"]|ERR_MODULE_NOT_FOUND",
1875
+ lambda m: {"pattern": "missing_node_dep", "diagnosis": "Missing Node.js module",
1876
+ "suggestion": "Run 'npm install' in the service directory"}),
1877
+ (r"ECONNREFUSED.*:(\d+)|connection refused.*:(\d+)",
1878
+ lambda m: {"pattern": "connection_refused",
1879
+ "diagnosis": f"Connection refused on port {m.group(1) or m.group(2)}",
1880
+ "suggestion": "A dependent service may not be ready. Add retry logic or health check wait."}),
1881
+ (r"address already in use|EADDRINUSE",
1882
+ lambda m: {"pattern": "port_conflict", "diagnosis": "Port already in use",
1883
+ "suggestion": "Another process is using the port. Change the port or stop the conflicting process."}),
1884
+ (r"SyntaxError: (.+)",
1885
+ lambda m: {"pattern": "syntax_error", "diagnosis": f"Syntax error: {m.group(1)[:100]}",
1886
+ "suggestion": "Fix the syntax error in the indicated file and line."}),
1887
+ (r"FATAL:.*password authentication failed",
1888
+ lambda m: {"pattern": "db_auth", "diagnosis": "Database authentication failed",
1889
+ "suggestion": "Check DATABASE_URL credentials match the postgres service environment."}),
1890
+ (r"error.*returned a non-zero code: (\d+)",
1891
+ lambda m: {"pattern": "build_failure", "diagnosis": f"Docker build failed (exit code {m.group(1)})",
1892
+ "suggestion": "Check the Dockerfile for errors. Common: missing system dependencies."}),
1893
+ (r"npm ERR!|npm error",
1894
+ lambda m: {"pattern": "npm_error", "diagnosis": "npm encountered an error",
1895
+ "suggestion": "Check package.json for invalid dependencies or run 'npm install' manually."}),
1896
+ ]
1897
+ seen: set[str] = set()
1898
+ for pattern, handler in patterns:
1899
+ for match in re.finditer(pattern, logs, re.IGNORECASE):
1900
+ diag = handler(match)
1901
+ key = diag["pattern"]
1902
+ if key not in seen:
1903
+ seen.add(key)
1904
+ diagnoses.append(diag)
1905
+ return diagnoses
1906
+
1907
+
1616
1908
  # ---------------------------------------------------------------------------
1617
1909
  # Helpers
1618
1910
  # ---------------------------------------------------------------------------
@@ -3135,6 +3427,25 @@ async def chat_session(session_id: str, req: ChatRequest) -> JSONResponse:
3135
3427
  "\n\nDEV SERVER ERROR (fix this):\n" + "\n".join(error_lines)
3136
3428
  )
3137
3429
 
3430
+ # Phase 1.5: Inject Docker Compose context
3431
+ ds_info_docker = dev_server_manager.servers.get(session_id)
3432
+ if ds_info_docker and ds_info_docker.get("framework") == "docker":
3433
+ try:
3434
+ docker_ctx = await _gather_docker_context(target)
3435
+ if docker_ctx.get("failing_services"):
3436
+ context_parts.append("\n\nDOCKER SERVICE STATUS:\n" + json.dumps(docker_ctx["service_status"], indent=2))
3437
+ for svc_name, svc_logs in docker_ctx.get("service_logs", {}).items():
3438
+ if svc_name != "_combined":
3439
+ context_parts.append(f"\n\nFAILING SERVICE '{svc_name}' LOGS:\n{svc_logs}")
3440
+ diagnoses = _diagnose_errors("\n".join(docker_ctx.get("service_logs", {}).values()))
3441
+ if diagnoses:
3442
+ context_parts.append("\n\nAUTO-DIAGNOSIS:\n" + "\n".join(
3443
+ f"- {d['diagnosis']}: {d['suggestion']}" for d in diagnoses))
3444
+ if docker_ctx.get("project_structure"):
3445
+ context_parts.append("\n\nPROJECT FILES:\n" + docker_ctx["project_structure"])
3446
+ except Exception:
3447
+ logger.debug("Docker context gathering failed", exc_info=True)
3448
+
3138
3449
  # Inject quality gate failures if any
3139
3450
  gate_file = target / ".loki" / "quality" / "gate-failures.txt"
3140
3451
  if gate_file.exists():
@@ -3653,22 +3964,29 @@ async def get_preview_info(session_id: str) -> JSONResponse:
3653
3964
  info["type"] = "docker"
3654
3965
  info["dev_command"] = f"docker compose -f {compose_file} up --build"
3655
3966
  info["description"] = "Containerized app -- runs via Docker Compose"
3656
- # Try to detect port from compose file
3967
+ # Use smart service resolution to find the primary port
3657
3968
  compose_port = 3000
3969
+ compose_services: list[dict] = []
3970
+ primary_service_name: Optional[str] = None
3658
3971
  try:
3659
3972
  import yaml
3660
3973
  with open(project_root / compose_file) as f:
3661
3974
  compose_data = yaml.safe_load(f)
3662
3975
  if compose_data and "services" in compose_data:
3663
- for svc in compose_data["services"].values():
3664
- ports = svc.get("ports", [])
3665
- for p in ports:
3976
+ for svc_name, svc in compose_data["services"].items():
3977
+ svc_ports: list[int] = []
3978
+ for p in svc.get("ports", []):
3666
3979
  p_str = str(p)
3667
3980
  if ":" in p_str:
3668
- compose_port = int(p_str.split(":")[0])
3669
- break
3670
- if compose_port != 3000:
3671
- break
3981
+ parts = p_str.split(":")
3982
+ svc_ports.append(int(parts[-2]))
3983
+ compose_services.append({
3984
+ "name": svc_name,
3985
+ "ports": svc_ports,
3986
+ "image": svc.get("image"),
3987
+ "has_build": "build" in svc,
3988
+ })
3989
+ primary_service_name, compose_port = dev_server_manager._resolve_primary_service(compose_services)
3672
3990
  except ImportError:
3673
3991
  try:
3674
3992
  content = (project_root / compose_file).read_text()
@@ -3680,6 +3998,10 @@ async def get_preview_info(session_id: str) -> JSONResponse:
3680
3998
  except Exception:
3681
3999
  pass
3682
4000
  info["port"] = compose_port
4001
+ if primary_service_name:
4002
+ info["primary_service"] = primary_service_name
4003
+ if compose_services:
4004
+ info["services"] = compose_services
3683
4005
  elif is_expo:
3684
4006
  info["type"] = "expo"
3685
4007
  info["port"] = 8081
@@ -3897,6 +4219,139 @@ async def get_devserver_status(session_id: str) -> JSONResponse:
3897
4219
  return JSONResponse(content=result)
3898
4220
 
3899
4221
 
4222
+ @app.get("/api/sessions/{session_id}/services")
4223
+ async def get_session_services(session_id: str) -> JSONResponse:
4224
+ """Get Docker Compose service list with primary detection."""
4225
+ if not re.match(r"^[a-zA-Z0-9._-]+$", session_id):
4226
+ return JSONResponse(status_code=400, content={"error": "Invalid session ID"})
4227
+ target = _find_session_dir(session_id)
4228
+ if target is None:
4229
+ return JSONResponse(status_code=404, content={"error": "Session not found"})
4230
+
4231
+ # Check if dev server info has cached services
4232
+ ds_info = dev_server_manager.servers.get(session_id)
4233
+ if ds_info and ds_info.get("docker_service_health"):
4234
+ return JSONResponse(content={
4235
+ "services": list(ds_info["docker_service_health"].values()),
4236
+ "framework": ds_info.get("framework"),
4237
+ })
4238
+
4239
+ # Parse compose file directly
4240
+ services_info: list[dict] = []
4241
+ for compose_file in ("docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"):
4242
+ if (target / compose_file).exists():
4243
+ try:
4244
+ import yaml
4245
+ with open(target / compose_file) as f:
4246
+ compose_data = yaml.safe_load(f)
4247
+ if compose_data and "services" in compose_data:
4248
+ for svc_name, svc in compose_data["services"].items():
4249
+ svc_ports: list[int] = []
4250
+ for p in svc.get("ports", []):
4251
+ p_str = str(p)
4252
+ if ":" in p_str:
4253
+ parts = p_str.split(":")
4254
+ svc_ports.append(int(parts[-2]))
4255
+ services_info.append({
4256
+ "name": svc_name,
4257
+ "ports": svc_ports,
4258
+ "image": svc.get("image"),
4259
+ "has_build": "build" in svc,
4260
+ })
4261
+ except (ImportError, Exception):
4262
+ pass
4263
+ break
4264
+
4265
+ primary_name, primary_port = dev_server_manager._resolve_primary_service(services_info)
4266
+ for svc in services_info:
4267
+ svc["is_primary"] = (svc["name"] == primary_name)
4268
+
4269
+ return JSONResponse(content={
4270
+ "services": services_info,
4271
+ "primary_service": primary_name,
4272
+ "primary_port": primary_port,
4273
+ })
4274
+
4275
+
4276
+ @app.get("/api/sessions/{session_id}/devserver/logs")
4277
+ async def get_devserver_logs(session_id: str, service: Optional[str] = None, tail: int = 50) -> JSONResponse:
4278
+ """Get Docker service logs (optionally filtered to one service)."""
4279
+ if not re.match(r"^[a-zA-Z0-9._-]+$", session_id):
4280
+ return JSONResponse(status_code=400, content={"error": "Invalid session ID"})
4281
+ target = _find_session_dir(session_id)
4282
+ if target is None:
4283
+ return JSONResponse(status_code=404, content={"error": "Session not found"})
4284
+
4285
+ ds_info = dev_server_manager.servers.get(session_id)
4286
+ if not ds_info:
4287
+ return JSONResponse(status_code=404, content={"error": "No dev server running"})
4288
+
4289
+ # If not a Docker project, return buffered output lines
4290
+ if ds_info.get("framework") != "docker":
4291
+ return JSONResponse(content={
4292
+ "logs": ds_info.get("output_lines", [])[-tail:],
4293
+ "service": None,
4294
+ })
4295
+
4296
+ # Docker project: use docker compose logs
4297
+ project_dir = ds_info.get("project_dir", str(target))
4298
+ tail = min(tail, 200) # Cap at 200 lines
4299
+ try:
4300
+ cmd = ["docker", "compose", "logs", "--tail", str(tail)]
4301
+ if service:
4302
+ # Validate service name to prevent injection
4303
+ if not re.match(r"^[a-zA-Z0-9._-]+$", service):
4304
+ return JSONResponse(status_code=400, content={"error": "Invalid service name"})
4305
+ cmd.append(service)
4306
+ loop = asyncio.get_running_loop()
4307
+ log_proc = await loop.run_in_executor(None, lambda: subprocess.run(
4308
+ cmd, capture_output=True, text=True, cwd=project_dir, timeout=15
4309
+ ))
4310
+ logs_text = log_proc.stdout or ""
4311
+ # Run diagnosis on the logs
4312
+ diagnoses = _diagnose_errors(logs_text)
4313
+ return JSONResponse(content={
4314
+ "logs": logs_text[-5000:],
4315
+ "service": service,
4316
+ "diagnoses": diagnoses,
4317
+ })
4318
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as exc:
4319
+ return JSONResponse(status_code=500, content={"error": f"Failed to get logs: {exc}"})
4320
+
4321
+
4322
+ @app.post("/api/sessions/{session_id}/devserver/restart-service")
4323
+ async def restart_service(session_id: str, req: dict = Body(...)) -> JSONResponse:
4324
+ """Restart a specific Docker Compose service."""
4325
+ if not re.match(r"^[a-zA-Z0-9._-]+$", session_id):
4326
+ return JSONResponse(status_code=400, content={"error": "Invalid session ID"})
4327
+ ds_info = dev_server_manager.servers.get(session_id)
4328
+ if not ds_info:
4329
+ return JSONResponse(status_code=404, content={"error": "No dev server running"})
4330
+ if ds_info.get("framework") != "docker":
4331
+ return JSONResponse(status_code=400, content={"error": "Not a Docker project"})
4332
+
4333
+ service_name = req.get("service")
4334
+ if not service_name or not re.match(r"^[a-zA-Z0-9._-]+$", service_name):
4335
+ return JSONResponse(status_code=400, content={"error": "Invalid or missing service name"})
4336
+
4337
+ project_dir = ds_info.get("project_dir", ".")
4338
+ try:
4339
+ loop = asyncio.get_running_loop()
4340
+ result = await loop.run_in_executor(None, lambda: subprocess.run(
4341
+ ["docker", "compose", "restart", service_name],
4342
+ capture_output=True, text=True, cwd=project_dir, timeout=30
4343
+ ))
4344
+ if result.returncode == 0:
4345
+ return JSONResponse(content={"status": "restarted", "service": service_name})
4346
+ else:
4347
+ return JSONResponse(status_code=500, content={
4348
+ "error": f"Restart failed: {result.stderr or result.stdout}",
4349
+ "service": service_name,
4350
+ })
4351
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as exc:
4352
+ return JSONResponse(status_code=500, content={"error": f"Restart failed: {exc}"})
4353
+
4354
+
3900
4355
  # ---------------------------------------------------------------------------
3901
4356
  # HTTP Proxy for dev server preview
3902
4357
  # ---------------------------------------------------------------------------
@@ -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:Inter,system-ui,-apple-system,sans-serif;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,Cascadia Code,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}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.terminal-scroll::-webkit-scrollbar{width:6px}.terminal-scroll::-webkit-scrollbar-track{background:transparent}.terminal-scroll::-webkit-scrollbar-thumb{background:#553de933;border-radius:3px}.terminal-scroll::-webkit-scrollbar-thumb:hover{background:#553de959}.\!container{width:100%!important}.container{width:100%}@media(min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media(min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media(min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media(min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.card{background:#fff;border:1px solid #ECEAE3;border-radius:5px;box-shadow:0 1px 3px #0000000f}.card:hover,.card-hover:hover{box-shadow:0 5px 10px #00000014}.pattern-nodes{position:absolute;top:0;right:0;bottom:0;left:0;opacity:.04;pointer-events:none;background-image:radial-gradient(circle at 10% 20%,#553DE9 1px,transparent 1px),radial-gradient(circle at 30% 60%,#553DE9 1px,transparent 1px),radial-gradient(circle at 50% 40%,#553DE9 1px,transparent 1px),radial-gradient(circle at 70% 80%,#553DE9 1px,transparent 1px),radial-gradient(circle at 90% 30%,#553DE9 1px,transparent 1px);background-size:200px 200px}*:focus-visible{outline:2px solid #553DE9;outline-offset:2px}::-moz-selection{background:#e8e4fd;color:#201515}::selection{background:#e8e4fd;color:#201515}@media(prefers-reduced-motion:reduce){.phase-active{animation:none}}@keyframes phase-pulse{0%,to{opacity:1}50%{opacity:.5}}.phase-active{animation:phase-pulse 2s ease-in-out infinite}@keyframes cursor-blink{0%,to{opacity:1}50%{opacity:0}}.terminal-cursor:after{content:"";display:inline-block;width:8px;height:16px;background:#553de9;animation:cursor-blink 1s step-end infinite;margin-left:2px;vertical-align:text-bottom}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.\!visible{visibility:visible!important}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.bottom-6{bottom:1.5rem}.bottom-full{bottom:100%}.left-0{left:0}.left-3{left:.75rem}.right-0{right:0}.right-2\.5{right:.625rem}.right-6{right:1.5rem}.top-1\/2{top:50%}.top-2\.5{top:.625rem}.top-8{top:2rem}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.col-span-3{grid-column:span 3 / span 3}.col-span-4{grid-column:span 4 / span 4}.col-span-5{grid-column:span 5 / span 5}.col-span-6{grid-column:span 6 / span 6}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-10{margin-bottom:2.5rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-0\.5{margin-left:.125rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-28{height:7rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-32{max-height:8rem}.max-h-40{max-height:10rem}.max-h-64{max-height:16rem}.max-h-\[400px\]{max-height:400px}.min-h-0{min-height:0px}.min-h-\[120px\]{min-height:120px}.min-h-\[280px\]{min-height:280px}.min-h-\[60vh\]{min-height:60vh}.min-h-screen{min-height:100vh}.w-1{width:.25rem}.w-1\.5{width:.375rem}.w-1\/2{width:50%}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-28{width:7rem}.w-3{width:.75rem}.w-4{width:1rem}.w-44{width:11rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[160px\]{min-width:160px}.max-w-3xl{max-width:48rem}.max-w-\[1400px\]{max-width:1400px}.max-w-\[1920px\]{max-width:1920px}.max-w-\[200px\]{max-width:200px}.max-w-\[220px\]{max-width:220px}.max-w-\[80\%\]{max-width:80%}.max-w-\[800px\]{max-width:800px}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes ping{75%,to{transform:scale(2);opacity:0}}.animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-col-resize{cursor:col-resize}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-row-resize{cursor:row-resize}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0px}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.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-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * 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-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-\[3px\]{border-radius:3px}.rounded-\[5px\]{border-radius:5px}.rounded-btn{border-radius:4px}.rounded-card{border-radius:5px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-t-card{border-top-left-radius:5px;border-top-right-radius:5px}.border{border-width:1px}.border-0{border-width:0px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-\[\#553DE9\]{--tw-border-opacity: 1;border-color:rgb(85 61 233 / var(--tw-border-opacity, 1))}.border-\[\#553DE9\]\/30{border-color:#553de94d}.border-\[\#C45B5B\]\/20{border-color:#c45b5b33}.border-\[\#ECEAE3\],.border-border{--tw-border-opacity: 1;border-color:rgb(236 234 227 / var(--tw-border-opacity, 1))}.border-border-light{--tw-border-opacity: 1;border-color:rgb(197 192 177 / var(--tw-border-opacity, 1))}.border-border\/50{border-color:#eceae380}.border-danger\/10{border-color:#c45b5b1a}.border-danger\/20{border-color:#c45b5b33}.border-danger\/30{border-color:#c45b5b4d}.border-danger\/40{border-color:#c45b5b66}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity, 1))}.border-muted\/20{border-color:#93908433}.border-primary{--tw-border-opacity: 1;border-color:rgb(85 61 233 / var(--tw-border-opacity, 1))}.border-primary\/20{border-color:#553de933}.border-primary\/30{border-color:#553de94d}.border-primary\/40{border-color:#553de966}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-success\/20{border-color:#1fc5a833}.border-transparent{border-color:transparent}.border-warning\/10{border-color:#d4a03c1a}.border-warning\/20{border-color:#d4a03c33}.border-warning\/30{border-color:#d4a03c4d}.border-warning\/40{border-color:#d4a03c66}.border-yellow-200{--tw-border-opacity: 1;border-color:rgb(254 240 138 / var(--tw-border-opacity, 1))}.border-t-transparent{border-top-color:transparent}.bg-\[\#1FC5A8\]{--tw-bg-opacity: 1;background-color:rgb(31 197 168 / var(--tw-bg-opacity, 1))}.bg-\[\#1FC5A8\]\/10{background-color:#1fc5a81a}.bg-\[\#24292f\]{--tw-bg-opacity: 1;background-color:rgb(36 41 47 / var(--tw-bg-opacity, 1))}.bg-\[\#36342E\]{--tw-bg-opacity: 1;background-color:rgb(54 52 46 / var(--tw-bg-opacity, 1))}.bg-\[\#553DE9\]{--tw-bg-opacity: 1;background-color:rgb(85 61 233 / var(--tw-bg-opacity, 1))}.bg-\[\#553DE9\]\/10{background-color:#553de91a}.bg-\[\#C45B5B\]{--tw-bg-opacity: 1;background-color:rgb(196 91 91 / var(--tw-bg-opacity, 1))}.bg-\[\#C45B5B\]\/10{background-color:#c45b5b1a}.bg-\[\#D4A03C\]\/10{background-color:#d4a03c1a}.bg-\[\#ECEAE3\]\/60{background-color:#eceae399}.bg-\[\#F8F4F0\]{--tw-bg-opacity: 1;background-color:rgb(248 244 240 / var(--tw-bg-opacity, 1))}.bg-\[\#FAF9F6\]{--tw-bg-opacity: 1;background-color:rgb(250 249 246 / var(--tw-bg-opacity, 1))}.bg-background{--tw-bg-opacity: 1;background-color:rgb(255 254 251 / var(--tw-bg-opacity, 1))}.bg-black\/30{background-color:#0000004d}.bg-black\/40{background-color:#0006}.bg-black\/5{background-color:#0000000d}.bg-border{--tw-bg-opacity: 1;background-color:rgb(236 234 227 / var(--tw-bg-opacity, 1))}.bg-border\/30{background-color:#eceae34d}.bg-card{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-card\/50{background-color:#ffffff80}.bg-current{background-color:currentColor}.bg-danger{--tw-bg-opacity: 1;background-color:rgb(196 91 91 / var(--tw-bg-opacity, 1))}.bg-danger\/10{background-color:#c45b5b1a}.bg-danger\/5{background-color:#c45b5b0d}.bg-gray-400{--tw-bg-opacity: 1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-hover{--tw-bg-opacity: 1;background-color:rgb(248 244 240 / var(--tw-bg-opacity, 1))}.bg-info{--tw-bg-opacity: 1;background-color:rgb(47 113 227 / var(--tw-bg-opacity, 1))}.bg-ink\/20{background-color:#20151533}.bg-ink\/30{background-color:#2015154d}.bg-ink\/\[0\.03\]{background-color:#20151508}.bg-muted{--tw-bg-opacity: 1;background-color:rgb(147 144 132 / var(--tw-bg-opacity, 1))}.bg-muted\/10{background-color:#9390841a}.bg-muted\/30{background-color:#9390844d}.bg-muted\/40{background-color:#93908466}.bg-primary{--tw-bg-opacity: 1;background-color:rgb(85 61 233 / var(--tw-bg-opacity, 1))}.bg-primary\/10{background-color:#553de91a}.bg-primary\/5{background-color:#553de90d}.bg-primary\/60{background-color:#553de999}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-success{--tw-bg-opacity: 1;background-color:rgb(31 197 168 / var(--tw-bg-opacity, 1))}.bg-success\/10{background-color:#1fc5a81a}.bg-transparent{background-color:transparent}.bg-warning{--tw-bg-opacity: 1;background-color:rgb(212 160 60 / var(--tw-bg-opacity, 1))}.bg-warning\/10{background-color:#d4a03c1a}.bg-warning\/5{background-color:#d4a03c0d}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity, 1))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.fill-ink{fill:#201515}.fill-primary{fill:#553de9}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.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-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-20{padding-top:5rem;padding-bottom:5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pb-2\.5{padding-bottom:.625rem}.pb-4{padding-bottom:1rem}.pl-3{padding-left:.75rem}.pl-9{padding-left:2.25rem}.pr-2{padding-right:.5rem}.pr-3{padding-right:.75rem}.pr-6{padding-right:1.5rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-5{padding-top:1.25rem}.pt-\[20vh\]{padding-top:20vh}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-text-bottom{vertical-align:text-bottom}.font-heading{font-family:DM Serif Display,Georgia,Times New Roman,serif}.font-mono{font-family:JetBrains Mono,Fira Code,Cascadia Code,monospace}.font-sans{font-family:Inter,system-ui,-apple-system,sans-serif}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-6xl{font-size:3.75rem;line-height:1}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-h1{font-size:2.5rem;line-height:1;letter-spacing:-.01em}.text-h3{font-size:1.25rem;line-height:1.4}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#1FC5A8\]{--tw-text-opacity: 1;color:rgb(31 197 168 / var(--tw-text-opacity, 1))}.text-\[\#36342E\]{--tw-text-opacity: 1;color:rgb(54 52 46 / var(--tw-text-opacity, 1))}.text-\[\#553DE9\]{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.text-\[\#6B6960\]{--tw-text-opacity: 1;color:rgb(107 105 96 / var(--tw-text-opacity, 1))}.text-\[\#939084\]{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.text-\[\#B8B5AD\]{--tw-text-opacity: 1;color:rgb(184 181 173 / var(--tw-text-opacity, 1))}.text-\[\#C45B5B\]{--tw-text-opacity: 1;color:rgb(196 91 91 / var(--tw-text-opacity, 1))}.text-\[\#D4A03C\]{--tw-text-opacity: 1;color:rgb(212 160 60 / var(--tw-text-opacity, 1))}.text-cyan-600{--tw-text-opacity: 1;color:rgb(8 145 178 / var(--tw-text-opacity, 1))}.text-danger{--tw-text-opacity: 1;color:rgb(196 91 91 / var(--tw-text-opacity, 1))}.text-danger\/70{color:#c45b5bb3}.text-danger\/80{color:#c45b5bcc}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-info{--tw-text-opacity: 1;color:rgb(47 113 227 / var(--tw-text-opacity, 1))}.text-ink{--tw-text-opacity: 1;color:rgb(32 21 21 / var(--tw-text-opacity, 1))}.text-ink\/70{color:#201515b3}.text-muted{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.text-muted-accessible{--tw-text-opacity: 1;color:rgb(107 105 96 / var(--tw-text-opacity, 1))}.text-muted\/30{color:#9390844d}.text-muted\/40{color:#93908466}.text-muted\/50{color:#93908480}.text-muted\/60{color:#93908499}.text-muted\/70{color:#939084b3}.text-orange-500{--tw-text-opacity: 1;color:rgb(249 115 22 / var(--tw-text-opacity, 1))}.text-orange-600{--tw-text-opacity: 1;color:rgb(234 88 12 / var(--tw-text-opacity, 1))}.text-primary{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.text-primary\/20{color:#553de933}.text-primary\/60{color:#553de999}.text-primary\/80{color:#553de9cc}.text-purple-500{--tw-text-opacity: 1;color:rgb(168 85 247 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-secondary{--tw-text-opacity: 1;color:rgb(54 52 46 / var(--tw-text-opacity, 1))}.text-success{--tw-text-opacity: 1;color:rgb(31 197 168 / var(--tw-text-opacity, 1))}.text-warning{--tw-text-opacity: 1;color:rgb(212 160 60 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-white\/60{color:#fff9}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.text-yellow-700{--tw-text-opacity: 1;color:rgb(161 98 7 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-button{--tw-shadow: 0 1px 3px rgba(0,0,0,.08);--tw-shadow-colored: 0 1px 3px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-card{--tw-shadow: 0 1px 3px rgba(0,0,0,.06);--tw-shadow-colored: 0 1px 3px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-card-hover{--tw-shadow: 0 5px 10px rgba(0,0,0,.08);--tw-shadow-colored: 0 5px 10px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[\#553DE9\]\/20{--tw-shadow-color: rgb(85 61 233 / .2);--tw-shadow: var(--tw-shadow-colored)}.shadow-card{--tw-shadow-color: #FFFFFF;--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-\[\#553DE9\]{--tw-ring-opacity: 1;--tw-ring-color: rgb(85 61 233 / var(--tw-ring-opacity, 1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-\[width\]{transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.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}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-500{transition-duration:.5s}.placeholder\:text-\[\#939084\]::-moz-placeholder{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.placeholder\:text-\[\#939084\]::placeholder{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.placeholder\:text-muted::-moz-placeholder{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.placeholder\:text-muted::placeholder{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.placeholder\:text-primary\/60::-moz-placeholder{color:#553de999}.placeholder\:text-primary\/60::placeholder{color:#553de999}.last\:border-b-0:last-child{border-bottom-width:0px}.hover\:border-border:hover{--tw-border-opacity: 1;border-color:rgb(236 234 227 / var(--tw-border-opacity, 1))}.hover\:border-primary\/30:hover{border-color:#553de94d}.hover\:bg-\[\#1b1f23\]:hover{--tw-bg-opacity: 1;background-color:rgb(27 31 35 / var(--tw-bg-opacity, 1))}.hover\:bg-\[\#4432c4\]:hover{--tw-bg-opacity: 1;background-color:rgb(68 50 196 / var(--tw-bg-opacity, 1))}.hover\:bg-\[\#553DE9\]\/5:hover{background-color:#553de90d}.hover\:bg-\[\#553DE9\]\/90:hover{background-color:#553de9e6}.hover\:bg-\[\#C45B5B\]\/20:hover{background-color:#c45b5b33}.hover\:bg-\[\#E8E4FD\]:hover{--tw-bg-opacity: 1;background-color:rgb(232 228 253 / var(--tw-bg-opacity, 1))}.hover\:bg-\[\#ECEAE3\]:hover{--tw-bg-opacity: 1;background-color:rgb(236 234 227 / var(--tw-bg-opacity, 1))}.hover\:bg-\[\#F8F4F0\]:hover{--tw-bg-opacity: 1;background-color:rgb(248 244 240 / var(--tw-bg-opacity, 1))}.hover\:bg-card:hover{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.hover\:bg-danger\/10:hover{background-color:#c45b5b1a}.hover\:bg-danger\/20:hover{background-color:#c45b5b33}.hover\:bg-hover:hover{--tw-bg-opacity: 1;background-color:rgb(248 244 240 / var(--tw-bg-opacity, 1))}.hover\:bg-primary-hover:hover{--tw-bg-opacity: 1;background-color:rgb(68 50 196 / var(--tw-bg-opacity, 1))}.hover\:bg-primary\/20:hover{background-color:#553de933}.hover\:bg-primary\/30:hover{background-color:#553de94d}.hover\:bg-primary\/5:hover{background-color:#553de90d}.hover\:bg-primary\/90:hover{background-color:#553de9e6}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:bg-red-500\/20:hover{background-color:#ef444433}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:bg-warning\/10:hover{background-color:#d4a03c1a}.hover\:bg-warning\/20:hover{background-color:#d4a03c33}.hover\:text-\[\#36342E\]:hover{--tw-text-opacity: 1;color:rgb(54 52 46 / var(--tw-text-opacity, 1))}.hover\:text-\[\#553DE9\]:hover{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.hover\:text-danger:hover{--tw-text-opacity: 1;color:rgb(196 91 91 / var(--tw-text-opacity, 1))}.hover\:text-ink:hover{--tw-text-opacity: 1;color:rgb(32 21 21 / var(--tw-text-opacity, 1))}.hover\:text-primary:hover{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.hover\:text-primary\/80:hover{color:#553de9cc}.hover\:text-red-300:hover{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-card-hover:hover{--tw-shadow: 0 5px 10px rgba(0,0,0,.08);--tw-shadow-colored: 0 5px 10px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:not-sr-only:focus{position:static;width:auto;height:auto;padding:0;margin:0;overflow:visible;clip:auto;white-space:normal}.focus\:absolute:focus{position:absolute}.focus\:left-2:focus{left:.5rem}.focus\:top-2:focus{top:.5rem}.focus\:z-50:focus{z-index:50}.focus\:rounded-\[3px\]:focus{border-radius:3px}.focus\:border-\[\#553DE9\]:focus{--tw-border-opacity: 1;border-color:rgb(85 61 233 / var(--tw-border-opacity, 1))}.focus\:border-primary:focus{--tw-border-opacity: 1;border-color:rgb(85 61 233 / var(--tw-border-opacity, 1))}.focus\:border-primary\/30:focus{border-color:#553de94d}.focus\:bg-white:focus{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.focus\:px-4:focus{padding-left:1rem;padding-right:1rem}.focus\:py-2:focus{padding-top:.5rem;padding-bottom:.5rem}.focus\:text-\[\#553DE9\]:focus{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.focus\:shadow-card:focus{--tw-shadow: 0 1px 3px rgba(0,0,0,.06);--tw-shadow-colored: 0 1px 3px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);--tw-shadow-color: #FFFFFF;--tw-shadow: var(--tw-shadow-colored)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-\[\#553DE9\]\/20:focus{--tw-ring-color: rgb(85 61 233 / .2)}.focus\:ring-primary\/20:focus{--tw-ring-color: rgb(85 61 233 / .2)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-primary{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.group\/file:hover .group-hover\/file\:opacity-100{opacity:1}@media(prefers-reduced-motion:reduce){.motion-reduce\:animate-none{animation:none}}@media(min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}