ltcai 0.1.9 → 0.1.16

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 (43) hide show
  1. package/README.md +174 -305
  2. package/docs/CHANGELOG.md +307 -0
  3. package/docs/architecture.md +121 -0
  4. package/docs/mcp-tools.md +116 -0
  5. package/docs/privacy.md +74 -0
  6. package/docs/public-deploy.md +137 -0
  7. package/docs/security-model.md +121 -0
  8. package/knowledge_graph.py +123 -15
  9. package/llm_router.py +100 -28
  10. package/ltcai_cli.py +138 -5
  11. package/package.json +14 -2
  12. package/server.py +1756 -329
  13. package/skills/SKILL_TEMPLATE.md +61 -29
  14. package/skills/code_review/SKILL.md +28 -0
  15. package/skills/code_review/examples.md +59 -0
  16. package/skills/code_review/risk.json +9 -0
  17. package/skills/code_review/schema.json +65 -0
  18. package/skills/data_analysis/SKILL.md +28 -0
  19. package/skills/data_analysis/examples.md +62 -0
  20. package/skills/data_analysis/risk.json +9 -0
  21. package/skills/data_analysis/schema.json +61 -0
  22. package/skills/file_edit/SKILL.md +33 -0
  23. package/skills/file_edit/examples.md +45 -0
  24. package/skills/file_edit/risk.json +9 -0
  25. package/skills/file_edit/schema.json +60 -0
  26. package/skills/summarize_document/SKILL.md +68 -0
  27. package/skills/summarize_document/examples.md +65 -0
  28. package/skills/summarize_document/risk.json +9 -0
  29. package/skills/summarize_document/schema.json +71 -0
  30. package/skills/web_search/SKILL.md +28 -0
  31. package/skills/web_search/examples.md +61 -0
  32. package/skills/web_search/risk.json +9 -0
  33. package/skills/web_search/schema.json +62 -0
  34. package/static/account.html +53 -51
  35. package/static/admin.html +50 -46
  36. package/static/chat.html +124 -96
  37. package/static/graph.html +1231 -337
  38. package/static/manifest.json +2 -2
  39. package/tests/integration/__pycache__/__init__.cpython-314.pyc +0 -0
  40. package/tests/integration/__pycache__/test_api.cpython-314-pytest-9.0.3.pyc +0 -0
  41. package/tests/unit/__pycache__/test_tools.cpython-314-pytest-9.0.3.pyc +0 -0
  42. package/tests/unit/test_tools.py +194 -1
  43. package/tools.py +264 -4
@@ -0,0 +1,68 @@
1
+ # Skill: summarize_document
2
+
3
+ ## 메타데이터
4
+ - **버전**: 0.1.0
5
+ - **카테고리**: document
6
+ - **위험도**: low
7
+ - **필요 권한**: local_read
8
+
9
+ ## 설명
10
+ 텍스트 파일(.txt, .md, .pdf, .docx)을 읽어 핵심 내용을 요약하고, 섹션별 요점과 키워드를 추출한다.
11
+
12
+ ## 거버넌스
13
+
14
+ `risk.json` 참고. 요약: `risk=read`, `destructive=false`, `shell=false`, `network=false`, `auto_approve=true`, `sandbox=home`, `rollback=none`
15
+
16
+ ## 트리거 조건
17
+
18
+ 호출해야 하는 상황:
19
+ - 사용자가 "이 문서 요약해줘", "핵심만 뽑아줘", "이 파일 내용이 뭐야?"라고 요청할 때
20
+ - 에이전트가 Discover 단계에서 긴 문서의 구조를 빠르게 파악해야 할 때
21
+ - 여러 문서를 비교하기 전 각 문서의 핵심 파악이 필요할 때
22
+
23
+ 호출하면 **안** 되는 상황:
24
+ - 문서를 수정해야 할 때 → `edit_file` 사용
25
+ - CSV/Excel 데이터 분석 → `data_analysis` 사용
26
+ - 코드 파일 분석 → `code_review` 사용
27
+
28
+ ## Side Effects
29
+
30
+ | 항목 | 내용 |
31
+ |------|------|
32
+ | 파일 변경 | 없음 |
33
+ | 생성 파일 | 없음 |
34
+ | 프로세스 | 없음 |
35
+ | 네트워크 | 없음 |
36
+
37
+ ## Rollback
38
+
39
+ 없음. 읽기 전용 작업.
40
+
41
+ ## 입력 스키마
42
+ `schema.json` 참고.
43
+
44
+ ## 출력 스키마
45
+ `schema.json` 참고.
46
+
47
+ ## 실행 조건
48
+ - LLM 모델이 로드되어 있어야 함
49
+ - 파일이 존재해야 하며 .txt/.md/.pdf/.docx 형식이어야 함
50
+ - 파일 크기 20 MB 이하
51
+
52
+ ## 예제
53
+ `examples.md` 참고.
54
+
55
+ ## 실패 처리
56
+ | 에러 코드 | 원인 | 처리 방법 |
57
+ |-----------|------|-----------|
58
+ | `INVALID_INPUT` | path 누락 | 파일 경로 입력 |
59
+ | `FILE_NOT_FOUND` | 경로에 파일 없음 | 경로 확인 |
60
+ | `UNSUPPORTED_FORMAT` | 지원하지 않는 형식 | .txt/.md/.pdf/.docx로 변환 후 재시도 |
61
+ | `SIZE_LIMIT` | 20 MB 초과 | 파일 분할 후 재시도 |
62
+ | `MODEL_NOT_LOADED` | LLM 미로드 | `/model` 명령으로 모델 선택 |
63
+
64
+ ## 테스트 케이스
65
+ ```python
66
+ # tests/unit/test_tools.py::test_summarize_document_md
67
+ # tests/unit/test_tools.py::test_summarize_document_unsupported
68
+ ```
@@ -0,0 +1,65 @@
1
+ # summarize_document — Examples
2
+
3
+ ## 1. Markdown file summary (success)
4
+
5
+ **Input**
6
+ ```json
7
+ { "path": "~/project/README.md", "style": "bullet" }
8
+ ```
9
+ **Output**
10
+ ```json
11
+ {
12
+ "success": true,
13
+ "result": {
14
+ "title": "README.md",
15
+ "summary": "• Lattice AI는 Apple Silicon 기반 로컬 AI 에이전트\n• FastAPI 서버 + VS Code 익스텐션 + Telegram bot 구조\n• MLX 및 클라우드 모델(OpenAI/Groq) 지원",
16
+ "keywords": ["Lattice AI", "MLX", "FastAPI", "VS Code", "Telegram", "local LLM"],
17
+ "sections": [
18
+ { "heading": "Installation", "summary": "pip install ltcai 후 ltcai start로 실행" },
19
+ { "heading": "Features", "summary": "채팅, 에이전트 모드, 파일 편집, 웹 검색 등 지원" }
20
+ ],
21
+ "word_count": 1240
22
+ }
23
+ }
24
+ ```
25
+
26
+ ## 2. PDF summary with focus section (success)
27
+
28
+ **Input**
29
+ ```json
30
+ { "path": "~/docs/report.pdf", "style": "paragraph", "focus_sections": ["결론", "권고사항"], "max_length": 300 }
31
+ ```
32
+ **Output**
33
+ ```json
34
+ {
35
+ "success": true,
36
+ "result": {
37
+ "title": "report.pdf",
38
+ "summary": "보고서의 결론: 시스템 성능이 전분기 대비 23% 향상되었으며, 추가 최적화를 위해 캐시 레이어 도입이 권고됩니다.",
39
+ "keywords": ["성능", "최적화", "캐시", "권고"],
40
+ "word_count": 8500
41
+ }
42
+ }
43
+ ```
44
+
45
+ ## 3. Unsupported format (failure)
46
+
47
+ **Input**
48
+ ```json
49
+ { "path": "~/data/sales.csv" }
50
+ ```
51
+ **Output**
52
+ ```json
53
+ { "success": false, "error": "UNSUPPORTED_FORMAT", "message": "Supported formats: txt, md, pdf, docx. Use data_analysis for CSV files." }
54
+ ```
55
+
56
+ ## 4. File not found (failure)
57
+
58
+ **Input**
59
+ ```json
60
+ { "path": "~/docs/nonexistent.md" }
61
+ ```
62
+ **Output**
63
+ ```json
64
+ { "success": false, "error": "FILE_NOT_FOUND", "message": "No such file: /home/user/docs/nonexistent.md" }
65
+ ```
@@ -0,0 +1,9 @@
1
+ {
2
+ "risk": "read",
3
+ "destructive": false,
4
+ "shell": false,
5
+ "network": false,
6
+ "auto_approve": true,
7
+ "sandbox": "home",
8
+ "rollback": "none"
9
+ }
@@ -0,0 +1,71 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "summarize_document",
4
+ "input": {
5
+ "required": ["path"],
6
+ "optional": ["max_length", "style", "focus_sections"],
7
+ "properties": {
8
+ "path": { "type": "string", "description": "절대 경로 또는 ~/로 시작하는 경로 (.txt, .md, .pdf, .docx)" },
9
+ "max_length": { "type": "integer", "default": 500, "description": "요약 최대 글자 수" },
10
+ "style": { "type": "string", "enum": ["bullet","paragraph","table"], "default": "bullet", "description": "출력 형식" },
11
+ "focus_sections": { "type": "array", "items": { "type": "string" }, "description": "특정 섹션만 요약할 때 섹션 제목 목록" }
12
+ }
13
+ },
14
+ "output": {
15
+ "oneOf": [
16
+ {
17
+ "title": "success",
18
+ "properties": {
19
+ "success": { "const": true },
20
+ "result": {
21
+ "properties": {
22
+ "title": { "type": "string", "description": "문서 제목 또는 파일명" },
23
+ "summary": { "type": "string", "description": "핵심 요약" },
24
+ "keywords": { "type": "array", "items": { "type": "string" }, "description": "주요 키워드 (최대 10개)" },
25
+ "sections": {
26
+ "type": "array",
27
+ "items": {
28
+ "properties": {
29
+ "heading": { "type": "string" },
30
+ "summary": { "type": "string" }
31
+ },
32
+ "required": ["heading","summary"]
33
+ },
34
+ "description": "섹션별 요약 (있는 경우)"
35
+ },
36
+ "word_count": { "type": "integer" }
37
+ },
38
+ "required": ["title","summary","keywords"]
39
+ }
40
+ },
41
+ "required": ["success","result"]
42
+ },
43
+ {
44
+ "title": "failure",
45
+ "properties": {
46
+ "success": { "const": false },
47
+ "error": { "type": "string", "enum": ["INVALID_INPUT","FILE_NOT_FOUND","UNSUPPORTED_FORMAT","SIZE_LIMIT","MODEL_NOT_LOADED"] },
48
+ "message": { "type": "string" }
49
+ },
50
+ "required": ["success","error","message"]
51
+ }
52
+ ]
53
+ },
54
+ "evals": [
55
+ {
56
+ "id": "markdown_summary",
57
+ "input": { "path": "__TEST__/README.md", "style": "bullet" },
58
+ "pass_criteria": "success == true and len(keywords) >= 1"
59
+ },
60
+ {
61
+ "id": "unsupported_format",
62
+ "input": { "path": "__TEST__/data.csv" },
63
+ "pass_criteria": "error == UNSUPPORTED_FORMAT"
64
+ },
65
+ {
66
+ "id": "missing_path",
67
+ "input": {},
68
+ "pass_criteria": "error == INVALID_INPUT"
69
+ }
70
+ ]
71
+ }
@@ -9,6 +9,34 @@
9
9
  ## 설명
10
10
  외부 검색 엔진(DuckDuckGo/Brave)을 통해 웹 검색을 수행하고 상위 결과를 반환한다.
11
11
 
12
+ ## 거버넌스
13
+
14
+ `policies/policy.md` 참고. 요약: `risk=read`, `destructive=false`, `shell=false`, `network=true`, `auto_approve=true`, `sandbox=system`, `rollback=none`
15
+
16
+ ## 트리거 조건
17
+
18
+ 호출해야 하는 상황:
19
+ - 사용자가 "검색해줘", "찾아봐줘", "최신 정보 알려줘"라고 요청할 때
20
+ - 에이전트가 Discover 단계에서 외부 문서/API/라이브러리 정보를 수집해야 할 때
21
+ - LLM의 학습 데이터 이후 최신 정보(라이브러리 버전, 뉴스 등)가 필요할 때
22
+
23
+ 호출하면 **안** 되는 상황:
24
+ - 로컬 파일 내용 검색 시 → `grep` 사용
25
+ - LLM이 이미 알고 있는 일반 지식 질문 시
26
+
27
+ ## Side Effects
28
+
29
+ | 항목 | 내용 |
30
+ |------|------|
31
+ | 파일 변경 | 없음 |
32
+ | 생성 파일 | 없음 |
33
+ | 프로세스 | 없음 |
34
+ | 네트워크 | 외부 검색 API로 검색어 전송 (DuckDuckGo/Brave) |
35
+
36
+ ## Rollback
37
+
38
+ 없음. 읽기 전용 네트워크 요청.
39
+
12
40
  ## 입력 스키마
13
41
  ```json
14
42
  {
@@ -0,0 +1,61 @@
1
+ # web_search — Examples
2
+
3
+ ## 1. Basic search (success)
4
+
5
+ **Input**
6
+ ```json
7
+ { "query": "FastAPI 비동기 처리", "num_results": 3 }
8
+ ```
9
+ **Output**
10
+ ```json
11
+ {
12
+ "success": true,
13
+ "result": {
14
+ "query": "FastAPI 비동기 처리",
15
+ "results": [
16
+ { "title": "FastAPI - Async", "url": "https://fastapi.tiangolo.com/async/", "snippet": "FastAPI는 Python의 asyncio를 완벽히 지원합니다..." }
17
+ ]
18
+ }
19
+ }
20
+ ```
21
+
22
+ ## 2. Search with language (success)
23
+
24
+ **Input**
25
+ ```json
26
+ { "query": "Python type hints best practices", "num_results": 5, "lang": "en-US" }
27
+ ```
28
+ **Output**
29
+ ```json
30
+ {
31
+ "success": true,
32
+ "result": {
33
+ "query": "Python type hints best practices",
34
+ "results": [
35
+ { "title": "PEP 484 – Type Hints", "url": "https://peps.python.org/pep-0484/", "snippet": "This PEP introduces a standard syntax for type annotations..." }
36
+ ]
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## 3. Empty query (failure)
42
+
43
+ **Input**
44
+ ```json
45
+ { "query": "" }
46
+ ```
47
+ **Output**
48
+ ```json
49
+ { "success": false, "error": "INVALID_INPUT", "message": "query is required" }
50
+ ```
51
+
52
+ ## 4. Network error (failure)
53
+
54
+ **Input**
55
+ ```json
56
+ { "query": "offline test" }
57
+ ```
58
+ **Output**
59
+ ```json
60
+ { "success": false, "error": "NETWORK_ERROR", "message": "외부 검색 API에 연결할 수 없습니다. 잠시 후 재시도하세요." }
61
+ ```
@@ -0,0 +1,9 @@
1
+ {
2
+ "risk": "read",
3
+ "destructive": false,
4
+ "shell": false,
5
+ "network": true,
6
+ "auto_approve": true,
7
+ "sandbox": "system",
8
+ "rollback": "none"
9
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "web_search",
4
+ "input": {
5
+ "required": ["query"],
6
+ "optional": ["num_results", "lang"],
7
+ "properties": {
8
+ "query": { "type": "string", "description": "검색어" },
9
+ "num_results": { "type": "integer", "default": 5, "minimum": 1, "maximum": 20 },
10
+ "lang": { "type": "string", "default": "ko-KR" }
11
+ }
12
+ },
13
+ "output": {
14
+ "oneOf": [
15
+ {
16
+ "title": "success",
17
+ "properties": {
18
+ "success": { "const": true },
19
+ "result": {
20
+ "properties": {
21
+ "query": { "type": "string" },
22
+ "results": {
23
+ "type": "array",
24
+ "items": {
25
+ "properties": {
26
+ "title": { "type": "string" },
27
+ "url": { "type": "string", "format": "uri" },
28
+ "snippet": { "type": "string" }
29
+ },
30
+ "required": ["title","url","snippet"]
31
+ }
32
+ }
33
+ },
34
+ "required": ["query","results"]
35
+ }
36
+ },
37
+ "required": ["success","result"]
38
+ },
39
+ {
40
+ "title": "failure",
41
+ "properties": {
42
+ "success": { "const": false },
43
+ "error": { "type": "string", "enum": ["INVALID_INPUT","NETWORK_ERROR","TIMEOUT"] },
44
+ "message": { "type": "string" }
45
+ },
46
+ "required": ["success","error","message"]
47
+ }
48
+ ]
49
+ },
50
+ "evals": [
51
+ {
52
+ "id": "basic_search",
53
+ "input": { "query": "FastAPI async", "num_results": 3 },
54
+ "pass_criteria": "success == true and len(results) >= 1"
55
+ },
56
+ {
57
+ "id": "empty_query",
58
+ "input": { "query": "" },
59
+ "pass_criteria": "error == INVALID_INPUT"
60
+ }
61
+ ]
62
+ }
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6
6
  <title>Lattice AI</title>
7
7
  <link rel="manifest" href="/manifest.json">
8
- <meta name="theme-color" content="#2d5a3d">
8
+ <meta name="theme-color" content="#282a36">
9
9
  <meta name="apple-mobile-web-app-capable" content="yes">
10
10
  <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
11
11
  <link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png">
@@ -14,11 +14,13 @@
14
14
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
15
15
 
16
16
  :root {
17
- --bg: #080c0a;
18
- --text: #f3f1e8;
19
- --faint: #7c8078;
20
- --accent: #22d3a0;
21
- --shadow: 0 24px 70px rgba(0,0,0,0.5);
17
+ --bg: #282a36;
18
+ --text: #f7f7f2;
19
+ --faint: #8d93ab;
20
+ --muted: #c4c8d8;
21
+ --accent: #a77cff;
22
+ --accent-2: #20b8aa;
23
+ --shadow: 0 24px 70px rgba(5,7,12,0.46);
22
24
  }
23
25
 
24
26
  * { box-sizing: border-box; margin: 0; padding: 0; }
@@ -27,8 +29,9 @@
27
29
  font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
28
30
  color: var(--text);
29
31
  background:
30
- linear-gradient(180deg, rgba(255,255,255,0.012), transparent 28%),
31
- linear-gradient(135deg, #080c0a 0%, #060a08 52%, #0a0d09 100%);
32
+ radial-gradient(circle at 50% 35%, rgba(167,124,255,0.16), transparent 34%),
33
+ radial-gradient(circle at 80% 75%, rgba(32,184,170,0.10), transparent 28%),
34
+ linear-gradient(135deg, #292b38 0%, #242632 52%, #2f3040 100%);
32
35
  display: flex;
33
36
  align-items: center;
34
37
  justify-content: center;
@@ -43,9 +46,11 @@
43
46
  position: fixed;
44
47
  inset: 0;
45
48
  background:
46
- linear-gradient(rgba(255,255,255,0.022) 1px, transparent 1px),
47
- linear-gradient(90deg, rgba(255,255,255,0.016) 1px, transparent 1px);
48
- background-size: 44px 44px;
49
+ radial-gradient(circle, rgba(247,247,242,0.66) 1px, transparent 1.8px),
50
+ linear-gradient(rgba(157,177,255,0.12) 1px, transparent 1px),
51
+ linear-gradient(90deg, rgba(157,177,255,0.08) 1px, transparent 1px);
52
+ background-size: 82px 82px, 46px 46px, 46px 46px;
53
+ background-position: 16px 18px, 0 0, 0 0;
49
54
  mask-image: linear-gradient(180deg, rgba(0,0,0,0.35), rgba(0,0,0,0.06));
50
55
  pointer-events: none;
51
56
  }
@@ -55,27 +60,24 @@
55
60
  position: fixed;
56
61
  inset: 0;
57
62
  background:
58
- radial-gradient(ellipse 70% 60% at 18% 18%, rgba(34,211,160,0.16) 0%, transparent 55%),
59
- radial-gradient(ellipse 55% 50% at 82% 82%, rgba(129,140,248,0.13) 0%, transparent 55%);
63
+ linear-gradient(116deg, transparent 0 44%, rgba(125,183,255,0.08) 44.1%, transparent 44.3% 100%),
64
+ linear-gradient(28deg, transparent 0 62%, rgba(167,124,255,0.08) 62.1%, transparent 62.3% 100%);
60
65
  pointer-events: none;
61
66
  }
62
67
 
63
68
  .orb {
64
- position: fixed;
65
- border-radius: 50%;
66
- filter: blur(70px);
67
- pointer-events: none;
69
+ display: none;
68
70
  }
69
71
  .orb-1 { width: 440px; height: 440px; top: -170px; left: -120px; background: rgba(34,211,160,0.13); }
70
72
  .orb-2 { width: 380px; height: 380px; bottom: -130px; right: -90px; background: rgba(129,140,248,0.11); }
71
73
 
72
74
  .card {
73
75
  width: min(400px, 100%);
74
- background: rgba(10,14,12,0.88);
75
- border: 1px solid rgba(255,255,255,0.08);
76
- border-radius: 22px;
76
+ background: rgba(34,36,49,0.86);
77
+ border: 1px solid rgba(218,225,255,0.14);
78
+ border-radius: 10px;
77
79
  padding: 38px 34px;
78
- box-shadow: var(--shadow), 0 0 0 1px rgba(34,211,160,0.05);
80
+ box-shadow: var(--shadow), 0 0 0 1px rgba(167,124,255,0.06);
79
81
  position: relative;
80
82
  z-index: 1;
81
83
  backdrop-filter: blur(28px);
@@ -87,17 +89,17 @@
87
89
  top: 0; left: 50%;
88
90
  transform: translateX(-50%);
89
91
  width: 55%; height: 1px;
90
- background: linear-gradient(90deg, transparent, rgba(34,211,160,0.55), transparent);
92
+ background: linear-gradient(90deg, transparent, rgba(167,124,255,0.55), rgba(32,184,170,0.5), transparent);
91
93
  }
92
94
 
93
95
  .logo {
94
96
  width: 54px; height: 54px;
95
- background: linear-gradient(135deg, #22d3a0, #818cf8);
96
- border-radius: 15px;
97
+ background: radial-gradient(circle at 34% 30%, #ffffff 0 16%, #a77cff 17% 62%, #5b6cff 100%);
98
+ border-radius: 10px;
97
99
  display: flex; align-items: center; justify-content: center;
98
100
  font-size: 26px; color: #040706;
99
101
  margin: 0 auto 18px;
100
- box-shadow: 0 0 32px rgba(34,211,160,0.32), 0 8px 24px rgba(0,0,0,0.4);
102
+ box-shadow: 0 0 32px rgba(167,124,255,0.28), 0 8px 24px rgba(0,0,0,0.38);
101
103
  }
102
104
 
103
105
  .title {
@@ -105,7 +107,7 @@
105
107
  font-size: 23px;
106
108
  font-weight: 800;
107
109
  margin-bottom: 6px;
108
- background: linear-gradient(135deg, #fff 40%, rgba(34,211,160,0.9));
110
+ background: linear-gradient(135deg, #fff 40%, #cfc6ff 76%, #8be9df);
109
111
  -webkit-background-clip: text;
110
112
  -webkit-text-fill-color: transparent;
111
113
  background-clip: text;
@@ -113,7 +115,7 @@
113
115
 
114
116
  .subtitle {
115
117
  text-align: center;
116
- color: var(--faint);
118
+ color: var(--muted);
117
119
  font-size: 12.5px;
118
120
  margin-bottom: 26px;
119
121
  line-height: 1.5;
@@ -123,39 +125,39 @@
123
125
  width: 100%;
124
126
  padding: 12px 14px;
125
127
  margin-bottom: 10px;
126
- background: rgba(255,255,255,0.04);
127
- border: 1px solid rgba(255,255,255,0.08);
128
+ background: rgba(20,22,31,0.74);
129
+ border: 1px solid rgba(218,225,255,0.14);
128
130
  color: var(--text);
129
- border-radius: 10px;
131
+ border-radius: 8px;
130
132
  outline: none;
131
133
  font-size: 14px;
132
134
  font-family: inherit;
133
135
  transition: border-color .15s, box-shadow .15s;
134
136
  }
135
137
  .input:focus {
136
- border-color: rgba(34,211,160,0.5);
137
- box-shadow: 0 0 0 3px rgba(34,211,160,0.08);
138
+ border-color: rgba(167,124,255,0.5);
139
+ box-shadow: 0 0 0 3px rgba(167,124,255,0.09);
138
140
  }
139
141
  .input::placeholder { color: var(--faint); }
140
142
 
141
143
  .submit {
142
144
  width: 100%;
143
145
  padding: 13px;
144
- background: linear-gradient(135deg, #22d3a0, #1ab88c);
145
- color: #030d09;
146
+ background: linear-gradient(135deg, #f7f7f2, #cfc6ff);
147
+ color: #242632;
146
148
  border: none;
147
- border-radius: 10px;
149
+ border-radius: 8px;
148
150
  cursor: pointer;
149
151
  font-weight: 800;
150
152
  font-size: 14px;
151
153
  font-family: inherit;
152
- box-shadow: 0 0 20px rgba(34,211,160,0.28), 0 4px 12px rgba(0,0,0,0.3);
154
+ box-shadow: 0 0 22px rgba(167,124,255,0.18), 0 4px 12px rgba(0,0,0,0.3);
153
155
  transition: all .18s;
154
156
  margin-top: 4px;
155
157
  }
156
158
  .submit:hover {
157
- background: linear-gradient(135deg, #2de8b0, #22d3a0);
158
- box-shadow: 0 0 30px rgba(34,211,160,0.38), 0 4px 14px rgba(0,0,0,0.3);
159
+ background: linear-gradient(135deg, #ffffff, #d9d1ff);
160
+ box-shadow: 0 0 30px rgba(167,124,255,0.26), 0 4px 14px rgba(0,0,0,0.3);
159
161
  transform: translateY(-1px);
160
162
  }
161
163
  .submit:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
@@ -166,7 +168,7 @@
166
168
  font-size: 12.5px;
167
169
  color: var(--faint);
168
170
  }
169
- .switch a { color: var(--accent); text-decoration: none; font-weight: 700; }
171
+ .switch a { color: #cfc6ff; text-decoration: none; font-weight: 700; }
170
172
  .switch a:hover { text-decoration: underline; }
171
173
 
172
174
  .sso-divider {
@@ -181,10 +183,10 @@
181
183
  .sso-btn {
182
184
  width: 100%;
183
185
  padding: 12px;
184
- background: rgba(255,255,255,0.04);
185
- border: 1px solid rgba(255,255,255,0.1);
186
+ background: rgba(247,247,242,0.045);
187
+ border: 1px solid rgba(218,225,255,0.13);
186
188
  color: var(--text);
187
- border-radius: 10px;
189
+ border-radius: 8px;
188
190
  cursor: pointer;
189
191
  font-weight: 600;
190
192
  font-size: 13.5px;
@@ -193,8 +195,8 @@
193
195
  transition: all .18s;
194
196
  }
195
197
  .sso-btn:hover {
196
- background: rgba(255,255,255,0.08);
197
- border-color: rgba(34,211,160,0.3);
198
+ background: rgba(167,124,255,0.10);
199
+ border-color: rgba(167,124,255,0.3);
198
200
  }
199
201
 
200
202
  .msg {
@@ -214,8 +216,8 @@
214
216
  z-index: 10;
215
217
  }
216
218
  .lang-btn {
217
- background: rgba(255,255,255,0.05);
218
- border: 1px solid rgba(255,255,255,0.1);
219
+ background: rgba(34,36,49,0.72);
220
+ border: 1px solid rgba(218,225,255,0.14);
219
221
  color: var(--text);
220
222
  border-radius: 8px;
221
223
  padding: 7px 12px;
@@ -224,15 +226,15 @@
224
226
  cursor: pointer;
225
227
  transition: background .15s;
226
228
  }
227
- .lang-btn:hover { background: rgba(255,255,255,0.1); }
229
+ .lang-btn:hover { background: rgba(167,124,255,0.12); }
228
230
  .lang-menu {
229
231
  display: none;
230
232
  position: absolute;
231
233
  top: calc(100% + 6px);
232
234
  right: 0;
233
- background: #141a16;
234
- border: 1px solid rgba(255,255,255,0.1);
235
- border-radius: 10px;
235
+ background: #222431;
236
+ border: 1px solid rgba(218,225,255,0.14);
237
+ border-radius: 8px;
236
238
  padding: 4px;
237
239
  min-width: 130px;
238
240
  box-shadow: 0 8px 24px rgba(0,0,0,0.4);
@@ -245,7 +247,7 @@
245
247
  font-size: 13px;
246
248
  color: var(--faint);
247
249
  }
248
- .lang-opt:hover { background: rgba(255,255,255,0.06); color: var(--text); }
250
+ .lang-opt:hover { background: rgba(167,124,255,0.10); color: var(--text); }
249
251
  .lang-opt.active { color: var(--accent); font-weight: 700; }
250
252
  </style>
251
253
  </head>