adaptive-memory-multi-model-router 2.15.4 → 2.15.5

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 (173) hide show
  1. package/.github/workflows/adapters-ci.yml +142 -0
  2. package/.github/workflows/ci.yml +2 -5
  3. package/.github/workflows/pypi-publish.yml +102 -0
  4. package/AGENT_COUNCIL_FINDINGS.md +123 -98
  5. package/CAMPAIGN_SUMMARY.md +87 -0
  6. package/CHANGELOG.md +7 -1
  7. package/ENTERPRISE_INTEGRATIONS.md +94 -0
  8. package/INTEGRATION_PROGRESS.md +96 -0
  9. package/OPPORTUNITIES_100.md +277 -0
  10. package/POPULARITY_BOOSTERS.md +1 -1
  11. package/README.md +137 -142
  12. package/README_ja.md +17 -21
  13. package/README_zh.md +13 -17
  14. package/TODO_BROWSER_AUTOMATION.md +87 -0
  15. package/a3m-integrations-summary.md +73 -0
  16. package/adapters/a3m_adapter/__init__.py +39 -3
  17. package/adapters/a3m_adapter/adapter/__init__.py +16 -1
  18. package/adapters/a3m_adapter/adapter/autogen.py +169 -0
  19. package/adapters/a3m_adapter/adapter/haystack.py +197 -0
  20. package/adapters/a3m_adapter/adapter/langgraph.py +196 -0
  21. package/adapters/a3m_adapter/adapter/pinecone.py +217 -0
  22. package/adapters/a3m_adapter/adapter/vercel.py +188 -0
  23. package/adapters/a3m_adapter/tests/test_adapters.py +29 -31
  24. package/adapters/a3m_adapter/tests/test_integration.py +80 -0
  25. package/adapters/requirements-dev.txt +6 -0
  26. package/adapters/requirements.txt +4 -0
  27. package/apps/cost-calculator/README.md +72 -0
  28. package/apps/cost-calculator/calculator.css +280 -0
  29. package/apps/cost-calculator/calculator.js +150 -0
  30. package/apps/cost-calculator/index.html +321 -0
  31. package/apps/cost-calculator/package.json +13 -0
  32. package/articles/ANNOUNCEMENT_reddit_ml.md +76 -0
  33. package/articles/ANNOUNCEMENT_vc/347/244/276/345/214/272.md +71 -0
  34. package/articles/ANNOUNCEMENT_vercel.md +85 -0
  35. package/articles/CHINESE_DIRECTORIES.md +2 -2
  36. package/articles/CHINESE_SUBMISSIONS_READY.md +5 -5
  37. package/articles/HN_POST_READY.md +16 -20
  38. package/articles/INDIEHACKERS_READY.md +19 -34
  39. package/demo.py +251 -0
  40. package/dist/analytics/costAnalytics.d.ts +0 -1
  41. package/dist/benchmark/reproducible.d.ts +0 -1
  42. package/dist/cache/semanticCache.d.ts +0 -1
  43. package/dist/cost/budgetEnforcer.d.ts +0 -1
  44. package/dist/cost/costTracker.d.ts +0 -1
  45. package/dist/ensemble/multiRoundDialog.d.ts +0 -1
  46. package/dist/ensemble/shapleyValue.d.ts +0 -1
  47. package/dist/ensemble.d.ts +0 -1
  48. package/dist/index.d.ts +0 -1
  49. package/dist/integrations/oauth.d.ts +0 -1
  50. package/dist/integrations/scienceAdapter.d.ts +0 -1
  51. package/dist/memory/autoFetch.d.ts +0 -1
  52. package/dist/memory/hybridMemory.d.ts +0 -1
  53. package/dist/memory/memoryTree.d.ts +0 -1
  54. package/dist/memory/obsidianVault.d.ts +0 -1
  55. package/dist/memory/reasoningBank.d.ts +0 -1
  56. package/dist/observability/changeWatch.d.ts +0 -1
  57. package/dist/observability/fatigueDetector.d.ts +0 -1
  58. package/dist/observability/index.d.ts +0 -1
  59. package/dist/observability/metrics.d.ts +0 -1
  60. package/dist/observability/middleware.d.ts +0 -1
  61. package/dist/observability/tracer.d.ts +0 -1
  62. package/dist/observability/types.d.ts +0 -1
  63. package/dist/providers/providerConfig.d.ts +5 -2
  64. package/dist/providers/providerConfig.js +1006 -1
  65. package/dist/providers/providerConfig.js.map +1 -1
  66. package/dist/routing/advancedRouter.d.ts +1 -2
  67. package/dist/routing/advancedRouter.js +84 -0
  68. package/dist/routing/advancedRouter.js.map +1 -1
  69. package/dist/routing/crossModelValidation.d.ts +0 -1
  70. package/dist/routing/providerHealth.d.ts +77 -2
  71. package/dist/routing/providerHealth.js +190 -4
  72. package/dist/routing/providerHealth.js.map +1 -1
  73. package/dist/routing/providerRetry.d.ts +0 -1
  74. package/dist/routing/shadowSampler.d.ts +187 -0
  75. package/dist/routing/shadowSampler.js +306 -0
  76. package/dist/routing/shadowSampler.js.map +1 -0
  77. package/dist/sdk.d.ts +0 -1
  78. package/dist/security/guardrails.d.ts +0 -1
  79. package/dist/server/dashboard.d.ts +0 -1
  80. package/dist/server/handlers/chatHandler.d.ts +0 -1
  81. package/dist/server/handlers/completionsHandler.d.ts +0 -1
  82. package/dist/server/handlers/embeddingsHandler.d.ts +0 -1
  83. package/dist/server/handlers/healthHandler.d.ts +0 -1
  84. package/dist/server/handlers/metricsHandler.d.ts +0 -1
  85. package/dist/server/handlers/modelsHandler.d.ts +0 -1
  86. package/dist/server/metrics.d.ts +0 -1
  87. package/dist/server/modelMapper.d.ts +0 -1
  88. package/dist/server/proxyServer.d.ts +0 -1
  89. package/dist/server/router.d.ts +0 -1
  90. package/dist/server/state.d.ts +0 -1
  91. package/dist/tui/dashboard.d.ts +0 -1
  92. package/dist/utils/costUtils.d.ts +0 -1
  93. package/dist/utils/sorting.d.ts +0 -1
  94. package/dist/utils/tokenUtils.d.ts +0 -1
  95. package/docker-compose.yml +84 -60
  96. package/docs/comparison.md +2 -2
  97. package/docs/llms-full.txt +89 -0
  98. package/docs/llms.txt +25 -67
  99. package/docs-site/index.html +9 -9
  100. package/integrations/browser-use/README.md +155 -0
  101. package/integrations/browser-use/examples/form-filling.ts +179 -0
  102. package/integrations/browser-use/index.ts +145 -0
  103. package/integrations/browser-use/package.json +34 -0
  104. package/integrations/browser-use/tsconfig.json +17 -0
  105. package/integrations/mcp-browser/README.md +177 -0
  106. package/integrations/mcp-browser/package.json +38 -0
  107. package/integrations/mcp-browser/src/index.ts +297 -0
  108. package/integrations/sota-browser/README.md +191 -0
  109. package/integrations/sota-browser/package.json +34 -0
  110. package/llms-full.txt +6 -6
  111. package/llms.txt +25 -67
  112. package/package.json +34 -5
  113. package/packages/a3m-vercel-ai/README.md +161 -0
  114. package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts +12 -0
  115. package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts.map +1 -0
  116. package/packages/a3m-vercel-ai/dist/a3m-language-model.js +289 -0
  117. package/packages/a3m-vercel-ai/dist/a3m-language-model.js.map +1 -0
  118. package/packages/a3m-vercel-ai/dist/index.d.ts +82 -0
  119. package/packages/a3m-vercel-ai/dist/index.d.ts.map +1 -0
  120. package/packages/a3m-vercel-ai/dist/index.js +79 -0
  121. package/packages/a3m-vercel-ai/dist/index.js.map +1 -0
  122. package/packages/a3m-vercel-ai/dist/types.d.ts +97 -0
  123. package/packages/a3m-vercel-ai/dist/types.d.ts.map +1 -0
  124. package/packages/a3m-vercel-ai/dist/types.js +5 -0
  125. package/packages/a3m-vercel-ai/dist/types.js.map +1 -0
  126. package/packages/a3m-vercel-ai/package-lock.json +969 -0
  127. package/packages/a3m-vercel-ai/package.json +46 -0
  128. package/packages/a3m-vercel-ai/src/a3m-language-model.ts +381 -0
  129. package/packages/a3m-vercel-ai/src/index.ts +104 -0
  130. package/packages/a3m-vercel-ai/src/types.ts +116 -0
  131. package/packages/a3m-vercel-ai/tsconfig.json +20 -0
  132. package/packages/agentkit-adapter/LICENSE +21 -0
  133. package/packages/agentkit-adapter/README.md +126 -0
  134. package/packages/agentkit-adapter/examples/agentkit-example.ts +139 -0
  135. package/packages/agentkit-adapter/package.json +57 -0
  136. package/packages/agentkit-adapter/src/adapter.ts +381 -0
  137. package/packages/agentkit-adapter/src/index.ts +36 -0
  138. package/packages/agentkit-adapter/src/types.ts +105 -0
  139. package/packages/agentkit-adapter/src/util.ts +13 -0
  140. package/packages/agentkit-adapter/tsconfig.json +22 -0
  141. package/prometheus.yml +8 -0
  142. package/python/README.md +92 -65
  143. package/python/a3m/__init__.py +32 -3
  144. package/python/a3m/adapters/__init__.py +21 -0
  145. package/python/a3m/adapters/langchain.py +190 -0
  146. package/python/a3m/adapters/llamaindex.py +249 -0
  147. package/python/a3m/adapters/qdrant.py +240 -0
  148. package/python/a3m/adapters/weaviate.py +263 -0
  149. package/python/a3m/client.py +5 -0
  150. package/python/a3m_router.egg-info/PKG-INFO +172 -0
  151. package/python/a3m_router.egg-info/SOURCES.txt +17 -0
  152. package/python/a3m_router.egg-info/dependency_links.txt +1 -0
  153. package/python/a3m_router.egg-info/requires.txt +24 -0
  154. package/python/a3m_router.egg-info/top_level.txt +1 -0
  155. package/python/build_verify.sh +32 -0
  156. package/python/dist/a3m_router-2.2.1-py3-none-any.whl +0 -0
  157. package/python/dist/a3m_router-2.2.1.tar.gz +0 -0
  158. package/python/dist/a3m_router-2.2.2-py3-none-any.whl +0 -0
  159. package/python/dist/a3m_router-2.2.2.tar.gz +0 -0
  160. package/python/mcp-server/README.md +172 -0
  161. package/python/mcp-server/a3m_mcp/__init__.py +15 -0
  162. package/python/mcp-server/a3m_mcp/__main__.py +15 -0
  163. package/python/mcp-server/a3m_mcp/server.py +339 -0
  164. package/python/mcp-server/pyproject.toml +24 -0
  165. package/python/pyproject.toml +56 -5
  166. package/python/setup.py +3 -28
  167. package/src/providers/providerConfig.ts +1053 -1
  168. package/summary.txt +38 -0
  169. package/tsconfig.build.json +1 -2
  170. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -94
  171. package/.github/ISSUE_TEMPLATE/config.yml +0 -17
  172. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -71
  173. package/.well-known/ai-plugin.json +0 -16
@@ -18,73 +18,75 @@ sys.path.insert(0, '.')
18
18
  logging.basicConfig(level=logging.INFO)
19
19
  logger = logging.getLogger(__name__)
20
20
 
21
+
21
22
  def test_langchain_adapter():
22
23
  """Test LangChain adapter."""
23
24
  print("Testing LangChain adapter...")
24
25
 
25
26
  try:
26
- from a3m_llm_adapter import A3MChatModel
27
+ from a3m_adapter import A3MLangChainAdapter
27
28
 
28
29
  # Initialize
29
- llm = A3MChatModel(model="auto", temperature=0.7)
30
- print(f"��✅ Initialized: {llm}")
30
+ llm = A3MLangChainAdapter(model="auto", temperature=0.7)
31
+ print(f" Initialized: {llm}")
31
32
 
32
33
  # Test simple generation
33
34
  # Note: This would make actual API calls - we'll skip for now
34
35
  # In a real test, we'd mock the A3M router
35
- print("��✅ LangChain adapter structure OK")
36
- return True
36
+ print(" LangChain adapter structure OK")
37
+ assert llm is not None
37
38
 
38
39
  except Exception as e:
39
- print(f"��❌ LangChain adapter failed: {e}")
40
- return False
40
+ print(f" LangChain adapter failed: {e}")
41
+ raise
42
+
41
43
 
42
44
  def test_llamaindex_adapter():
43
45
  """Test LlamaIndex adapter."""
44
46
  print("Testing LlamaIndex adapter...")
45
47
 
46
48
  try:
47
- from a3m_llama_index_adapter import A3MLlamaIndexLLM
49
+ from a3m_adapter import A3MLlamaIndexAdapter
48
50
 
49
51
  # Initialize
50
- llm = A3MLlamaIndexLLM(model="auto", temperature=0.5)
51
- print(f"��✅ Initialized: {llm}")
52
+ llm = A3MLlamaIndexAdapter(model="auto", temperature=0.5)
53
+ print(f" Initialized: {llm}")
52
54
 
53
55
  # Check metadata
54
56
  metadata = llm.metadata
55
- print(f"��✅ Metadata: {metadata.model_name}, tokens: {metadata.num_output}")
56
- return True
57
+ print(f" Metadata: {metadata}")
58
+ assert metadata is not None
57
59
 
58
60
  except Exception as e:
59
- print(f"��❌ LlamaIndex adapter failed: {e}")
60
- return False
61
+ print(f" LlamaIndex adapter failed: {e}")
62
+ raise
63
+
61
64
 
62
65
  def test_config():
63
66
  """Test configuration."""
64
67
  print("Testing configuration...")
65
68
 
66
69
  try:
67
- from a3m_adapter_config import A3MConfig
70
+ from a3m_adapter import A3MConfig
68
71
 
69
72
  # Test defaults
70
73
  config = A3MConfig()
71
- print(f"��✅ Default config: model={config.model}")
74
+ print(f" Default config: model={config.model}")
72
75
 
73
76
  # Test to_dict
74
77
  data = config.to_dict()
75
78
  assert 'model' in data
76
- print("��✅ Config to_dict works")
79
+ print(" Config to_dict works")
77
80
 
78
81
  # Test JSON serialization
79
82
  json_str = config.to_json()
80
83
  assert '"model"' in json_str
81
- print("��✅ Config JSON serialization works")
82
-
83
- return True
84
+ print(" Config JSON serialization works")
84
85
 
85
86
  except Exception as e:
86
- print(f"��❌ Config test failed: {e}")
87
- return False
87
+ print(f" Config test failed: {e}")
88
+ raise
89
+
88
90
 
89
91
  def main():
90
92
  """Run all tests."""
@@ -102,19 +104,15 @@ def main():
102
104
  total = len(tests)
103
105
 
104
106
  for test in tests:
105
- if test():
106
- passed += 1
107
+ test()
108
+ passed += 1
107
109
  print()
108
110
 
109
111
  print("=" * 50)
110
112
  print(f"Results: {passed}/{total} tests passed")
111
-
112
- if passed == total:
113
- print("���🎉 All tests passed!")
114
- return 0
115
- else:
116
- print("��❌ Some tests failed")
117
- return 1
113
+ print("🎉 All tests passed!")
114
+ return 0
115
+
118
116
 
119
117
  if __name__ == "__main__":
120
118
  sys.exit(main())
@@ -0,0 +1,80 @@
1
+ """
2
+ Integration tests for A3M Router adapters.
3
+ Requires A3M Router server running on localhost:8787
4
+ """
5
+
6
+ import pytest
7
+ import os
8
+
9
+
10
+ @pytest.fixture
11
+ def a3m_server_url():
12
+ """Get A3M Router server URL."""
13
+ return os.environ.get("A3M_SERVER_URL", "http://localhost:8787")
14
+
15
+
16
+ @pytest.fixture
17
+ def skip_if_no_server():
18
+ """Skip test if server is not available."""
19
+ import requests
20
+ try:
21
+ resp = requests.get("http://localhost:8787/health", timeout=2)
22
+ if resp.status_code != 200:
23
+ pytest.skip("A3M Router server not running")
24
+ except:
25
+ pytest.skip("A3M Router server not running")
26
+
27
+
28
+ @pytest.mark.integration
29
+ def test_simple_chat_completion(a3m_server_url, skip_if_no_server):
30
+ """Test simple chat completion via HTTP API."""
31
+ import requests
32
+
33
+ response = requests.post(
34
+ f"{a3m_server_url}/v1/chat/completions",
35
+ json={
36
+ "model": "auto",
37
+ "messages": [{"role": "user", "content": "What is 2+2?"}]
38
+ },
39
+ timeout=30
40
+ )
41
+
42
+ assert response.status_code == 200
43
+ data = response.json()
44
+ assert "choices" in data
45
+ assert len(data["choices"]) > 0
46
+ assert "message" in data["choices"][0]
47
+ assert data["choices"][0]["message"]["content"]
48
+
49
+
50
+ @pytest.mark.integration
51
+ def test_parallel_ensemble(a3m_server_url, skip_if_no_server):
52
+ """Test parallel ensemble with multiple providers."""
53
+ import requests
54
+
55
+ response = requests.post(
56
+ f"{a3m_server_url}/v1/chat/completions",
57
+ json={
58
+ "model": "auto",
59
+ "messages": [{"role": "user", "content": "Explain gravity"}],
60
+ "parallel_ensemble": 3,
61
+ },
62
+ timeout=60
63
+ )
64
+
65
+ assert response.status_code == 200
66
+ data = response.json()
67
+ assert "choices" in data
68
+ assert "provider" in data
69
+
70
+
71
+ @pytest.mark.integration
72
+ def test_health_endpoint(a3m_server_url, skip_if_no_server):
73
+ """Test health endpoint."""
74
+ import requests
75
+
76
+ response = requests.get(f"{a3m_server_url}/health", timeout=10)
77
+
78
+ assert response.status_code == 200
79
+ data = response.json()
80
+ assert "providers" in data or "status" in data
@@ -0,0 +1,6 @@
1
+ -r requirements.txt
2
+ black>=21.12
3
+ isort>=5.10.1
4
+ flake8>=3.9.0
5
+ build>=0.10.0
6
+ twine>=4.0.0
@@ -0,0 +1,4 @@
1
+ requests>=2.25.1
2
+ pydantic>=1.9.0
3
+ pytest>=6.0
4
+ pytest-asyncio>=0.21.0
@@ -0,0 +1,72 @@
1
+ # A3M Router Cost Calculator
2
+
3
+ **Live:** https://a3m.router/calculator
4
+
5
+ Drop-in interactive calculator. No build step. No npm dependencies for the calculator itself.
6
+
7
+ ## What it does
8
+
9
+ - Input: monthly request volume + model mix
10
+ - Output: side-by-side cost comparison (baseline vs A3M routed)
11
+ - Generates shareable URL with encoded config (base64url)
12
+ - Exports comparison as CSV
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ # Open directly
18
+ open apps/cost-calculator/index.html
19
+
20
+ # Or serve locally
21
+ npx serve apps/cost-calculator
22
+ ```
23
+
24
+ ## Embed in any site
25
+
26
+ ```html
27
+ <iframe
28
+ src="https://a3m.router/calculator"
29
+ width="100%"
30
+ height="700"
31
+ frameborder="0"
32
+ style="border-radius: 12px;"
33
+ ></iframe>
34
+ ```
35
+
36
+ ## Architecture
37
+
38
+ ```
39
+ calculator.js — Pure JS, no dependencies. All pricing, routing, and encoding logic.
40
+ calculator.css — Standalone styles. Dark theme, responsive.
41
+ index.html — Self-contained. Vanilla JS. Loads calculator.js.
42
+ package.json — vitest for unit testing the pure JS logic.
43
+ ```
44
+
45
+ ## Testing
46
+
47
+ ```bash
48
+ cd apps/cost-calculator
49
+ npm install
50
+ npm test
51
+ ```
52
+
53
+ ## Models Supported
54
+
55
+ | Premium Model | Routed To | Savings |
56
+ |---------------|-----------|---------|
57
+ | GPT-4o | GPT-4o-mini | ~35% |
58
+ | Claude 3.5 Sonnet | Claude 3 Haiku | ~40% |
59
+ | Gemini 1.5 Pro | Gemini 1.5 Flash | ~45% |
60
+ | Groq Llama 3.3 70B | Groq Mixtral 8x7B | ~30% |
61
+
62
+ ## Pricing Source
63
+
64
+ OpenRouter provider rates (per 1M tokens). Update `PRICING` constant in `calculator.js` to reflect latest rates.
65
+
66
+ ## Next Steps
67
+
68
+ - [ ] Deploy to `a3m.router/calculator` (Vercel static)
69
+ - [ ] Add UTM tracking on share links
70
+ - [ ] Add "embed widget" script for dynamic resize
71
+ - [ ] Write HN + Reddit post with actual example calculations
72
+ - [ ] Add unit tests with vitest
@@ -0,0 +1,280 @@
1
+ /* A3M Router Cost Calculator Styles */
2
+
3
+ :root {
4
+ --a3m-primary: #6366f1;
5
+ --a3m-accent: #818cf8;
6
+ --a3m-bg: #0f0f1a;
7
+ --a3m-surface: #1a1a2e;
8
+ --a3m-border: #2d2d4a;
9
+ --a3m-text: #e2e8f0;
10
+ --a3m-muted: #94a3b8;
11
+ --a3m-green: #22c55e;
12
+ --a3m-red: #ef4444;
13
+ }
14
+
15
+ * { box-sizing: border-box; margin: 0; padding: 0; }
16
+
17
+ body {
18
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
19
+ background: var(--a3m-bg);
20
+ color: var(--a3m-text);
21
+ min-height: 100vh;
22
+ padding: 2rem 1rem;
23
+ }
24
+
25
+ .container {
26
+ max-width: 900px;
27
+ margin: 0 auto;
28
+ }
29
+
30
+ h1 {
31
+ font-size: 1.75rem;
32
+ font-weight: 700;
33
+ text-align: center;
34
+ margin-bottom: 0.5rem;
35
+ }
36
+
37
+ .subtitle {
38
+ text-align: center;
39
+ color: var(--a3m-muted);
40
+ margin-bottom: 2rem;
41
+ }
42
+
43
+ .card {
44
+ background: var(--a3m-surface);
45
+ border: 1px solid var(--a3m-border);
46
+ border-radius: 12px;
47
+ padding: 1.5rem;
48
+ margin-bottom: 1.5rem;
49
+ }
50
+
51
+ .card-title {
52
+ font-size: 0.875rem;
53
+ font-weight: 600;
54
+ text-transform: uppercase;
55
+ letter-spacing: 0.05em;
56
+ color: var(--a3m-muted);
57
+ margin-bottom: 1rem;
58
+ }
59
+
60
+ /* Input grid */
61
+ .input-grid {
62
+ display: grid;
63
+ grid-template-columns: 1fr 1fr;
64
+ gap: 1rem;
65
+ }
66
+
67
+ @media (max-width: 600px) {
68
+ .input-grid { grid-template-columns: 1fr; }
69
+ }
70
+
71
+ .input-group {
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: 0.5rem;
75
+ }
76
+
77
+ .input-group label {
78
+ font-size: 0.875rem;
79
+ font-weight: 500;
80
+ color: var(--a3m-muted);
81
+ }
82
+
83
+ .input-group input,
84
+ .input-group select {
85
+ padding: 0.625rem 0.875rem;
86
+ background: var(--a3m-bg);
87
+ border: 1px solid var(--a3m-border);
88
+ border-radius: 8px;
89
+ color: var(--a3m-text);
90
+ font-size: 1rem;
91
+ width: 100%;
92
+ }
93
+
94
+ .input-group input:focus,
95
+ .input-group select:focus {
96
+ outline: none;
97
+ border-color: var(--a3m-primary);
98
+ }
99
+
100
+ /* Model mix rows */
101
+ .model-mix {
102
+ display: flex;
103
+ flex-direction: column;
104
+ gap: 0.75rem;
105
+ margin-top: 1rem;
106
+ }
107
+
108
+ .model-row {
109
+ display: grid;
110
+ grid-template-columns: 1fr 100px auto;
111
+ gap: 0.5rem;
112
+ align-items: center;
113
+ }
114
+
115
+ .model-row select,
116
+ .model-row input {
117
+ padding: 0.5rem;
118
+ background: var(--a3m-bg);
119
+ border: 1px solid var(--a3m-border);
120
+ border-radius: 6px;
121
+ color: var(--a3m-text);
122
+ font-size: 0.875rem;
123
+ }
124
+
125
+ .model-row span {
126
+ color: var(--a3m-muted);
127
+ font-size: 0.875rem;
128
+ white-space: nowrap;
129
+ }
130
+
131
+ .btn {
132
+ display: inline-flex;
133
+ align-items: center;
134
+ gap: 0.5rem;
135
+ padding: 0.625rem 1.25rem;
136
+ background: var(--a3m-primary);
137
+ color: white;
138
+ border: none;
139
+ border-radius: 8px;
140
+ font-size: 0.875rem;
141
+ font-weight: 600;
142
+ cursor: pointer;
143
+ transition: background 0.2s;
144
+ }
145
+
146
+ .btn:hover { background: var(--a3m-accent); }
147
+ .btn-secondary {
148
+ background: var(--a3m-border);
149
+ color: var(--a3m-text);
150
+ }
151
+ .btn-secondary:hover { background: #3d3d5a; }
152
+
153
+ .btn-row {
154
+ display: flex;
155
+ gap: 0.75rem;
156
+ margin-top: 1.25rem;
157
+ flex-wrap: wrap;
158
+ }
159
+
160
+ /* Results */
161
+ .results-grid {
162
+ display: grid;
163
+ grid-template-columns: repeat(3, 1fr);
164
+ gap: 1rem;
165
+ margin-bottom: 1.5rem;
166
+ }
167
+
168
+ @media (max-width: 600px) {
169
+ .results-grid { grid-template-columns: 1fr; }
170
+ }
171
+
172
+ .result-card {
173
+ background: var(--a3m-bg);
174
+ border: 1px solid var(--a3m-border);
175
+ border-radius: 10px;
176
+ padding: 1.25rem;
177
+ text-align: center;
178
+ }
179
+
180
+ .result-label {
181
+ font-size: 0.75rem;
182
+ font-weight: 600;
183
+ text-transform: uppercase;
184
+ letter-spacing: 0.05em;
185
+ color: var(--a3m-muted);
186
+ margin-bottom: 0.5rem;
187
+ }
188
+
189
+ .result-value {
190
+ font-size: 1.75rem;
191
+ font-weight: 700;
192
+ }
193
+
194
+ .result-value.savings { color: var(--a3m-green); }
195
+ .result-value.baseline { color: var(--a3m-muted); text-decoration: line-through; }
196
+
197
+ /* Breakdown table */
198
+ .breakdown-table {
199
+ width: 100%;
200
+ border-collapse: collapse;
201
+ font-size: 0.875rem;
202
+ margin-top: 1rem;
203
+ }
204
+
205
+ .breakdown-table th {
206
+ text-align: left;
207
+ padding: 0.625rem 0.75rem;
208
+ color: var(--a3m-muted);
209
+ font-weight: 600;
210
+ font-size: 0.75rem;
211
+ text-transform: uppercase;
212
+ border-bottom: 1px solid var(--a3m-border);
213
+ }
214
+
215
+ .breakdown-table td {
216
+ padding: 0.625rem 0.75rem;
217
+ border-bottom: 1px solid var(--a3m-border);
218
+ }
219
+
220
+ .breakdown-table tr:last-child td { border-bottom: none; }
221
+ .breakdown-table .num { text-align: right; font-variant-numeric: tabular-nums; }
222
+ .breakdown-table .savings-cell { color: var(--a3m-green); }
223
+
224
+ /* Share bar */
225
+ .share-bar {
226
+ display: flex;
227
+ gap: 0.5rem;
228
+ align-items: center;
229
+ margin-top: 1rem;
230
+ }
231
+
232
+ .share-bar input {
233
+ flex: 1;
234
+ padding: 0.5rem 0.75rem;
235
+ background: var(--a3m-bg);
236
+ border: 1px solid var(--a3m-border);
237
+ border-radius: 6px;
238
+ color: var(--a3m-text);
239
+ font-size: 0.8rem;
240
+ font-family: monospace;
241
+ }
242
+
243
+ /* Chart */
244
+ .chart-container {
245
+ position: relative;
246
+ height: 250px;
247
+ margin: 1.5rem 0;
248
+ }
249
+
250
+ .chart-bar {
251
+ position: absolute;
252
+ bottom: 0;
253
+ width: 40%;
254
+ border-radius: 6px 6px 0 0;
255
+ transition: height 0.5s ease;
256
+ }
257
+
258
+ .chart-bar.baseline {
259
+ left: 15%;
260
+ background: var(--a3m-muted);
261
+ opacity: 0.5;
262
+ }
263
+
264
+ .chart-bar.a3m {
265
+ right: 15%;
266
+ background: var(--a3m-primary);
267
+ }
268
+
269
+ /* Hidden until calculated */
270
+ .hidden { display: none; }
271
+
272
+ /* Footer */
273
+ .footer {
274
+ text-align: center;
275
+ color: var(--a3m-muted);
276
+ font-size: 0.75rem;
277
+ margin-top: 2rem;
278
+ }
279
+
280
+ .footer a { color: var(--a3m-accent); text-decoration: none; }