cn0303 commited on
Commit
c97ad08
·
verified ·
1 Parent(s): c2e1718

Deploy FitCheck UI brick

Browse files
Files changed (7) hide show
  1. README.md +48 -5
  2. app.py +274 -0
  3. requirements.txt +1 -0
  4. static/app.js +283 -0
  5. static/icons.js +54 -0
  6. static/index.html +155 -0
  7. static/style.css +376 -0
README.md CHANGED
@@ -1,13 +1,56 @@
1
  ---
2
  title: FitCheck
3
- emoji: 🏃
4
- colorFrom: pink
5
  colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.17.3
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: FitCheck
3
+ emoji:
4
+ colorFrom: indigo
5
  colorTo: green
6
  sdk: gradio
7
+ sdk_version: 6.16.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ short_description: Honest, plain answers about what AI your computer can run
12
  ---
13
 
14
+ # FitCheck
15
+
16
+ **What AI can your computer actually run?**
17
+
18
+ Tell FitCheck about your computer in plain words. It tells you honestly what AI
19
+ you can run today, from chatbots to object detection, what it will feel like,
20
+ and exactly how to start. No jargon, no hype, no fake promises.
21
+
22
+ Built for the [Build Small hackathon](https://huggingface.co/build-small-hackathon)
23
+ (Backyard AI track).
24
+
25
+ ## What makes it different
26
+
27
+ - **Plain words.** Built for people who do not know what VRAM or quantisation
28
+ means, and it explains those words when they appear.
29
+ - **The whole realm, not just chatbots.** Goals span LLMs and specialised deep
30
+ learning: object detection (YOLO), segmentation, 6-DoF pose (FoundationPose),
31
+ image and video generation, speech-to-text, embeddings, fine-tuning, plus a
32
+ custom "describe it" option.
33
+ - **Honest by design.** Three plain bands (Runs great, Tight but works, Will not
34
+ fit) with conservative estimates that under-promise rather than over.
35
+
36
+ ## How it is built
37
+
38
+ A hand-built HTML, CSS and JS frontend (no framework, no build step) served by
39
+ Gradio server mode (`gr.Server`, which is a FastAPI app). The interface talks to
40
+ a single connector, `POST /api/advise`.
41
+
42
+ > Note: this is the UI view. The advice endpoint currently returns input-aware
43
+ > placeholder data so the interface is complete and live. A deterministic
44
+ > calculation engine and a small local model plug into the same `/api/advise`
45
+ > contract next, with no frontend changes.
46
+
47
+ ## Run it locally
48
+
49
+ ```
50
+ python -m venv .venv
51
+ .venv\Scripts\activate
52
+ pip install -r requirements.txt
53
+ python app.py
54
+ ```
55
+
56
+ Then open http://127.0.0.1:7860/ (add `?go` for a sample result).
app.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FitCheck — what AI can your computer actually run?
3
+
4
+ This file is the UI BRICK's backend: a `gr.Server` (which IS a FastAPI app)
5
+ that serves the hand-built frontend in static/ and exposes ONE connector
6
+ endpoint, /api/advise.
7
+
8
+ IMPORTANT: /api/advise currently returns input-aware *placeholder* results so
9
+ the interface is complete and feels alive (the gauge moves, bands change). The
10
+ real deterministic engine (a separate brick) will plug into this same contract
11
+ later — the frontend won't need to change. Mock logic is fenced below.
12
+ """
13
+
14
+ import re
15
+ from pathlib import Path
16
+
17
+ import gradio as gr
18
+ from fastapi.responses import FileResponse
19
+ from fastapi.staticfiles import StaticFiles
20
+ from pydantic import BaseModel
21
+
22
+ CATALOGUE_VERSION = "2026-06-07"
23
+ STATIC = Path(__file__).parent / "static"
24
+
25
+ app = gr.Server()
26
+
27
+
28
+ # ==========================================================================
29
+ # PLACEHOLDER engine — to be replaced by the real calculation brick.
30
+ # Numbers are plausible and conservative but are NOT the audited engine yet.
31
+ # ==========================================================================
32
+
33
+ _COLORS = {"model": "#818CF8", "chat": "#A5F3C4", "work": "#868E9C"}
34
+
35
+ # Which broad family each goal belongs to (drives models/tools/commands).
36
+ _FAMILY = {
37
+ "chat": "llm", "writing": "llm", "coding": "llm", "agents": "llm",
38
+ "rag": "llm", "translate": "llm", "finetune": "llm",
39
+ "detect": "vision", "segment": "vision", "pose": "vision",
40
+ "classify": "vision", "depth": "vision", "ocr": "vision", "train-vision": "vision",
41
+ "imagegen": "gen", "inpaint": "gen", "upscale": "gen", "videogen": "gen", "bgremove": "gen",
42
+ "stt": "audio", "tts": "audio", "music": "audio",
43
+ "embed": "data", "forecast": "data", "tabular": "data",
44
+ "custom": "llm",
45
+ }
46
+
47
+ # variant = (name, description, memory-need-GB at sensible setting, setting label)
48
+ _VARIANTS = {
49
+ "llm": [
50
+ ("Huge (70B)", "Top open quality. Serious hardware only.", 42, "4-bit"),
51
+ ("Very large (32B)", "Near-premium. Wants a strong GPU.", 20, "4-bit"),
52
+ ("Large (14B)", "Noticeably smarter and more reliable.", 9, "4-bit"),
53
+ ("Medium (7-9B)", "Solid all-rounder: chat, coding, reasoning.", 5.5, "4-bit"),
54
+ ("Small (3-4B)", "Surprisingly capable everyday assistant.", 2.5, "4-bit"),
55
+ ("Tiny (~1B)", "Quick simple chat. Runs on almost anything.", 1.2, "4-bit"),
56
+ ],
57
+ "vision": [
58
+ ("Extra-large model", "Highest accuracy, slowest.", 6.0, "full"),
59
+ ("Large model", "Great accuracy for real work.", 3.5, "full"),
60
+ ("Medium model", "Balanced accuracy and speed.", 2.0, "full"),
61
+ ("Small model", "Fast, good for live video.", 1.2, "full"),
62
+ ("Nano model", "Real-time even on weak hardware.", 0.6, "full"),
63
+ ],
64
+ "gen": [
65
+ ("Flux.1 (dev)", "State-of-the-art image quality.", 18, "full"),
66
+ ("Flux.1 (schnell)", "Near-top quality, much faster.", 12, "8-bit"),
67
+ ("SDXL", "Excellent 1024px images.", 8, "full"),
68
+ ("SDXL Turbo", "Fast SDXL, fewer steps.", 7, "full"),
69
+ ("Stable Diffusion 1.5", "Light, fast, huge community.", 3.5, "full"),
70
+ ],
71
+ "audio": [
72
+ ("Whisper large-v3", "Best transcription accuracy.", 5.0, "full"),
73
+ ("Whisper medium", "Strong accuracy, lighter.", 3.0, "full"),
74
+ ("Whisper small", "Good for clear audio, fast.", 1.5, "full"),
75
+ ("Whisper base", "Quick drafts, light hardware.", 0.8, "full"),
76
+ ],
77
+ "data": [
78
+ ("Large embedder", "Best search relevance.", 2.5, "full"),
79
+ ("Base embedder", "Great balance for most search.", 1.2, "full"),
80
+ ("Small embedder", "Fast, runs on anything.", 0.5, "full"),
81
+ ],
82
+ }
83
+
84
+ _TOOLS = {
85
+ "llm": [
86
+ ("Ollama", "Type one line; it downloads and runs the model for you.", "Get it from ollama.com", "Easiest"),
87
+ ("LM Studio", "A point-and-click app with a chat window, no commands.", "Download from lmstudio.ai", "Easy"),
88
+ ("llama.cpp", "The lightweight engine under the hood. Runs GGUF files directly.", "Releases on GitHub", "Advanced"),
89
+ ],
90
+ "vision": [
91
+ ("Ultralytics", "One pip install, then detect objects from a webcam or file.", "pip install ultralytics", "Easiest"),
92
+ ("Roboflow", "Browser tools to label data and run models, little code.", "roboflow.com", "Easy"),
93
+ ("PyTorch", "Full control for custom pipelines and training.", "pytorch.org", "Advanced"),
94
+ ],
95
+ "gen": [
96
+ ("Fooocus", "Image generation that 'just works': one folder, double-click.", "Download from GitHub", "Easiest"),
97
+ ("ComfyUI", "Powerful visual node editor for image/video pipelines.", "Download from GitHub", "Moderate"),
98
+ ("Automatic1111", "The classic full-featured Stable Diffusion web UI.", "Download from GitHub", "Moderate"),
99
+ ],
100
+ "audio": [
101
+ ("faster-whisper", "Fast, accurate transcription with a tiny install.", "pip install faster-whisper", "Easiest"),
102
+ ("whisper.cpp", "Runs Whisper efficiently on CPU and small machines.", "Build from GitHub", "Advanced"),
103
+ ],
104
+ "data": [
105
+ ("sentence-transformers", "Turn text into searchable vectors in a few lines.", "pip install sentence-transformers", "Easiest"),
106
+ ("Chroma", "A simple local database to store and search those vectors.", "pip install chromadb", "Easy"),
107
+ ],
108
+ }
109
+
110
+ _COMMANDS = {
111
+ "llm": [
112
+ ("Easy way (Ollama)", "ollama run llama3.1:8b"),
113
+ ("Power way (llama.cpp)", "llama-server -hf bartowski/Meta-Llama-3.1-8B-Instruct-GGUF:Q4_K_M -c 4096"),
114
+ ],
115
+ "vision": [
116
+ ("Install", "pip install ultralytics"),
117
+ ("Detect from your webcam", "yolo predict model=yolo11n.pt source=0 show=True"),
118
+ ],
119
+ "gen": [
120
+ ("Easiest (Fooocus)", "# Download Fooocus, unzip, double-click run.bat"),
121
+ ("Power (ComfyUI)", "git clone https://github.com/comfyanonymous/ComfyUI && cd ComfyUI && python main.py"),
122
+ ],
123
+ "audio": [
124
+ ("Install", "pip install faster-whisper"),
125
+ ("Transcribe a file", 'python -c "from faster_whisper import WhisperModel; m=WhisperModel(\'small\'); print(list(m.transcribe(\'audio.mp3\')[0]))"'),
126
+ ],
127
+ "data": [
128
+ ("Install", "pip install sentence-transformers"),
129
+ ("Make searchable vectors", 'python -c "from sentence_transformers import SentenceTransformer as S; print(S(\'all-MiniLM-L6-v2\').encode(\'hello world\').shape)"'),
130
+ ],
131
+ }
132
+
133
+
134
+ def _parse_vram(label: str) -> float:
135
+ m = re.search(r"(\d+(?:\.\d+)?)\s*GB", label or "")
136
+ return float(m.group(1)) if m else 0.0
137
+
138
+
139
+ def _budgets(p: dict):
140
+ ram = float(p.get("ram_gb") or 16)
141
+ provider = p.get("provider", "none")
142
+ computer = p.get("computer", "Windows laptop")
143
+
144
+ if provider == "apple":
145
+ fast = round(ram * 0.70, 1)
146
+ return fast, fast, ram, True
147
+
148
+ vram = p.get("vram_gb")
149
+ if not vram:
150
+ vram = _parse_vram(p.get("gpu", ""))
151
+ vram = float(vram or 0)
152
+
153
+ fast = round(vram * 0.85, 1)
154
+ reserve = 1.0 if "Mini PC" in computer else (3.0 if "desktop" in computer.lower() or computer == "Mac" else 3.5)
155
+ total = round(vram + max(0.0, ram - reserve) * 0.9, 1)
156
+ return fast, vram, total, False
157
+
158
+
159
+ def advise_mock(p: dict) -> dict:
160
+ fam = _FAMILY.get(p.get("usecase", "chat"), "llm")
161
+ variants = _VARIANTS[fam]
162
+ fast, _vram, total, is_apple = _budgets(p)
163
+ is_finetune = p.get("usecase") in ("finetune", "train-vision")
164
+ factor = 2.4 if is_finetune else 1.0
165
+
166
+ # Classify each variant.
167
+ def verdict_for(need):
168
+ need *= factor
169
+ if fast >= 1 and need <= fast * 0.9:
170
+ return "great", need
171
+ if need <= total * 0.9:
172
+ return "tight", need
173
+ return "no", need
174
+
175
+ options = []
176
+ for name, desc, need, setting in variants:
177
+ vd, real_need = verdict_for(need)
178
+ feel = {"great": "Snappy on your graphics card",
179
+ "tight": "Runs, but slower (uses normal memory)",
180
+ "no": "Not enough memory"}[vd]
181
+ options.append({
182
+ "verdict": vd, "model": name, "desc": desc,
183
+ "setting": setting, "memory": ("—" if vd == "no" else f"{round(real_need,1):g} GB"),
184
+ "feel": feel,
185
+ })
186
+
187
+ # Headline = best that runs great, else best that's tight, else smallest.
188
+ great = [o for o in options if o["verdict"] == "great"]
189
+ tight = [o for o in options if o["verdict"] == "tight"]
190
+ head = great[0] if great else (tight[0] if tight else options[-1])
191
+ hv = head["verdict"]
192
+ need_gb = float(re.search(r"[\d.]+", head["memory"]).group()) if head["memory"] != "—" else verdict_for(variants[-1][2])[1]
193
+
194
+ if is_apple and hv == "great":
195
+ where = "on your Mac"
196
+ elif hv == "great" and fast >= 1:
197
+ where = "on your graphics card"
198
+ elif hv == "tight":
199
+ where = "using your computer's memory"
200
+ else:
201
+ where = ""
202
+ verdict_word = {"great": "Runs great", "tight": "Tight, but works", "no": "Won't fit"}[hv]
203
+ if hv == "great":
204
+ headline = f"Yes, you can run the {head['model']} {where}, today."
205
+ elif hv == "tight":
206
+ headline = f"Sort of. The {head['model']} will run {where}, with trade-offs."
207
+ else:
208
+ headline = "This goal is a stretch on this machine. Here's the honest picture."
209
+
210
+ detail = (f"For this goal, the sweet spot is a <b>{head['model']}</b> model "
211
+ f"at the <b>{head['setting']}</b> setting. {head['desc']} "
212
+ f"It needs about <b>{round(need_gb,1):g} GB</b>, and you have roughly "
213
+ f"<b>{fast:g} GB</b> fast / <b>{total:g} GB</b> total to work with.")
214
+ note = ("Fine-tuning needs roughly 2 to 3 times the memory of just using a model. That's baked in above."
215
+ if is_finetune else "")
216
+
217
+ # Gauge
218
+ scale = max(total, need_gb, 1) * 1.05
219
+ gauge = {
220
+ "need_gb": f"{round(need_gb,1):g} GB needed",
221
+ "fast_gb": f"{fast:g} GB", "total_gb": f"{total:g} GB",
222
+ "fill_pct": round(need_gb / scale * 100, 1),
223
+ "mark_pct": round(fast / scale * 100, 1),
224
+ "breakdown": [
225
+ {"label": f"Model {round(need_gb*0.8,1):g} GB", "color": _COLORS["model"]},
226
+ {"label": f"Working space {round(need_gb*0.2,1):g} GB", "color": _COLORS["work"]},
227
+ ],
228
+ }
229
+
230
+ tools = [{"name": n, "what": w, "install": i, "tag": t} for n, w, i, t in _TOOLS[fam]]
231
+ cmd_intro = ("These get you a running model in minutes. Pick the easy one or the power one; "
232
+ "they do the same job.")
233
+ commands = {"intro": cmd_intro,
234
+ "items": [{"label": l, "code": c} for l, c in _COMMANDS[fam]]}
235
+
236
+ return {
237
+ "catalogue_version": CATALOGUE_VERSION,
238
+ "verdict": hv, "verdict_word": verdict_word,
239
+ "headline": headline, "detail": detail, "note": note,
240
+ "gauge": gauge, "options": options, "tools": tools, "commands": commands,
241
+ }
242
+
243
+
244
+ # ==========================================================================
245
+ # Connector endpoint + static frontend
246
+ # ==========================================================================
247
+
248
+ class AdviseIn(BaseModel):
249
+ computer: str = "Windows laptop"
250
+ ram_gb: float | None = 16
251
+ provider: str = "none"
252
+ gpu: str = ""
253
+ vram_gb: float | None = None
254
+ paste: str = ""
255
+ usecase: str = "chat"
256
+ custom: str = ""
257
+ priority: str = "balanced"
258
+
259
+
260
+ @app.post("/api/advise")
261
+ def api_advise(payload: AdviseIn):
262
+ return advise_mock(payload.model_dump())
263
+
264
+
265
+ app.mount("/static", StaticFiles(directory=STATIC), name="static")
266
+
267
+
268
+ @app.get("/")
269
+ def index():
270
+ return FileResponse(STATIC / "index.html")
271
+
272
+
273
+ if __name__ == "__main__":
274
+ app.launch(server_name="0.0.0.0", server_port=7860)
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ gradio==6.16.0
static/app.js ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================
2
+ FitCheck — frontend logic (UI brick)
3
+ Gathers inputs, calls the /api/advise connector, renders results.
4
+ The backend currently returns input-aware PLACEHOLDER data; the
5
+ real engine plugs into the same /api/advise contract later.
6
+ ============================================================ */
7
+
8
+ // ---- Inline icon helpers (icons.js loads first) --------------------------
9
+ function ic(name) { return (window.ICONS && window.ICONS[name]) || ""; }
10
+ function hydrate(root) {
11
+ (root || document).querySelectorAll("[data-ic]").forEach(el => {
12
+ if (!el.dataset.done) { el.innerHTML = ic(el.dataset.ic); el.dataset.done = "1"; }
13
+ });
14
+ }
15
+
16
+ // ---- Use-case taxonomy: the full realm, not just LLMs --------------------
17
+ const USE_CASES = [
18
+ { icon: "cat-text", name: "Text & chat", items: [
19
+ { id: "chat", icon: "chat", label: "Chatbot / assistant" },
20
+ { id: "writing", icon: "writing", label: "Writing & summarising" },
21
+ { id: "coding", icon: "coding", label: "Coding help" },
22
+ { id: "agents", icon: "agents", label: "Agents & tool use" },
23
+ { id: "rag", icon: "rag", label: "Chat with my documents" },
24
+ { id: "translate", icon: "translate", label: "Translation" },
25
+ ]},
26
+ { icon: "cat-vision", name: "See & understand images", items: [
27
+ { id: "detect", icon: "detect", label: "Object detection (YOLO)" },
28
+ { id: "segment", icon: "segment", label: "Image segmentation" },
29
+ { id: "pose", icon: "pose", label: "Pose / 6-DoF (FoundationPose)" },
30
+ { id: "classify",icon: "classify", label: "Image classification" },
31
+ { id: "depth", icon: "depth", label: "Depth estimation" },
32
+ { id: "ocr", icon: "ocr", label: "Read text from images (OCR)" },
33
+ ]},
34
+ { icon: "cat-gen", name: "Create images & video", items: [
35
+ { id: "imagegen", icon: "imagegen", label: "Generate images (SD / Flux)" },
36
+ { id: "inpaint", icon: "inpaint", label: "Edit / inpaint images" },
37
+ { id: "upscale", icon: "upscale", label: "Upscale / restore" },
38
+ { id: "videogen", icon: "videogen", label: "Generate video" },
39
+ { id: "bgremove", icon: "bgremove", label: "Remove backgrounds" },
40
+ ]},
41
+ { icon: "cat-audio", name: "Audio & speech", items: [
42
+ { id: "stt", icon: "stt", label: "Speech to text (Whisper)" },
43
+ { id: "tts", icon: "tts", label: "Text to speech / voice" },
44
+ { id: "music", icon: "music", label: "Generate music" },
45
+ ]},
46
+ { icon: "cat-data", name: "Data & search", items: [
47
+ { id: "embed", icon: "embed", label: "Semantic search / embeddings" },
48
+ { id: "forecast", icon: "forecast", label: "Time-series forecasting" },
49
+ { id: "tabular", icon: "tabular", label: "Predict from spreadsheets" },
50
+ ]},
51
+ { icon: "cat-train", name: "Train your own", items: [
52
+ { id: "finetune", icon: "finetune", label: "Fine-tune an LLM (LoRA)" },
53
+ { id: "train-vision", icon: "train-vision", label: "Train a vision model" },
54
+ ]},
55
+ { icon: "cat-custom", name: "Something else", items: [
56
+ { id: "custom", icon: "custom", label: "Custom: describe it" },
57
+ ]},
58
+ ];
59
+
60
+ // ---- GPU lists per provider ----------------------------------------------
61
+ const GPUS = {
62
+ none: [],
63
+ unsure: [],
64
+ nvidia: [
65
+ "RTX 5090 (32 GB)","RTX 5080 (16 GB)","RTX 5070 Ti (16 GB)","RTX 5070 (12 GB)","RTX 5060 (8 GB)",
66
+ "RTX 4090 (24 GB)","RTX 4080 (16 GB)","RTX 4070 (12 GB)","RTX 4060 (8 GB)",
67
+ "RTX 3090 (24 GB)","RTX 3080 (10 GB)","RTX 3070 (8 GB)","RTX 3060 (12 GB)","RTX 3050 (8 GB)",
68
+ "GTX 1660 (6 GB)","GTX 1650 (4 GB)","Laptop RTX (not sure of model)",
69
+ ],
70
+ amd: ["RX 7900 XTX (24 GB)","RX 7800 XT (16 GB)","RX 7600 (8 GB)","RX 6700 XT (12 GB)","Built-in Radeon"],
71
+ apple: ["M-series base (8 GB)","M-series Pro (16 GB)","M-series Max (32 GB)","M-series Ultra (64 GB)"],
72
+ intel: ["Arc A770 (16 GB)","Arc A750 (8 GB)","Arc A380 (6 GB)","Built-in Intel graphics"],
73
+ };
74
+
75
+ const $ = (s) => document.querySelector(s);
76
+ const state = { computer: "Windows laptop", provider: "none", priority: "balanced", usecase: "chat", checked: false };
77
+
78
+ // ---- Build the use-case picker -------------------------------------------
79
+ function buildPicker() {
80
+ const wrap = $("#usecase-picker");
81
+ wrap.innerHTML = USE_CASES.map(g => `
82
+ <div class="uc-group">
83
+ <div class="uc-cat"><span class="ic" data-ic="${g.icon}"></span>${g.name}</div>
84
+ <div class="uc-grid">
85
+ ${g.items.map(it => `
86
+ <button class="uc-pill${it.id === "chat" ? " active" : ""}" data-uc="${it.id}">
87
+ <span class="pic ic" data-ic="${it.icon}"></span>${it.label}
88
+ </button>`).join("")}
89
+ </div>
90
+ </div>`).join("");
91
+ hydrate(wrap);
92
+
93
+ wrap.querySelectorAll(".uc-pill").forEach(p => p.addEventListener("click", () => {
94
+ wrap.querySelectorAll(".uc-pill").forEach(x => x.classList.remove("active"));
95
+ p.classList.add("active");
96
+ state.usecase = p.dataset.uc;
97
+ $("#custom-uc-field").style.display = state.usecase === "custom" ? "block" : "none";
98
+ maybeLiveUpdate();
99
+ }));
100
+ }
101
+
102
+ // ---- Segmented controls ---------------------------------------------------
103
+ function wireSegmented(id, key, after) {
104
+ $(id).querySelectorAll(".seg-btn").forEach(b => b.addEventListener("click", () => {
105
+ $(id).querySelectorAll(".seg-btn").forEach(x => x.classList.remove("active"));
106
+ b.classList.add("active");
107
+ state[key] = b.dataset.val;
108
+ if (after) after();
109
+ maybeLiveUpdate();
110
+ }));
111
+ }
112
+ function setActive(id, val) {
113
+ $(id).querySelectorAll(".seg-btn").forEach(b => b.classList.toggle("active", b.dataset.val === val));
114
+ }
115
+
116
+ // ---- GPU select depends on provider --------------------------------------
117
+ function fillGpu() {
118
+ const list = GPUS[state.provider] || [];
119
+ const sel = $("#gpu");
120
+ if (!list.length) { sel.style.display = "none"; sel.innerHTML = ""; }
121
+ else { sel.style.display = "block"; sel.innerHTML = list.map(g => `<option>${g}</option>`).join(""); }
122
+ }
123
+ function syncProviderForComputer() {
124
+ const isMac = state.computer === "Mac";
125
+ const isPi = state.computer.includes("Mini PC");
126
+ if (isMac) { state.provider = "apple"; setActive("#provider-seg", "apple"); }
127
+ else if (isPi) { state.provider = "none"; setActive("#provider-seg", "none"); }
128
+ fillGpu();
129
+ }
130
+
131
+ // ---- Find-my-specs help text ---------------------------------------------
132
+ function findSpecsText() {
133
+ const c = state.computer;
134
+ if (c === "Mac") return `
135
+ <ul style="margin:0;padding-left:18px">
136
+ <li>Click the Apple menu (top-left), then <b>About This Mac</b>.</li>
137
+ <li>It shows your chip (e.g. <i>Apple M2</i>) and <b>Memory</b> (e.g. <i>16 GB</i>).</li>
138
+ <li>On a Mac that one memory number is all you need. The graphics share it.</li>
139
+ </ul>`;
140
+ if (c === "Linux PC" || c.includes("Mini PC")) return `
141
+ <ul style="margin:0;padding-left:18px">
142
+ <li><b>RAM:</b> run <code>free -h</code> in a terminal.</li>
143
+ <li><b>Graphics card:</b> <code>nvidia-smi</code> (NVIDIA) or <code>lspci | grep VGA</code>.</li>
144
+ </ul>`;
145
+ return `
146
+ <ul style="margin:0;padding-left:18px">
147
+ <li><b>RAM:</b> press <code>Ctrl + Shift + Esc</code>, then <b>Performance</b>, then <b>Memory</b>.</li>
148
+ <li><b>Graphics card:</b> same window, <b>GPU</b>. The name is top-right (e.g. <i>NVIDIA RTX 3060</i>).</li>
149
+ <li>No real GPU listed? You have built-in graphics. Pick “None / built-in”.</li>
150
+ </ul>`;
151
+ }
152
+
153
+ // ---- Gather inputs & call the connector ----------------------------------
154
+ function gather() {
155
+ const sel = $("#gpu");
156
+ return {
157
+ computer: state.computer,
158
+ ram_gb: parseFloat($("#ram").value),
159
+ provider: state.provider,
160
+ gpu: sel.style.display === "none" ? "" : sel.value,
161
+ vram_gb: $("#vram").value ? parseFloat($("#vram").value) : null,
162
+ paste: $("#paste").value.trim(),
163
+ usecase: state.usecase,
164
+ custom: $("#custom-uc").value.trim(),
165
+ priority: state.priority,
166
+ };
167
+ }
168
+
169
+ let debounce;
170
+ function maybeLiveUpdate() {
171
+ if (!state.checked) return;
172
+ clearTimeout(debounce);
173
+ debounce = setTimeout(check, 200);
174
+ }
175
+
176
+ async function check() {
177
+ state.checked = true;
178
+ try {
179
+ const res = await fetch("/api/advise", {
180
+ method: "POST", headers: { "Content-Type": "application/json" },
181
+ body: JSON.stringify(gather()),
182
+ });
183
+ render(await res.json());
184
+ } catch (e) {
185
+ $("#results").innerHTML = `<div class="empty-state"><div class="big"><span class="ic" data-ic="monitor"></span></div>
186
+ <p>Couldn't reach the advisor. Is the server running?</p></div>`;
187
+ hydrate($("#results"));
188
+ }
189
+ }
190
+
191
+ // ---- Render results -------------------------------------------------------
192
+ const VMAP = {
193
+ great: { cls: "var(--ok)", soft: "var(--ok-soft)", word: "Runs great", em: "✓" },
194
+ tight: { cls: "var(--warn)", soft: "var(--warn-soft)", word: "Tight, but works", em: "≈" },
195
+ no: { cls: "var(--no)", soft: "var(--no-soft)", word: "Won't fit", em: "✕" },
196
+ };
197
+
198
+ function render(d) {
199
+ const v = VMAP[d.verdict] || VMAP.tight;
200
+ const g = d.gauge || {};
201
+ $("#cat-version").textContent = d.catalogue_version || "—";
202
+
203
+ const opts = (d.options || []).map(o => {
204
+ const ov = VMAP[o.verdict] || VMAP.tight;
205
+ return `<div class="opt" style="--status:${ov.cls};--status-soft:${ov.soft}">
206
+ <div class="vdot">${ov.em}</div>
207
+ <div><div class="name">${o.model}</div><div class="desc">${o.desc}</div></div>
208
+ <div class="meta"><b>${o.memory}</b><div class="feel">${o.setting} · ${o.feel}</div></div>
209
+ </div>`;
210
+ }).join("");
211
+
212
+ const tools = (d.tools || []).map((t, i) => `
213
+ <div class="tool">
214
+ <div class="tool-head"><span class="tname">${t.name}</span>
215
+ <span class="tag ${i===0?"best":"mid"}">${i===0?"Start here":t.tag}</span></div>
216
+ <div class="twhat">${t.what}</div>
217
+ <div class="tinstall"><span class="ic" data-ic="download"></span>${t.install}</div>
218
+ </div>`).join("");
219
+
220
+ const cmds = (d.commands?.items || []).map((c) => `
221
+ <div class="cmd-box">
222
+ <div class="cmd-label">${c.label}<button class="copy-btn" data-code="${encodeURIComponent(c.code)}">Copy</button></div>
223
+ <pre><code>${c.code}</code></pre>
224
+ </div>`).join("");
225
+
226
+ $("#results").innerHTML = `
227
+ <div class="reveal">
228
+ <div class="verdict" style="--status:${v.cls};--status-soft:${v.soft}">
229
+ <span class="badge"><span class="dot"></span>${d.verdict_word || v.word}</span>
230
+ <h2>${d.headline || ""}</h2>
231
+ <p>${d.detail || ""}</p>
232
+ ${d.note ? `<div class="note">${d.note}</div>` : ""}
233
+ </div>
234
+
235
+ ${g.fill_pct != null ? `
236
+ <div class="gauge-card" style="--status:${v.cls}">
237
+ <div class="gauge-top"><span class="label" style="margin:0">Memory needed vs. what you have</span>
238
+ <span class="need">${g.need_gb}</span></div>
239
+ <div class="gauge" style="--pct:${g.fill_pct};--gcolor:${v.cls};--needpct:${g.mark_pct}">
240
+ <div class="gauge-fill"></div><div class="gauge-need"></div>
241
+ </div>
242
+ <div class="gauge-legend">
243
+ ${(g.breakdown||[]).map(b=>`<span class="item"><span class="sw" style="background:${b.color}"></span>${b.label}</span>`).join("")}
244
+ <span class="item marker">Fast: ${g.fast_gb}</span>
245
+ <span class="item marker">Total: ${g.total_gb}</span>
246
+ </div>
247
+ </div>` : ""}
248
+
249
+ ${opts ? `<div class="section-title">What you can run <span class="sub">honestly, biggest to smallest</span></div>
250
+ <div class="opt-grid">${opts}</div>` : ""}
251
+
252
+ ${tools ? `<div class="section-title">How to actually run it</div>
253
+ <div class="tool-grid">${tools}</div>` : ""}
254
+
255
+ ${cmds ? `<div class="section-title">Copy-paste to get started</div>
256
+ <p class="cmd-intro">${d.commands.intro || ""}</p>
257
+ <div class="cmd">${cmds}</div>` : ""}
258
+ </div>`;
259
+
260
+ hydrate($("#results"));
261
+ $("#results").querySelectorAll(".copy-btn").forEach(b => b.addEventListener("click", () => {
262
+ navigator.clipboard.writeText(decodeURIComponent(b.dataset.code));
263
+ b.textContent = "Copied ✓"; b.classList.add("done");
264
+ setTimeout(() => { b.textContent = "Copy"; b.classList.remove("done"); }, 1500);
265
+ }));
266
+ }
267
+
268
+ // ---- Init -----------------------------------------------------------------
269
+ function init() {
270
+ hydrate(document);
271
+ buildPicker();
272
+ wireSegmented("#computer-seg", "computer", () => { syncProviderForComputer(); $("#find-specs-body").innerHTML = findSpecsText(); });
273
+ wireSegmented("#provider-seg", "provider", fillGpu);
274
+ wireSegmented("#priority-seg", "priority");
275
+ ["#ram","#gpu","#vram","#custom-uc"].forEach(s => $(s).addEventListener("change", maybeLiveUpdate));
276
+ $("#paste").addEventListener("input", maybeLiveUpdate);
277
+ $("#check-btn").addEventListener("click", check);
278
+ fillGpu();
279
+ $("#find-specs-body").innerHTML = findSpecsText();
280
+ // Pre-filled share/preview links: /?go renders results immediately.
281
+ if (new URLSearchParams(location.search).has("go")) check();
282
+ }
283
+ init();
static/icons.js ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Auto-generated by build_icons.py. Inline icons = offline-safe. */
2
+ window.ICONS = {
3
+ "chat": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719\" /> </svg>",
4
+ "writing": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\" /> <path d=\"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z\" /> </svg>",
5
+ "coding": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"m18 16 4-4-4-4\" /> <path d=\"m6 8-4 4 4 4\" /> <path d=\"m14.5 4-5 16\" /> </svg>",
6
+ "agents": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M12 8V4H8\" /> <rect x=\"4\" y=\"8\" rx=\"2\" /> <path d=\"M2 14h2\" /> <path d=\"M20 14h2\" /> <path d=\"M15 13v2\" /> <path d=\"M9 13v2\" /> </svg>",
7
+ "rag": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M12 7v14\" /> <path d=\"M16 12h2\" /> <path d=\"M16 8h2\" /> <path d=\"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\" /> <path d=\"M6 12h2\" /> <path d=\"M6 8h2\" /> </svg>",
8
+ "translate": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"m5 8 6 6\" /> <path d=\"m4 14 6-6 2-3\" /> <path d=\"M2 5h12\" /> <path d=\"M7 2h1\" /> <path d=\"m22 22-5-10-5 10\" /> <path d=\"M14 18h6\" /> </svg>",
9
+ "detect": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M3 7V5a2 2 0 0 1 2-2h2\" /> <path d=\"M17 3h2a2 2 0 0 1 2 2v2\" /> <path d=\"M21 17v2a2 2 0 0 1-2 2h-2\" /> <path d=\"M7 21H5a2 2 0 0 1-2-2v-2\" /> <circle cx=\"12\" cy=\"12\" r=\"3\" /> <path d=\"m16 16-1.9-1.9\" /> </svg>",
10
+ "segment": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z\" /> <path d=\"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12\" /> <path d=\"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17\" /> </svg>",
11
+ "pose": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\" /> <path d=\"m3.3 7 8.7 5 8.7-5\" /> <path d=\"M12 22V12\" /> </svg>",
12
+ "classify": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M13.172 2a2 2 0 0 1 1.414.586l6.71 6.71a2.4 2.4 0 0 1 0 3.408l-4.592 4.592a2.4 2.4 0 0 1-3.408 0l-6.71-6.71A2 2 0 0 1 6 9.172V3a1 1 0 0 1 1-1z\" /> <path d=\"M2 7v6.172a2 2 0 0 0 .586 1.414l6.71 6.71a2.4 2.4 0 0 0 3.191.193\" /> <circle cx=\"10.5\" cy=\"6.5\" r=\".5\" fill=\"currentColor\" /> </svg>",
13
+ "depth": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"m8 3 4 8 5-5 5 15H2L8 3z\" /> </svg>",
14
+ "ocr": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M3 7V5a2 2 0 0 1 2-2h2\" /> <path d=\"M17 3h2a2 2 0 0 1 2 2v2\" /> <path d=\"M21 17v2a2 2 0 0 1-2 2h-2\" /> <path d=\"M7 21H5a2 2 0 0 1-2-2v-2\" /> <path d=\"M7 8h8\" /> <path d=\"M7 12h10\" /> <path d=\"M7 16h6\" /> </svg>",
15
+ "imagegen": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <rect x=\"3\" y=\"3\" rx=\"2\" ry=\"2\" /> <circle cx=\"9\" cy=\"9\" r=\"2\" /> <path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\" /> </svg>",
16
+ "inpaint": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72\" /> <path d=\"m14 7 3 3\" /> <path d=\"M5 6v4\" /> <path d=\"M19 14v4\" /> <path d=\"M10 2v2\" /> <path d=\"M7 8H3\" /> <path d=\"M21 16h-4\" /> <path d=\"M11 3H9\" /> </svg>",
17
+ "upscale": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\" /> <path d=\"M14 15H9v-5\" /> <path d=\"M16 3h5v5\" /> <path d=\"M21 3 9 15\" /> </svg>",
18
+ "videogen": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <rect x=\"3\" y=\"3\" rx=\"2\" /> <path d=\"M7 3v18\" /> <path d=\"M3 7.5h4\" /> <path d=\"M3 12h18\" /> <path d=\"M3 16.5h4\" /> <path d=\"M17 3v18\" /> <path d=\"M17 7.5h4\" /> <path d=\"M17 16.5h4\" /> </svg>",
19
+ "bgremove": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <circle cx=\"6\" cy=\"6\" r=\"3\" /> <path d=\"M8.12 8.12 12 12\" /> <path d=\"M20 4 8.12 15.88\" /> <circle cx=\"6\" cy=\"18\" r=\"3\" /> <path d=\"M14.8 14.8 20 20\" /> </svg>",
20
+ "stt": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M12 19v3\" /> <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" /> <rect x=\"9\" y=\"2\" rx=\"3\" /> </svg>",
21
+ "tts": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M2 10v3\" /> <path d=\"M6 6v11\" /> <path d=\"M10 3v18\" /> <path d=\"M14 8v7\" /> <path d=\"M18 5v13\" /> <path d=\"M22 10v3\" /> </svg>",
22
+ "music": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M9 18V5l12-2v13\" /> <circle cx=\"6\" cy=\"18\" r=\"3\" /> <circle cx=\"18\" cy=\"16\" r=\"3\" /> </svg>",
23
+ "embed": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <circle cx=\"12\" cy=\"12\" r=\"10\" /> <path d=\"m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z\" /> </svg>",
24
+ "forecast": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M16 7h6v6\" /> <path d=\"m22 7-8.5 8.5-5-5L2 17\" /> </svg>",
25
+ "tabular": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M12 3v18\" /> <rect x=\"3\" y=\"3\" rx=\"2\" /> <path d=\"M3 9h18\" /> <path d=\"M3 15h18\" /> </svg>",
26
+ "finetune": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z\" /> <path d=\"M22 10v6\" /> <path d=\"M6 12.5V16a6 3 0 0 0 12 0v-3.5\" /> </svg>",
27
+ "train-vision": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z\" /> </svg>",
28
+ "custom": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z\" /> <path d=\"m15 5 4 4\" /> </svg>",
29
+ "cat-text": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z\" /> </svg>",
30
+ "cat-vision": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\" /> <circle cx=\"12\" cy=\"12\" r=\"3\" /> </svg>",
31
+ "cat-gen": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z\" /> <circle cx=\"13.5\" cy=\"6.5\" r=\".5\" fill=\"currentColor\" /> <circle cx=\"17.5\" cy=\"10.5\" r=\".5\" fill=\"currentColor\" /> <circle cx=\"6.5\" cy=\"12.5\" r=\".5\" fill=\"currentColor\" /> <circle cx=\"8.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\" /> </svg>",
32
+ "cat-audio": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M2 13a2 2 0 0 0 2-2V7a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0V4a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0v-4a2 2 0 0 1 2-2\" /> </svg>",
33
+ "cat-data": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M3 3v16a2 2 0 0 0 2 2h16\" /> <path d=\"M18 17V9\" /> <path d=\"M13 17V5\" /> <path d=\"M8 17v-3\" /> </svg>",
34
+ "cat-train": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z\" /> <path d=\"M22 10v6\" /> <path d=\"M6 12.5V16a6 3 0 0 0 12 0v-3.5\" /> </svg>",
35
+ "cat-custom": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z\" /> <path d=\"m15 5 4 4\" /> </svg>",
36
+ "check": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M20 6 9 17l-5-5\" /> </svg>",
37
+ "search": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"m21 21-4.34-4.34\" /> <circle cx=\"11\" cy=\"11\" r=\"8\" /> </svg>",
38
+ "book": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M12 7v14\" /> <path d=\"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\" /> </svg>",
39
+ "balanced": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M12 3v18\" /> <path d=\"m19 8 3 8a5 5 0 0 1-6 0zV7\" /> <path d=\"M3 7h1a17 17 0 0 0 8-2 17 17 0 0 0 8 2h1\" /> <path d=\"m5 8 3 8a5 5 0 0 1-6 0zV7\" /> <path d=\"M7 21h10\" /> </svg>",
40
+ "quality": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z\" /> <path d=\"M20 2v4\" /> <path d=\"M22 4h-4\" /> <circle cx=\"4\" cy=\"20\" r=\"2\" /> </svg>",
41
+ "speed": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z\" /> </svg>",
42
+ "offline": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z\" /> </svg>",
43
+ "monitor": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"m9 10 2 2 4-4\" /> <rect x=\"2\" y=\"3\" rx=\"2\" /> <path d=\"M12 17v4\" /> <path d=\"M8 21h8\" /> </svg>",
44
+ "download": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M12 15V3\" /> <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" /> <path d=\"m7 10 5 5 5-5\" /> </svg>",
45
+ "chevron": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"m6 9 6 6 6-6\" /> </svg>",
46
+ "arrow": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" > <path d=\"M5 12h14\" /> <path d=\"m12 5 7 7-7 7\" /> </svg>",
47
+ "brand-windows": "<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><rect x=\"3\" y=\"3\" width=\"8\" height=\"8\" rx=\"1\"/><rect x=\"13\" y=\"3\" width=\"8\" height=\"8\" rx=\"1\"/><rect x=\"3\" y=\"13\" width=\"8\" height=\"8\" rx=\"1\"/><rect x=\"13\" y=\"13\" width=\"8\" height=\"8\" rx=\"1\"/></svg>",
48
+ "brand-apple": "<svg fill=\"currentColor\" role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701\"/></svg>",
49
+ "brand-linux": "<svg fill=\"currentColor\" role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.424.424 0 00-.11.135c-.26.268-.45.6-.663.839-.199.199-.485.267-.797.4-.313.136-.658.269-.864.68-.09.189-.136.394-.132.602 0 .199.027.4.055.536.058.399.116.728.04.97-.249.68-.28 1.145-.106 1.484.174.334.535.47.94.601.81.2 1.91.135 2.774.6.926.466 1.866.67 2.616.47.526-.116.97-.464 1.208-.946.587-.003 1.23-.269 2.26-.334.699-.058 1.574.267 2.577.2.025.134.063.198.114.333l.003.003c.391.778 1.113 1.132 1.884 1.071.771-.06 1.592-.536 2.257-1.306.631-.765 1.683-1.084 2.378-1.503.348-.199.629-.469.649-.853.023-.4-.2-.811-.714-1.376v-.097l-.003-.003c-.17-.2-.25-.535-.338-.926-.085-.401-.182-.786-.492-1.046h-.003c-.059-.054-.123-.067-.188-.135a.357.357 0 00-.19-.064c.431-1.278.264-2.55-.173-3.694-.533-1.41-1.465-2.638-2.175-3.483-.796-1.005-1.576-1.957-1.56-3.368.026-2.152.236-6.133-3.544-6.139zm.529 3.405h.013c.213 0 .396.062.584.198.19.135.33.332.438.533.105.259.158.459.166.724 0-.02.006-.04.006-.06v.105a.086.086 0 01-.004-.021l-.004-.024a1.807 1.807 0 01-.15.706.953.953 0 01-.213.335.71.71 0 00-.088-.042c-.104-.045-.198-.064-.284-.133a1.312 1.312 0 00-.22-.066c.05-.06.146-.133.183-.198.053-.128.082-.264.088-.402v-.02a1.21 1.21 0 00-.061-.4c-.045-.134-.101-.2-.183-.333-.084-.066-.167-.132-.267-.132h-.016c-.093 0-.176.03-.262.132a.8.8 0 00-.205.334 1.18 1.18 0 00-.09.4v.019c.002.089.008.179.02.267-.193-.067-.438-.135-.607-.202a1.635 1.635 0 01-.018-.2v-.02a1.772 1.772 0 01.15-.768c.082-.22.232-.406.43-.533a.985.985 0 01.594-.2zm-2.962.059h.036c.142 0 .27.048.399.135.146.129.264.288.344.465.09.199.14.4.153.667v.004c.007.134.006.2-.002.266v.08c-.03.007-.056.018-.083.024-.152.055-.274.135-.393.2.012-.09.013-.18.003-.267v-.015c-.012-.133-.04-.2-.082-.333a.613.613 0 00-.166-.267.248.248 0 00-.183-.064h-.021c-.071.006-.13.04-.186.132a.552.552 0 00-.12.27.944.944 0 00-.023.33v.015c.012.135.037.2.08.334.046.134.098.2.166.268.01.009.02.018.034.024-.07.057-.117.07-.176.136a.304.304 0 01-.131.068 2.62 2.62 0 01-.275-.402 1.772 1.772 0 01-.155-.667 1.759 1.759 0 01.08-.668 1.43 1.43 0 01.283-.535c.128-.133.26-.2.418-.2zm1.37 1.706c.332 0 .733.065 1.216.399.293.2.523.269 1.052.468h.003c.255.136.405.266.478.399v-.131a.571.571 0 01.016.47c-.123.31-.516.643-1.063.842v.002c-.268.135-.501.333-.775.465-.276.135-.588.292-1.012.267a1.139 1.139 0 01-.448-.067 3.566 3.566 0 01-.322-.198c-.195-.135-.363-.332-.612-.465v-.005h-.005c-.4-.246-.616-.512-.686-.71-.07-.268-.005-.47.193-.6.224-.135.38-.271.483-.336.104-.074.143-.102.176-.131h.002v-.003c.169-.202.436-.47.839-.601.139-.036.294-.065.466-.065zm2.8 2.142c.358 1.417 1.196 3.475 1.735 4.473.286.534.855 1.659 1.102 3.024.156-.005.33.018.513.064.646-1.671-.546-3.467-1.089-3.966-.22-.2-.232-.335-.123-.335.59.534 1.365 1.572 1.646 2.757.13.535.16 1.104.021 1.67.067.028.135.06.205.067 1.032.534 1.413.938 1.23 1.537v-.043c-.06-.003-.12 0-.18 0h-.016c.151-.467-.182-.825-1.065-1.224-.915-.4-1.646-.336-1.77.465-.008.043-.013.066-.018.135-.068.023-.139.053-.209.064-.43.268-.662.669-.793 1.187-.13.533-.17 1.156-.205 1.869v.003c-.02.334-.17.838-.319 1.35-1.5 1.072-3.58 1.538-5.348.334a2.645 2.645 0 00-.402-.533 1.45 1.45 0 00-.275-.333c.182 0 .338-.03.465-.067a.615.615 0 00.314-.334c.108-.267 0-.697-.345-1.163-.345-.467-.931-.995-1.788-1.521-.63-.4-.986-.87-1.15-1.396-.165-.534-.143-1.085-.015-1.645.245-1.07.873-2.11 1.274-2.763.107-.065.037.135-.408.974-.396.751-1.14 2.497-.122 3.854a8.123 8.123 0 01.647-2.876c.564-1.278 1.743-3.504 1.836-5.268.048.036.217.135.289.202.218.133.38.333.59.465.21.201.477.335.876.335.039.003.075.006.11.006.412 0 .73-.134.997-.268.29-.134.52-.334.74-.4h.005c.467-.135.835-.402 1.044-.7zm2.185 8.958c.037.6.343 1.245.882 1.377.588.134 1.434-.333 1.791-.765l.211-.01c.315-.007.577.01.847.268l.003.003c.208.199.305.53.391.876.085.4.154.78.409 1.066.486.527.645.906.636 1.14l.003-.007v.018l-.003-.012c-.015.262-.185.396-.498.595-.63.401-1.746.712-2.457 1.57-.618.737-1.37 1.14-2.036 1.191-.664.053-1.237-.2-1.574-.898l-.005-.003c-.21-.4-.12-1.025.056-1.69.176-.668.428-1.344.463-1.897.037-.714.076-1.335.195-1.814.12-.465.308-.797.641-.984l.045-.022zm-10.814.049h.01c.053 0 .105.005.157.014.376.055.706.333 1.023.752l.91 1.664.003.003c.243.533.754 1.064 1.189 1.637.434.598.77 1.131.729 1.57v.006c-.057.744-.48 1.148-1.125 1.294-.645.135-1.52.002-2.395-.464-.968-.536-2.118-.469-2.857-.602-.369-.066-.61-.2-.723-.4-.11-.2-.113-.602.123-1.23v-.004l.002-.003c.117-.334.03-.752-.027-1.118-.055-.401-.083-.71.043-.94.16-.334.396-.4.69-.533.294-.135.64-.202.915-.47h.002v-.002c.256-.268.445-.601.668-.838.19-.201.38-.336.663-.336zm7.159-9.074c-.435.201-.945.535-1.488.535-.542 0-.97-.267-1.28-.466-.154-.134-.28-.268-.373-.335-.164-.134-.144-.333-.074-.333.109.016.129.134.199.2.096.066.215.2.36.333.292.2.68.467 1.167.467.485 0 1.053-.267 1.398-.466.195-.135.445-.334.648-.467.156-.136.149-.267.279-.267.128.016.034.134-.147.332a8.097 8.097 0 01-.69.468zm-1.082-1.583V5.64c-.006-.02.013-.042.029-.05.074-.043.18-.027.26.004.063 0 .16.067.15.135-.006.049-.085.066-.135.066-.055 0-.092-.043-.141-.068-.052-.018-.146-.008-.163-.065zm-.551 0c-.02.058-.113.049-.166.066-.047.025-.086.068-.14.068-.05 0-.13-.02-.136-.068-.01-.066.088-.133.15-.133.08-.031.184-.047.259-.005.019.009.036.03.03.05v.02h.003z\"/></svg>",
50
+ "brand-raspberrypi": "<svg fill=\"currentColor\" role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m19.8955 10.8961-.1726-.3028c.0068-2.1746-1.0022-3.061-2.1788-3.7348.356-.0938.7237-.1711.8245-.6182.6118-.1566.7397-.4398.8011-.7398.16-.1066.6955-.4061.6394-.9211.2998-.2069.4669-.4725.3819-.8487.3222-.3515.407-.6419.2702-.9096.3868-.4805.2152-.7295.05-.9817.2897-.5254.0341-1.0887-.7758-.9944-.3221-.4733-1.0244-.3659-1.133-.3637-.1215-.1519-.2819-.2821-.7755-.219-.3197-.2851-.6771-.2364-1.0458-.0964-.4378-.3403-.7275-.0675-1.0584.0356-.53-.1706-.6513.0631-.9117.1583-.5781-.1203-.7538.1416-1.0309.4182l-.3224-.0063c-.8719.5061-1.305 1.5366-1.4585 2.0664-.1536-.5299-.5858-1.5604-1.4575-2.0664l-.3223.0063C9.942.5014 9.7663.2394 9.1883.3597 8.9279.2646 8.807.0309 8.2766.2015c-.2172-.0677-.417-.2084-.6522-.2012l.0004.0002C7.5017.0041 7.369.049 7.2185.166c-.3688-.1401-.7262-.1887-1.0459.0964-.4936-.0631-.654.0671-.7756.219C5.2887.4791 4.5862.3717 4.264.845c-.8096-.0943-1.0655.4691-.7756.9944-.1653.2521-.3366.5013.05.9819-.1367.2677-.0519.5581.2703.9096-.085.3763.0822.6418.3819.8487-.0561.515.4795.8144.6394.9211.0614.3001.1894.5832.8011.7398.1008.4472.4685.5244.8245.6183-1.1766.6737-2.1856 1.56-2.1788 3.7348l-.1724.3028c-1.3491.8082-2.5629 3.4056-.6648 5.5167.124.6609.3319 1.1355.5171 1.6609.2769 2.117 2.0841 3.1082 2.5608 3.2255.6984.524 1.4423 1.0212 2.449 1.3696.949.964 1.977 1.3314 3.0107 1.3308.0152 0 .0306.0002.0457 0 1.0337.0006 2.0618-.3668 3.0107-1.3308 1.0067-.3483 1.7506-.8456 2.4491-1.3696.4766-.1173 2.2838-1.1085 2.5607-3.2255.1851-.5253.3931-1 .517-1.6609 1.8981-2.1113.6843-4.7089-.6649-5.517zm-1.0386-.3715c-.0704.8759-4.6354-3.0504-3.8472-3.1808 2.1391-.3558 3.9191.896 3.8472 3.1808zm-2.0155 4.3649c-1.1481.7409-2.8025.2626-3.6953-1.0681-.8928-1.3306-.6858-3.0101.4623-3.7509 1.1481-.7409 2.8025-.2627 3.6953 1.068.8927 1.3307.6858 3.0101-.4623 3.751zM13.6591 1.3721c.0396.1967.0843.321.1354.3577.2537-.272.4611-.5506.7878-.8123.0011.1537-.0776.3205.1169.4425.1752-.2356.4119-.4459.7263-.6244-.1514.2611-.026.3404.0554.4486.24-.2059.4681-.4144.9109-.5759-.121.1474-.2902.2914-.1108.4607.2473-.1544.496-.3086 1.0833-.4183-.1323.1475-.4059.295-.2401.4426.3104-.1186.6539-.2047 1.034-.2546-.182.1496-.3337.2963-.1846.4122.3323-.1022.7899-.2398 1.2372-.1212l-.2832.2849c-.0314.0382.6623.0297 1.1202.0364-.167.2321-.3375.4562-.437.8548.0454.0459.2723.0204.4862 0-.2194.4618-.6004.5783-.6893.776.134.1015.32.075.5232.006-.158.3254-.4892.5484-.7509.8123.0662.047.1818.075.4555.0425-.2418.257-.5339.492-.8802.7032.0614.0708.2722.0681.4678.0727-.3136.3069-.7173.466-1.0955.6668.1885.1288.3234.0988.4678.097-.2676.2198-.7225.3342-1.1448.4668.0803.1249.1607.1589.3324.194-.447.2473-1.0873.1343-1.2679.2607.0435.1243.1665.2053.3139.2728-.7197.0418-2.6879-.0262-3.0652-1.5156.7367-.8094 2.0813-1.7593 4.394-2.934-1.7994.6022-3.4229 1.405-4.7817 2.5096-1.5978-.7436-.4965-2.6197.283-3.3645zm-1.6126 5.3718c1.1329-.0123 2.5356.8325 2.53 1.6286-.005.7027-.9851 1.2715-2.5213 1.2607-1.5043-.0177-2.5172-.7148-2.5137-1.3957.003-.5603 1.2282-1.5263 2.505-1.4936zm-5.7646-.6006c.1717-.0351.252-.0692.3323-.194-.4223-.1327-.8772-.247-1.1448-.4668.1444.0018.2792.0318.4678-.097-.3783-.2008-.782-.3599-1.0956-.6668.1955-.0048.4064-.002.4677-.0728-.3462-.2113-.6383-.4463-.8801-.7033.2738.0325.3893.0045.4555-.0425-.2617-.264-.593-.487-.7509-.8123.2032.069.3892.0954.5232-.006-.089-.1977-.47-.3142-.6894-.776.214.0204.4409.0459.4863 0-.0994-.3985-.2698-.6226-.4369-.8547.4579-.0067 1.1516.0018 1.1202-.0364l-.2831-.2849c.4472-.1186.9049.019 1.2371.1213.1492-.1159-.0026-.2626-.1847-.4123.3801.05.7236.1361 1.034.2547.1659-.1476-.1076-.2951-.24-.4426.5872.1097.8361.2639 1.0833.4183.1794-.1694.0103-.3133-.1108-.4607.4428.1615.6709.37.911.5759.0814-.1082.2068-.1875.0554-.4486.3143.1785.5511.3888.7263.6244.1945-.122.1159-.2888.1169-.4426.3267.2618.534.5404.7879.8124.0511-.0366.0959-.161.1354-.3577.7794.7448 1.8807 2.6208.2831 3.3646-1.3589-1.1039-2.9817-1.9064-4.78-2.5086 2.3115 1.174 3.6556 2.1239 4.392 2.9328-.3773 1.4895-2.3455 1.5575-3.0651 1.5157.1473-.0676.2703-.1485.3139-.2728-.1806-.1264-.8209-.0134-1.2679-.2607zm2.8175 1.1334c.7881.1304-3.7769 4.0567-3.8472 3.1809-.0719-2.2846 1.7079-3.5367 3.8472-3.1809zm-4.847 8.7567c-1.1094-.8789-1.4668-3.4529.5901-4.6097 1.2394-.3273.4184 5.051-.5901 4.6097zm4.2656 4.5989c-.6257.3719-2.1452.2187-3.2252-1.3095-.7283-1.2823-.6345-2.5872-.123-2.9705.7648-.4589 1.9464.1609 2.8559 1.2003.7923.9405 1.1536 2.5927.4923 3.0797zm-1.2415-5.6086c-1.1481-.7409-1.3551-2.4203-.4623-3.7511.8928-1.3307 2.5472-1.8089 3.6952-1.068 1.1481.7409 1.3551 2.4203.4623 3.7509-.8926 1.3308-2.5471 1.809-3.6952 1.0682zm4.7948 8.2279c-1.3763.0584-2.7258-1.1105-2.7081-1.5157-.0206-.594 1.6758-1.0578 2.782-1.0306 1.1131-.0479 2.6068.3531 2.6097.8851.0184.5166-1.3547 1.6838-2.6836 1.6612zm2.7584-5.8578c.0081 1.3899-1.226 2.5225-2.7562 2.5299-1.5302.0073-2.7773-1.1135-2.7854-2.5033v-.0265c-.008-1.3899 1.2259-2.5226 2.7562-2.5299 1.5302-.0073 2.7773 1.1134 2.7853 2.5033a.7794.7794 0 0 1 .0001.0265zm3.855 2.0029c-1.186 1.6208-2.7916 1.684-3.3896 1.2325-.6255-.5811-.148-2.3854.7094-3.3747v-.0003c.9812-1.0912 2.0302-1.8037 2.7609-1.2469.4919.4828.7805 2.3008-.0807 3.3894zm1.0724-3.4301c-1.0086.4413-1.8298-4.9372-.5901-4.61 2.0568 1.1569 1.6994 3.731.5901 4.61zm-.0256-8.3279h.2985v-.5304h.2986c.1502 0 .2053.0624.2262.2052.0152.1088.0113.2395.0477.3253h.2984c-.0533-.0763-.0515-.2358-.0571-.3213-.0097-.1373-.0513-.2796-.1977-.3176v-.0037c.1502-.061.2149-.1807.2149-.341 0-.2048-.1539-.3738-.3974-.3738h-.732v1.3573zm.2985-1.1255h.3269c.1333 0 .2054.0573.2054.188 0 .1369-.0721.1942-.2054.1942H20.03v-.3822zm-1.0337.4633c0 .7009.5682 1.2694 1.2695 1.2694s1.2695-.5684 1.2695-1.2694c0-.7013-.5683-1.2697-1.2695-1.2697-.7013 0-1.2695.5684-1.2695 1.2697zm2.3275 0c0 .5845-.4737 1.058-1.058 1.058s-1.058-.4735-1.058-1.058c0-.5849.4737-1.058 1.058-1.058s1.058.4731 1.058 1.058z\"/></svg>",
51
+ "brand-nvidia": "<svg fill=\"currentColor\" role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M8.948 8.798v-1.43a6.7 6.7 0 0 1 .424-.018c3.922-.124 6.493 3.374 6.493 3.374s-2.774 3.851-5.75 3.851c-.398 0-.787-.062-1.158-.185v-4.346c1.528.185 1.837.857 2.747 2.385l2.04-1.714s-1.492-1.952-4-1.952a6.016 6.016 0 0 0-.796.035m0-4.735v2.138l.424-.027c5.45-.185 9.01 4.47 9.01 4.47s-4.08 4.964-8.33 4.964c-.37 0-.733-.035-1.095-.097v1.325c.3.035.61.062.91.062 3.957 0 6.82-2.023 9.593-4.408.459.371 2.34 1.263 2.73 1.652-2.633 2.208-8.772 3.984-12.253 3.984-.335 0-.653-.018-.971-.053v1.864H24V4.063zm0 10.326v1.131c-3.657-.654-4.673-4.46-4.673-4.46s1.758-1.944 4.673-2.262v1.237H8.94c-1.528-.186-2.73 1.245-2.73 1.245s.68 2.412 2.739 3.11M2.456 10.9s2.164-3.197 6.5-3.533V6.201C4.153 6.59 0 10.653 0 10.653s2.35 6.802 8.948 7.42v-1.237c-4.84-.6-6.492-5.936-6.492-5.936z\"/></svg>",
52
+ "brand-amd": "<svg fill=\"currentColor\" role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M18.324 9.137l1.559 1.56h2.556v2.557L24 14.814V9.137zM2 9.52l-2 4.96h1.309l.37-.982H3.9l.408.982h1.338L3.432 9.52zm4.209 0v4.955h1.238v-3.092l1.338 1.562h.188l1.338-1.556v3.091h1.238V9.52H10.47l-1.592 1.845L7.287 9.52zm6.283 0v4.96h2.057c1.979 0 2.88-1.046 2.88-2.472 0-1.36-.937-2.488-2.747-2.488zm1.237.91h.792c1.17 0 1.63.711 1.63 1.57 0 .728-.372 1.572-1.616 1.572h-.806zm-10.985.273l.791 1.932H2.008zm17.137.307l-1.604 1.603v2.25h2.246l1.604-1.607h-2.246z\"/></svg>",
53
+ "brand-intel": "<svg fill=\"currentColor\" role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M20.42 7.345v9.18h1.651v-9.18zM0 7.475v1.737h1.737V7.474zm9.78.352v6.053c0 .513.044.945.13 1.292.087.34.235.618.44.828.203.21.475.359.803.451.334.093.754.136 1.255.136h.216v-1.533c-.24 0-.445-.012-.593-.037a.672.672 0 0 1-.39-.173.693.693 0 0 1-.173-.377 4.002 4.002 0 0 1-.037-.606v-2.182h1.193v-1.416h-1.193V7.827zm-3.505 2.312c-.396 0-.76.08-1.082.241-.327.161-.6.384-.822.668l-.087.117v-.902H2.658v6.256h1.639v-3.214c.018-.588.16-1.02.433-1.299.29-.297.642-.445 1.044-.445.476 0 .841.149 1.082.433.235.284.359.686.359 1.2v3.324h1.663V12.97c.006-.89-.229-1.595-.686-2.09-.458-.495-1.1-.742-1.917-.742zm10.065.006a3.252 3.252 0 0 0-2.306.946c-.29.29-.525.637-.692 1.033a3.145 3.145 0 0 0-.254 1.273c0 .452.08.878.241 1.274.161.395.39.742.674 1.032.284.29.637.526 1.045.693.408.173.86.26 1.342.26 1.397 0 2.262-.637 2.782-1.23l-1.187-.904c-.248.297-.841.699-1.583.699-.464 0-.847-.105-1.138-.321a1.588 1.588 0 0 1-.593-.872l-.019-.056h4.915v-.587c0-.451-.08-.872-.235-1.267a3.393 3.393 0 0 0-.661-1.033 3.013 3.013 0 0 0-1.02-.692 3.345 3.345 0 0 0-1.311-.248zm-16.297.118v6.256h1.651v-6.256zm16.278 1.286c1.132 0 1.664.797 1.664 1.255l-3.32.006c0-.458.525-1.255 1.656-1.261zm7.073 3.814a.606.606 0 0 0-.606.606.606.606 0 0 0 .606.606.606.606 0 0 0 .606-.606.606.606 0 0 0-.606-.606zm-.008.105a.5.5 0 0 1 .002 0 .5.5 0 0 1 .5.501.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .498-.5zm-.233.155v.699h.13v-.285h.093l.173.285h.136l-.18-.297a.191.191 0 0 0 .118-.056c.03-.03.05-.074.05-.136 0-.068-.02-.117-.063-.154-.037-.038-.105-.056-.185-.056zm.13.099h.154c.019 0 .037.006.056.012a.064.064 0 0 1 .037.031c.013.013.012.031.012.056a.124.124 0 0 1-.012.055.164.164 0 0 1-.037.031c-.019.006-.037.013-.056.013h-.154Z\"/></svg>"
54
+ };
static/index.html ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>FitCheck — what AI can your computer run?</title>
7
+ <meta name="description" content="Tell FitCheck about your computer in plain words. Get an honest answer about what AI you can actually run on it, from chatbots to object detection, and how to start." />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@600;700;800&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="/static/style.css" />
11
+ </head>
12
+ <body>
13
+ <header class="topbar">
14
+ <div class="logo">
15
+ <span class="logo-mark"><span class="ic" data-ic="check"></span></span>
16
+ <span>Fit<span class="tick">Check</span></span>
17
+ </div>
18
+ <div class="spacer"></div>
19
+ <nav>
20
+ <a href="#how">How it works</a>
21
+ <a href="#glossary-anchor">Jargon, explained</a>
22
+ </nav>
23
+ </header>
24
+
25
+ <main class="container">
26
+ <section class="hero">
27
+ <h1>What AI can <span class="accent">your computer</span> actually run?</h1>
28
+ <p>Tell FitCheck about your setup in plain words. It tells you honestly what you
29
+ can run today, from chatbots to object detection, what it will feel like, and exactly how to start.</p>
30
+ </section>
31
+
32
+ <div class="layout">
33
+ <!-- ============================== INPUTS ============================== -->
34
+ <aside class="panel form-panel">
35
+ <!-- Step 1: machine -->
36
+ <div class="step">
37
+ <div class="step-head"><span class="step-num">1</span><h2>Your computer</h2></div>
38
+
39
+ <div class="field">
40
+ <span class="label">What kind of computer?</span>
41
+ <div class="segmented" id="computer-seg">
42
+ <button class="seg-btn active" data-val="Windows laptop"><span class="ic brand" data-ic="brand-windows"></span>Windows laptop</button>
43
+ <button class="seg-btn" data-val="Windows desktop"><span class="ic brand" data-ic="brand-windows"></span>Windows desktop</button>
44
+ <button class="seg-btn" data-val="Mac"><span class="ic brand" data-ic="brand-apple"></span>Mac</button>
45
+ <button class="seg-btn" data-val="Linux PC"><span class="ic brand" data-ic="brand-linux"></span>Linux PC</button>
46
+ <button class="seg-btn" data-val="Mini PC / Raspberry Pi" style="grid-column:1/-1"><span class="ic brand" data-ic="brand-raspberrypi"></span>Mini PC / Raspberry Pi</button>
47
+ </div>
48
+ </div>
49
+
50
+ <div class="field">
51
+ <span class="label">Memory (RAM)</span>
52
+ <select id="ram">
53
+ <option>2 GB</option><option>4 GB</option><option>8 GB</option>
54
+ <option selected>16 GB</option><option>24 GB</option><option>32 GB</option>
55
+ <option>48 GB</option><option>64 GB</option><option>96 GB</option><option>128 GB</option>
56
+ </select>
57
+ <div class="hint">Not sure? See “How do I find my specs?” below.</div>
58
+ </div>
59
+
60
+ <div class="field" id="provider-field">
61
+ <span class="label">Graphics (GPU)</span>
62
+ <div class="segmented cols-3" id="provider-seg">
63
+ <button class="seg-btn active" data-val="none">None / built-in</button>
64
+ <button class="seg-btn" data-val="nvidia"><span class="ic brand" data-ic="brand-nvidia"></span>NVIDIA</button>
65
+ <button class="seg-btn" data-val="amd"><span class="ic brand" data-ic="brand-amd"></span>AMD</button>
66
+ <button class="seg-btn" data-val="apple"><span class="ic brand" data-ic="brand-apple"></span>Apple</button>
67
+ <button class="seg-btn" data-val="intel"><span class="ic brand" data-ic="brand-intel"></span>Intel</button>
68
+ <button class="seg-btn" data-val="unsure">Not sure</button>
69
+ </div>
70
+ <select id="gpu" style="margin-top:var(--s-3); display:none"></select>
71
+ </div>
72
+
73
+ <details class="disc" id="advanced-disc">
74
+ <summary>Advanced: enter exact numbers <span class="chev ic" data-ic="chevron"></span></summary>
75
+ <div class="disc-body">
76
+ <div class="field">
77
+ <span class="label">Graphics memory (VRAM), if you know it</span>
78
+ <input type="number" id="vram" placeholder="e.g. 8" min="0" max="200" step="1" />
79
+ <div class="hint">Overrides the GPU picker above. Leave blank to use the picker.</div>
80
+ </div>
81
+ <div class="field" style="margin-bottom:0">
82
+ <span class="label">Or paste / describe your specs</span>
83
+ <textarea id="paste" placeholder="Paste output from 'dxdiag' or 'Task Manager → Performance', or just describe it: 'Dell XPS, RTX 3050, 16GB'…"></textarea>
84
+ </div>
85
+ </div>
86
+ </details>
87
+ </div>
88
+
89
+ <!-- Step 2: goal -->
90
+ <div class="step">
91
+ <div class="step-head"><span class="step-num">2</span><h2>What do you want to do?</h2></div>
92
+ <div id="usecase-picker"><!-- rendered by app.js --></div>
93
+ <div class="field" id="custom-uc-field" style="display:none; margin-top:var(--s-3)">
94
+ <span class="label">Describe what you want to build</span>
95
+ <input type="text" id="custom-uc" placeholder="e.g. real-time hand tracking from my webcam" />
96
+ </div>
97
+ </div>
98
+
99
+ <!-- Step 3: preference -->
100
+ <div class="step">
101
+ <div class="step-head"><span class="step-num">3</span><h2>What matters most? <span class="optional">(optional)</span></h2></div>
102
+ <div class="field" style="margin-bottom:0">
103
+ <div class="segmented" id="priority-seg">
104
+ <button class="seg-btn active" data-val="balanced"><span class="ic" data-ic="balanced"></span>Balanced</button>
105
+ <button class="seg-btn" data-val="quality"><span class="ic" data-ic="quality"></span>Best quality</button>
106
+ <button class="seg-btn" data-val="speed"><span class="ic" data-ic="speed"></span>Fastest</button>
107
+ <button class="seg-btn" data-val="offline"><span class="ic" data-ic="offline"></span>Fully offline</button>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ <button class="cta" id="check-btn">Check my setup <span class="ic" data-ic="arrow"></span></button>
113
+
114
+ <details class="disc" id="find-specs">
115
+ <summary><span class="ic sum-ic" data-ic="search"></span>How do I find my specs? <span class="chev ic" data-ic="chevron"></span></summary>
116
+ <div class="disc-body" id="find-specs-body"><!-- filled by app.js per computer type --></div>
117
+ </details>
118
+
119
+ <span id="glossary-anchor"></span>
120
+ <details class="disc" id="glossary">
121
+ <summary><span class="ic sum-ic" data-ic="book"></span>What do these words mean? <span class="chev ic" data-ic="chevron"></span></summary>
122
+ <div class="disc-body">
123
+ <dl>
124
+ <div><dt>Model</dt><dd>The AI’s “brain”. Bigger means smarter, but heavier.</dd></div>
125
+ <div><dt>Parameters (e.g. 7B)</dt><dd>How big the brain is. 7B is 7 billion. More means smarter and hungrier for memory.</dd></div>
126
+ <div><dt>Quantisation (4-bit, 8-bit)</dt><dd>Shrinking a model so it fits. 4-bit is the popular sweet spot: much smaller, barely-noticeable quality loss.</dd></div>
127
+ <div><dt>VRAM</dt><dd>The fast memory on a graphics card. The biggest factor in what runs quickly.</dd></div>
128
+ <div><dt>RAM</dt><dd>Your computer’s normal memory. Models can use it too, but it is slower.</dd></div>
129
+ <div><dt>GGUF</dt><dd>A single-file model format made for running locally.</dd></div>
130
+ </dl>
131
+ </div>
132
+ </details>
133
+ </aside>
134
+
135
+ <!-- ============================== RESULTS ============================== -->
136
+ <section class="panel results-panel" id="results">
137
+ <div class="empty-state">
138
+ <div class="big"><span class="ic" data-ic="monitor"></span></div>
139
+ <h2>Your honest answer appears here</h2>
140
+ <p>Fill in your computer and what you want to do, then press <b>Check my setup</b>. Results update as you change things.</p>
141
+ </div>
142
+ </section>
143
+ </div>
144
+
145
+ <footer class="foot" id="how">
146
+ <p>FitCheck gives <b>conservative</b> estimates from a transparent rules engine.
147
+ It would rather under-promise than over-promise. Real speed depends on your exact chip, drivers and settings.</p>
148
+ <p style="margin-top:8px">Catalogue last updated <b id="cat-version">—</b>. Built for the Hugging Face Build Small hackathon.</p>
149
+ </footer>
150
+ </main>
151
+
152
+ <script src="/static/icons.js"></script>
153
+ <script src="/static/app.js"></script>
154
+ </body>
155
+ </html>
static/style.css ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================
2
+ FitCheck — design system
3
+ Dark-first. Every text/status colour clears WCAG AA on the
4
+ card surface (#181B20). Tokens verified by research agent.
5
+ ============================================================ */
6
+
7
+ :root {
8
+ /* Background layers */
9
+ --bg-base: #0F1115;
10
+ --bg-raised: #181B20;
11
+ --bg-inset: #22262E;
12
+ --border: #2A2F38;
13
+ --border-hi: #3A4150;
14
+
15
+ /* Text */
16
+ --text-primary: #ECEEF2;
17
+ --text-secondary: #B4BAC4;
18
+ --text-muted: #868E9C;
19
+
20
+ /* Accent — warm indigo */
21
+ --accent: #818CF8;
22
+ --accent-strong: #6366F1;
23
+ --accent-soft: rgba(129,140,248,0.14);
24
+
25
+ /* Status */
26
+ --ok: #4ADE80;
27
+ --warn: #FBBF24;
28
+ --no: #F87171;
29
+ --ok-soft: rgba(74,222,128,0.13);
30
+ --warn-soft: rgba(251,191,36,0.13);
31
+ --no-soft: rgba(248,113,113,0.13);
32
+
33
+ /* Spacing scale (4px base) */
34
+ --s-1:4px; --s-2:8px; --s-3:12px; --s-4:16px;
35
+ --s-5:24px; --s-6:32px; --s-7:48px; --s-8:64px;
36
+
37
+ /* Radius */
38
+ --r-sm:8px; --r-md:14px; --r-lg:20px; --r-pill:999px;
39
+
40
+ /* Shadow */
41
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.30);
42
+ --shadow-md: 0 4px 16px rgba(0,0,0,0.35);
43
+ --shadow-lg: 0 12px 32px rgba(0,0,0,0.45);
44
+ --glow: 0 0 0 1px var(--accent-soft), 0 8px 24px rgba(99,102,241,0.22);
45
+
46
+ --font-head: 'Plus Jakarta Sans', system-ui, sans-serif;
47
+ --font-body: 'Inter', system-ui, sans-serif;
48
+ }
49
+
50
+ *, *::before, *::after { box-sizing: border-box; }
51
+ * { margin: 0; }
52
+
53
+ html { scroll-behavior: smooth; }
54
+ body {
55
+ font-family: var(--font-body);
56
+ background:
57
+ radial-gradient(1200px 600px at 80% -10%, rgba(99,102,241,0.10), transparent 60%),
58
+ radial-gradient(1000px 500px at -10% 10%, rgba(74,222,128,0.05), transparent 55%),
59
+ var(--bg-base);
60
+ color: var(--text-primary);
61
+ font-size: 16px;
62
+ line-height: 1.6;
63
+ -webkit-font-smoothing: antialiased;
64
+ min-height: 100vh;
65
+ }
66
+
67
+ h1,h2,h3 { font-family: var(--font-head); line-height: 1.2; letter-spacing: -0.02em; }
68
+ a { color: var(--accent); text-decoration: none; }
69
+ a:hover { text-decoration: underline; }
70
+ button { font-family: inherit; cursor: pointer; }
71
+
72
+ .label {
73
+ display: block;
74
+ font-size: 13px; font-weight: 600;
75
+ text-transform: uppercase; letter-spacing: 0.04em;
76
+ color: var(--text-secondary);
77
+ margin-bottom: var(--s-2);
78
+ }
79
+ .hint { font-size: 13px; color: var(--text-muted); margin-top: var(--s-1); }
80
+
81
+ /* Icons (inline SVG; lucide uses stroke, brand logos use fill) */
82
+ .ic { display: inline-flex; align-items: center; justify-content: center; line-height: 0; flex: none; }
83
+ .ic svg { width: 1em; height: 1em; display: block; }
84
+ .optional { font-weight: 500; color: var(--text-muted); font-size: 13px; }
85
+
86
+ /* ----------------------------------------------------------- Topbar */
87
+ .topbar {
88
+ display: flex; align-items: center; gap: var(--s-3);
89
+ padding: var(--s-4) var(--s-5);
90
+ max-width: 1180px; margin: 0 auto;
91
+ }
92
+ .logo {
93
+ display: flex; align-items: center; gap: var(--s-2);
94
+ font-family: var(--font-head); font-weight: 800; font-size: 20px;
95
+ letter-spacing: -0.03em;
96
+ }
97
+ .logo-mark {
98
+ width: 32px; height: 32px; border-radius: 9px;
99
+ background: linear-gradient(135deg, var(--accent), var(--accent-strong));
100
+ display: grid; place-items: center;
101
+ box-shadow: var(--glow);
102
+ color: #fff;
103
+ }
104
+ .logo-mark .ic { font-size: 18px; }
105
+ .logo-mark .ic svg { stroke-width: 3; }
106
+ .logo .tick { color: var(--ok); }
107
+ .topbar .spacer { flex: 1; }
108
+ .topbar nav a { color: var(--text-secondary); font-size: 14px; font-weight: 500; margin-left: var(--s-4); }
109
+ .topbar nav a:hover { color: var(--text-primary); text-decoration: none; }
110
+
111
+ /* ----------------------------------------------------------- Layout */
112
+ .container { max-width: 1180px; margin: 0 auto; padding: 0 var(--s-5) var(--s-8); }
113
+
114
+ .hero { text-align: center; padding: var(--s-6) 0 var(--s-7); }
115
+ .hero .eyebrow {
116
+ display: inline-flex; align-items: center; gap: var(--s-2);
117
+ font-size: 13px; font-weight: 600; color: var(--accent);
118
+ background: var(--accent-soft); padding: 5px 12px; border-radius: var(--r-pill);
119
+ margin-bottom: var(--s-4);
120
+ }
121
+ .hero h1 { font-size: clamp(28px, 4vw, 44px); font-weight: 800; }
122
+ .hero h1 .accent { color: var(--accent); }
123
+ .hero p { color: var(--text-secondary); font-size: clamp(15px,1.6vw,18px); max-width: 620px; margin: var(--s-4) auto 0; }
124
+
125
+ .layout {
126
+ display: grid;
127
+ grid-template-columns: 420px 1fr;
128
+ gap: var(--s-5);
129
+ align-items: start;
130
+ }
131
+ @media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
132
+
133
+ .panel {
134
+ background: var(--bg-raised);
135
+ border: 1px solid var(--border);
136
+ border-radius: var(--r-lg);
137
+ box-shadow: var(--shadow-md);
138
+ }
139
+ .form-panel { padding: var(--s-5); position: sticky; top: var(--s-4); }
140
+ @media (max-width: 900px) { .form-panel { position: static; } }
141
+
142
+ .step { margin-bottom: var(--s-5); }
143
+ .step-head { display: flex; align-items: center; gap: var(--s-3); margin-bottom: var(--s-4); }
144
+ .step-num {
145
+ width: 26px; height: 26px; border-radius: var(--r-pill); flex: none;
146
+ background: var(--accent-soft); color: var(--accent);
147
+ display: grid; place-items: center; font-weight: 700; font-size: 14px;
148
+ font-family: var(--font-head);
149
+ }
150
+ .step-head h2 { font-size: 17px; font-weight: 700; }
151
+
152
+ .field { margin-bottom: var(--s-4); }
153
+
154
+ /* Segmented control */
155
+ .segmented { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-2); }
156
+ .segmented.cols-3 { grid-template-columns: repeat(3, 1fr); }
157
+ .seg-btn {
158
+ background: var(--bg-inset); border: 1px solid var(--border);
159
+ color: var(--text-secondary); border-radius: var(--r-sm);
160
+ padding: 10px 12px; font-size: 14px; font-weight: 500;
161
+ display: flex; align-items: center; gap: var(--s-2); justify-content: center;
162
+ transition: transform .15s, border-color .15s, color .15s, background .15s;
163
+ }
164
+ .seg-btn:hover { border-color: var(--border-hi); color: var(--text-primary); }
165
+ .seg-btn.active {
166
+ background: var(--accent-soft); border-color: var(--accent);
167
+ color: var(--text-primary); box-shadow: var(--shadow-sm);
168
+ }
169
+ .seg-btn .ic { font-size: 17px; color: var(--text-muted); }
170
+ .seg-btn.active .ic { color: var(--accent); }
171
+ .seg-btn .ic.brand { color: var(--text-secondary); }
172
+ .seg-btn.active .ic.brand { color: var(--text-primary); }
173
+
174
+ /* Native select / input styling */
175
+ select, input[type="text"], input[type="number"], textarea {
176
+ width: 100%; font-family: inherit; font-size: 15px;
177
+ color: var(--text-primary);
178
+ background: var(--bg-inset);
179
+ border: 1px solid var(--border); border-radius: var(--r-sm);
180
+ padding: 11px 13px;
181
+ transition: border-color .15s, box-shadow .15s;
182
+ }
183
+ select:hover, input:hover, textarea:hover { border-color: var(--border-hi); }
184
+ select:focus, input:focus, textarea:focus {
185
+ outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
186
+ }
187
+ select {
188
+ appearance: none;
189
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23868E9C' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
190
+ background-repeat: no-repeat; background-position: right 12px center;
191
+ padding-right: 38px;
192
+ }
193
+ textarea { resize: vertical; min-height: 70px; line-height: 1.5; }
194
+ .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-3); }
195
+
196
+ /* Use-case picker */
197
+ .uc-group { margin-bottom: var(--s-4); }
198
+ .uc-cat {
199
+ font-size: 12px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
200
+ color: var(--text-muted); margin-bottom: var(--s-2); display: flex; align-items: center; gap: 6px;
201
+ }
202
+ .uc-grid { display: flex; flex-wrap: wrap; gap: var(--s-2); }
203
+ .uc-pill {
204
+ background: var(--bg-inset); border: 1px solid var(--border);
205
+ color: var(--text-secondary); border-radius: var(--r-pill);
206
+ padding: 7px 13px; font-size: 13.5px; font-weight: 500;
207
+ display: inline-flex; align-items: center; gap: 6px;
208
+ transition: transform .15s, border-color .15s, color .15s, background .15s;
209
+ }
210
+ .uc-pill:hover { border-color: var(--border-hi); color: var(--text-primary); transform: translateY(-1px); }
211
+ .uc-pill.active {
212
+ background: var(--accent-soft); border-color: var(--accent); color: var(--text-primary);
213
+ }
214
+ .uc-pill .pic { font-size: 15px; color: var(--text-muted); }
215
+ .uc-pill:hover .pic { color: var(--text-secondary); }
216
+ .uc-pill.active .pic { color: var(--accent); }
217
+ .uc-cat .ic { font-size: 14px; color: var(--text-muted); }
218
+
219
+ /* Disclosure (advanced / accordions) */
220
+ details.disc {
221
+ border: 1px solid var(--border); border-radius: var(--r-md);
222
+ background: var(--bg-inset); margin-top: var(--s-3); overflow: hidden;
223
+ }
224
+ details.disc > summary {
225
+ list-style: none; cursor: pointer; padding: 12px 14px;
226
+ font-size: 14px; font-weight: 600; color: var(--text-secondary);
227
+ display: flex; align-items: center; gap: var(--s-2);
228
+ }
229
+ details.disc > summary::-webkit-details-marker { display: none; }
230
+ details.disc > summary .chev { margin-left: auto; transition: transform .2s; color: var(--text-muted); font-size: 16px; }
231
+ details.disc[open] > summary .chev { transform: rotate(180deg); }
232
+ details.disc > summary .sum-ic { font-size: 16px; color: var(--text-muted); }
233
+ details.disc > summary:hover { color: var(--text-primary); }
234
+ .disc-body { padding: 0 14px 14px; font-size: 14px; color: var(--text-secondary); line-height: 1.6; }
235
+ .disc-body code {
236
+ background: var(--bg-base); border: 1px solid var(--border); border-radius: 5px;
237
+ padding: 1px 6px; font-size: 13px; color: var(--text-primary);
238
+ }
239
+ .disc-body dl { display: grid; gap: var(--s-3); }
240
+ .disc-body dt { font-weight: 700; color: var(--text-primary); font-family: var(--font-head); }
241
+ .disc-body dd { color: var(--text-muted); }
242
+
243
+ /* Primary CTA */
244
+ .cta {
245
+ width: 100%; border: none; border-radius: var(--r-md);
246
+ background: linear-gradient(135deg, var(--accent), var(--accent-strong));
247
+ color: #fff; font-weight: 700; font-size: 16px; font-family: var(--font-head);
248
+ padding: 14px; margin-top: var(--s-2);
249
+ display: flex; align-items: center; justify-content: center; gap: var(--s-2);
250
+ transition: transform .15s, box-shadow .15s, filter .15s;
251
+ }
252
+ .cta:hover { transform: translateY(-2px); box-shadow: var(--glow); filter: brightness(1.05); }
253
+ .cta:active { transform: translateY(0); }
254
+ .cta .ic { font-size: 18px; }
255
+
256
+ /* ----------------------------------------------------------- Results */
257
+ .results-panel { padding: var(--s-5); min-height: 400px; }
258
+
259
+ .empty-state { text-align: center; color: var(--text-muted); padding: var(--s-8) var(--s-4); }
260
+ .empty-state .big { font-size: 46px; margin-bottom: var(--s-3); color: var(--text-muted); display: flex; justify-content: center; }
261
+ .empty-state p { max-width: 360px; margin: var(--s-2) auto 0; }
262
+
263
+ .verdict {
264
+ border: 1px solid var(--border); border-left: 4px solid var(--status, var(--accent));
265
+ border-radius: var(--r-md); background: var(--bg-raised);
266
+ padding: var(--s-5); margin-bottom: var(--s-5);
267
+ }
268
+ .verdict .badge {
269
+ display: inline-flex; align-items: center; gap: 7px;
270
+ padding: 5px 12px; border-radius: var(--r-pill);
271
+ background: var(--status-soft, var(--accent-soft)); color: var(--status, var(--accent));
272
+ font: 700 13px/1 var(--font-head); text-transform: uppercase; letter-spacing: .04em;
273
+ margin-bottom: var(--s-3);
274
+ }
275
+ .verdict .badge .dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
276
+ .verdict h2 { font-size: 22px; margin-bottom: var(--s-3); }
277
+ .verdict p { color: var(--text-secondary); margin-bottom: var(--s-2); }
278
+ .verdict .note {
279
+ font-size: 13.5px; color: var(--text-muted); margin-top: var(--s-3);
280
+ padding-top: var(--s-3); border-top: 1px solid var(--border);
281
+ }
282
+
283
+ .section-title {
284
+ font-size: 15px; font-weight: 700; font-family: var(--font-head);
285
+ color: var(--text-primary); margin: var(--s-5) 0 var(--s-3);
286
+ display: flex; align-items: center; gap: var(--s-2);
287
+ }
288
+ .section-title .sub { font-size: 13px; font-weight: 500; color: var(--text-muted); font-family: var(--font-body); }
289
+
290
+ /* Gauge */
291
+ .gauge-card { background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--s-4) var(--s-4) var(--s-3); }
292
+ .gauge-top { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px; }
293
+ .gauge-top .need { font-weight: 700; color: var(--status, var(--text-primary)); font-family: var(--font-head); }
294
+ .gauge {
295
+ position: relative; height: 16px; border-radius: var(--r-pill);
296
+ background: var(--bg-base); overflow: hidden; border: 1px solid var(--border);
297
+ }
298
+ .gauge-fill {
299
+ height: 100%; width: calc(var(--pct,0) * 1%); border-radius: var(--r-pill);
300
+ background: linear-gradient(90deg, var(--gcolor, var(--accent)), color-mix(in srgb, var(--gcolor, var(--accent)) 65%, #fff));
301
+ transition: width .8s cubic-bezier(.22,1,.36,1), background .4s;
302
+ }
303
+ .gauge-need {
304
+ position: absolute; top: -4px; width: 2px; height: 24px;
305
+ background: var(--text-primary); border-radius: 2px; opacity: .85;
306
+ left: calc(var(--needpct,50)*1%); transition: left .8s cubic-bezier(.22,1,.36,1);
307
+ }
308
+ .gauge-legend { display: flex; flex-wrap: wrap; gap: var(--s-4); margin-top: 12px; font-size: 12.5px; color: var(--text-muted); }
309
+ .gauge-legend .item { display: inline-flex; align-items: center; gap: 6px; }
310
+ .gauge-legend .sw { width: 10px; height: 10px; border-radius: 3px; }
311
+ .gauge-legend .marker { font-weight: 600; color: var(--text-secondary); }
312
+
313
+ /* Option cards (the matrix, re-imagined) */
314
+ .opt-grid { display: grid; gap: var(--s-3); }
315
+ .opt {
316
+ display: grid; grid-template-columns: 40px 1fr auto; align-items: center; gap: var(--s-3);
317
+ background: var(--bg-raised); border: 1px solid var(--border);
318
+ border-left: 4px solid var(--status, var(--border)); border-radius: var(--r-md);
319
+ padding: var(--s-4);
320
+ transition: transform .2s, box-shadow .2s, border-color .2s;
321
+ }
322
+ .opt:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); }
323
+ .opt .vdot {
324
+ width: 30px; height: 30px; border-radius: 50%;
325
+ background: var(--status-soft); color: var(--status);
326
+ display: grid; place-items: center; font-size: 15px; font-weight: 700;
327
+ }
328
+ .opt .name { font-weight: 700; font-family: var(--font-head); font-size: 15px; }
329
+ .opt .desc { font-size: 13px; color: var(--text-muted); margin-top: 2px; }
330
+ .opt .meta { text-align: right; font-size: 13px; color: var(--text-secondary); white-space: nowrap; }
331
+ .opt .meta b { color: var(--text-primary); font-family: var(--font-head); }
332
+ .opt .feel { font-size: 12.5px; color: var(--text-muted); margin-top: 2px; }
333
+
334
+ /* Tool cards */
335
+ .tool-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px,1fr)); gap: var(--s-3); }
336
+ .tool {
337
+ background: var(--bg-raised); border: 1px solid var(--border);
338
+ border-radius: var(--r-md); padding: var(--s-4);
339
+ transition: transform .2s, box-shadow .2s, border-color .2s;
340
+ }
341
+ .tool:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--border-hi); }
342
+ .tool .tool-head { display: flex; align-items: center; justify-content: space-between; }
343
+ .tool .tname { font-weight: 700; font-family: var(--font-head); font-size: 15px; }
344
+ .tool .tag { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; padding: 3px 9px; border-radius: var(--r-pill); }
345
+ .tool .tag.best { background: var(--ok-soft); color: var(--ok); }
346
+ .tool .tag.mid { background: var(--accent-soft); color: var(--accent); }
347
+ .tool .twhat { font-size: 13.5px; color: var(--text-secondary); margin: var(--s-2) 0; line-height: 1.5; }
348
+ .tool .tinstall { font-size: 12.5px; color: var(--text-muted); display: flex; align-items: center; gap: 6px; }
349
+ .tool .tinstall .ic { font-size: 14px; }
350
+
351
+ /* Command block */
352
+ .cmd-intro { font-size: 14px; color: var(--text-secondary); margin: var(--s-2) 0 var(--s-3); }
353
+ .cmd { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-3); }
354
+ @media (max-width: 620px) { .cmd { grid-template-columns: 1fr; } }
355
+ .cmd-box { background: var(--bg-base); border: 1px solid var(--border); border-radius: var(--r-md); overflow: hidden; }
356
+ .cmd-box .cmd-label { font-size: 12px; font-weight: 600; color: var(--text-muted); padding: 10px 13px 0; display:flex; justify-content:space-between; align-items:center; }
357
+ .cmd-box pre { padding: 8px 13px 13px; overflow-x: auto; }
358
+ .cmd-box code { font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; font-size: 13px; color: #a5f3c4; }
359
+ .copy-btn {
360
+ background: var(--bg-inset); border: 1px solid var(--border); color: var(--text-secondary);
361
+ border-radius: 6px; padding: 3px 9px; font-size: 12px; font-weight: 600;
362
+ }
363
+ .copy-btn:hover { color: var(--text-primary); border-color: var(--border-hi); }
364
+ .copy-btn.done { color: var(--ok); border-color: var(--ok); }
365
+
366
+ /* Footer */
367
+ .foot { text-align: center; color: var(--text-muted); font-size: 13px; margin-top: var(--s-7); line-height: 1.6; }
368
+ .foot b { color: var(--text-secondary); }
369
+
370
+ /* Reveal animation */
371
+ .reveal { animation: reveal .5s cubic-bezier(.22,1,.36,1) both; }
372
+ @keyframes reveal { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
373
+
374
+ @media (prefers-reduced-motion: reduce) {
375
+ *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
376
+ }