[object Object]

← back to Exo

Remove build

3475be9e9e8885b6d8da4aacc5f606ab1bae883c · 2024-12-30 18:39:17 -0500 · Pranav Veldurthi

Files touched

Diff

commit 3475be9e9e8885b6d8da4aacc5f606ab1bae883c
Author: Pranav Veldurthi <veldurthipranav@gmail.com>
Date:   Mon Dec 30 18:39:17 2024 -0500

    Remove build
---
 build/lib/exo/__init__.py                          |    1 -
 build/lib/exo/api/__init__.py                      |    1 -
 build/lib/exo/api/chatgpt_api.py                   |  539 ---------
 build/lib/exo/apputil/__init__.py                  |    1 -
 build/lib/exo/apputil/anim.py                      |  161 ---
 build/lib/exo/download/__init__.py                 |    0
 build/lib/exo/download/download_progress.py        |   61 -
 build/lib/exo/download/hf/__init__.py              |    0
 build/lib/exo/download/hf/hf_helpers.py            |  447 --------
 build/lib/exo/download/hf/hf_shard_download.py     |   79 --
 build/lib/exo/download/shard_download.py           |   36 -
 build/lib/exo/helpers.py                           |  274 -----
 build/lib/exo/inference/__init__.py                |    0
 build/lib/exo/inference/debug_inference_engine.py  |   58 -
 build/lib/exo/inference/dummy_inference_engine.py  |   34 -
 build/lib/exo/inference/inference_engine.py        |   58 -
 build/lib/exo/inference/mlx/__init__.py            |    0
 .../mlx/models/StableDiffusionPipeline.py          |  307 -----
 build/lib/exo/inference/mlx/models/__init__.py     |    0
 build/lib/exo/inference/mlx/models/base.py         |    9 -
 build/lib/exo/inference/mlx/models/deepseek_v2.py  |  127 --
 build/lib/exo/inference/mlx/models/gemma2.py       |  118 --
 build/lib/exo/inference/mlx/models/llama.py        |  125 --
 build/lib/exo/inference/mlx/models/llava.py        |  585 ----------
 build/lib/exo/inference/mlx/models/qwen2.py        |  128 ---
 .../exo/inference/mlx/sharded_inference_engine.py  |   77 --
 build/lib/exo/inference/mlx/sharded_utils.py       |  256 -----
 build/lib/exo/inference/mlx/stateful_model.py      |   45 -
 build/lib/exo/inference/mlx/test_sharded_llama.py  |   40 -
 build/lib/exo/inference/mlx/test_sharded_llava.py  |   64 --
 build/lib/exo/inference/mlx/test_sharded_model.py  |   52 -
 build/lib/exo/inference/shard.py                   |   39 -
 .../exo/inference/test_dummy_inference_engine.py   |   53 -
 build/lib/exo/inference/test_inference_engine.py   |   56 -
 build/lib/exo/inference/tinygrad/__init__.py       |    0
 build/lib/exo/inference/tinygrad/inference.py      |   99 --
 .../lib/exo/inference/tinygrad/models/__init__.py  |    0
 build/lib/exo/inference/tinygrad/models/llama.py   |  282 -----
 build/lib/exo/inference/tinygrad/stateful_model.py |   42 -
 .../lib/exo/inference/tinygrad/tinygrad_helpers.py |   52 -
 build/lib/exo/inference/tokenizers.py              |   64 --
 build/lib/exo/main.py                              |  274 -----
 build/lib/exo/models.py                            |  151 ---
 build/lib/exo/networking/__init__.py               |    5 -
 build/lib/exo/networking/discovery.py              |   17 -
 build/lib/exo/networking/grpc/__init__.py          |    0
 build/lib/exo/networking/grpc/grpc_peer_handle.py  |  173 ---
 build/lib/exo/networking/grpc/grpc_server.py       |  147 ---
 build/lib/exo/networking/grpc/node_service_pb2.py  |   78 --
 .../exo/networking/grpc/node_service_pb2_grpc.py   |  360 ------
 build/lib/exo/networking/manual/__init__.py        |    0
 .../lib/exo/networking/manual/manual_discovery.py  |   71 --
 .../networking/manual/network_topology_config.py   |   31 -
 .../exo/networking/manual/test_manual_discovery.py |  103 --
 .../manual/test_network_topology_config.py         |   49 -
 build/lib/exo/networking/peer_handle.py            |   56 -
 build/lib/exo/networking/server.py                 |   11 -
 build/lib/exo/networking/tailscale/__init__.py     |    0
 .../networking/tailscale/tailscale_discovery.py    |  178 ---
 .../exo/networking/tailscale/tailscale_helpers.py  |  125 --
 .../tailscale/test_tailscale_discovery.py          |   43 -
 build/lib/exo/networking/udp/__init__.py           |    0
 build/lib/exo/networking/udp/test_udp_discovery.py |   77 --
 build/lib/exo/networking/udp/udp_discovery.py      |  215 ----
 build/lib/exo/orchestration/__init__.py            |    4 -
 build/lib/exo/orchestration/node.py                |   47 -
 build/lib/exo/orchestration/standard_node.py       |  488 --------
 build/lib/exo/orchestration/test_node.py           |   57 -
 build/lib/exo/stats/__init__.py                    |    0
 build/lib/exo/stats/metrics.py                     |   29 -
 build/lib/exo/test_callbacks.py                    |   50 -
 build/lib/exo/tinychat/common.css                  |  130 ---
 build/lib/exo/tinychat/favicon.svg                 |   25 -
 build/lib/exo/tinychat/index.css                   |  484 --------
 build/lib/exo/tinychat/index.html                  |  255 ----
 build/lib/exo/tinychat/index.js                    |  687 -----------
 .../toolkit@1.0.2/dist/cdn.min.js                  |    1 -
 .../npm/@alpinejs/focus@3.x.x/dist/cdn.min.js      |   15 -
 .../npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js  |    1 -
 .../npm/purecss@3.0.0/build/base-min.css           |   11 -
 .../ajax/libs/font-awesome/6.5.2/css/all.min.css   |    9 -
 .../font-awesome/6.5.2/webfonts/fa-brands-400.ttf  |  Bin 209128 -> 0 bytes
 .../6.5.2/webfonts/fa-brands-400.woff2             |  Bin 117852 -> 0 bytes
 .../font-awesome/6.5.2/webfonts/fa-regular-400.ttf |  Bin 67860 -> 0 bytes
 .../6.5.2/webfonts/fa-regular-400.woff2            |  Bin 25392 -> 0 bytes
 .../font-awesome/6.5.2/webfonts/fa-solid-900.ttf   |  Bin 420332 -> 0 bytes
 .../font-awesome/6.5.2/webfonts/fa-solid-900.woff2 |  Bin 156400 -> 0 bytes
 .../6.5.2/webfonts/fa-v4compatibility.ttf          |  Bin 10832 -> 0 bytes
 .../6.5.2/webfonts/fa-v4compatibility.woff2        |  Bin 4792 -> 0 bytes
 .../exo/tinychat/static/fonts.googleapis.com/css2  |    7 -
 .../cdn-assets@11.9.0/highlight.min.js             | 1213 --------------------
 .../cdn-assets@11.9.0/styles/vs2015.min.css        |    1 -
 .../dist/alpine-autosize.min.js                    |    2 -
 .../unpkg.com/alpinejs@3.x.x/dist/cdn.min.js       |    5 -
 .../unpkg.com/dompurify@3.1.5/dist/purify.min.js   |    3 -
 .../marked-highlight@2.1.2/lib/index.umd.js        |   97 --
 .../static/unpkg.com/marked@13.0.0/marked.min.js   |    6 -
 build/lib/exo/tinychat/update_deps.py              |   93 --
 build/lib/exo/topology/__init__.py                 |    0
 build/lib/exo/topology/device_capabilities.py      |  217 ----
 build/lib/exo/topology/partitioning_strategy.py    |   40 -
 .../ring_memory_weighted_partitioning_strategy.py  |   18 -
 build/lib/exo/topology/test_device_capabilities.py |   91 --
 build/lib/exo/topology/test_map_partitions.py      |   81 --
 ...t_ring_memory_weighted_partitioning_strategy.py |   90 --
 build/lib/exo/topology/topology.py                 |   49 -
 build/lib/exo/viz/__init__.py                      |    0
 build/lib/exo/viz/test_topology_viz.py             |  129 ---
 build/lib/exo/viz/topology_viz.py                  |  307 -----
 109 files changed, 11276 deletions(-)

diff --git a/build/lib/exo/__init__.py b/build/lib/exo/__init__.py
deleted file mode 100644
index 596559f3..00000000
--- a/build/lib/exo/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from exo.helpers import DEBUG as DEBUG, DEBUG_DISCOVERY as DEBUG_DISCOVERY, VERSION as VERSION
\ No newline at end of file
diff --git a/build/lib/exo/api/__init__.py b/build/lib/exo/api/__init__.py
deleted file mode 100644
index 660e7507..00000000
--- a/build/lib/exo/api/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from exo.api.chatgpt_api import ChatGPTAPI as ChatGPTAPI
diff --git a/build/lib/exo/api/chatgpt_api.py b/build/lib/exo/api/chatgpt_api.py
deleted file mode 100644
index ee75aea6..00000000
--- a/build/lib/exo/api/chatgpt_api.py
+++ /dev/null
@@ -1,539 +0,0 @@
-import uuid
-import time
-import asyncio
-import json
-from pathlib import Path
-from transformers import AutoTokenizer
-from typing import List, Literal, Union, Dict
-from aiohttp import web
-import aiohttp_cors
-import traceback
-import signal
-from exo import DEBUG, VERSION
-from exo.download.download_progress import RepoProgressEvent
-from exo.helpers import PrefixDict, shutdown, get_exo_images_dir
-from exo.inference.tokenizers import resolve_tokenizer
-from exo.orchestration import Node
-from exo.models import build_base_shard, model_cards, get_repo, pretty_name, get_supported_models
-from exo.apputil import create_animation_mp4
-from typing import Callable, Optional
-from PIL import Image
-import numpy as np
-import base64
-from io import BytesIO
-import mlx.core as mx
-import tempfile
-
-class Message:
-  def __init__(self, role: str, content: Union[str, List[Dict[str, Union[str, Dict[str, str]]]]]):
-    self.role = role
-    self.content = content
-
-  def to_dict(self):
-    return {"role": self.role, "content": self.content}
-
-
-
-class ChatCompletionRequest:
-  def __init__(self, model: str, messages: List[Message], temperature: float):
-    self.model = model
-    self.messages = messages
-    self.temperature = temperature
-
-  def to_dict(self):
-    return {"model": self.model, "messages": [message.to_dict() for message in self.messages], "temperature": self.temperature}
-
-
-def generate_completion(
-  chat_request: ChatCompletionRequest,
-  tokenizer,
-  prompt: str,
-  request_id: str,
-  tokens: List[int],
-  stream: bool,
-  finish_reason: Union[Literal["length", "stop"], None],
-  object_type: Literal["chat.completion", "text_completion"],
-) -> dict:
-  completion = {
-    "id": f"chatcmpl-{request_id}",
-    "object": object_type,
-    "created": int(time.time()),
-    "model": chat_request.model,
-    "system_fingerprint": f"exo_{VERSION}",
-    "choices": [{
-      "index": 0,
-      "message": {"role": "assistant", "content": tokenizer.decode(tokens)},
-      "logprobs": None,
-      "finish_reason": finish_reason,
-    }],
-  }
-
-  if not stream:
-    completion["usage"] = {
-      "prompt_tokens": len(tokenizer.encode(prompt)),
-      "completion_tokens": len(tokens),
-      "total_tokens": len(tokenizer.encode(prompt)) + len(tokens),
-    }
-
-  choice = completion["choices"][0]
-  if object_type.startswith("chat.completion"):
-    key_name = "delta" if stream else "message"
-    choice[key_name] = {"role": "assistant", "content": tokenizer.decode(tokens)}
-  elif object_type == "text_completion":
-    choice["text"] = tokenizer.decode(tokens)
-  else:
-    ValueError(f"Unsupported response type: {object_type}")
-
-  return completion
-
-
-def remap_messages(messages: List[Message]) -> List[Message]:
-  remapped_messages = []
-  last_image = None
-  for message in messages:
-    if not isinstance(message.content, list):
-      remapped_messages.append(message)
-      continue
-
-    remapped_content = []
-    for content in message.content:
-      if isinstance(content, dict):
-        if content.get("type") in ["image_url", "image"]:
-          image_url = content.get("image_url", {}).get("url") or content.get("image")
-          if image_url:
-            last_image = {"type": "image", "image": image_url}
-            remapped_content.append({"type": "text", "text": "[An image was uploaded but is not displayed here]"})
-        else:
-          remapped_content.append(content)
-      else:
-        remapped_content.append(content)
-    remapped_messages.append(Message(role=message.role, content=remapped_content))
-
-  if last_image:
-    # Replace the last image placeholder with the actual image content
-    for message in reversed(remapped_messages):
-      for i, content in enumerate(message.content):
-        if isinstance(content, dict):
-          if content.get("type") == "text" and content.get("text") == "[An image was uploaded but is not displayed here]":
-            message.content[i] = last_image
-            return remapped_messages
-
-  return remapped_messages
-
-
-def build_prompt(tokenizer, _messages: List[Message]):
-  messages = remap_messages(_messages)
-  prompt = tokenizer.apply_chat_template([m.to_dict() for m in messages], tokenize=False, add_generation_prompt=True)
-  for message in messages:
-    if not isinstance(message.content, list):
-      continue
-
-  return prompt
-
-
-def parse_message(data: dict):
-  if "role" not in data or "content" not in data:
-    raise ValueError(f"Invalid message: {data}. Must have 'role' and 'content'")
-  return Message(data["role"], data["content"])
-
-
-def parse_chat_request(data: dict, default_model: str):
-  return ChatCompletionRequest(
-    data.get("model", default_model),
-    [parse_message(msg) for msg in data["messages"]],
-    data.get("temperature", 0.0),
-  )
-
-
-class PromptSession:
-  def __init__(self, request_id: str, timestamp: int, prompt: str):
-    self.request_id = request_id
-    self.timestamp = timestamp
-    self.prompt = prompt
-
-class ChatGPTAPI:
-  def __init__(self, node: Node, inference_engine_classname: str, response_timeout: int = 90, on_chat_completion_request: Callable[[str, ChatCompletionRequest, str], None] = None, default_model: Optional[str] = None):
-    self.node = node
-    self.inference_engine_classname = inference_engine_classname
-    self.response_timeout = response_timeout
-    self.on_chat_completion_request = on_chat_completion_request
-    self.app = web.Application(client_max_size=100*1024*1024)  # 100MB to support image upload
-    self.prompts: PrefixDict[str, PromptSession] = PrefixDict()
-    self.prev_token_lens: Dict[str, int] = {}
-    self.stream_tasks: Dict[str, asyncio.Task] = {}
-    self.default_model = default_model or "llama-3.2-1b"
-
-    cors = aiohttp_cors.setup(self.app)
-    cors_options = aiohttp_cors.ResourceOptions(
-      allow_credentials=True,
-      expose_headers="*",
-      allow_headers="*",
-      allow_methods="*",
-    )
-    cors.add(self.app.router.add_get("/models", self.handle_get_models), {"*": cors_options})
-    cors.add(self.app.router.add_get("/v1/models", self.handle_get_models), {"*": cors_options})
-    cors.add(self.app.router.add_post("/chat/token/encode", self.handle_post_chat_token_encode), {"*": cors_options})
-    cors.add(self.app.router.add_post("/v1/chat/token/encode", self.handle_post_chat_token_encode), {"*": cors_options})
-    cors.add(self.app.router.add_post("/chat/completions", self.handle_post_chat_completions), {"*": cors_options})
-    cors.add(self.app.router.add_post("/v1/chat/completions", self.handle_post_chat_completions), {"*": cors_options})
-    cors.add(self.app.router.add_post("/v1/image/generations", self.handle_post_image_generations), {"*": cors_options})
-    cors.add(self.app.router.add_get("/v1/download/progress", self.handle_get_download_progress), {"*": cors_options})
-    cors.add(self.app.router.add_get("/modelpool", self.handle_model_support), {"*": cors_options})
-    cors.add(self.app.router.add_get("/healthcheck", self.handle_healthcheck), {"*": cors_options})
-    cors.add(self.app.router.add_post("/quit", self.handle_quit), {"*": cors_options})
-    cors.add(self.app.router.add_post("/create_animation", self.handle_create_animation), {"*": cors_options})
-    cors.add(self.app.router.add_post("/download", self.handle_post_download), {"*": cors_options})
-
-      
-    if "__compiled__" not in globals():
-      self.static_dir = Path(__file__).parent.parent/"tinychat"
-      self.app.router.add_get("/", self.handle_root)
-      self.app.router.add_static("/", self.static_dir, name="static")
-      self.app.router.add_static('/images/', get_exo_images_dir(), name='static_images')
-
-    self.app.middlewares.append(self.timeout_middleware)
-    self.app.middlewares.append(self.log_request)
-
-  async def handle_quit(self, request):
-    if DEBUG>=1: print("Received quit signal")
-    response = web.json_response({"detail": "Quit signal received"}, status=200)
-    await response.prepare(request)
-    await response.write_eof()
-    await shutdown(signal.SIGINT, asyncio.get_event_loop(), self.node.server)
-
-  async def timeout_middleware(self, app, handler):
-    async def middleware(request):
-      try:
-        return await asyncio.wait_for(handler(request), timeout=self.response_timeout)
-      except asyncio.TimeoutError:
-        return web.json_response({"detail": "Request timed out"}, status=408)
-
-    return middleware
-
-  async def log_request(self, app, handler):
-    async def middleware(request):
-      if DEBUG >= 2: print(f"Received request: {request.method} {request.path}")
-      return await handler(request)
-
-    return middleware
-
-  async def handle_root(self, request):
-    return web.FileResponse(self.static_dir/"index.html")
-
-  async def handle_healthcheck(self, request):
-    return web.json_response({"status": "ok"})
-
-  async def handle_model_support(self, request):
-    return web.json_response({
-      "model pool": {
-        model_name: pretty_name.get(model_name, model_name)
-        for model_name in get_supported_models(self.node.topology_inference_engines_pool)
-      }
-    })
-
-  async def handle_get_models(self, request):
-    return web.json_response([{"id": model_name, "object": "model", "owned_by": "exo", "ready": True} for model_name, _ in model_cards.items()])
-
-  async def handle_post_chat_token_encode(self, request):
-    data = await request.json()
-    shard = build_base_shard(self.default_model, self.inference_engine_classname)
-    messages = [parse_message(msg) for msg in data.get("messages", [])]
-    tokenizer = await resolve_tokenizer(get_repo(shard.model_id, self.inference_engine_classname))
-    return web.json_response({"length": len(build_prompt(tokenizer, messages)[0])})
-
-  async def handle_get_download_progress(self, request):
-    progress_data = {}
-    for node_id, progress_event in self.node.node_download_progress.items():
-      if isinstance(progress_event, RepoProgressEvent):
-        progress_data[node_id] = progress_event.to_dict()
-      else:
-        print(f"Unknown progress event type: {type(progress_event)}. {progress_event}")
-    return web.json_response(progress_data)
-
-  async def handle_post_chat_completions(self, request):
-    data = await request.json()
-    if DEBUG >= 2: print(f"Handling chat completions request from {request.remote}: {data}")
-    stream = data.get("stream", False)
-    chat_request = parse_chat_request(data, self.default_model)
-    if chat_request.model and chat_request.model.startswith("gpt-"):  # to be compatible with ChatGPT tools, point all gpt- model requests to default model
-      chat_request.model = self.default_model
-    if not chat_request.model or chat_request.model not in model_cards:
-      if DEBUG >= 1: print(f"Invalid model: {chat_request.model}. Supported: {list(model_cards.keys())}. Defaulting to {self.default_model}")
-      chat_request.model = self.default_model
-    shard = build_base_shard(chat_request.model, self.inference_engine_classname)
-    if not shard:
-      supported_models = [model for model, info in model_cards.items() if self.inference_engine_classname in info.get("repo", {})]
-      return web.json_response(
-        {"detail": f"Unsupported model: {chat_request.model} with inference engine {self.inference_engine_classname}. Supported models for this engine: {supported_models}"},
-        status=400,
-      )
-
-    tokenizer = await resolve_tokenizer(get_repo(shard.model_id, self.inference_engine_classname))
-    if DEBUG >= 4: print(f"Resolved tokenizer: {tokenizer}")
-
-    prompt = build_prompt(tokenizer, chat_request.messages)
-    request_id = str(uuid.uuid4())
-    if self.on_chat_completion_request:
-      try:
-        self.on_chat_completion_request(request_id, chat_request, prompt)
-      except Exception as e:
-        if DEBUG >= 2: traceback.print_exc()
-    # request_id = None
-    # match = self.prompts.find_longest_prefix(prompt)
-    # if match and len(prompt) > len(match[1].prompt):
-    #     if DEBUG >= 2:
-    #       print(f"Prompt for request starts with previous prompt {len(match[1].prompt)} of {len(prompt)}: {match[1].prompt}")
-    #     request_id = match[1].request_id
-    #     self.prompts.add(prompt, PromptSession(request_id=request_id, timestamp=int(time.time()), prompt=prompt))
-    #     # remove the matching prefix from the prompt
-    #     prompt = prompt[len(match[1].prompt):]
-    # else:
-    #   request_id = str(uuid.uuid4())
-    #   self.prompts.add(prompt, PromptSession(request_id=request_id, timestamp=int(time.time()), prompt=prompt))
-
-    callback_id = f"chatgpt-api-wait-response-{request_id}"
-    callback = self.node.on_token.register(callback_id)
-
-    if DEBUG >= 2: print(f"Sending prompt from ChatGPT api {request_id=} {shard=} {prompt=}")
-
-    try:
-      await asyncio.wait_for(asyncio.shield(asyncio.create_task(self.node.process_prompt(shard, prompt, request_id=request_id))), timeout=self.response_timeout)
-
-      if DEBUG >= 2: print(f"Waiting for response to finish. timeout={self.response_timeout}s")
-
-      if stream:
-        response = web.StreamResponse(
-          status=200,
-          reason="OK",
-          headers={
-            "Content-Type": "text/event-stream",
-            "Cache-Control": "no-cache",
-          },
-        )
-        await response.prepare(request)
-
-        async def stream_result(_request_id: str, tokens: List[int], is_finished: bool):
-          prev_last_tokens_len = self.prev_token_lens.get(_request_id, 0)
-          self.prev_token_lens[_request_id] = max(prev_last_tokens_len, len(tokens))
-          new_tokens = tokens[prev_last_tokens_len:]
-          finish_reason = None
-          eos_token_id = tokenizer.special_tokens_map.get("eos_token_id") if hasattr(tokenizer, "_tokenizer") and isinstance(tokenizer._tokenizer,
-                                                                                                                             AutoTokenizer) else getattr(tokenizer, "eos_token_id", None)
-          if len(new_tokens) > 0 and new_tokens[-1] == eos_token_id:
-            new_tokens = new_tokens[:-1]
-            if is_finished:
-              finish_reason = "stop"
-          if is_finished and not finish_reason:
-            finish_reason = "length"
-
-          completion = generate_completion(
-            chat_request,
-            tokenizer,
-            prompt,
-            request_id,
-            new_tokens,
-            stream,
-            finish_reason,
-            "chat.completion",
-          )
-          if DEBUG >= 2: print(f"Streaming completion: {completion}")
-          try:
-            await response.write(f"data: {json.dumps(completion)}\n\n".encode())
-          except Exception as e:
-            if DEBUG >= 2: print(f"Error streaming completion: {e}")
-            if DEBUG >= 2: traceback.print_exc()
-
-        def on_result(_request_id: str, tokens: List[int], is_finished: bool):
-          if _request_id == request_id: self.stream_tasks[_request_id] = asyncio.create_task(stream_result(_request_id, tokens, is_finished))
-
-          return _request_id == request_id and is_finished
-
-        _, tokens, _ = await callback.wait(on_result, timeout=self.response_timeout)
-        if request_id in self.stream_tasks:  # in case there is still a stream task running, wait for it to complete
-          if DEBUG >= 2: print("Pending stream task. Waiting for stream task to complete.")
-          try:
-            await asyncio.wait_for(self.stream_tasks[request_id], timeout=30)
-          except asyncio.TimeoutError:
-            print("WARNING: Stream task timed out. This should not happen.")
-        await response.write_eof()
-        return response
-      else:
-        _, tokens, _ = await callback.wait(
-          lambda _request_id, tokens, is_finished: _request_id == request_id and is_finished,
-          timeout=self.response_timeout,
-        )
-
-        finish_reason = "length"
-        eos_token_id = tokenizer.special_tokens_map.get("eos_token_id") if isinstance(getattr(tokenizer, "_tokenizer", None), AutoTokenizer) else tokenizer.eos_token_id
-        if DEBUG >= 2: print(f"Checking if end of tokens result {tokens[-1]=} is {eos_token_id=}")
-        if tokens[-1] == eos_token_id:
-          tokens = tokens[:-1]
-          finish_reason = "stop"
-
-        return web.json_response(generate_completion(chat_request, tokenizer, prompt, request_id, tokens, stream, finish_reason, "chat.completion"))
-    except asyncio.TimeoutError:
-      return web.json_response({"detail": "Response generation timed out"}, status=408)
-    except Exception as e:
-      if DEBUG >= 2: traceback.print_exc()
-      return web.json_response({"detail": f"Error processing prompt (see logs with DEBUG>=2): {str(e)}"}, status=500)
-    finally:
-      deregistered_callback = self.node.on_token.deregister(callback_id)
-      if DEBUG >= 2: print(f"Deregister {callback_id=} {deregistered_callback=}")
-
-  
-  async def handle_post_image_generations(self, request):
-    data = await request.json()
-
-    if DEBUG >= 2: print(f"Handling chat completions request from {request.remote}: {data}")
-    stream = data.get("stream", False)
-    model = data.get("model", "")
-    prompt = data.get("prompt", "")
-    image_url = data.get("image_url", "")
-    print(f"model: {model}, prompt: {prompt}, stream: {stream}")
-    shard = build_base_shard(model, self.inference_engine_classname)
-    print(f"shard: {shard}")
-    if not shard:
-        return web.json_response({"error": f"Unsupported model: {model} with inference engine {self.inference_engine_classname}"}, status=400)
-
-    request_id = str(uuid.uuid4())
-    callback_id = f"chatgpt-api-wait-response-{request_id}"
-    callback = self.node.on_token.register(callback_id)
-    try:
-      if image_url != "" and image_url != None:
-        img = self.base64_decode(image_url)
-      else:
-        img = None
-      await asyncio.wait_for(asyncio.shield(asyncio.create_task(self.node.process_prompt(shard, prompt, request_id=request_id, inference_state={"image": img}))), timeout=self.response_timeout)
-
-
-      response = web.StreamResponse(status=200, reason='OK', headers={'Content-Type': 'application/octet-stream',"Cache-Control": "no-cache",})
-      await response.prepare(request)
-
-      def get_progress_bar(current_step, total_steps, bar_length=50):
-        # Calculate the percentage of completion
-        percent = float(current_step) / total_steps
-        # Calculate the number of hashes to display
-        arrow = '-' * int(round(percent * bar_length) - 1) + '>'
-        spaces = ' ' * (bar_length - len(arrow))
-        
-        # Create the progress bar string
-        progress_bar = f'Progress: [{arrow}{spaces}] {int(percent * 100)}% ({current_step}/{total_steps})'
-        return progress_bar
-
-      async def stream_image(_request_id: str, result, is_finished: bool):
-          if isinstance(result, list):
-              await response.write(json.dumps({'progress': get_progress_bar((result[0]), (result[1]))}).encode('utf-8') + b'\n')
-
-          elif isinstance(result, np.ndarray):
-            im = Image.fromarray(np.array(result))
-            images_folder = get_exo_images_dir()
-            # Save the image to a file
-            image_filename = f"{_request_id}.png"
-            image_path = images_folder / image_filename
-            im.save(image_path)
-            image_url = request.app.router['static_images'].url_for(filename=image_filename)
-            base_url = f"{request.scheme}://{request.host}"
-            # Construct the full URL correctly
-            full_image_url = base_url + str(image_url)
-            
-            await response.write(json.dumps({'images': [{'url': str(full_image_url), 'content_type': 'image/png'}]}).encode('utf-8') + b'\n')
-            if is_finished:
-              await response.write_eof()
-              
-
-      stream_task = None
-      def on_result(_request_id: str, result, is_finished: bool):
-          nonlocal stream_task
-          stream_task = asyncio.create_task(stream_image(_request_id, result, is_finished))
-          return _request_id == request_id and is_finished
-
-      await callback.wait(on_result, timeout=self.response_timeout*10)
-      
-      if stream_task:
-          # Wait for the stream task to complete before returning
-          await stream_task
-
-      return response
-
-    except Exception as e:
-        if DEBUG >= 2: traceback.print_exc()
-        return web.json_response({"detail": f"Error processing prompt (see logs with DEBUG>=2): {str(e)}"}, status=500)
-  
-  async def handle_create_animation(self, request):
-    try:
-      data = await request.json()
-      replacement_image_path = data.get("replacement_image_path")
-      device_name = data.get("device_name", "Local Device")
-      prompt_text = data.get("prompt", "")
-
-      if DEBUG >= 2: print(f"Creating animation with params: replacement_image={replacement_image_path}, device={device_name}, prompt={prompt_text}")
-
-      if not replacement_image_path:
-        return web.json_response({"error": "replacement_image_path is required"}, status=400)
-
-      # Create temp directory if it doesn't exist
-      tmp_dir = Path(tempfile.gettempdir())/"exo_animations"
-      tmp_dir.mkdir(parents=True, exist_ok=True)
-
-      # Generate unique output filename in temp directory
-      output_filename = f"animation_{uuid.uuid4()}.mp4"
-      output_path = str(tmp_dir/output_filename)
-
-      if DEBUG >= 2: print(f"Animation temp directory: {tmp_dir}, output file: {output_path}, directory exists: {tmp_dir.exists()}, directory permissions: {oct(tmp_dir.stat().st_mode)[-3:]}")
-
-      # Create the animation
-      create_animation_mp4(
-        replacement_image_path,
-        output_path,
-        device_name,
-        prompt_text
-      )
-
-      return web.json_response({
-        "status": "success",
-        "output_path": output_path
-      })
-
-    except Exception as e:
-      if DEBUG >= 2: traceback.print_exc()
-      return web.json_response({"error": str(e)}, status=500)
-
-  async def handle_post_download(self, request):
-    try:
-      data = await request.json()
-      model_name = data.get("model")
-      if not model_name: return web.json_response({"error": "model parameter is required"}, status=400)
-      if model_name not in model_cards: return web.json_response({"error": f"Invalid model: {model_name}. Supported models: {list(model_cards.keys())}"}, status=400)
-      shard = build_base_shard(model_name, self.inference_engine_classname)
-      if not shard: return web.json_response({"error": f"Could not build shard for model {model_name}"}, status=400)
-      asyncio.create_task(self.node.inference_engine.ensure_shard(shard))
-
-      return web.json_response({
-        "status": "success",
-        "message": f"Download started for model: {model_name}"
-      })
-    except Exception as e:
-      if DEBUG >= 2: traceback.print_exc()
-      return web.json_response({"error": str(e)}, status=500)
-
-  async def run(self, host: str = "0.0.0.0", port: int = 52415):
-    runner = web.AppRunner(self.app)
-    await runner.setup()
-    site = web.TCPSite(runner, host, port)
-    await site.start()
-
-  def base64_decode(self, base64_string):
-    #decode and reshape image
-    if base64_string.startswith('data:image'):
-        base64_string = base64_string.split(',')[1]
-    image_data = base64.b64decode(base64_string)
-    img = Image.open(BytesIO(image_data))
-    W, H = (dim - dim % 64 for dim in (img.width, img.height))
-    if W != img.width or H != img.height:
-        print(f"Warning: image shape is not divisible by 64, downsampling to {W}x{H}")
-        img = img.resize((W, H), Image.NEAREST)  # use desired downsampling filter
-    img = mx.array(np.array(img))
-    img = (img[:, :, :3].astype(mx.float32) / 255) * 2 - 1
-    img = img[None]
-    return img
-  
diff --git a/build/lib/exo/apputil/__init__.py b/build/lib/exo/apputil/__init__.py
deleted file mode 100644
index b6ac6e25..00000000
--- a/build/lib/exo/apputil/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from exo.apputil.anim import create_animation_mp4
\ No newline at end of file
diff --git a/build/lib/exo/apputil/anim.py b/build/lib/exo/apputil/anim.py
deleted file mode 100644
index 654ca470..00000000
--- a/build/lib/exo/apputil/anim.py
+++ /dev/null
@@ -1,161 +0,0 @@
-from PIL import Image, ImageDraw, ImageFont, ImageFilter
-import os
-import numpy as np
-import cv2
-
-def draw_rounded_rectangle(draw, coords, radius, fill):
-  left, top, right, bottom = coords
-  diameter = radius * 2
-  draw.rectangle([left + radius, top, right - radius, bottom], fill=fill)
-  draw.rectangle([left, top + radius, right, bottom - radius], fill=fill)
-  draw.pieslice([left, top, left + diameter, top + diameter], 180, 270, fill=fill)
-  draw.pieslice([right - diameter, top, right, top + diameter], 270, 360, fill=fill)
-  draw.pieslice([left, bottom - diameter, left + diameter, bottom], 90, 180, fill=fill)
-  draw.pieslice([right - diameter, bottom - diameter, right, bottom], 0, 90, fill=fill)
-
-def draw_centered_text_rounded(draw, text, font, rect_coords, radius=10, text_color="yellow", bg_color=(43,33,44)):
-  bbox = font.getbbox(text)
-  text_width = bbox[2] - bbox[0]
-  text_height = bbox[3] - bbox[1]
-  rect_left, rect_top, rect_right, rect_bottom = rect_coords
-  rect_width = rect_right - rect_left
-  rect_height = rect_bottom - rect_top
-  text_x = rect_left + (rect_width - text_width) // 2
-  text_y = rect_top + (rect_height - text_height) // 2
-  draw_rounded_rectangle(draw, rect_coords, radius, bg_color)
-  draw.text((text_x, text_y), text, fill=text_color, font=font)
-
-def draw_left_aligned_text_rounded(draw, text, font, rect_coords, padding_left=20, radius=10, text_color="yellow", bg_color=(43,33,44)):
-  bbox = font.getbbox(text)
-  text_height = bbox[3] - bbox[1]
-  rect_left, rect_top, rect_right, rect_bottom = rect_coords
-  rect_height = rect_bottom - rect_top
-  text_y = rect_top + (rect_height - text_height) // 2
-  text_x = rect_left + padding_left
-  draw_rounded_rectangle(draw, rect_coords, radius, bg_color)
-  draw.text((text_x, text_y), text, fill=text_color, font=font)
-
-def draw_right_text_dynamic_width_rounded(draw, text, font, base_coords, padding=20, radius=10, text_color="yellow", bg_color=(43,33,44)):
-  bbox = font.getbbox(text)
-  text_width = bbox[2] - bbox[0]
-  text_height = bbox[3] - bbox[1]
-  _, rect_top, rect_right, rect_bottom = base_coords
-  rect_height = rect_bottom - rect_top
-  new_rect_left = rect_right - (text_width + (padding * 2))
-  text_y = rect_top + (rect_height - text_height) // 2
-  text_x = new_rect_left + padding
-  draw_rounded_rectangle(draw, (new_rect_left, rect_top, rect_right, rect_bottom), radius, bg_color)
-  draw.text((text_x, text_y), text, fill=text_color, font=font)
-  return new_rect_left
-
-def draw_progress_bar(draw, progress, coords, color="yellow", bg_color=(70, 70, 70)):
-  left, top, right, bottom = coords
-  total_width = right - left
-  draw.rectangle(coords, fill=bg_color)
-  progress_width = int(total_width * progress)
-  if progress_width > 0:
-    draw.rectangle((left, top, left + progress_width, bottom), fill=color)
-
-def crop_image(image, top_crop=70):
-  width, height = image.size
-  return image.crop((0, top_crop, width, height))
-
-def create_animation_mp4(
-  replacement_image_path,
-  output_path,
-  device_name,
-  prompt_text,
-  fps=30,
-  target_size=(512, 512),
-  target_position=(139, 755),
-  progress_coords=(139, 1285, 655, 1295),
-  device_coords=(1240, 370, 1640, 416),
-  prompt_coords=(332, 1702, 2662, 1745)
-):
-  frames = []
-  try:
-    font = ImageFont.truetype("/System/Library/Fonts/SFNSMono.ttf", 20)
-    promptfont = ImageFont.truetype("/System/Library/Fonts/SFNSMono.ttf", 24)
-  except:
-    font = ImageFont.load_default()
-    promptfont = ImageFont.load_default()
-
-  # Process first frame
-  base_img = Image.open(os.path.join(os.path.dirname(__file__), "baseimages", "image1.png"))
-  draw = ImageDraw.Draw(base_img)
-  draw_centered_text_rounded(draw, device_name, font, device_coords)
-  frames.extend([crop_image(base_img)] * 30)  # 1 second at 30fps
-
-  # Process second frame with typing animation
-  base_img2 = Image.open(os.path.join(os.path.dirname(__file__), "baseimages", "image2.png"))
-  for i in range(len(prompt_text) + 1):
-    current_frame = base_img2.copy()
-    draw = ImageDraw.Draw(current_frame)
-    draw_centered_text_rounded(draw, device_name, font, device_coords)
-    if i > 0:  # Only draw if we have at least one character
-      draw_left_aligned_text_rounded(draw, prompt_text[:i], promptfont, prompt_coords)
-    frames.extend([crop_image(current_frame)] * 2)  # 2 frames per character for smooth typing
-  
-  # Hold the complete prompt for a moment
-  frames.extend([frames[-1]] * 30)  # Hold for 1 second
-
-  # Create blur sequence
-  replacement_img = Image.open(replacement_image_path)
-  base_img = Image.open(os.path.join(os.path.dirname(__file__), "baseimages", "image3.png"))
-  blur_steps = [int(80 * (1 - i/8)) for i in range(9)]
-
-  for i, blur_amount in enumerate(blur_steps):
-    new_frame = base_img.copy()
-    draw = ImageDraw.Draw(new_frame)
-
-    replacement_copy = replacement_img.copy()
-    replacement_copy.thumbnail(target_size, Image.Resampling.LANCZOS)
-    if blur_amount > 0:
-      replacement_copy = replacement_copy.filter(ImageFilter.GaussianBlur(radius=blur_amount))
-
-    mask = replacement_copy.split()[-1] if replacement_copy.mode in ('RGBA', 'LA') else None
-    new_frame.paste(replacement_copy, target_position, mask)
-
-    draw_progress_bar(draw, (i + 1) / 9, progress_coords)
-    draw_centered_text_rounded(draw, device_name, font, device_coords)
-    draw_right_text_dynamic_width_rounded(draw, prompt_text, promptfont, (None, 590, 2850, 685), padding=30)
-
-    frames.extend([crop_image(new_frame)] * 15)  # 0.5 seconds at 30fps
-
-  # Create and add final frame (image4)
-  final_base = Image.open(os.path.join(os.path.dirname(__file__), "baseimages", "image4.png"))
-  draw = ImageDraw.Draw(final_base)
-
-  draw_centered_text_rounded(draw, device_name, font, device_coords)
-  draw_right_text_dynamic_width_rounded(draw, prompt_text, promptfont, (None, 590, 2850, 685), padding=30)
-
-  replacement_copy = replacement_img.copy()
-  replacement_copy.thumbnail(target_size, Image.Resampling.LANCZOS)
-  mask = replacement_copy.split()[-1] if replacement_copy.mode in ('RGBA', 'LA') else None
-  final_base.paste(replacement_copy, target_position, mask)
-
-  frames.extend([crop_image(final_base)] * 30)  # 1 second at 30fps
-
-  # Convert frames to video using H.264 codec
-  if frames:
-    first_frame = np.array(frames[0])
-    height, width = first_frame.shape[:2]
-    fourcc = cv2.VideoWriter_fourcc(*'avc1')
-    out = cv2.VideoWriter(
-      output_path,
-      fourcc,
-      fps,
-      (width, height),
-      isColor=True
-    )
-
-    if not out.isOpened():
-      print("Error: VideoWriter failed to open")
-      return
-
-    for frame in frames:
-      frame_array = cv2.cvtColor(np.array(frame), cv2.COLOR_RGB2BGR)
-      out.write(frame_array)
-    
-    out.release()
-    print(f"Video saved successfully to {output_path}")
\ No newline at end of file
diff --git a/build/lib/exo/download/__init__.py b/build/lib/exo/download/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/download/download_progress.py b/build/lib/exo/download/download_progress.py
deleted file mode 100644
index 779e5328..00000000
--- a/build/lib/exo/download/download_progress.py
+++ /dev/null
@@ -1,61 +0,0 @@
-from typing import Dict, Callable, Coroutine, Any, Literal
-from dataclasses import dataclass
-from datetime import timedelta
-
-
-@dataclass
-class RepoFileProgressEvent:
-  repo_id: str
-  repo_revision: str
-  file_path: str
-  downloaded: int
-  downloaded_this_session: int
-  total: int
-  speed: int
-  eta: timedelta
-  status: Literal["not_started", "in_progress", "complete"]
-
-  def to_dict(self):
-    return {
-      "repo_id": self.repo_id, "repo_revision": self.repo_revision, "file_path": self.file_path, "downloaded": self.downloaded, "downloaded_this_session": self.downloaded_this_session,
-      "total": self.total, "speed": self.speed, "eta": self.eta.total_seconds(), "status": self.status
-    }
-
-  @classmethod
-  def from_dict(cls, data):
-    if 'eta' in data: data['eta'] = timedelta(seconds=data['eta'])
-    return cls(**data)
-
-
-@dataclass
-class RepoProgressEvent:
-  repo_id: str
-  repo_revision: str
-  completed_files: int
-  total_files: int
-  downloaded_bytes: int
-  downloaded_bytes_this_session: int
-  total_bytes: int
-  overall_speed: int
-  overall_eta: timedelta
-  file_progress: Dict[str, RepoFileProgressEvent]
-  status: Literal["not_started", "in_progress", "complete"]
-
-  def to_dict(self):
-    return {
-      "repo_id": self.repo_id, "repo_revision": self.repo_revision, "completed_files": self.completed_files, "total_files": self.total_files, "downloaded_bytes": self.downloaded_bytes,
-      "downloaded_bytes_this_session": self.downloaded_bytes_this_session, "total_bytes": self.total_bytes, "overall_speed": self.overall_speed, "overall_eta": self.overall_eta.total_seconds(),
-      "file_progress": {k: v.to_dict()
-                        for k, v in self.file_progress.items()}, "status": self.status
-    }
-
-  @classmethod
-  def from_dict(cls, data):
-    if 'overall_eta' in data: data['overall_eta'] = timedelta(seconds=data['overall_eta'])
-    if 'file_progress' in data: data['file_progress'] = {k: RepoFileProgressEvent.from_dict(v) for k, v in data['file_progress'].items()}
-
-    return cls(**data)
-
-
-RepoFileProgressCallback = Callable[[RepoFileProgressEvent], Coroutine[Any, Any, None]]
-RepoProgressCallback = Callable[[RepoProgressEvent], Coroutine[Any, Any, None]]
diff --git a/build/lib/exo/download/hf/__init__.py b/build/lib/exo/download/hf/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/download/hf/hf_helpers.py b/build/lib/exo/download/hf/hf_helpers.py
deleted file mode 100644
index 6e8220ea..00000000
--- a/build/lib/exo/download/hf/hf_helpers.py
+++ /dev/null
@@ -1,447 +0,0 @@
-import aiofiles.os as aios
-from typing import Union
-import asyncio
-import aiohttp
-import json
-import os
-import sys
-import shutil
-from urllib.parse import urljoin
-from typing import Callable, Optional, Coroutine, Any, Dict, List, Union, Literal
-from datetime import datetime, timedelta
-from fnmatch import fnmatch
-from pathlib import Path
-from typing import Generator, Iterable, TypeVar, TypedDict
-from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
-from exo.helpers import DEBUG, is_frozen
-from exo.download.download_progress import RepoProgressEvent, RepoFileProgressEvent, RepoProgressCallback, RepoFileProgressCallback
-from exo.inference.shard import Shard
-import aiofiles
-
-T = TypeVar("T")
-
-async def get_local_snapshot_dir(repo_id: str, revision: str = "main") -> Optional[Path]:
-  refs_dir = get_repo_root(repo_id)/"refs"
-  refs_file = refs_dir/revision
-  if await aios.path.exists(refs_file):
-    async with aiofiles.open(refs_file, 'r') as f:
-      commit_hash = (await f.read()).strip()
-      snapshot_dir = get_repo_root(repo_id)/"snapshots"/commit_hash
-      return snapshot_dir
-  return None
-
-
-def filter_repo_objects(
-  items: Iterable[T],
-  *,
-  allow_patterns: Optional[Union[List[str], str]] = None,
-  ignore_patterns: Optional[Union[List[str], str]] = None,
-  key: Optional[Callable[[T], str]] = None,
-) -> Generator[T, None, None]:
-  if isinstance(allow_patterns, str):
-    allow_patterns = [allow_patterns]
-  if isinstance(ignore_patterns, str):
-    ignore_patterns = [ignore_patterns]
-  if allow_patterns is not None:
-    allow_patterns = [_add_wildcard_to_directories(p) for p in allow_patterns]
-  if ignore_patterns is not None:
-    ignore_patterns = [_add_wildcard_to_directories(p) for p in ignore_patterns]
-
-  if key is None:
-
-    def _identity(item: T) -> str:
-      if isinstance(item, str):
-        return item
-      if isinstance(item, Path):
-        return str(item)
-      raise ValueError(f"Please provide `key` argument in `filter_repo_objects`: `{item}` is not a string.")
-
-    key = _identity
-
-  for item in items:
-    path = key(item)
-    if allow_patterns is not None and not any(fnmatch(path, r) for r in allow_patterns):
-      continue
-    if ignore_patterns is not None and any(fnmatch(path, r) for r in ignore_patterns):
-      continue
-    yield item
-
-
-def _add_wildcard_to_directories(pattern: str) -> str:
-  if pattern[-1] == "/":
-    return pattern + "*"
-  return pattern
-
-
-def get_hf_endpoint() -> str:
-  return os.environ.get('HF_ENDPOINT', "https://huggingface.co")
-
-
-def get_hf_home() -> Path:
-  """Get the Hugging Face home directory."""
-  return Path(os.environ.get("HF_HOME", Path.home()/".cache"/"huggingface"))
-
-
-async def get_hf_token():
-  """Retrieve the Hugging Face token from the user's HF_HOME directory."""
-  token_path = get_hf_home()/"token"
-  if await aios.path.exists(token_path):
-    async with aiofiles.open(token_path, 'r') as f:
-      return (await f.read()).strip()
-  return None
-
-
-async def get_auth_headers():
-  """Get authentication headers if a token is available."""
-  token = await get_hf_token()
-  if token:
-    return {"Authorization": f"Bearer {token}"}
-  return {}
-
-
-def get_repo_root(repo_id: str) -> Path:
-  """Get the root directory for a given repo ID in the Hugging Face cache."""
-  sanitized_repo_id = str(repo_id).replace("/", "--")
-  return get_hf_home()/"hub"/f"models--{sanitized_repo_id}"
-
-async def move_models_to_hf(seed_dir: Union[str, Path]):
-  """Move model in resources folder of app to .cache/huggingface/hub"""
-  source_dir = Path(seed_dir)
-  dest_dir = get_hf_home()/"hub"
-  await aios.makedirs(dest_dir, exist_ok=True)  
-  for path in source_dir.iterdir():
-    if path.is_dir() and path.name.startswith("models--"):
-      dest_path = dest_dir / path.name
-      if await aios.path.exists(dest_path):
-        print('Skipping moving model to .cache directory')
-      else:
-        try:
-          await aios.rename(str(path), str(dest_path))
-        except Exception as e:
-          print(f'Error moving model to .cache: {e}')
-    
-    
-    
-async def fetch_file_list(session, repo_id, revision, path=""):
-  api_url = f"{get_hf_endpoint()}/api/models/{repo_id}/tree/{revision}"
-  url = f"{api_url}/{path}" if path else api_url
-
-  headers = await get_auth_headers()
-  async with session.get(url, headers=headers) as response:
-    if response.status == 200:
-      data = await response.json()
-      files = []
-      for item in data:
-        if item["type"] == "file":
-          files.append({"path": item["path"], "size": item["size"]})
-        elif item["type"] == "directory":
-          subfiles = await fetch_file_list(session, repo_id, revision, item["path"])
-          files.extend(subfiles)
-      return files
-    else:
-      raise Exception(f"Failed to fetch file list: {response.status}")
-
-
-@retry(
-  stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=4, max=60), retry=retry_if_exception_type((aiohttp.ClientError, asyncio.TimeoutError, aiohttp.ClientResponseError)), reraise=True
-)
-async def download_file(
-  session: aiohttp.ClientSession, repo_id: str, revision: str, file_path: str, save_directory: str, progress_callback: Optional[RepoFileProgressCallback] = None, use_range_request: bool = True
-):
-  base_url = f"{get_hf_endpoint()}/{repo_id}/resolve/{revision}/"
-  url = urljoin(base_url, file_path)
-  local_path = os.path.join(save_directory, file_path)
-
-  await aios.makedirs(os.path.dirname(local_path), exist_ok=True)
-
-  # Check if file already exists and get its size
-  local_file_size = await aios.path.getsize(local_path) if await aios.path.exists(local_path) else 0
-
-  headers = await get_auth_headers()
-  if use_range_request:
-    headers["Range"] = f"bytes={local_file_size}-"
-
-  async with session.get(url, headers=headers) as response:
-    total_size = int(response.headers.get('Content-Length', 0))
-    downloaded_size = local_file_size
-    downloaded_this_session = 0
-    mode = 'ab' if use_range_request else 'wb'
-    if downloaded_size == total_size:
-      if DEBUG >= 2: print(f"File already downloaded: {file_path}")
-      if progress_callback:
-        await progress_callback(RepoFileProgressEvent(repo_id, revision, file_path, downloaded_size, downloaded_this_session, total_size, 0, timedelta(0), "complete"))
-      return
-
-    if response.status == 200:
-      # File doesn't support range requests or we're not using them, start from beginning
-      mode = 'wb'
-      downloaded_size = 0
-    elif response.status == 206:
-      # Partial content, resume download
-      content_range = response.headers.get('Content-Range', '')
-      try:
-        total_size = int(content_range.split('/')[-1])
-      except ValueError:
-        if DEBUG >= 1: print(f"Failed to parse Content-Range header: {content_range}. Starting download from scratch...")
-        return await download_file(session, repo_id, revision, file_path, save_directory, progress_callback, use_range_request=False)
-    elif response.status == 416:
-      # Range not satisfiable, get the actual file size
-      content_range = response.headers.get('Content-Range', '')
-      try:
-        total_size = int(content_range.split('/')[-1])
-        if downloaded_size == total_size:
-          if DEBUG >= 2: print(f"File fully downloaded on first pass: {file_path}")
-          if progress_callback:
-            await progress_callback(RepoFileProgressEvent(repo_id, revision, file_path, downloaded_size, downloaded_this_session, total_size, 0, timedelta(0), "complete"))
-          return
-      except ValueError:
-        if DEBUG >= 1: print(f"Failed to parse Content-Range header: {content_range}. Starting download from scratch...")
-        return await download_file(session, repo_id, revision, file_path, save_directory, progress_callback, use_range_request=False)
-    else:
-      raise aiohttp.ClientResponseError(response.request_info, response.history, status=response.status, message=f"Failed to download {file_path}: {response.status}")
-
-    if downloaded_size == total_size:
-      print(f"File already downloaded: {file_path}")
-      if progress_callback:
-        await progress_callback(RepoFileProgressEvent(repo_id, revision, file_path, downloaded_size, downloaded_this_session, total_size, 0, timedelta(0), "complete"))
-      return
-
-    DOWNLOAD_CHUNK_SIZE = 32768
-    start_time = datetime.now()
-    async with aiofiles.open(local_path, mode) as f:
-      async for chunk in response.content.iter_chunked(DOWNLOAD_CHUNK_SIZE):
-        await f.write(chunk)
-        downloaded_size += len(chunk)
-        downloaded_this_session += len(chunk)
-        if progress_callback and total_size:
-          elapsed_time = (datetime.now() - start_time).total_seconds()
-          speed = int(downloaded_this_session/elapsed_time) if elapsed_time > 0 else 0
-          remaining_size = total_size - downloaded_size
-          eta = timedelta(seconds=remaining_size/speed) if speed > 0 else timedelta(0)
-          status = "in_progress" if downloaded_size < total_size else "complete"
-          if DEBUG >= 8: print(f"HF repo file download progress: {file_path=} {elapsed_time=} {speed=} Downloaded={downloaded_size}/{total_size} {remaining_size=} {eta=} {status=}")
-          await progress_callback(RepoFileProgressEvent(repo_id, revision, file_path, downloaded_size, downloaded_this_session, total_size, speed, eta, status))
-    if DEBUG >= 2: print(f"Downloaded: {file_path}")
-
-
-async def resolve_revision_to_commit_hash(repo_id: str, revision: str) -> str:
-  repo_root = get_repo_root(repo_id)
-  refs_dir = repo_root/"refs"
-  refs_file = refs_dir/revision
-
-  # Check if we have a cached commit hash
-  if await aios.path.exists(refs_file):
-    async with aiofiles.open(refs_file, 'r') as f:
-      commit_hash = (await f.read()).strip()
-      if DEBUG >= 2: print(f"Commit hash is already cached at {refs_file}: {commit_hash}")
-      return commit_hash
-
-  # Fetch the commit hash for the given revision
-  async with aiohttp.ClientSession() as session:
-    api_url = f"{get_hf_endpoint()}/api/models/{repo_id}/revision/{revision}"
-    headers = await get_auth_headers()
-    async with session.get(api_url, headers=headers) as response:
-      if response.status != 200:
-        raise Exception(f"Failed to fetch revision info from {api_url}: {response.status}")
-      revision_info = await response.json()
-      commit_hash = revision_info['sha']
-
-  # Cache the commit hash
-  await aios.makedirs(refs_dir, exist_ok=True)
-  async with aiofiles.open(refs_file, 'w') as f:
-    await f.write(commit_hash)
-
-  return commit_hash
-
-
-async def download_repo_files(
-  repo_id: str,
-  revision: str = "main",
-  progress_callback: Optional[RepoProgressCallback] = None,
-  allow_patterns: Optional[Union[List[str], str]] = None,
-  ignore_patterns: Optional[Union[List[str], str]] = None,
-  max_parallel_downloads: int = 4
-) -> Path:
-  repo_root = get_repo_root(repo_id)
-  snapshots_dir = repo_root/"snapshots"
-  cachedreqs_dir = repo_root/"cachedreqs"
-
-  # Ensure directories exist
-  await aios.makedirs(snapshots_dir, exist_ok=True)
-  await aios.makedirs(cachedreqs_dir, exist_ok=True)
-
-  # Resolve revision to commit hash
-  commit_hash = await resolve_revision_to_commit_hash(repo_id, revision)
-
-  # Set up the snapshot directory
-  snapshot_dir = snapshots_dir/commit_hash
-  await aios.makedirs(snapshot_dir, exist_ok=True)
-
-  # Set up the cached file list directory
-  cached_file_list_dir = cachedreqs_dir/commit_hash
-  await aios.makedirs(cached_file_list_dir, exist_ok=True)
-  cached_file_list_path = cached_file_list_dir/"fetch_file_list.json"
-
-  async with aiohttp.ClientSession() as session:
-    # Check if we have a cached file list
-    if await aios.path.exists(cached_file_list_path):
-      async with aiofiles.open(cached_file_list_path, 'r') as f:
-        file_list = json.loads(await f.read())
-      if DEBUG >= 2: print(f"Using cached file list from {cached_file_list_path}")
-    else:
-      file_list = await fetch_file_list(session, repo_id, revision)
-      # Cache the file list
-      async with aiofiles.open(cached_file_list_path, 'w') as f:
-        await f.write(json.dumps(file_list))
-      if DEBUG >= 2: print(f"Cached file list at {cached_file_list_path}")
-
-    model_index_exists = any(file["path"] == "model_index.json" for file in file_list)
-    if model_index_exists:
-      allow_patterns = ["**/*.json", "**/*.txt", "**/*model.safetensors", "*.json"]
-
-    filtered_file_list = list(filter_repo_objects(file_list, allow_patterns=allow_patterns, ignore_patterns=ignore_patterns, key=lambda x: x["path"]))
-    total_files = len(filtered_file_list)
-    total_bytes = sum(file["size"] for file in filtered_file_list)
-    file_progress: Dict[str, RepoFileProgressEvent] = {
-      file["path"]: RepoFileProgressEvent(repo_id, revision, file["path"], 0, 0, file["size"], 0, timedelta(0), "not_started")
-      for file in filtered_file_list
-    }
-    start_time = datetime.now()
-
-    async def download_with_progress(file_info, progress_state):
-      local_path = snapshot_dir/file_info["path"]
-      if await aios.path.exists(local_path) and (await aios.stat(local_path)).st_size == file_info["size"]:
-        if DEBUG >= 2: print(f"File already fully downloaded: {file_info['path']}")
-        progress_state['completed_files'] += 1
-        progress_state['downloaded_bytes'] += file_info["size"]
-        file_progress[file_info["path"]] = RepoFileProgressEvent(repo_id, revision, file_info["path"], file_info["size"], 0, file_info["size"], 0, timedelta(0), "complete")
-        if progress_callback:
-          elapsed_time = (datetime.now() - start_time).total_seconds()
-          overall_speed = int(progress_state['downloaded_bytes_this_session']/elapsed_time) if elapsed_time > 0 else 0
-          remaining_bytes = total_bytes - progress_state['downloaded_bytes']
-          overall_eta = timedelta(seconds=remaining_bytes/overall_speed) if overall_speed > 0 else timedelta(seconds=0)
-          status = "in_progress" if progress_state['completed_files'] < total_files else "complete"
-          await progress_callback(
-            RepoProgressEvent(
-              repo_id, revision, progress_state['completed_files'], total_files, progress_state['downloaded_bytes'], progress_state['downloaded_bytes_this_session'], total_bytes, overall_speed,
-              overall_eta, file_progress, status
-            )
-          )
-        return
-
-      async def file_progress_callback(event: RepoFileProgressEvent):
-        progress_state['downloaded_bytes'] += event.downloaded - file_progress[event.file_path].downloaded
-        progress_state['downloaded_bytes_this_session'] += event.downloaded_this_session - file_progress[event.file_path].downloaded_this_session
-        file_progress[event.file_path] = event
-        if progress_callback:
-          elapsed_time = (datetime.now() - start_time).total_seconds()
-          overall_speed = int(progress_state['downloaded_bytes_this_session']/elapsed_time) if elapsed_time > 0 else 0
-          remaining_bytes = total_bytes - progress_state['downloaded_bytes']
-          overall_eta = timedelta(seconds=remaining_bytes/overall_speed) if overall_speed > 0 else timedelta(seconds=0)
-          status = "in_progress" if progress_state['downloaded_bytes'] < total_bytes else "complete"
-          await progress_callback(
-            RepoProgressEvent(
-              repo_id, revision, progress_state['completed_files'], total_files, progress_state['downloaded_bytes'], progress_state['downloaded_bytes_this_session'], total_bytes, overall_speed,
-              overall_eta, file_progress, status
-            )
-          )
-
-      await download_file(session, repo_id, revision, file_info["path"], snapshot_dir, file_progress_callback)
-      progress_state['completed_files'] += 1
-      file_progress[
-        file_info["path"]
-      ] = RepoFileProgressEvent(repo_id, revision, file_info["path"], file_info["size"], file_progress[file_info["path"]].downloaded_this_session, file_info["size"], 0, timedelta(0), "complete")
-      if progress_callback:
-        elapsed_time = (datetime.now() - start_time).total_seconds()
-        overall_speed = int(progress_state['downloaded_bytes_this_session']/elapsed_time) if elapsed_time > 0 else 0
-        remaining_bytes = total_bytes - progress_state['downloaded_bytes']
-        overall_eta = timedelta(seconds=remaining_bytes/overall_speed) if overall_speed > 0 else timedelta(seconds=0)
-        status = "in_progress" if progress_state['completed_files'] < total_files else "complete"
-        await progress_callback(
-          RepoProgressEvent(
-            repo_id, revision, progress_state['completed_files'], total_files, progress_state['downloaded_bytes'], progress_state['downloaded_bytes_this_session'], total_bytes, overall_speed,
-            overall_eta, file_progress, status
-          )
-        )
-
-    progress_state = {'completed_files': 0, 'downloaded_bytes': 0, 'downloaded_bytes_this_session': 0}
-
-    semaphore = asyncio.Semaphore(max_parallel_downloads)
-
-    async def download_with_semaphore(file_info):
-      async with semaphore:
-        await download_with_progress(file_info, progress_state)
-
-    tasks = [asyncio.create_task(download_with_semaphore(file_info)) for file_info in filtered_file_list]
-    await asyncio.gather(*tasks)
-
-  return snapshot_dir
-
-
-async def get_weight_map(repo_id: str, revision: str = "main") -> Optional[Dict[str, str]]:
-  """
-    Retrieve the weight map from the model.safetensors.index.json file.
-
-    Args:
-        repo_id (str): The Hugging Face repository ID.
-        revision (str): The revision of the repository to use.
-
-    Returns:
-        Optional[Dict[str, str]]: The weight map if it exists, otherwise None.
-    """
-
-  # Download the index file
-  await download_repo_files(repo_id=repo_id, revision=revision, allow_patterns="model.safetensors.index.json")
-
-  # Check if the file exists
-  repo_root = get_repo_root(repo_id)
-  commit_hash = await resolve_revision_to_commit_hash(repo_id, revision)
-  snapshot_dir = repo_root/"snapshots"/commit_hash
-  index_file = next((f for f in await aios.listdir(snapshot_dir) if f.endswith("model.safetensors.index.json")), None)
-
-  if index_file:
-    index_file_path = snapshot_dir/index_file
-    if await aios.path.exists(index_file_path):
-      async with aiofiles.open(index_file_path, 'r') as f:
-        index_data = json.loads(await f.read())
-      return index_data.get("weight_map")
-
-  return None
-
-
-def extract_layer_num(tensor_name: str) -> Optional[int]:
-  # This is a simple example and might need to be adjusted based on the actual naming convention
-  parts = tensor_name.split('.')
-  for part in parts:
-    if part.isdigit():
-      return int(part)
-  return None
-
-
-def get_allow_patterns(weight_map: Dict[str, str], shard: Shard) -> List[str]:
-  default_patterns = set(["*.json", "*.py", "tokenizer.model", "*.tiktoken", "*.txt"])
-  shard_specific_patterns = set()
-  if weight_map:
-    for tensor_name, filename in weight_map.items():
-      layer_num = extract_layer_num(tensor_name)
-      if layer_num is not None and shard.start_layer <= layer_num <= shard.end_layer:
-        shard_specific_patterns.add(filename)
-    sorted_file_names = sorted(weight_map.values())
-    if shard.is_first_layer():
-      shard_specific_patterns.add(sorted_file_names[0])
-    elif shard.is_last_layer():
-      shard_specific_patterns.add(sorted_file_names[-1])
-  else:
-    shard_specific_patterns = set(["*.safetensors"])
-  if DEBUG >= 2: print(f"get_allow_patterns {weight_map=} {shard=} {shard_specific_patterns=}")
-  return list(default_patterns | shard_specific_patterns)
-
-async def has_hf_home_read_access() -> bool:
-  hf_home = get_hf_home()
-  try: return await aios.access(hf_home, os.R_OK)
-  except OSError: return False
-
-async def has_hf_home_write_access() -> bool:
-  hf_home = get_hf_home()
-  try: return await aios.access(hf_home, os.W_OK)
-  except OSError: return False
diff --git a/build/lib/exo/download/hf/hf_shard_download.py b/build/lib/exo/download/hf/hf_shard_download.py
deleted file mode 100644
index 22f4fa37..00000000
--- a/build/lib/exo/download/hf/hf_shard_download.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import asyncio
-import traceback
-from pathlib import Path
-from typing import Dict, List, Tuple
-from exo.inference.shard import Shard
-from exo.download.shard_download import ShardDownloader
-from exo.download.download_progress import RepoProgressEvent
-from exo.download.hf.hf_helpers import download_repo_files, RepoProgressEvent, get_weight_map, get_allow_patterns, get_repo_root
-from exo.helpers import AsyncCallbackSystem, DEBUG
-from exo.models import model_cards, get_repo
-
-
-class HFShardDownloader(ShardDownloader):
-  def __init__(self, quick_check: bool = False, max_parallel_downloads: int = 4):
-    self.quick_check = quick_check
-    self.max_parallel_downloads = max_parallel_downloads
-    self.active_downloads: Dict[Shard, asyncio.Task] = {}
-    self.completed_downloads: Dict[Shard, Path] = {}
-    self._on_progress = AsyncCallbackSystem[str, Tuple[Shard, RepoProgressEvent]]()
-
-  async def ensure_shard(self, shard: Shard, inference_engine_name: str) -> Path:
-    repo_name = get_repo(shard.model_id, inference_engine_name)
-    if shard in self.completed_downloads:
-      return self.completed_downloads[shard]
-    if self.quick_check:
-      repo_root = get_repo_root(repo_name)
-      snapshots_dir = repo_root/"snapshots"
-      if snapshots_dir.exists():
-        visible_dirs = [d for d in snapshots_dir.iterdir() if not d.name.startswith('.')]
-        if visible_dirs:
-          most_recent_dir = max(visible_dirs, key=lambda x: x.stat().st_mtime)
-          return most_recent_dir
-
-    # If a download on this shard is already in progress, keep that one
-    for active_shard in self.active_downloads:
-      if active_shard == shard:
-        if DEBUG >= 2: print(f"Download already in progress for {shard}. Keeping that one.")
-        return await self.active_downloads[shard]
-
-    # Cancel any downloads for this model_id on a different shard
-    existing_active_shards = [active_shard for active_shard in self.active_downloads.keys() if active_shard.model_id == shard.model_id]
-    for active_shard in existing_active_shards:
-      if DEBUG >= 2: print(f"Cancelling download for {active_shard} (replacing with {shard})")
-      task = self.active_downloads[active_shard]
-      task.cancel()
-      try:
-        await task
-      except asyncio.CancelledError:
-        pass  # This is expected when cancelling a task
-      except Exception as e:
-        if DEBUG >= 2: print(f"Error in cancelling download {active_shard}: {e}")
-        traceback.print_exc()
-    self.active_downloads = {active_shard: task for active_shard, task in self.active_downloads.items() if active_shard.model_id != shard.model_id}
-
-    # Start new download
-    download_task = asyncio.create_task(self._download_shard(shard, repo_name))
-    self.active_downloads[shard] = download_task
-    try:
-      path = await download_task
-      self.completed_downloads[shard] = path
-      return path
-    finally:
-      # Ensure the task is removed even if an exception occurs
-      print(f"Removing download task for {shard}: {shard in self.active_downloads}")
-      if shard in self.active_downloads:
-        self.active_downloads.pop(shard)
-
-  async def _download_shard(self, shard: Shard, repo_name: str) -> Path:
-    async def wrapped_progress_callback(event: RepoProgressEvent):
-      self._on_progress.trigger_all(shard, event)
-
-    weight_map = await get_weight_map(repo_name)
-    allow_patterns = get_allow_patterns(weight_map, shard)
-
-    return await download_repo_files(repo_name, progress_callback=wrapped_progress_callback, allow_patterns=allow_patterns, max_parallel_downloads=self.max_parallel_downloads)
-
-  @property
-  def on_progress(self) -> AsyncCallbackSystem[str, Tuple[Shard, RepoProgressEvent]]:
-    return self._on_progress
diff --git a/build/lib/exo/download/shard_download.py b/build/lib/exo/download/shard_download.py
deleted file mode 100644
index 78189962..00000000
--- a/build/lib/exo/download/shard_download.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from abc import ABC, abstractmethod
-from typing import Optional, Tuple
-from pathlib import Path
-from exo.inference.shard import Shard
-from exo.download.download_progress import RepoProgressEvent
-from exo.helpers import AsyncCallbackSystem
-
-
-class ShardDownloader(ABC):
-  @abstractmethod
-  async def ensure_shard(self, shard: Shard, inference_engine_name: str) -> Path:
-    """
-        Ensures that the shard is downloaded.
-        Does not allow multiple overlapping downloads at once.
-        If you try to download a Shard which overlaps a Shard that is already being downloaded,
-        the download will be cancelled and a new download will start.
-
-        Args:
-            shard (Shard): The shard to download.
-            inference_engine_name (str): The inference engine used on the node hosting the shard
-        """
-    pass
-
-  @property
-  @abstractmethod
-  def on_progress(self) -> AsyncCallbackSystem[str, Tuple[Shard, RepoProgressEvent]]:
-    pass
-
-
-class NoopShardDownloader(ShardDownloader):
-  async def ensure_shard(self, shard: Shard, inference_engine_name: str) -> Path:
-    return Path("/tmp/noop_shard")
-
-  @property
-  def on_progress(self) -> AsyncCallbackSystem[str, Tuple[Shard, RepoProgressEvent]]:
-    return AsyncCallbackSystem()
diff --git a/build/lib/exo/helpers.py b/build/lib/exo/helpers.py
deleted file mode 100644
index ea11bcd9..00000000
--- a/build/lib/exo/helpers.py
+++ /dev/null
@@ -1,274 +0,0 @@
-import os
-import sys
-import asyncio
-from typing import Callable, TypeVar, Optional, Dict, Generic, Tuple, List
-import socket
-import random
-import platform
-import psutil
-import uuid
-import netifaces
-from pathlib import Path
-import tempfile
-
-DEBUG = int(os.getenv("DEBUG", default="0"))
-DEBUG_DISCOVERY = int(os.getenv("DEBUG_DISCOVERY", default="0"))
-VERSION = "0.0.1"
-
-exo_text = r"""
-  _____  _____  
- / _ \ \/ / _ \ 
-|  __/>  < (_) |
- \___/_/\_\___/ 
-    """
-
-
-def get_system_info():
-  if psutil.MACOS:
-    if platform.machine() == "arm64":
-      return "Apple Silicon Mac"
-    if platform.machine() in ["x86_64", "i386"]:
-      return "Intel Mac"
-    return "Unknown Mac architecture"
-  if psutil.LINUX:
-    return "Linux"
-  return "Non-Mac, non-Linux system"
-
-
-def find_available_port(host: str = "", min_port: int = 49152, max_port: int = 65535) -> int:
-  used_ports_file = os.path.join(tempfile.gettempdir(), "exo_used_ports")
-
-  def read_used_ports():
-    if os.path.exists(used_ports_file):
-      with open(used_ports_file, "r") as f:
-        return [int(line.strip()) for line in f if line.strip().isdigit()]
-    return []
-
-  def write_used_port(port, used_ports):
-    with open(used_ports_file, "w") as f:
-      print(used_ports[-19:])
-      for p in used_ports[-19:] + [port]:
-        f.write(f"{p}\n")
-
-  used_ports = read_used_ports()
-  available_ports = set(range(min_port, max_port + 1)) - set(used_ports)
-
-  while available_ports:
-    port = random.choice(list(available_ports))
-    if DEBUG >= 2: print(f"Trying to find available port {port=}")
-    try:
-      with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
-        s.bind((host, port))
-      write_used_port(port, used_ports)
-      return port
-    except socket.error:
-      available_ports.remove(port)
-
-  raise RuntimeError("No available ports in the specified range")
-
-
-def print_exo():
-  print(exo_text)
-
-
-def print_yellow_exo():
-  yellow = "\033[93m"  # ANSI escape code for yellow
-  reset = "\033[0m"  # ANSI escape code to reset color
-  print(f"{yellow}{exo_text}{reset}")
-
-
-def terminal_link(uri, label=None):
-  if label is None:
-    label = uri
-  parameters = ""
-
-  # OSC 8 ; params ; URI ST <name> OSC 8 ;; ST
-  escape_mask = "\033]8;{};{}\033\\{}\033]8;;\033\\"
-
-  return escape_mask.format(parameters, uri, label)
-
-
-T = TypeVar("T")
-K = TypeVar("K")
-
-
-class AsyncCallback(Generic[T]):
-  def __init__(self) -> None:
-    self.condition: asyncio.Condition = asyncio.Condition()
-    self.result: Optional[Tuple[T, ...]] = None
-    self.observers: list[Callable[..., None]] = []
-
-  async def wait(self, check_condition: Callable[..., bool], timeout: Optional[float] = None) -> Tuple[T, ...]:
-    async with self.condition:
-      await asyncio.wait_for(self.condition.wait_for(lambda: self.result is not None and check_condition(*self.result)), timeout)
-      assert self.result is not None  # for type checking
-      return self.result
-
-  def on_next(self, callback: Callable[..., None]) -> None:
-    self.observers.append(callback)
-
-  def set(self, *args: T) -> None:
-    self.result = args
-    for observer in self.observers:
-      observer(*args)
-    asyncio.create_task(self.notify())
-
-  async def notify(self) -> None:
-    async with self.condition:
-      self.condition.notify_all()
-
-
-class AsyncCallbackSystem(Generic[K, T]):
-  def __init__(self) -> None:
-    self.callbacks: Dict[K, AsyncCallback[T]] = {}
-
-  def register(self, name: K) -> AsyncCallback[T]:
-    if name not in self.callbacks:
-      self.callbacks[name] = AsyncCallback[T]()
-    return self.callbacks[name]
-
-  def deregister(self, name: K) -> None:
-    if name in self.callbacks:
-      del self.callbacks[name]
-
-  def trigger(self, name: K, *args: T) -> None:
-    if name in self.callbacks:
-      self.callbacks[name].set(*args)
-
-  def trigger_all(self, *args: T) -> None:
-    for callback in self.callbacks.values():
-      callback.set(*args)
-
-
-K = TypeVar('K', bound=str)
-V = TypeVar('V')
-
-
-class PrefixDict(Generic[K, V]):
-  def __init__(self):
-    self.items: Dict[K, V] = {}
-
-  def add(self, key: K, value: V) -> None:
-    self.items[key] = value
-
-  def find_prefix(self, argument: str) -> List[Tuple[K, V]]:
-    return [(key, value) for key, value in self.items.items() if argument.startswith(key)]
-
-  def find_longest_prefix(self, argument: str) -> Optional[Tuple[K, V]]:
-    matches = self.find_prefix(argument)
-    if len(matches) == 0:
-      return None
-
-    return max(matches, key=lambda x: len(x[0]))
-
-
-def is_valid_uuid(val):
-  try:
-    uuid.UUID(str(val))
-    return True
-  except ValueError:
-    return False
-
-
-def get_or_create_node_id():
-  NODE_ID_FILE = Path(tempfile.gettempdir())/".exo_node_id"
-  try:
-    if NODE_ID_FILE.is_file():
-      with open(NODE_ID_FILE, "r") as f:
-        stored_id = f.read().strip()
-      if is_valid_uuid(stored_id):
-        if DEBUG >= 2: print(f"Retrieved existing node ID: {stored_id}")
-        return stored_id
-      else:
-        if DEBUG >= 2: print("Stored ID is not a valid UUID. Generating a new one.")
-
-    new_id = str(uuid.uuid4())
-    with open(NODE_ID_FILE, "w") as f:
-      f.write(new_id)
-
-    if DEBUG >= 2: print(f"Generated and stored new node ID: {new_id}")
-    return new_id
-  except IOError as e:
-    if DEBUG >= 2: print(f"IO error creating node_id: {e}")
-    return str(uuid.uuid4())
-  except Exception as e:
-    if DEBUG >= 2: print(f"Unexpected error creating node_id: {e}")
-    return str(uuid.uuid4())
-
-
-def pretty_print_bytes(size_in_bytes: int) -> str:
-  if size_in_bytes < 1024:
-    return f"{size_in_bytes} B"
-  elif size_in_bytes < 1024**2:
-    return f"{size_in_bytes / 1024:.2f} KB"
-  elif size_in_bytes < 1024**3:
-    return f"{size_in_bytes / (1024 ** 2):.2f} MB"
-  elif size_in_bytes < 1024**4:
-    return f"{size_in_bytes / (1024 ** 3):.2f} GB"
-  else:
-    return f"{size_in_bytes / (1024 ** 4):.2f} TB"
-
-
-def pretty_print_bytes_per_second(bytes_per_second: int) -> str:
-  if bytes_per_second < 1024:
-    return f"{bytes_per_second} B/s"
-  elif bytes_per_second < 1024**2:
-    return f"{bytes_per_second / 1024:.2f} KB/s"
-  elif bytes_per_second < 1024**3:
-    return f"{bytes_per_second / (1024 ** 2):.2f} MB/s"
-  elif bytes_per_second < 1024**4:
-    return f"{bytes_per_second / (1024 ** 3):.2f} GB/s"
-  else:
-    return f"{bytes_per_second / (1024 ** 4):.2f} TB/s"
-
-
-def get_all_ip_addresses():
-  try:
-    ip_addresses = []
-    for interface in netifaces.interfaces():
-      ifaddresses = netifaces.ifaddresses(interface)
-      if netifaces.AF_INET in ifaddresses:
-        for link in ifaddresses[netifaces.AF_INET]:
-          ip = link['addr']
-          ip_addresses.append(ip)
-    return list(set(ip_addresses))
-  except:
-    if DEBUG >= 1: print("Failed to get all IP addresses. Defaulting to localhost.")
-    return ["localhost"]
-
-
-async def shutdown(signal, loop, server):
-  """Gracefully shutdown the server and close the asyncio loop."""
-  print(f"Received exit signal {signal.name}...")
-  print("Thank you for using exo.")
-  print_yellow_exo()
-  server_tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
-  [task.cancel() for task in server_tasks]
-  print(f"Cancelling {len(server_tasks)} outstanding tasks")
-  await asyncio.gather(*server_tasks, return_exceptions=True)
-  await server.stop()
-
-
-def is_frozen():
-  return getattr(sys, 'frozen', False) or os.path.basename(sys.executable) == "exo" \
-    or ('Contents/MacOS' in str(os.path.dirname(sys.executable))) \
-    or '__nuitka__' in globals() or getattr(sys, '__compiled__', False)
-
-
-def get_exo_home() -> Path:
-  if os.name == "nt":  # Check if the OS is Windows
-    docs_folder = Path(os.environ["USERPROFILE"]) / "Documents"
-  else:
-    docs_folder = Path.home() / "Documents"
-  exo_folder = docs_folder / "Exo"
-  if not exo_folder.exists():
-    exo_folder.mkdir()
-  return exo_folder
-
-def get_exo_images_dir() -> Path:
-  exo_home = get_exo_home()
-  images_dir = exo_home / "Images"
-  if not images_dir.exists():
-    images_dir.mkdir()
-  return images_dir
-  
\ No newline at end of file
diff --git a/build/lib/exo/inference/__init__.py b/build/lib/exo/inference/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/inference/debug_inference_engine.py b/build/lib/exo/inference/debug_inference_engine.py
deleted file mode 100644
index c1ae4b40..00000000
--- a/build/lib/exo/inference/debug_inference_engine.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from exo.inference.inference_engine import InferenceEngine
-from exo.inference.shard import Shard
-from exo.inference.tinygrad.inference import TinygradDynamicShardInferenceEngine
-import asyncio
-import numpy as np
-
-
-# An inference engine should work the same for any number of Shards, as long as the Shards are continuous.
-async def test_inference_engine(inference_engine_1: InferenceEngine, inference_engine_2: InferenceEngine, model_id: str):
-  from exo.inference.tinygrad.inference import Tokenizer
-  from pathlib import Path
-
-  _tokenizer = Tokenizer(str(Path(model_id)/"tokenizer.model"))
-
-  prompt = "In a single word only, what is the last name of the president of the United States? "
-  resp_full = await inference_engine_1.infer_prompt("A", shard=Shard(model_id=model_id, start_layer=0, end_layer=31, n_layers=32), prompt=prompt)
-  token_full = await inference_engine_1.sample(resp_full)
-
-  next_resp_full = await inference_engine_1.infer_tensor(
-    "A",
-    shard=Shard(model_id=model_id, start_layer=0, end_layer=31, n_layers=32),
-    input_data=token_full,
-  )
-
-  resp1 = await inference_engine_1.infer_prompt("B", shard=Shard(model_id=model_id, start_layer=0, end_layer=30, n_layers=32), prompt=prompt)
-  resp2 = await inference_engine_2.infer_tensor(
-    "B",
-    shard=Shard(model_id=model_id, start_layer=31, end_layer=31, n_layers=32),
-    input_data=resp1,
-  )
-  token2 = await inference_engine_2.sample(resp2)
-  resp3 = await inference_engine_1.infer_tensor(
-    "B",
-    shard=Shard(model_id=model_id, start_layer=0, end_layer=30, n_layers=32),
-    input_data=token2,
-  )
-  resp4 = await inference_engine_2.infer_tensor(
-    "B",
-    shard=Shard(model_id=model_id, start_layer=31, end_layer=31, n_layers=32),
-    input_data=resp3,
-  )
-
-  print(f"{resp2=}")
-  print(f"full: {_tokenizer.decode(resp_full)}")
-  print(f"next full: {_tokenizer.decode(next_resp_full)}")
-  print(f"resp2: {_tokenizer.decode(resp2)}")
-  print(f"{resp4=}")
-  print(f"resp4: {_tokenizer.decode(resp4)}")
-
-  assert np.array_equal(resp_full, resp2)
-  assert np.array_equal(next_resp_full, resp4)
-
-
-asyncio.run(test_inference_engine(
-  TinygradDynamicShardInferenceEngine(),
-  TinygradDynamicShardInferenceEngine(),
-  "llama3-8b-sfr",
-))
diff --git a/build/lib/exo/inference/dummy_inference_engine.py b/build/lib/exo/inference/dummy_inference_engine.py
deleted file mode 100644
index 09026109..00000000
--- a/build/lib/exo/inference/dummy_inference_engine.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from typing import Optional, Tuple, TYPE_CHECKING
-import numpy as np
-from exo.inference.inference_engine import InferenceEngine
-from exo.inference.shard import Shard
-from exo.inference.tokenizers import DummyTokenizer
-
-class DummyInferenceEngine(InferenceEngine):
-  def __init__(self):
-    self.shard = None
-    self.vocab_size = 1000
-    self.hidden_size = 256
-    self.eos_token_id = 0
-    self.latency_mean = 0.1
-    self.latency_stddev = 0.02
-    self.num_generate_dummy_tokens = 10
-    self.tokenizer = DummyTokenizer()
-
-  async def encode(self, shard: Shard, prompt: str) -> np.ndarray:
-    return np.array(self.tokenizer.encode(prompt))
-  
-  async def sample(self, x: np.ndarray, temp: float = 0.0, top_p: float = 1.0) -> np.ndarray:
-    if x[0] > self.num_generate_dummy_tokens: return np.array([self.tokenizer.eos_token_id])
-    return x
-
-  async def decode(self, shard: Shard, tokens: np.ndarray) -> str:
-    return self.tokenizer.decode(tokens)
-
-  async def infer_tensor(self, request_id: str, shard: Shard, input_data: np.ndarray) -> np.ndarray:
-    await self.ensure_shard(shard)
-    return input_data + 1 if self.shard.is_last_layer() else input_data
-
-  async def ensure_shard(self, shard: Shard):
-    if self.shard == shard: return
-    self.shard = shard
diff --git a/build/lib/exo/inference/inference_engine.py b/build/lib/exo/inference/inference_engine.py
deleted file mode 100644
index 3f82e0e2..00000000
--- a/build/lib/exo/inference/inference_engine.py
+++ /dev/null
@@ -1,58 +0,0 @@
-import numpy as np
-import os
-from exo.helpers import DEBUG  # Make sure to import DEBUG
-
-from typing import Tuple, Optional
-from abc import ABC, abstractmethod
-from .shard import Shard
-
-
-class InferenceEngine(ABC):
-  @abstractmethod
-  async def encode(self, shard: Shard, prompt: str) -> np.ndarray:
-    pass
-  
-  @abstractmethod
-  async def sample(self, x: np.ndarray) -> np.ndarray:
-    pass
-
-  @abstractmethod
-  async def decode(self, shard: Shard, tokens: np.ndarray) -> str:
-    pass
-
-  @abstractmethod
-  async def infer_tensor(self, request_id: str, shard: Shard, input_data: np.ndarray) -> np.ndarray:
-    pass
-  
-  async def infer_prompt(self, request_id: str, shard: Shard, prompt: str, inference_state: Optional[dict] = None) -> np.ndarray:
-    tokens = await self.encode(shard, prompt)
-    if shard.model_id != 'stable-diffusion-2-1-base':
-      x = tokens.reshape(1, -1)
-    else:
-      x = tokens
-    output_data, inference_state = await self.infer_tensor(request_id, shard, x, inference_state)
-    return output_data, inference_state
-
-inference_engine_classes = {
-  "mlx": "MLXDynamicShardInferenceEngine",
-  "tinygrad": "TinygradDynamicShardInferenceEngine",
-  "dummy": "DummyInferenceEngine",
-}
-
-def get_inference_engine(inference_engine_name: str, shard_downloader: 'ShardDownloader'):
-  if DEBUG >= 2:
-    print(f"get_inference_engine called with: {inference_engine_name}")
-  if inference_engine_name == "mlx":
-    from exo.inference.mlx.sharded_inference_engine import MLXDynamicShardInferenceEngine
-
-    return MLXDynamicShardInferenceEngine(shard_downloader)
-  elif inference_engine_name == "tinygrad":
-    from exo.inference.tinygrad.inference import TinygradDynamicShardInferenceEngine
-    import tinygrad.helpers
-    tinygrad.helpers.DEBUG.value = int(os.getenv("TINYGRAD_DEBUG", default="0"))
-
-    return TinygradDynamicShardInferenceEngine(shard_downloader)
-  elif inference_engine_name == "dummy":
-    from exo.inference.dummy_inference_engine import DummyInferenceEngine
-    return DummyInferenceEngine()
-  raise ValueError(f"Unsupported inference engine: {inference_engine_name}")
diff --git a/build/lib/exo/inference/mlx/__init__.py b/build/lib/exo/inference/mlx/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/inference/mlx/models/StableDiffusionPipeline.py b/build/lib/exo/inference/mlx/models/StableDiffusionPipeline.py
deleted file mode 100644
index 1f3bd1ca..00000000
--- a/build/lib/exo/inference/mlx/models/StableDiffusionPipeline.py
+++ /dev/null
@@ -1,307 +0,0 @@
-# Adapted from https://github.com/ml-explore/mlx-examples/blob/main/stable_diffusion/stable_diffusion/__init__.py
-
-import time
-from typing import Optional, Tuple
-import inspect
-
-import mlx.core as mx
-import mlx.nn as nn
-from pathlib import Path
-
-from tqdm import tqdm
-
-from .sd_models.vae import ModelArgs as VAEArgs
-from .sd_models.vae import Autoencoder
-from .sd_models.tokenizer import load_tokenizer
-from .sd_models.clip import CLIPTextModel
-from .sd_models.clip import ModelArgs as CLIPArgs
-from .sd_models.unet import UNetConfig, UNetModel
-
-from dataclasses import dataclass, field
-from exo.inference.shard import Shard
-
-@dataclass
-class DiffusionConfig:
-    beta_schedule: str = "scaled_linear"
-    beta_start: float = 0.00085
-    beta_end: float = 0.012
-    num_train_steps: int = 1000
-
-    @classmethod
-    def from_dict(cls, params):
-        return cls(**{k: v for k, v in params.items() if k in inspect.signature(cls).parameters})
-
-
-#Sampler
-def _linspace(a, b, num):
-    x = mx.arange(0, num) / (num - 1)
-    return (b - a) * x + a
-
-
-def _interp(y, x_new):
-    """Interpolate the function defined by (arange(0, len(y)), y) at positions x_new."""
-    x_low = x_new.astype(mx.int32)
-    x_high = mx.minimum(x_low + 1, len(y) - 1)
-
-    y_low = y[x_low]
-    y_high = y[x_high]
-    delta_x = x_new - x_low
-    y_new = y_low * (1 - delta_x) + delta_x * y_high
-
-    return y_new
-
-class SimpleEulerSampler:
-    """A simple Euler integrator that can be used to sample from our diffusion models.
-
-    The method ``step()`` performs one Euler step from x_t to x_t_prev.
-    """
-
-    def __init__(self, config: DiffusionConfig):
-        # Compute the noise schedule
-        if config.beta_schedule == "linear":
-            betas = _linspace(
-                config.beta_start, config.beta_end, config.num_train_steps
-            )
-        elif config.beta_schedule == "scaled_linear":
-            betas = _linspace(
-                config.beta_start**0.5, config.beta_end**0.5, config.num_train_steps
-            ).square()
-        else:
-            raise NotImplementedError(f"{config.beta_schedule} is not implemented.")
-
-        alphas = 1 - betas
-        alphas_cumprod = mx.cumprod(alphas)
-
-        self._sigmas = mx.concatenate(
-            [mx.zeros(1), ((1 - alphas_cumprod) / alphas_cumprod).sqrt()]
-        )
-
-    @property
-    def max_time(self):
-        return len(self._sigmas) - 1
-
-    def sample_prior(self, shape, dtype=mx.float32, key=None):
-        noise = mx.random.normal(shape, key=key)
-        return (
-            noise * self._sigmas[-1] * (self._sigmas[-1].square() + 1).rsqrt()
-        ).astype(dtype)
-
-    def add_noise(self, x, t, key=None):
-        noise = mx.random.normal(x.shape, key=key)
-        s = self.sigmas(t)
-        return (x + noise * s) * (s.square() + 1).rsqrt()
-
-    def sigmas(self, t):
-        return _interp(self._sigmas, t)
-
-    def timesteps(self, num_steps: int, start_time=None, dtype=mx.float32):
-        start_time = start_time or (len(self._sigmas) - 1)
-        assert 0 < start_time <= (len(self._sigmas) - 1)
-        steps = _linspace(start_time, 0, num_steps + 1).astype(dtype)
-        return list(zip(steps, steps[1:]))
-
-    def current_timestep(self, step, total_steps, start_time=None):
-        if step < total_steps:
-            steps = self.timesteps(total_steps, start_time)
-            return steps[step]
-        else:
-            return mx.array(0),mx.array(0)
-
-    def step(self, eps_pred, x_t, t, t_prev):
-        sigma = self.sigmas(t).astype(eps_pred.dtype)
-        sigma_prev = self.sigmas(t_prev).astype(eps_pred.dtype)
-
-        dt = sigma_prev - sigma
-        x_t_prev = (sigma.square() + 1).sqrt() * x_t + eps_pred * dt
-
-        x_t_prev = x_t_prev * (sigma_prev.square() + 1).rsqrt()
-
-        return x_t_prev
-
-@dataclass
-class ShardConfig:
-    model_id:str
-    start_layer:int
-    end_layer:int
-    n_layers:int
-
-@dataclass
-class StableDiffusionConfig:
-    model_type:str
-    vae:VAEArgs
-    text_encoder:CLIPArgs
-    scheduler:DiffusionConfig
-    unet:UNetConfig
-    shard:ShardConfig
-    
-    @classmethod
-    def from_dict(cls, params):
-        return cls(**{k: v for k, v in params.items() if k in inspect.signature(cls).parameters})
-
-@dataclass
-class ModelArgs(StableDiffusionConfig):
-    shard:Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
-    def __post_init__(self):
-        if isinstance(self.shard, dict):
-            self.shard = Shard(**self.shard)
-
-        if not isinstance(self.shard, Shard):
-            raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
-
-class Model(nn.Module):
-    def __init__(self, config):
-        super().__init__()
-        self.model_type = config.model_type
-        self.config = config
-        self.model_path = config.vae['path'].split('/vae')[0]
-        self.shard = config.shard
-        self.shard_clip, self.shard_encoder, self.shard_unet, self.shard_decoder  = model_shards(config.shard)
-        self.config_clip=CLIPArgs.from_dict(config.text_encoder['config'])
-        if self.shard_clip.start_layer != -1:
-            self.text_encoder = CLIPTextModel(self.config_clip, shard=self.shard_clip)
-        else:
-            self.text_encoder = nn.Identity()    
-        self.tokenizer = load_tokenizer(Path(self.model_path), "vocab.json", "merges.txt")
-        self.diffusion_config = DiffusionConfig.from_dict(config.scheduler['config'])
-        self.sampler = SimpleEulerSampler(self.diffusion_config)
-        if self.shard_unet.start_layer!=-1:
-            self.config_unet = UNetConfig.from_dict(config.unet['config'])
-            self.unet = UNetModel(self.config_unet, self.shard_unet)
-        else:
-            self.unet = nn.Identity()
-        self.config_vae=VAEArgs.from_dict(config.vae['config'])
-        if self.shard_encoder.start_layer != -1:
-            self.encoder=Autoencoder(self.config_vae, self.shard_encoder, "vae_encoder") 
-        else:
-            self.encoder = nn.Identity()            
-        if self.shard_decoder.start_layer != -1:
-            self.decoder=Autoencoder(self.config_vae, self.shard_decoder, "vae_decoder") 
-        else:
-            self.decoder = nn.Identity()            
-
-    def __call__(self,x, step= 0, cfg_weight: float = 7.5,total_steps=50,conditioning=None,mask=None,residual=None,x_t_prev=None,is_finished=False,is_step_finished=False, image=None, strength=0.7, start_step=None):
-        t, t_prev = self.sampler.current_timestep(step=step, total_steps=total_steps, start_time=start_step)
-        is_finished = False
-        is_step_finished = False
-        if t.item()==1000:
-            if self.shard_clip.start_layer == 0:
-                conditioning = x
-            if self.shard_clip.start_layer != -1:
-                conditioning, mask= self.text_encoder(conditioning,mask)
-            seed = int(time.time()) 
-            mx.random.seed(seed)
-            if image is None:
-                if self.shard_encoder.is_last_layer():
-                    x = self.sampler.sample_prior((1, *(64, 64), self.config_vae.latent_channels_in), dtype=mx.float32)
-                    x_t_prev=x
-                    start_step = self.sampler.max_time
-            else:
-                if self.shard_encoder.start_layer != -1:
-                    image= self.encoder.encode(image)
-                    if self.shard_encoder.is_last_layer():
-                        start_step = self.sampler.max_time*strength
-                        total_steps = int(total_steps*strength)
-                        image = mx.broadcast_to(image, (1,) + image.shape[1:])
-                        x_t_prev=self.sampler.add_noise(image, mx.array(start_step))
-                        image = None
-                        t, t_prev = self.sampler.current_timestep(step=step, total_steps=total_steps, start_time=start_step)
-        # Perform the denoising loop
-        if self.shard_unet.start_layer != -1:
-            with tqdm(total=total_steps,initial=step+1) as pbar:
-                if step<total_steps:
-                    x = x_t_prev
-                    if self.shard_unet.is_first_layer():
-                        x_t_unet = mx.concatenate([x] * 2, axis=0) if cfg_weight> 1 else x
-                    else:
-                        x_t_unet = x
-                    t_unet = mx.broadcast_to(t, [len(x_t_unet)])
-                    x, residual= self.unet(x_t_unet, t_unet, encoder_x=conditioning, residuals=residual)
-                    if self.shard_unet.is_last_layer():
-                        if cfg_weight > 1:
-                            eps_text, eps_neg = x.split(2)
-                            eps_pred = eps_neg + cfg_weight * (eps_text - eps_neg)
-                        x = self.sampler.step(eps_pred, x_t_prev, t, t_prev)
-                        x_t_prev=x
-                    mx.eval(x)
-                    
-        if self.shard_decoder.is_last_layer():
-            is_step_finished=True
-            if self.shard_decoder.start_layer != -1:
-                x=self.decoder.decode(x)
-            if self.shard_decoder.is_last_layer():
-                x = mx.clip(x / 2 + 0.5, 0, 1)
-                B, H, W, C = x.shape
-                x = x.reshape(1, B // 1, H, W, C).transpose(0, 2, 1, 3, 4)
-                x = x.reshape(1 * H, B // 1 * W, C)
-                x = (x * 255).astype(mx.uint8)
-                if t_prev.item() ==0:
-                    is_finished=True   
-        mx.eval(x)
-         
-        return x, {'conditioning':conditioning, 'mask':mask,'residual':residual,'x_t_prev':x_t_prev,'is_finished':is_finished,'is_step_finished':is_step_finished, 'step':step, 'total_steps':total_steps, 'start_step':start_step, 'image':image}
-    
-
-    def load(self):
-        if self.shard_encoder.start_layer != -1:    
-            vae_weights =  mx.load(self.config_vae.weight_files[0])
-            vae_weights = self.encoder.sanitize(vae_weights)
-            self.encoder.load_weights(list(vae_weights.items()), strict=True)
-        if self.shard_decoder.start_layer != -1:
-            vae_weights =  mx.load(self.config_vae.weight_files[0])
-            vae_weights = self.decoder.sanitize(vae_weights)
-            self.decoder.load_weights(list(vae_weights.items()), strict=True)
-        if self.shard_clip.start_layer != -1:
-            clip_weights = mx.load(self.config_clip.weight_files[0])
-            clip_weights = self.text_encoder.sanitize(clip_weights)
-            self.text_encoder.load_weights(list(clip_weights.items()), strict=True)
-        if self.shard_unet.start_layer !=-1:
-            unet_weights = mx.load(self.config_unet.weight_files[0])
-            unet_weights = self.unet.sanitize(unet_weights)
-            self.unet.load_weights(list(unet_weights.items()), strict=True)
-
-def model_shards(shard:ShardConfig):
-    def create_shard(shard, model_ranges):
-        start_layer = shard.start_layer
-        end_layer = shard.end_layer
-        
-        shards = {}
-        
-        for model_name, (range_start, range_end) in model_ranges.items():
-            if start_layer < range_end and end_layer >= range_start:
-                # Calculate the overlap with the model range
-                overlap_start = max(start_layer, range_start)
-                overlap_end = min(end_layer, range_end - 1)
-
-                # Adjust the layers relative to the model's range
-                relative_start = overlap_start - range_start
-                relative_end = overlap_end - range_start
-                shards[model_name] = Shard(model_name, relative_start, relative_end, range_end - range_start)
-            else:
-                # If no overlap, create a zero-layer shard
-                shards[model_name] = Shard(model_name, -1, -1, range_end - range_start)
-        
-        return shards
-
-    # Define the ranges for different models
-    model_ranges = {
-        'clip': (0, 12),
-        'vae_encoder':(12,17),
-        'unet':(17,26),
-        'vae_decoder': (26, 31) # Example range for unet
-    }
-
-    # Call the function and get the shards for all models
-    shards = create_shard(shard, model_ranges)
-
-    # Access individual shards
-    shard_clip = shards['clip']
-    shard_encoder = shards['vae_encoder']
-    shard_unet = shards['unet']
-    shard_decoder = shards['vae_decoder']
-    
-    return shard_clip, shard_encoder, shard_unet, shard_decoder
-
-
-
diff --git a/build/lib/exo/inference/mlx/models/__init__.py b/build/lib/exo/inference/mlx/models/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/inference/mlx/models/base.py b/build/lib/exo/inference/mlx/models/base.py
deleted file mode 100644
index 5181adfd..00000000
--- a/build/lib/exo/inference/mlx/models/base.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from typing import Optional
-import mlx.core as mx
-import mlx.nn as nn
-from mlx_lm.models.cache import KVCache
-
-
-class IdentityBlock(nn.Module):
-  def __call__(self, x: mx.array, mask: Optional[mx.array] = None, cache: Optional[KVCache] = None) -> mx.array:
-    return x
diff --git a/build/lib/exo/inference/mlx/models/deepseek_v2.py b/build/lib/exo/inference/mlx/models/deepseek_v2.py
deleted file mode 100644
index 8952e9e6..00000000
--- a/build/lib/exo/inference/mlx/models/deepseek_v2.py
+++ /dev/null
@@ -1,127 +0,0 @@
-from dataclasses import dataclass, field
-from typing import Optional
-
-import mlx.core as mx
-import mlx.nn as nn
-
-from mlx_lm.models.cache import KVCache
-from mlx_lm.models.deepseek_v2 import ModelArgs, DeepseekV2DecoderLayer
-from .base import IdentityBlock
-from exo.inference.shard import Shard
-
-
-@dataclass
-class ModelArgs(ModelArgs):
-  shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
-  def __post_init__(self):
-    if isinstance(self.shard, Shard):
-      return
-    if not isinstance(self.shard, dict):
-      raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
-    self.shard = Shard(**self.shard)
-
-
-class DeepseekV2Model(nn.Module):
-  def __init__(self, config: ModelArgs):
-    super().__init__()
-    self.args = config
-    self.num_hidden_layers = config.num_hidden_layers
-    self.vocab_size = config.vocab_size
-    if self.args.shard.is_first_layer():
-      self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
-
-    self.layers = []
-    for i in range(self.num_hidden_layers):
-      if self.args.shard.start_layer <= i <= self.args.shard.end_layer:
-        self.layers.append(DeepseekV2DecoderLayer(config, i))
-      else:
-        self.layers.append(IdentityBlock())
-
-    if self.args.shard.is_last_layer():
-      self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
-
-  def __call__(
-    self,
-    x: mx.array,
-    cache: Optional[KVCache] = None,
-  ) -> mx.array:
-    if self.args.shard.is_first_layer():
-      h = self.embed_tokens(x)
-    else:
-      h = x
-
-    mask = None
-    T = h.shape[1]
-    if T > 1:
-      mask = nn.MultiHeadAttention.create_additive_causal_mask(T)
-      mask = mask.astype(h.dtype)
-
-    if cache is None:
-      cache = [None]*len(self.layers)
-
-    for layer, c in zip(self.layers, cache):
-      h = layer(h, mask, c)
-
-    if self.args.shard.is_last_layer():
-      h = self.norm(h)
-    return h
-
-
-class Model(nn.Module):
-  def __init__(self, config: ModelArgs):
-    super().__init__()
-    self.args = config
-    self.model_type = config.model_type
-    self.model = DeepseekV2Model(config)
-    if self.args.shard.is_last_layer():
-      self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
-
-  def __call__(
-    self,
-    inputs: mx.array,
-    cache: Optional[KVCache] = None,
-  ):
-    out = self.model(inputs, cache)
-    if self.args.shard.is_last_layer():
-      return self.lm_head(out)
-    return out
-
-  def sanitize(self, weights):
-    shard_state_dict = {}
-
-    for key, value in weights.items():
-      if key.startswith('model.layers.'):
-        layer_num = int(key.split('.')[2])
-        if self.args.shard.start_layer <= layer_num <= self.args.shard.end_layer:
-          shard_state_dict[key] = value
-      elif self.args.shard.is_first_layer() and key.startswith('model.embed_tokens'):
-        shard_state_dict[key] = value
-      elif self.args.shard.is_last_layer() and (key.startswith('model.norm') or key.startswith('lm_head')):
-        shard_state_dict[key] = value
-
-    for l in range(self.args.num_hidden_layers):
-      prefix = f"model.layers.{l}"
-      for n, m in [("w1", "gate_proj"), ("w2", "down_proj"), ("w3", "up_proj")]:
-        for k in ["weight", "scales", "biases"]:
-          if f"{prefix}.mlp.experts.0.{m}.{k}" in shard_state_dict:
-            to_join = [shard_state_dict.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}") for e in range(self.args.n_routed_experts)]
-            shard_state_dict[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
-
-    return shard_state_dict
-
-  @property
-  def layers(self):
-    return self.model.layers
-
-  @property
-  def head_dim(self):
-    return (
-      self.args.qk_nope_head_dim + self.args.qk_rope_head_dim,
-      self.args.v_head_dim,
-    )
-
-  @property
-  def n_kv_heads(self):
-    return self.args.num_key_value_heads
diff --git a/build/lib/exo/inference/mlx/models/gemma2.py b/build/lib/exo/inference/mlx/models/gemma2.py
deleted file mode 100644
index 88957fd9..00000000
--- a/build/lib/exo/inference/mlx/models/gemma2.py
+++ /dev/null
@@ -1,118 +0,0 @@
-from dataclasses import dataclass, field
-
-import mlx.core as mx
-import mlx.nn as nn
-
-from mlx_lm.models.base import create_attention_mask
-from mlx_lm.models.gemma2 import TransformerBlock, ModelArgs, RMSNorm
-
-from ...shard import Shard
-from .base import IdentityBlock
-
-
-@dataclass
-class ModelArgs(ModelArgs):
-  shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
-  def __post_init__(self):
-    if isinstance(self.shard, Shard):
-      return
-    if not isinstance(self.shard, dict):
-      raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
-    self.shard = Shard(**self.shard)
-
-
-class GemmaModel(nn.Module):
-  def __init__(self, args: ModelArgs):
-    super().__init__()
-    self.args = args
-    self.vocab_size = args.vocab_size
-    self.num_hidden_layers = args.num_hidden_layers
-    assert self.vocab_size > 0
-    if args.shard.is_first_layer() or args.shard.is_last_layer():
-      self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
-    self.layers = []
-    for i in range(self.num_hidden_layers):
-      if args.shard.start_layer <= i <= args.shard.end_layer:
-        self.layers.append(TransformerBlock(args=args))
-      else:
-        self.layers.append(IdentityBlock())
-    if args.shard.is_last_layer():
-      self.norm = RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
-
-  def __call__(
-    self,
-    inputs: mx.array,
-    cache=None,
-  ):
-    if self.args.shard.is_first_layer():
-      h = self.embed_tokens(inputs)
-      h = h * (self.args.hidden_size**0.5)
-    else:
-      h = inputs
-
-    mask = None
-    if h.ndim > 1 and h.shape[1] > 1:
-      mask = create_attention_mask(h, cache)
-
-    if cache is None:
-      cache = [None]*len(self.layers)
-
-    for layer, c in zip(self.layers, cache):
-      h = layer(h, mask, cache=c)
-
-    if self.args.shard.is_last_layer():
-      h = self.norm(h)
-    return h
-
-
-class Model(nn.Module):
-  def __init__(self, args: ModelArgs):
-    super().__init__()
-    self.args = args
-    self.model_type = args.model_type
-    self.model = GemmaModel(args)
-    if args.shard.is_last_layer():
-      self.final_logit_softcapping = args.final_logit_softcapping
-
-  def __call__(
-    self,
-    inputs: mx.array,
-    cache=None,
-  ):
-    out = self.model(inputs, cache)
-    if self.args.shard.is_last_layer():
-      out = self.model.embed_tokens.as_linear(out)
-      out = mx.tanh(out / self.final_logit_softcapping)
-      out = out * self.final_logit_softcapping
-    return out
-
-  def sanitize(self, weights):
-    shard_state_dict = {}
-
-    for key, value in weights.items():
-      if "self_attn.rotary_emb.inv_freq" in key:
-        continue
-      if key.startswith('model.layers.'):
-        layer_num = int(key.split('.')[2])
-        if self.args.shard.start_layer <= layer_num <= self.args.shard.end_layer:
-          shard_state_dict[key] = value
-      elif (self.args.shard.is_first_layer() or self.args.shard.is_last_layer()) and key.startswith('model.embed_tokens'):
-        shard_state_dict[key] = value
-      elif self.args.shard.is_last_layer() and (key.startswith('model.norm')):
-        shard_state_dict[key] = value
-
-    return shard_state_dict
-
-  @property
-  def layers(self):
-    return self.model.layers
-
-  @property
-  def head_dim(self):
-    return self.args.head_dim
-
-  @property
-  def n_kv_heads(self):
-    return self.args.num_key_value_heads
diff --git a/build/lib/exo/inference/mlx/models/llama.py b/build/lib/exo/inference/mlx/models/llama.py
deleted file mode 100644
index 8b069ecc..00000000
--- a/build/lib/exo/inference/mlx/models/llama.py
+++ /dev/null
@@ -1,125 +0,0 @@
-from dataclasses import dataclass, field
-
-import mlx.core as mx
-import mlx.nn as nn
-
-from mlx_lm.models.base import create_attention_mask
-from mlx_lm.models.llama import TransformerBlock, ModelArgs
-
-from ...shard import Shard
-from .base import IdentityBlock
-
-
-@dataclass
-class ModelArgs(ModelArgs):
-  shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
-  def __post_init__(self):
-    super().__post_init__()  # Ensure parent initializations are respected
-
-    if isinstance(self.shard, Shard):
-      return
-    if not isinstance(self.shard, dict):
-      raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
-    self.shard = Shard(**self.shard)
-
-
-class LlamaModel(nn.Module):
-  def __init__(self, args: ModelArgs):
-    super().__init__()
-    self.args = args
-    self.vocab_size = args.vocab_size
-    self.num_hidden_layers = args.num_hidden_layers
-    assert self.vocab_size > 0
-    if args.shard.is_first_layer() or (args.shard.is_last_layer() and args.tie_word_embeddings):
-      self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
-    self.layers = []
-    for i in range(self.num_hidden_layers):
-      if args.shard.start_layer <= i <= args.shard.end_layer:
-        self.layers.append(TransformerBlock(args=args))
-      else:
-        self.layers.append(IdentityBlock())
-    if args.shard.is_last_layer():
-      self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
-
-  def __call__(
-    self,
-    inputs: mx.array,
-    cache=None,
-  ):
-    if self.args.shard.is_first_layer():
-      h = self.embed_tokens(inputs)
-    else:
-      h = inputs
-
-    mask = None
-    if h.ndim > 1 and h.shape[1] > 1:
-      mask = create_attention_mask(h, cache)
-
-    if cache is None:
-      cache = [None]*len(self.layers)
-
-    for layer, c in zip(self.layers, cache):
-      h = layer(h, mask, cache=c)
-
-    if self.args.shard.is_last_layer():
-      h = self.norm(h)
-    return h
-
-
-class Model(nn.Module):
-  def __init__(self, args: ModelArgs):
-    super().__init__()
-    self.args = args
-    self.model_type = args.model_type
-    self.model = LlamaModel(args)
-    if args.shard.is_last_layer():
-      if not args.tie_word_embeddings:
-        self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
-
-  def __call__(
-    self,
-    inputs: mx.array,
-    cache=None,
-  ):
-    out = self.model(inputs, cache)
-    if self.args.shard.is_last_layer():
-      if self.args.tie_word_embeddings:
-        out = self.model.embed_tokens.as_linear(out)
-      else:
-        out = self.lm_head(out)
-    return out
-
-  def sanitize(self, weights):
-    shard_state_dict = {}
-
-    for key, value in weights.items():
-      if "self_attn.rotary_emb.inv_freq" in key:
-        continue
-      if key.startswith('model.layers.'):
-        layer_num = int(key.split('.')[2])
-        if self.args.shard.start_layer <= layer_num <= self.args.shard.end_layer:
-          shard_state_dict[key] = value
-      elif self.args.shard.is_first_layer() and key.startswith('model.embed_tokens'):
-        shard_state_dict[key] = value
-      elif (self.args.shard.is_last_layer() and self.args.tie_word_embeddings) and key.startswith('model.embed_tokens'):
-        shard_state_dict[key] = value
-      elif (self.args.shard.is_last_layer() and not self.args.tie_word_embeddings) and key.startswith('lm_head'):
-        shard_state_dict[key] = value
-      elif self.args.shard.is_last_layer() and (key.startswith('model.norm')):
-        shard_state_dict[key] = value
-
-    return shard_state_dict
-
-  @property
-  def layers(self):
-    return self.model.layers
-
-  @property
-  def head_dim(self):
-    return (self.args.head_dim or self.args.hidden_size // self.args.num_attention_heads)
-
-  @property
-  def n_kv_heads(self):
-    return self.args.num_key_value_heads
diff --git a/build/lib/exo/inference/mlx/models/llava.py b/build/lib/exo/inference/mlx/models/llava.py
deleted file mode 100644
index b734b09b..00000000
--- a/build/lib/exo/inference/mlx/models/llava.py
+++ /dev/null
@@ -1,585 +0,0 @@
-# Copyright © 2024 Apple Inc.
-
-import math
-import inspect
-from dataclasses import dataclass, field
-from typing import Optional, Dict, Union
-
-import mlx.core as mx
-import mlx.nn as nn
-from mlx_lm.models.base import BaseModelArgs, KVCache
-from exo.inference.shard import Shard
-from .base import IdentityBlock
-import numpy as np
-
-
-@dataclass
-class VisionConfig:
-  model_type: str
-  num_hidden_layers: int = 24
-  hidden_size: int = 1024
-  intermediate_size: int = 4096
-  num_attention_heads: int = 16
-  image_size: int = 336
-  patch_size: int = 14
-  projection_dim: int = 768
-  vocab_size: int = 32000
-  num_channels: int = 3
-  layer_norm_eps: float = 1e-5
-
-  @classmethod
-  def from_dict(cls, params):
-    return cls(**{k: v for k, v in params.items() if k in inspect.signature(cls).parameters})
-
-
-class VisionAttention(nn.Module):
-  def __init__(
-    self,
-    dims: int,
-    num_heads: int,
-    query_input_dims: Optional[int] = None,
-    key_input_dims: Optional[int] = None,
-    value_input_dims: Optional[int] = None,
-    value_dims: Optional[int] = None,
-    value_output_dims: Optional[int] = None,
-    bias: bool = False,
-  ):
-    super().__init__()
-
-    if (dims % num_heads) != 0:
-      raise ValueError("The input feature dimensions should be divisible by the "
-                       f"number of heads ({dims} % {num_heads}) != 0")
-
-    query_input_dims = query_input_dims or dims
-    key_input_dims = key_input_dims or dims
-    value_input_dims = value_input_dims or key_input_dims
-    value_dims = value_dims or dims
-    value_output_dims = value_output_dims or dims
-
-    self.num_heads = num_heads
-    self.q_proj = nn.Linear(query_input_dims, dims, bias=bias)
-    self.k_proj = nn.Linear(key_input_dims, dims, bias=bias)
-    self.v_proj = nn.Linear(value_input_dims, value_dims, bias=bias)
-    self.out_proj = nn.Linear(value_dims, value_output_dims, bias=bias)
-
-  def __call__(self, queries, keys, values, mask=None):
-    queries = self.q_proj(queries)
-    keys = self.k_proj(keys)
-    values = self.v_proj(values)
-
-    num_heads = self.num_heads
-    B, L, D = queries.shape
-    _, S, _ = keys.shape
-    queries = queries.reshape(B, L, num_heads, -1).transpose(0, 2, 1, 3)
-    keys = keys.reshape(B, S, num_heads, -1).transpose(0, 2, 3, 1)
-    values = values.reshape(B, S, num_heads, -1).transpose(0, 2, 1, 3)
-
-    scale = math.sqrt(1/queries.shape[-1])
-    scores = (queries*scale) @ keys
-    if mask is not None:
-      scores = scores + mask.astype(scores.dtype)
-    scores = mx.softmax(scores, axis=-1)
-    values_hat = (scores @ values).transpose(0, 2, 1, 3).reshape(B, L, -1)
-
-    return self.out_proj(values_hat)
-
-
-class VisionMLP(nn.Module):
-  def __init__(self, config: VisionConfig):
-    super().__init__()
-    self.activation_fn = nn.GELU(approx="fast")
-    self.fc1 = nn.Linear(config.hidden_size, config.intermediate_size)
-    self.fc2 = nn.Linear(config.intermediate_size, config.hidden_size)
-
-  def __call__(self, x: mx.array) -> mx.array:
-    x = self.activation_fn(self.fc1(x))
-    x = self.fc2(x)
-    return x
-
-
-class VisionEncoderLayer(nn.Module):
-  def __init__(self, config: VisionConfig):
-    super().__init__()
-    self.embed_dim = config.hidden_size
-    self.self_attn = VisionAttention(config.hidden_size, config.num_attention_heads, bias=True)
-    self.layer_norm1 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
-    self.mlp = VisionMLP(config)
-    self.layer_norm2 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
-
-  def __call__(self, x: mx.array, mask: Optional[mx.array] = None) -> mx.array:
-    y = self.layer_norm1(x)
-    y = self.self_attn(y, y, y, mask)
-    x = x + y
-    y = self.layer_norm2(x)
-    y = self.mlp(y)
-    return x + y
-
-
-class VisionEncoder(nn.Module):
-  def __init__(self, config: VisionConfig):
-    super().__init__()
-    self.layers = [VisionEncoderLayer(config) for _ in range(config.num_hidden_layers)]
-
-
-class VisionEmbeddings(nn.Module):
-  def __init__(self, config: VisionConfig):
-    super().__init__()
-    self.config = config
-    self.embed_dim = config.hidden_size
-    self.image_size = config.image_size
-    self.patch_size = config.patch_size
-
-    self.class_embedding = mx.zeros((config.hidden_size,))
-
-    self.patch_embedding = nn.Conv2d(
-      in_channels=config.num_channels,
-      out_channels=self.embed_dim,
-      kernel_size=self.patch_size,
-      stride=self.patch_size,
-      bias=False,
-    )
-
-    self.num_patches = (self.image_size // self.patch_size)**2
-    self.num_positions = self.num_patches + 1
-    self.position_embedding = nn.Embedding(self.num_positions, self.embed_dim)
-
-  def __call__(self, x: mx.array) -> mx.array:
-    batch_size = x.shape[0]
-    patch_embeddings = self.patch_embedding(x)
-    patch_embeddings = mx.flatten(patch_embeddings, start_axis=1, end_axis=2)
-    embed_dim = patch_embeddings.shape[-1]
-    cls_embeddings = mx.broadcast_to(self.class_embedding, (batch_size, 1, embed_dim))
-    embeddings = mx.concatenate((cls_embeddings, patch_embeddings), axis=1)
-    embeddings += self.position_embedding.weight
-    return embeddings
-
-
-class ClipVisionModel(nn.Module):
-  def __init__(self, config: VisionConfig):
-    super().__init__()
-    self.embeddings = VisionEmbeddings(config)
-    self.pre_layrnorm = nn.LayerNorm(config.hidden_size)
-    self.encoder = VisionEncoder(config)
-    self.post_layernorm = nn.LayerNorm(config.hidden_size)
-
-  def __call__(
-    self,
-    x: mx.array,
-    output_hidden_states: Optional[bool] = None,
-  ) -> mx.array:
-    x = self.embeddings(x)
-    x = self.pre_layrnorm(x)
-
-    encoder_states = (x,) if output_hidden_states else None
-
-    for l in self.encoder.layers:
-      x = l(x, mask=None)
-      if output_hidden_states:
-        encoder_states = encoder_states + (x,)
-
-    pooler_output = self.post_layernorm(x[:, 0, :])
-    return pooler_output, x, encoder_states
-
-
-class VisionModel(nn.Module):
-  def __init__(self, config: VisionConfig):
-    super().__init__()
-
-    self.model_type = config.model_type
-    if self.model_type != "clip_vision_model":
-      raise ValueError(f"Unsupported model type: {self.model_type}")
-
-    self.vision_model = ClipVisionModel(config)
-
-  def __call__(self, x: mx.array, output_hidden_states: Optional[bool] = None) -> mx.array:
-    return self.vision_model(x, output_hidden_states)
-
-  def sanitize(self, weights):
-    sanitized_weights = {}
-    for k, v in weights.items():
-      if "position_ids" in k:
-        # Remove unused position_ids
-        continue
-      elif "patch_embedding.weight" in k:
-        # PyTorch conv2d weight tensors have shape:
-        #   [out_channels, in_channels, kH, KW]
-        # MLX conv2d expects the weight be of shape:
-        #   [out_channels, kH, KW, in_channels]
-        sanitized_weights[k] = v.transpose(0, 2, 3, 1)
-      else:
-        sanitized_weights[k] = v
-
-    return sanitized_weights
-
-
-@dataclass
-class TextConfig:
-  model_type: str
-  hidden_size: int = 4096
-  num_hidden_layers: int = 32
-  intermediate_size: int = 11008
-  num_attention_heads: int = 32
-  head_dim: int = None
-  rms_norm_eps: float = 1e-6
-  vocab_size: int = 32000
-  num_key_value_heads: int = None
-  rope_theta: float = 10000
-  rope_traditional: bool = False
-  rope_scaling: Optional[Dict[str, Union[float, str]]] = None
-
-  @classmethod
-  def from_dict(cls, params):
-    return cls(**{k: v for k, v in params.items() if k in inspect.signature(cls).parameters})
-
-  def __post_init__(self):
-    if self.num_key_value_heads is None:
-      self.num_key_value_heads = self.num_attention_heads
-
-    if self.head_dim is None:
-      self.head_dim = self.hidden_size // self.num_attention_heads
-
-    if self.model_type is None:
-      self.model_type = "llama"
-
-    if self.rope_scaling:
-      required_keys = {"factor", "type"}
-      if not all(key in self.rope_scaling for key in required_keys):
-        raise ValueError(f"rope_scaling must contain keys {required_keys}")
-
-      if self.rope_scaling["type"] != "linear":
-        raise ValueError("rope_scaling 'type' currently only supports 'linear'")
-
-
-class TextAttention(nn.Module):
-  def __init__(self, config: TextConfig):
-    super().__init__()
-
-    dim = config.hidden_size
-    self.n_heads = n_heads = config.num_attention_heads
-    self.n_kv_heads = n_kv_heads = config.num_key_value_heads
-
-    self.repeats = n_heads // n_kv_heads
-
-    head_dim = config.hidden_size // n_heads
-    self.scale = head_dim**-0.5
-
-    self.q_proj = nn.Linear(dim, n_heads*head_dim, bias=False)
-    self.k_proj = nn.Linear(dim, n_kv_heads*head_dim, bias=False)
-    self.v_proj = nn.Linear(dim, n_kv_heads*head_dim, bias=False)
-    self.o_proj = nn.Linear(n_heads*head_dim, dim, bias=False)
-
-    rope_scale = (1/config.rope_scaling["factor"] if config.rope_scaling is not None and config.rope_scaling["type"] == "linear" else 1)
-    self.rope = nn.RoPE(
-      head_dim,
-      traditional=config.rope_traditional,
-      base=config.rope_theta,
-      scale=rope_scale,
-    )
-
-  def __call__(
-    self,
-    x: mx.array,
-    mask: Optional[mx.array] = None,
-    cache: Optional[KVCache] = None,
-  ) -> mx.array:
-    B, L, D = x.shape
-
-    queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
-
-    # Prepare the queries, keys and values for the attention computation
-    queries = queries.reshape(B, L, self.n_heads, -1).transpose(0, 2, 1, 3)
-    keys = keys.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
-    values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
-
-    if cache is not None:
-      queries = self.rope(queries, offset=cache.offset)
-      keys = self.rope(keys, offset=cache.offset)
-      keys, values = cache.update_and_fetch(keys, values)
-    else:
-      queries = self.rope(queries)
-      keys = self.rope(keys)
-
-    output = mx.fast.scaled_dot_product_attention(queries, keys, values, scale=self.scale, mask=mask)
-    output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
-    return self.o_proj(output)
-
-
-class TextMLP(nn.Module):
-  def __init__(self, dim, hidden_dim):
-    super().__init__()
-    self.gate_proj = nn.Linear(dim, hidden_dim, bias=False)
-    self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
-    self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
-
-  def __call__(self, x) -> mx.array:
-    return self.down_proj(nn.silu(self.gate_proj(x))*self.up_proj(x))
-
-
-class TransformerBlock(nn.Module):
-  def __init__(self, config: TextConfig):
-    super().__init__()
-    self.num_attention_heads = config.num_attention_heads
-    self.hidden_size = config.hidden_size
-    self.self_attn = TextAttention(config)
-    self.mlp = TextMLP(config.hidden_size, config.intermediate_size)
-    self.input_layernorm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
-    self.post_attention_layernorm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
-    self.config = config
-
-  def __call__(
-    self,
-    x: mx.array,
-    mask: Optional[mx.array] = None,
-    cache: Optional[KVCache] = None,
-  ) -> mx.array:
-    r = self.self_attn(self.input_layernorm(x), mask, cache)
-    h = x + r
-    r = self.mlp(self.post_attention_layernorm(h))
-    out = h + r
-    return out
-
-
-class Llama(nn.Module):
-  def __init__(self, config: TextConfig, shard: Shard):
-    super().__init__()
-    self.config = config
-    self.shard = shard
-    self.vocab_size = config.vocab_size
-    self.model_type = config.model_type
-    self.num_hidden_layers = config.num_hidden_layers
-    self.num_key_value_heads = config.num_key_value_heads
-    self.head_dim = config.head_dim
-    assert self.vocab_size > 0
-    if self.shard.is_first_layer():
-      self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
-    self.layers = []
-    for i in range(self.num_hidden_layers):
-      if self.shard.start_layer <= i <= self.shard.end_layer:
-        self.layers.append(TransformerBlock(config=config))
-      else:
-        self.layers.append(IdentityBlock())
-    if self.shard.is_last_layer():
-      self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
-
-  def __call__(
-    self,
-    inputs: mx.array,
-    cache=None,
-    inputs_embeds=None,
-  ):
-    # for passing merged input embeddings
-    if inputs_embeds is None:
-      if self.shard.is_first_layer():
-        h = self.embed_tokens(inputs)
-      else:
-        h = inputs
-    else:
-      h = inputs_embeds
-
-    mask = None
-    if h.shape[1] > 1:
-      mask = nn.MultiHeadAttention.create_additive_causal_mask(h.shape[1])
-      mask = mask.astype(h.dtype)
-
-    if cache is None:
-      cache = [None]*len(self.layers)
-
-    for layer, c in zip(self.layers, cache):
-      h = layer(h, mask, c)
-
-    if self.shard.is_last_layer():
-      h = self.norm(h)
-    return h
-
-
-class LanguageModel(nn.Module):
-  def __init__(self, config: TextConfig, shard: Shard):
-    super().__init__()
-    self.model_type = config.model_type
-    if self.model_type != "llama":
-      raise ValueError(f"Model type {self.model_type} not supported. Currently only 'llama' is supported")
-    self.shard = shard
-    self.model = Llama(config, shard)
-    if self.shard.is_last_layer():
-      self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
-
-  def __call__(
-    self,
-    inputs: mx.array,
-    cache=None,
-    inputs_embeds=None,
-  ):
-    out = self.model(inputs, cache, inputs_embeds)
-    if self.shard.is_last_layer():
-      out = self.lm_head(out)
-    return out
-
-  def sanitize(self, weights):
-    shard_state_dict = {}
-    for key, value in weights.items():
-      if "self_attn.rotary_emb.inv_freq" in key:
-        continue
-
-      if key.startswith('language_model.model.layers.'):
-        layer_num = int(key.split('.')[3])
-        if layer_num < self.shard.start_layer or layer_num > self.shard.end_layer:
-          continue
-      if not self.shard.is_first_layer() and key.startswith('language_model.model.embed_tokens'):
-        continue
-      elif not self.shard.is_last_layer() and (key.startswith('language_model.model.norm') or key.startswith('language_model.lm_head')):
-        continue
-
-      shard_state_dict[key] = value
-
-    return shard_state_dict
-
-
-@dataclass
-class LlaVAConfig(BaseModelArgs):
-  text_config: TextConfig
-  vision_config: VisionConfig = None
-  model_type: str = "llava"
-  ignore_index: int = -100
-  image_token_index: int = 32000
-  vision_feature_select_strategy: str = "default"
-  vision_feature_layer: int = -2
-  vocab_size: int = 32000
-
-  @classmethod
-  def from_dict(cls, params):
-    updated_params = {}
-    class_params = inspect.signature(cls).parameters
-    for k, v in params.items():
-      if k in class_params:
-        if k in ["text_config", "vision_config"]:
-          v = class_params[k].annotation.from_dict(v)
-        updated_params.update({k: v})
-
-    return cls(**updated_params)
-
-
-@dataclass
-class ModelArgs(LlaVAConfig):
-  shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
-  def __post_init__(self):
-    if isinstance(self.shard, dict):
-      self.shard = Shard(**self.shard)
-
-    if not isinstance(self.shard, Shard):
-      raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
-    if not self.shard.is_first_layer():
-      self.vision_config = None
-
-
-class LlavaMultiModalProjector(nn.Module):
-  def __init__(self, config: LlaVAConfig):
-    super().__init__()
-    self.linear_1 = nn.Linear(config.vision_config.hidden_size, config.text_config.hidden_size, bias=True)
-    self.gelu = nn.GELU()
-    self.linear_2 = nn.Linear(config.text_config.hidden_size, config.text_config.hidden_size, bias=True)
-
-  def __call__(self, x: mx.array) -> mx.array:
-    x = self.linear_1(x)
-    x = self.gelu(x)
-    x = self.linear_2(x)
-    return x
-
-
-class Model(nn.Module):
-  def __init__(self, config: ModelArgs):
-    super().__init__()
-    self.config = config
-    self.model_type = config.model_type
-    if config.vision_config:
-      self.vision_tower = VisionModel(config.vision_config)
-      self.multi_modal_projector = LlavaMultiModalProjector(config)
-      self.vision_feature_layer = config.vision_feature_layer
-      self.vision_feature_select_strategy = config.vision_feature_select_strategy
-    self.language_model = LanguageModel(config.text_config, config.shard)
-
-  def get_input_embeddings(
-    self,
-    input_ids: Optional[mx.array] = None,
-    pixel_values: Optional[mx.array] = None,
-  ):
-    if pixel_values is None:
-      return self.language_model(input_ids)
-
-    # Get the input embeddings from the language model
-    inputs_embeds = self.language_model.model.embed_tokens(input_ids)
-
-    # Get the ouptut hidden states from the vision model
-    *_, hidden_states = self.vision_tower(pixel_values.transpose(0, 2, 3, 1), output_hidden_states=True)
-
-    # Select the hidden states from the desired layer
-    selected_image_feature = hidden_states[self.vision_feature_layer]
-
-    if self.vision_feature_select_strategy == "default":
-      selected_image_feature = selected_image_feature[:, 1:]
-    elif self.vision_feature_select_strategy == "full":
-      selected_image_feature = selected_image_feature
-    else:
-      raise ValueError("Unexpected feature selection strategy: "
-                       f"{self.vision_feature_select_strategy}")
-
-    # Pass image features through the multi-modal projector
-    image_features = self.multi_modal_projector(selected_image_feature)
-
-    # Insert special image tokens in the input_ids
-    final_inputs_embeds = self._merge_input_ids_with_image_features(image_features, inputs_embeds, input_ids)
-    return final_inputs_embeds
-
-  def _merge_input_ids_with_image_features(self, image_features, inputs_embeds, input_ids):
-    image_token_index = self.config.image_token_index
-    num_images, num_image_patches, embed_dim = image_features.shape
-
-    # Positions of <image> tokens in input_ids, assuming batch size is 1
-    image_positions = np.where(input_ids[0] == image_token_index)[0].tolist()
-
-    if len(image_positions) != num_images:
-      raise ValueError(f"The number of image tokens ({len(image_positions)}) does not "
-                       f" match the number of image inputs ({num_images}).")
-
-    text_segments = []
-    start_idx = 0
-
-    for position in image_positions:
-      text_segments.append(inputs_embeds[:, start_idx:position])
-      start_idx = position + 1
-
-    image_embeddings = mx.split(image_features, image_features.shape[0])
-    final_embeddings = [v for p in zip(text_segments, image_embeddings) for v in p]
-    final_embeddings += [inputs_embeds[:, start_idx:]]
-
-    # Create a final embedding of shape
-    # (1, num_image_patches*num_images + sequence_len, embed_dim)
-    return mx.concatenate(final_embeddings, axis=1)
-
-  def __call__(self, input_ids: mx.array, pixel_values: mx.array = None, cache=None):
-    input_embddings = None
-    if pixel_values is not None:
-      input_embddings = self.get_input_embeddings(input_ids, pixel_values)
-    logits = self.language_model(input_ids, cache=cache, inputs_embeds=input_embddings)
-    return logits
-
-  def sanitize(self, weights):
-    if self.config.vision_config:
-      weights = self.vision_tower.sanitize(weights)
-    else:
-      weights = {k: v for k, v in weights.items() if not k.startswith(('vision_tower', 'multi_modal_projector', 'vision_feature_layer', 'vision_feature_select_strategy'))}
-    weights = self.language_model.sanitize(weights)
-    return weights
-
-  @property
-  def layers(self):
-    return self.language_model.model.layers
-
-  @property
-  def head_dim(self):
-    return (self.language_model.model.head_dim or self.language_model.model.hidden_size // self.language_model.model.num_attention_heads)
-
-  @property
-  def n_kv_heads(self):
-    return self.language_model.model.num_key_value_heads
diff --git a/build/lib/exo/inference/mlx/models/qwen2.py b/build/lib/exo/inference/mlx/models/qwen2.py
deleted file mode 100644
index 46b5ce78..00000000
--- a/build/lib/exo/inference/mlx/models/qwen2.py
+++ /dev/null
@@ -1,128 +0,0 @@
-from dataclasses import dataclass, field
-
-import mlx.core as mx
-import mlx.nn as nn
-
-from mlx_lm.models.base import create_attention_mask
-from mlx_lm.models.qwen2 import TransformerBlock, ModelArgs
-
-from ...shard import Shard
-from .base import IdentityBlock
-
-
-@dataclass
-class ModelArgs(ModelArgs):
-  shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
-  def __post_init__(self):
-    super().__post_init__()  # Ensure parent initializations are respected
-
-    if isinstance(self.shard, Shard):
-      return
-    if not isinstance(self.shard, dict):
-      raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
-    self.shard = Shard(**self.shard)
-
-
-class Qwen2Model(nn.Module):
-  def __init__(self, args: ModelArgs):
-    super().__init__()
-    self.args = args
-    self.vocab_size = args.vocab_size
-    self.num_hidden_layers = args.num_hidden_layers
-    assert self.vocab_size > 0
-    if self.args.shard.is_first_layer():
-      self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
-    self.layers = []
-    for i in range(self.num_hidden_layers):
-      if self.args.shard.start_layer <= i <= self.args.shard.end_layer:
-        self.layers.append(TransformerBlock(args=args))
-      else:
-        self.layers.append(IdentityBlock())
-    if self.args.shard.is_last_layer():
-      self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
-
-  def __call__(
-    self,
-    inputs: mx.array,
-    cache=None,
-  ):
-    if self.args.shard.is_first_layer():
-      h = self.embed_tokens(inputs)
-    else:
-      h = inputs
-
-    mask = None
-    if h.shape[1] > 1:
-      mask = create_attention_mask(h, cache)
-
-    if cache is None:
-      cache = [None]*len(self.layers)
-
-    for layer, c in zip(self.layers, cache):
-      h = layer(h, mask, c)
-
-    if self.args.shard.is_last_layer():
-      h = self.norm(h)
-    return h
-
-
-class Model(nn.Module):
-  def __init__(self, args: ModelArgs):
-    super().__init__()
-    self.args = args
-    self.model_type = args.model_type
-    self.model = Qwen2Model(args)
-    if self.args.shard.is_last_layer():
-      if not args.tie_word_embeddings:
-        self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
-
-  def __call__(
-    self,
-    inputs: mx.array,
-    cache=None,
-  ):
-    out = self.model(inputs, cache)
-    if self.args.shard.is_last_layer():
-      if self.args.tie_word_embeddings:
-        out = self.model.embed_tokens.as_linear(out)
-      else:
-        out = self.lm_head(out)
-    return out
-
-  def sanitize(self, weights):
-    shard_state_dict = {}
-
-    for key, value in weights.items():
-      if "self_attn.rotary_emb.inv_freq" in key:
-        continue
-      if key.startswith('model.layers.'):
-        layer_num = int(key.split('.')[2])
-        if self.args.shard.start_layer <= layer_num <= self.args.shard.end_layer:
-          shard_state_dict[key] = value
-      elif self.args.shard.is_first_layer() and key.startswith('model.embed_tokens'):
-        shard_state_dict[key] = value
-      elif (self.args.shard.is_last_layer() and self.args.tie_word_embeddings) and key.startswith('model.embed_tokens'):
-        shard_state_dict[key] = value
-      elif (self.args.shard.is_last_layer() and not self.args.tie_word_embeddings) and key.startswith('lm_head'):
-        shard_state_dict[key] = value
-      elif self.args.shard.is_last_layer() and (key.startswith('model.norm')):
-        shard_state_dict[key] = value
-
-    if self.args.tie_word_embeddings:
-      shard_state_dict.pop("lm_head.weight", None)
-
-    return shard_state_dict
-
-  @property
-  def layers(self):
-    return self.model.layers
-
-  @property
-  def head_dim(self):
-    return self.args.hidden_size // self.args.num_attention_heads
-
-  @property
-  def n_kv_heads(self):
-    return self.args.num_key_value_heads
diff --git a/build/lib/exo/inference/mlx/sharded_inference_engine.py b/build/lib/exo/inference/mlx/sharded_inference_engine.py
deleted file mode 100644
index 74a42212..00000000
--- a/build/lib/exo/inference/mlx/sharded_inference_engine.py
+++ /dev/null
@@ -1,77 +0,0 @@
-import numpy as np
-import mlx.core as mx
-import mlx.nn as nn
-from mlx_lm.sample_utils import top_p_sampling
-from ..inference_engine import InferenceEngine
-from .stateful_model import StatefulModel
-from .sharded_utils import load_shard
-from ..shard import Shard
-from typing import Dict, Optional, Tuple
-from exo.download.shard_download import ShardDownloader
-import asyncio
-from concurrent.futures import ThreadPoolExecutor
-
-def sample_logits(
-  logits: mx.array,
-  temp: float = 0.0,
-  top_p: float = 1.0,
-  logit_bias: Optional[Dict[int, float]] = None
-) -> Tuple[mx.array, float]:
-  if logit_bias:
-    indices = mx.array(list(logit_bias.keys()))
-    values = mx.array(list(logit_bias.values()))
-    logits[:, indices] += values
-
-  if temp == 0:
-    token = mx.argmax(logits, axis=-1)
-  else:
-    if top_p > 0 and top_p < 1.0:
-      token = top_p_sampling(logits, top_p, temp)
-    else:
-      token = mx.random.categorical(logits*(1/temp))
-
-  return token
-
-class MLXDynamicShardInferenceEngine(InferenceEngine):
-  def __init__(self, shard_downloader: ShardDownloader):
-    self.shard = None
-    self.shard_downloader = shard_downloader
-    self.executor = ThreadPoolExecutor(max_workers=1)
-
-  async def sample(self, x, temp: float = 0.0, top_p: float = 1.0) -> np.ndarray:
-    y = mx.array(x)
-    logits = y[:, -1, :]
-    out = np.array(sample_logits(logits, temp=temp, top_p=top_p), dtype=int)
-    return out
-
-  async def encode(self, shard: Shard, prompt: str) -> np.ndarray:
-    await self.ensure_shard(shard)
-    tokens = await asyncio.get_running_loop().run_in_executor(self.executor, self.tokenizer.encode, prompt)
-    return np.array(tokens)
-
-  async def decode(self, shard: Shard, tokens) -> str:
-    await self.ensure_shard(shard)
-    tokens = await asyncio.get_running_loop().run_in_executor(self.executor, self.tokenizer.decode, tokens)
-    return tokens
-    
-  async def infer_tensor(self, request_id: str, shard: Shard, input_data: np.ndarray, inference_state: Optional[dict] = None) -> np.ndarray:
-    await self.ensure_shard(shard)
-    output_data, inference_state = await asyncio.get_running_loop().run_in_executor(self.executor, self.model, mx.array(input_data), request_id, inference_state)
-    output_data = np.array(output_data)
-    return output_data, inference_state
-
-  async def ensure_shard(self, shard: Shard):
-    if self.shard == shard:
-      return
-
-    model_path = await self.shard_downloader.ensure_shard(shard, self.__class__.__name__)
-
-    if self.shard != shard:
-      loop = asyncio.get_running_loop()
-
-      def load_shard_wrapper():
-        return asyncio.run(load_shard(model_path, shard))
-
-      model_shard, self.tokenizer = await loop.run_in_executor(self.executor, load_shard_wrapper)
-      self.shard = shard
-      self.model = await loop.run_in_executor(self.executor, StatefulModel, model_shard) 
diff --git a/build/lib/exo/inference/mlx/sharded_utils.py b/build/lib/exo/inference/mlx/sharded_utils.py
deleted file mode 100644
index 2f37e57a..00000000
--- a/build/lib/exo/inference/mlx/sharded_utils.py
+++ /dev/null
@@ -1,256 +0,0 @@
-# Adapted from https://github.com/ml-explore/mlx-examples/blob/main/llms/mlx_lm/utils.py
-
-import glob
-import importlib
-import json
-import logging
-import asyncio
-import aiohttp
-from functools import partial
-from pathlib import Path
-from typing import Optional, Tuple, Union, List, Callable
-from PIL import Image
-from io import BytesIO
-import base64
-import traceback
-
-import mlx.core as mx
-import mlx.nn as nn
-from transformers import AutoProcessor
-
-from mlx_lm.tokenizer_utils import load_tokenizer, TokenizerWrapper
-
-from exo import DEBUG
-from exo.inference.tokenizers import resolve_tokenizer
-from ..shard import Shard
-
-
-class ModelNotFoundError(Exception):
-  def __init__(self, message):
-    self.message = message
-    super().__init__(self.message)
-
-
-MODEL_REMAPPING = {
-  "mistral": "llama",  # mistral is compatible with llama
-  "phi-msft": "phixtral",
-}
-
-
-def _get_classes(config: dict):
-  """
-  Retrieve the model and model args classes based on the configuration.
-
-  Args:
-   config (dict): The model configuration.
-
-  Returns:
-   A tuple containing the Model class and the ModelArgs class.
-  """
-  model_type = config["model_type"]
-  model_type = MODEL_REMAPPING.get(model_type, model_type)
-  try:
-    arch = importlib.import_module(f"exo.inference.mlx.models.{model_type}")
-  except ImportError:
-    msg = f"Model type {model_type} not supported."
-    logging.error(msg)
-    traceback.print_exc()
-    raise ValueError(msg)
-
-  return arch.Model, arch.ModelArgs
-
-
-def load_config(model_path: Path) -> dict:
-  try:
-    config_path = model_path / "config.json"
-    if config_path.exists():
-      with open(config_path, "r") as f:
-        config = json.load(f)
-      return config
-    
-    model_index_path = model_path / "model_index.json"
-    if model_index_path.exists():
-      config = load_model_index(model_path, model_index_path)
-      return config
-  except FileNotFoundError:
-    logging.error(f"Config file not found in {model_path}")
-    raise
-  return config
-
-def load_model_shard(
-  model_path: Path,
-  shard: Shard,
-  lazy: bool = False,
-  model_config: dict = {},
-) -> nn.Module:
-  """
-  Load and initialize the model from a given path.
-
-  Args:
-   model_path (Path): The path to load the model from.
-   lazy (bool): If False eval the model parameters to make sure they are
-    loaded in memory before returning, otherwise they will be loaded
-    when needed. Default: ``False``
-   model_config(dict, optional): Configuration parameters for the model.
-    Defaults to an empty dictionary.
-
-  Returns:
-   nn.Module: The loaded and initialized model.
-
-  Raises:
-   FileNotFoundError: If the weight files (.safetensors) are not found.
-   ValueError: If the model class or args class are not found or cannot be instantiated.
-  """
-  config = load_config(model_path)
-  config.update(model_config)
-
-  # TODO hack
-  config["shard"] = {
-    "model_id": model_path.name,
-    "start_layer": shard.start_layer,
-    "end_layer": shard.end_layer,
-    "n_layers": shard.n_layers,
-  }
-
-  weight_files = glob.glob(str(model_path/"model*.safetensors"))
-
-  if not weight_files:
-    # Try weight for back-compat
-    weight_files = glob.glob(str(model_path/"weight*.safetensors"))
-
-  model_class, model_args_class = _get_classes(config=config)
-
-  class ShardedModel(model_class):
-    def __init__(self, args):
-      super().__init__(args)
-      self.shard = Shard(args.shard.model_id, args.shard.start_layer, args.shard.end_layer, args.shard.n_layers)
-
-    def __call__(self, x, *args, **kwargs):
-      y = super().__call__(x, *args, **kwargs)
-      return y
-
-  model_args = model_args_class.from_dict(config)
-  model = ShardedModel(model_args)
-
-  if config.get("model_index", False):
-    model.load()
-    return model
-
-  if not weight_files:
-    logging.error(f"No safetensors found in {model_path}")
-    raise FileNotFoundError(f"No safetensors found in {model_path}")
-
-  weights = {}
-  for wf in sorted(weight_files):
-    if DEBUG >= 8:
-      layer_nums = set()
-      for k in mx.load(wf):
-        if k.startswith("model.layers."):
-          layer_num = int(k.split(".")[2])
-          layer_nums.add(layer_num)
-        if k.startswith("language_model.model.layers."):
-          layer_num = int(k.split(".")[3])
-          layer_nums.add(layer_num)
-      print(f"\"{wf.split('/')[-1]}\": {sorted(layer_nums)},")
-
-    weights.update(mx.load(wf))
-
-  
-
-  if hasattr(model, "sanitize"):
-    weights = model.sanitize(weights)
-
-  if (quantization := config.get("quantization", None)) is not None:
-    # Handle legacy models which may not have everything quantized
-    def class_predicate(p, m):
-      if not hasattr(m, "to_quantized"):
-        return False
-      return f"{p}.scales" in weights
-
-    nn.quantize(
-      model,
-      **quantization,
-      class_predicate=class_predicate,
-    )
-
-  model.load_weights(list(weights.items()), strict=True)
-
-  if not lazy:
-    mx.eval(model.parameters())
-
-  model.eval()
-  return model
-
-async def load_shard(
-  model_path: str,
-  shard: Shard,
-  tokenizer_config={},
-  model_config={},
-  adapter_path: Optional[str] = None,
-  lazy: bool = False,
-) -> Tuple[nn.Module, TokenizerWrapper]:
-  model = load_model_shard(model_path, shard, lazy, model_config)
-
-  # TODO: figure out a generic solution
-  if model.model_type == "llava":
-    processor = AutoProcessor.from_pretrained(model_path)
-    processor.eos_token_id = processor.tokenizer.eos_token_id
-    processor.encode = processor.tokenizer.encode
-    return model, processor
-  elif hasattr(model, "tokenizer"):
-    tokenizer = model.tokenizer
-    return model, tokenizer
-  else:
-    tokenizer = await resolve_tokenizer(model_path)
-    return model, tokenizer
-
-
-async def get_image_from_str(_image_str: str):
-  image_str = _image_str.strip()
-
-  if image_str.startswith("http"):
-    async with aiohttp.ClientSession() as session:
-      async with session.get(image_str, timeout=10) as response:
-        content = await response.read()
-        return Image.open(BytesIO(content)).convert("RGB")
-  elif image_str.startswith("data:image/"):
-    # Extract the image format and base64 data
-    format_prefix, base64_data = image_str.split(";base64,")
-    image_format = format_prefix.split("/")[1].lower()
-    if DEBUG >= 2: print(f"{image_str=} {image_format=}")
-    imgdata = base64.b64decode(base64_data)
-    img = Image.open(BytesIO(imgdata))
-
-    # Convert to RGB if not already
-    if img.mode != "RGB":
-      img = img.convert("RGB")
-
-    return img
-  else:
-    raise ValueError("Invalid image_str format. Must be a URL or a base64 encoded image.")
-
-# loading a combined config for all models in the index
-def load_model_index(model_path: Path, model_index_path: Path):
-  models_config = {}
-  with open(model_index_path, "r") as f:
-      model_index = json.load(f)
-  models_config["model_index"] = True
-  models_config["model_type"] = model_index["_class_name"]
-  models_config["models"] = {}
-  for model in model_index.keys():
-    model_config_path = glob.glob(str(model_path / model / "*config.json"))
-    if len(model_config_path)>0:
-      with open(model_config_path[0], "r") as f:
-        model_config = { }
-        model_config["model_type"] = model
-        model_config["config"] = json.load(f)
-        model_config["path"] = model_path / model
-        if model_config["path"]/"*model.safetensors":
-          model_config["config"].update({"weight_files": list(glob.glob(str(model_config["path"]/"*model.safetensors")))})
-        model_config["path"] = str(model_path / model)
-        m = {}
-        m[model] = model_config
-        models_config.update(m)
-  models_config = json.dumps(models_config)
-  models_config = json.loads(models_config)
-  return models_config
diff --git a/build/lib/exo/inference/mlx/stateful_model.py b/build/lib/exo/inference/mlx/stateful_model.py
deleted file mode 100644
index 79e9baeb..00000000
--- a/build/lib/exo/inference/mlx/stateful_model.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from typing import Dict, Tuple, Optional
-from collections import OrderedDict
-
-import mlx.core as mx
-import mlx.nn as nn
-from mlx_lm.models.cache import make_prompt_cache
-
-from ..shard import Shard
-
-class StatefulModel(nn.Module):
-  def __init__(self, model, max_kv_size: int = 1024, max_caches: int = 2):
-    super().__init__()
-    self.model = model
-    self.max_kv_size = max_kv_size
-    self.max_caches = max_caches
-    self.caches = OrderedDict()
-  
-  def init_cache(self, request_id: str):
-    kv_heads = ([self.model.n_kv_heads]*len(self.model.layers) if isinstance(self.model.n_kv_heads, int) else self.model.n_kv_heads)
-    # if self.max_kv_size is not None:
-      # cache = [RotatingKVCache(self.model.head_dim, n, max_size=self.max_kv_size, keep=4) for n in kv_heads]
-      # cache = [KVCache(self.model.head_dim, n) for n in kv_heads]
-    # else:
-      # cache = [KVCache(self.model.head_dim, n) for n in kv_heads]
-    cache = make_prompt_cache(self.model)
-
-    if len(self.caches) >= self.max_caches:
-      self.caches.popitem(last=False)
-
-    self.caches[request_id] = cache
-
-  def __call__(self, x, request_id: str, inference_state: Optional[dict] = None):
-    if self.model.model_type !='StableDiffusionPipeline':
-      if request_id not in self.caches:
-        self.init_cache(request_id)
-      else:
-        self.caches.move_to_end(request_id)
-
-      cache = self.caches[request_id]
-
-      y = self.model(x, cache=cache)
-    else:
-      y, inference_state = self.model(x, **inference_state)
-    return y, inference_state
-    
diff --git a/build/lib/exo/inference/mlx/test_sharded_llama.py b/build/lib/exo/inference/mlx/test_sharded_llama.py
deleted file mode 100644
index f98f9954..00000000
--- a/build/lib/exo/inference/mlx/test_sharded_llama.py
+++ /dev/null
@@ -1,40 +0,0 @@
-import mlx.core as mx
-from exo.inference.mlx.stateful_model import StatefulModel
-from exo.inference.mlx.sharded_utils import load_shard
-from exo.inference.shard import Shard
-
-# 79, 80 for Llama-3-70B
-shard_full = Shard("llama", 0, 31, 32)
-shard1 = Shard("llama", 0, 12, 32)
-shard2 = Shard("llama", 13, 31, 32)
-
-full_model_shard, full_tokenizer = load_shard("mlx-community/Meta-Llama-3-8B-Instruct-4bit", shard=shard_full)
-model_shard1, tokenizer1 = load_shard("mlx-community/Meta-Llama-3-8B-Instruct-4bit", shard=shard1)
-model_shard2, tokenizer2 = load_shard("mlx-community/Meta-Llama-3-8B-Instruct-4bit", shard=shard2)
-
-full = StatefulModel(shard_full, full_model_shard)
-m1 = StatefulModel(shard1, model_shard1)
-m2 = StatefulModel(shard2, model_shard2)
-
-prompt = "write a beautiful haiku about a utopia where people own their AI with edge intelligence:"
-prompt_tokens = mx.array(full_tokenizer.encode(prompt))
-max_tokens = 50
-
-resp = prompt_tokens
-full_generated_tokens = []
-for _ in range(max_tokens):
-  resp = full.step(resp)
-  full_generated_tokens.append(resp.item())
-
-print("full response: ", full_tokenizer.decode(full_generated_tokens))
-
-sharded_generated_tokens = []
-sharded_resp = prompt_tokens
-for _ in range(max_tokens):
-  resp1 = m1.step(sharded_resp)
-  sharded_resp = m2.step(resp1)
-  sharded_generated_tokens.append(sharded_resp.item())
-
-print("sharded response: ", tokenizer1.decode(sharded_generated_tokens))
-
-assert tokenizer1.decode(full_generated_tokens) == tokenizer1.decode(sharded_generated_tokens)
diff --git a/build/lib/exo/inference/mlx/test_sharded_llava.py b/build/lib/exo/inference/mlx/test_sharded_llava.py
deleted file mode 100644
index 53cd90c1..00000000
--- a/build/lib/exo/inference/mlx/test_sharded_llava.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import codecs
-import asyncio
-import requests
-from PIL import Image
-from io import BytesIO
-
-import mlx.core as mx
-from mlx_lm.models.cache import KVCache
-
-from exo.inference.mlx.stateful_model import StatefulModel
-from exo.inference.mlx.sharded_utils import load_shard
-from exo.inference.shard import Shard
-
-shard_full = Shard("llava", 0, 31, 32)
-shard1 = Shard("llava", 0, 12, 32)
-shard2 = Shard("llava", 13, 31, 32)
-
-model_path = "llava-hf/llava-1.5-7b-hf"
-
-full_model_shard, full_processor = asyncio.run(load_shard(model_path, shard=shard_full))
-model_shard1, processor1 = asyncio.run(load_shard(model_path, shard=shard1))
-model_shard2, processor2 = asyncio.run(load_shard(model_path, shard=shard2))
-
-full = StatefulShardedModel(shard_full, full_model_shard)
-m1 = StatefulShardedModel(shard1, model_shard1)
-m2 = StatefulShardedModel(shard2, model_shard2)
-
-PROMPT = "USER: <image>\nWhat are these?\nASSISTANT:"
-IMAGE_FILE = "http://images.cocodataset.org/val2017/000000039769.jpg"
-response = requests.get(IMAGE_FILE)
-img = Image.open(BytesIO(response.content))
-prompt = codecs.decode(PROMPT, "unicode_escape")
-inputs = full_processor(prompt, img, return_tensors="np")
-pixel_values = mx.array(inputs["pixel_values"])
-input_ids = mx.array(inputs["input_ids"])
-
-print(prompt)
-y = full.step("full", input_ids, pixel_values, temp=0)
-full_generated_tokens = [y.item()]
-
-for _ in range(13):
-  y = full.step("full", y, temp=0)
-  full_generated_tokens.append(y.item())
-
-full_response = full_processor.tokenizer.decode(full_generated_tokens)
-print("full response:", full_response)
-
-inputs = processor1(prompt, img, return_tensors="np")
-pixel_values = mx.array(inputs["pixel_values"])
-input_ids = mx.array(inputs["input_ids"])
-
-y = m1.step("shard", input_ids, pixel_values, temp=0)
-y = m2.step("shard", y, temp=0)
-full_generated_tokens = [y.item()]
-
-for _ in range(13):
-  y = m1.step("shard", y, temp=0)
-  y = m2.step("shard", y, temp=0)
-  full_generated_tokens.append(y.item())
-
-sharded_response = processor2.tokenizer.decode(full_generated_tokens)
-print("sharded response:", sharded_response)
-
-assert full_response == sharded_response
diff --git a/build/lib/exo/inference/mlx/test_sharded_model.py b/build/lib/exo/inference/mlx/test_sharded_model.py
deleted file mode 100644
index c9743d07..00000000
--- a/build/lib/exo/inference/mlx/test_sharded_model.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from exo.inference.shard import Shard
-import mlx.core as mx
-import mlx.nn as nn
-from typing import Optional
-import numpy as np
-
-
-class DummyModel(nn.Module):
-  def __init__(self, shard: Optional[Shard] = None):
-    self.shard = shard
-    self.layers = [
-      nn.Linear(8, 128),
-      nn.Linear(128, 128),
-      nn.Linear(128, 128),
-      nn.Linear(128, 128),
-      nn.Linear(128, 8),
-    ]
-
-    self.n_kv_heads = 4
-    self.head_dim = 4
-
-  def __call__(self, x, cache=None):
-    if self.shard:
-      for layer in self.layers[self.shard.start_layer:self.shard.end_layer + 1]:
-        x = layer(x)
-      if self.shard.is_last_layer():
-        x = x.reshape((1, 2, 4))
-    else:
-      for layer in self.layers:
-        x = layer(x)
-      x = x.reshape((1, 2, 4))
-
-    return x
-
-
-model = DummyModel()
-model.save_weights("./test_weights.npz")
-n_layers = 5
-shard1 = Shard("test", 0, n_layers // 2, n_layers)
-sharded_model1 = DummyModel(shard1)
-shard2 = Shard("test", n_layers//2 + 1, n_layers - 1, n_layers)
-sharded_model2 = DummyModel(shard2)
-
-model.load_weights("./test_weights.npz")
-sharded_model1.load_weights("./test_weights.npz")
-sharded_model2.load_weights("./test_weights.npz")
-
-fullresp = model(mx.array([1, 2, 3, 4, 5, 6, 7, 8]))
-resp1 = sharded_model1(mx.array([1, 2, 3, 4, 5, 6, 7, 8]))
-resp2 = sharded_model2(resp1)
-
-assert np.all(np.array(fullresp) == np.array(resp2))
diff --git a/build/lib/exo/inference/shard.py b/build/lib/exo/inference/shard.py
deleted file mode 100644
index 21b662f6..00000000
--- a/build/lib/exo/inference/shard.py
+++ /dev/null
@@ -1,39 +0,0 @@
-from dataclasses import dataclass, field
-
-
-@dataclass(frozen=True)
-class Shard:
-  model_id: str
-  start_layer: int
-  end_layer: int
-  n_layers: int
-
-  def __hash__(self):
-    return hash((self.model_id, self.start_layer, self.end_layer, self.n_layers))
-
-  def is_first_layer(self) -> bool:
-    return self.start_layer == 0
-
-  def is_last_layer(self) -> bool:
-    return self.end_layer == self.n_layers - 1
-
-  def get_layer_count(self) -> int:
-    return self.end_layer - self.start_layer + 1
-
-  def to_dict(self) -> dict:
-    return {
-      "model_id": self.model_id,
-      "start_layer": self.start_layer,
-      "end_layer": self.end_layer,
-      "n_layers": self.n_layers,
-    }
-
-  def from_dict(data: dict) -> 'Shard':
-    return Shard(**data)
-
-  def overlaps(self, other: 'Shard') -> bool:
-    return shards_overlap(self, other)
-
-
-def shards_overlap(shard1: Shard, shard2: Shard) -> bool:
-  return (shard1.model_id == shard2.model_id and max(shard1.start_layer, shard2.start_layer) <= min(shard1.end_layer, shard2.end_layer))
diff --git a/build/lib/exo/inference/test_dummy_inference_engine.py b/build/lib/exo/inference/test_dummy_inference_engine.py
deleted file mode 100644
index cfd33df6..00000000
--- a/build/lib/exo/inference/test_dummy_inference_engine.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import pytest
-import json
-import numpy as np
-from exo.inference.dummy_inference_engine import DummyInferenceEngine
-from exo.inference.shard import Shard
-
-
-class MockShardDownloader:
-  async def ensure_shard(self, shard):
-    pass
-
-
-@pytest.mark.asyncio
-async def test_dummy_inference_specific():
-  engine = DummyInferenceEngine(MockShardDownloader())
-  test_shard = Shard(model_id="test_model", start_layer=0, end_layer=1, n_layers=1)
-  test_prompt = "This is a test prompt"
-
-  result = await engine.infer_prompt("test_request", test_shard, test_prompt)
-
-  print(f"Inference result shape: {result.shape}")
-
-  assert result.shape[0] == 1, "Result should be a 2D array with first dimension 1"
-
-
-@pytest.mark.asyncio
-async def test_dummy_inference_engine():
-  # Initialize the DummyInferenceEngine
-  engine = DummyInferenceEngine(MockShardDownloader())
-
-  # Create a test shard
-  shard = Shard(model_id="test_model", start_layer=0, end_layer=1, n_layers=1)
-
-  # Test infer_prompt
-  output = await engine.infer_prompt("test_id", shard, "Test prompt")
-
-  assert isinstance(output, np.ndarray), "Output should be a numpy array"
-  assert output.ndim == 2, "Output should be 2-dimensional"
-
-  # Test infer_tensor
-  input_tensor = np.array([[1, 2, 3]])
-  output = await engine.infer_tensor("test_id", shard, input_tensor)
-
-  assert isinstance(output, np.ndarray), "Output should be a numpy array"
-  assert output.ndim == 2, "Output should be 2-dimensional"
-
-  print("All tests passed!")
-
-
-if __name__ == "__main__":
-  import asyncio
-  asyncio.run(test_dummy_inference_engine())
-  asyncio.run(test_dummy_inference_specific())
diff --git a/build/lib/exo/inference/test_inference_engine.py b/build/lib/exo/inference/test_inference_engine.py
deleted file mode 100644
index db69aebf..00000000
--- a/build/lib/exo/inference/test_inference_engine.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from exo.inference.mlx.sharded_inference_engine import MLXDynamicShardInferenceEngine
-from exo.download.hf.hf_shard_download import HFShardDownloader
-from exo.inference.inference_engine import InferenceEngine
-from exo.inference.shard import Shard
-from exo.helpers import DEBUG
-import os
-import asyncio
-import numpy as np
-
-
-# An inference engine should work the same for any number of Shards, as long as the Shards are continuous.
-async def test_inference_engine(inference_engine_1: InferenceEngine, inference_engine_2: InferenceEngine, model_id: str, n_layers: int):
-  prompt = "In a single word only, what is the last name of the current president of the USA?"
-  resp_full = await inference_engine_1.infer_prompt("A", shard=Shard(model_id=model_id, start_layer=0, end_layer=n_layers - 1, n_layers=n_layers), prompt=prompt)
-  token_full = await inference_engine_1.sample(resp_full)
-  token_full = token_full.reshape(1, -1)
-  next_resp_full = await inference_engine_1.infer_tensor(
-    "A",
-    shard=Shard(model_id=model_id, start_layer=0, end_layer=n_layers - 1, n_layers=n_layers),
-    input_data=token_full,
-  )
-
-  pp = n_layers // 2
-  resp1 = await inference_engine_1.infer_prompt("B", shard=Shard(model_id=model_id, start_layer=0, end_layer=pp, n_layers=n_layers), prompt=prompt)
-  resp2 = await inference_engine_2.infer_tensor(
-    "B",
-    shard=Shard(model_id=model_id, start_layer=pp + 1, end_layer=n_layers - 1, n_layers=n_layers),
-    input_data=resp1,
-  )
-  tokens2 = await inference_engine_1.sample(resp2)
-  tokens2 = tokens2.reshape(1, -1)
-  resp3 = await inference_engine_1.infer_tensor(
-    "B",
-    shard=Shard(model_id=model_id, start_layer=0, end_layer=pp, n_layers=n_layers),
-    input_data=tokens2,
-  )
-  resp4 = await inference_engine_2.infer_tensor(
-    "B",
-    shard=Shard(model_id=model_id, start_layer=pp + 1, end_layer=n_layers - 1, n_layers=n_layers),
-    input_data=resp3,
-  )
-
-  assert np.array_equal(resp_full, resp2)
-  assert np.array_equal(next_resp_full, resp4)
-
-
-asyncio.run(test_inference_engine(MLXDynamicShardInferenceEngine(HFShardDownloader()), MLXDynamicShardInferenceEngine(HFShardDownloader()), "llama-3.2-1b", 16))
-
-if os.getenv("RUN_TINYGRAD", default="0") == "1":
-  import tinygrad
-  import os
-  from exo.inference.tinygrad.inference import TinygradDynamicShardInferenceEngine
-  tinygrad.helpers.DEBUG.value = int(os.getenv("TINYGRAD_DEBUG", default="0"))
-  asyncio.run(
-    test_inference_engine(TinygradDynamicShardInferenceEngine(HFShardDownloader()), TinygradDynamicShardInferenceEngine(HFShardDownloader()), "llama-3-8b", 32)
-  )
diff --git a/build/lib/exo/inference/tinygrad/__init__.py b/build/lib/exo/inference/tinygrad/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/inference/tinygrad/inference.py b/build/lib/exo/inference/tinygrad/inference.py
deleted file mode 100644
index afa9c5df..00000000
--- a/build/lib/exo/inference/tinygrad/inference.py
+++ /dev/null
@@ -1,99 +0,0 @@
-from pathlib import Path
-import json
-import os
-from exo.inference.tinygrad.models.llama import Transformer, convert_from_huggingface, fix_bf16, sample_logits
-from exo.inference.shard import Shard
-from exo.inference.tokenizers import resolve_tokenizer
-from tinygrad.nn.state import load_state_dict
-from tinygrad import Tensor, nn, Context
-from exo.inference.inference_engine import InferenceEngine
-import numpy as np
-from exo.inference.tinygrad.tinygrad_helpers import concat_weights, load
-from exo.download.shard_download import ShardDownloader
-from concurrent.futures import ThreadPoolExecutor
-from .stateful_model import StatefulModel
-import asyncio
-
-Tensor.no_grad = True
-# default settings
-TEMPERATURE = int(os.getenv("TEMPERATURE", 0.85))
-TOP_K = 25
-TOP_P = 0.9
-ALPHA_F = 0.1
-ALPHA_P = 0.0
-MODEL_PARAMS = {
-  "1B": {
-    "args": {
-      "dim": 2048, "n_heads": 32, "n_kv_heads": 8, "n_layers": 16, "norm_eps": 1e-5, "rope_theta": 500000, "vocab_size": 128256, "hidden_dim": 8192,
-      "rope_scaling": {"factor": 32.0, "high_freq_factor": 4.0, "low_freq_factor": 1.0, "original_max_position_embeddings": 8192, "rope_type": "llama3"}, "tie_word_embeddings": True
-    }, "files": 1
-  }, "3B": {
-    "args": {
-      "dim": 3072, "n_heads": 24, "n_kv_heads": 8, "n_layers": 28, "norm_eps": 1e-5, "rope_theta": 500000, "vocab_size": 128256, "hidden_dim": 8192,
-      "rope_scaling": {"factor": 32.0, "high_freq_factor": 4.0, "low_freq_factor": 1.0, "original_max_position_embeddings": 8192, "rope_type": "llama3"}, "tie_word_embeddings": True
-    }, "files": 1
-  }, "8B": {"args": {"dim": 4096, "n_heads": 32, "n_kv_heads": 8, "n_layers": 32, "norm_eps": 1e-5, "rope_theta": 500000, "vocab_size": 128256, "hidden_dim": 14336}, "files": 1},
-  "70B": {"args": {"dim": 8192, "n_heads": 64, "n_kv_heads": 8, "n_layers": 80, "norm_eps": 1e-5, "rope_theta": 500000, "vocab_size": 128256, "hidden_dim": 28672}, "files": 8}
-}
-
-
-def build_transformer(model_path: Path, shard: Shard, model_size="8B", device=None):
-  # build model
-  linear = nn.Linear
-  model = Transformer(**MODEL_PARAMS[model_size]["args"], linear=linear, max_context=8192, jit=True, shard=shard)
-
-  # load weights
-  if model_path.is_dir():
-    if (model_path/"model.safetensors.index.json").exists(): weights = load(str(model_path/"model.safetensors.index.json"), shard)
-    elif (model_path/"model.safetensors").exists(): weights = load(str(model_path/"model.safetensors"), shard)
-    else: weights = concat_weights([load(str(model_path/f"consolidated.{i:02d}.pth"), shard) for i in range(MODEL_PARAMS[model_size]["files"])], device[0] if isinstance(device, tuple) else device)
-  else:
-    weights = load(str(model_path), shard)
-  weights = convert_from_huggingface(weights, model, MODEL_PARAMS[model_size]["args"]["n_heads"], MODEL_PARAMS[model_size]["args"]["n_kv_heads"])
-  weights = fix_bf16(weights)
-
-  with Context(BEAM=0):
-    # replace weights in model
-    load_state_dict(model, weights, strict=False, consume=False)  # consume=True
-  return model
-
-class TinygradDynamicShardInferenceEngine(InferenceEngine):
-  def __init__(self, shard_downloader: ShardDownloader):
-    self.shard = None
-    self.shard_downloader = shard_downloader
-    self.executor = ThreadPoolExecutor(max_workers=1)
-
-  async def sample(self, x: np.ndarray, temp=TEMPERATURE, top_p: float = 0.0) -> np.ndarray:
-    logits = x[:, -1, :]
-    def sample_wrapper():
-      return sample_logits(Tensor(logits).flatten(), temp, 0, 0.8, top_p, 0.0).realize().numpy().astype(int)
-    return await asyncio.get_running_loop().run_in_executor(self.executor, sample_wrapper)
-
-  async def encode(self, shard: Shard, prompt: str) -> np.ndarray:
-    await self.ensure_shard(shard)
-    tokens = await asyncio.get_running_loop().run_in_executor(self.executor, self.tokenizer.encode, prompt)
-    return await asyncio.get_running_loop().run_in_executor(self.executor, np.array, tokens)
-  
-  async def decode(self, shard: Shard, tokens) -> str:
-    await self.ensure_shard(shard)
-    return await asyncio.get_running_loop().run_in_executor(self.executor, self.tokenizer.decode, tokens)
-
-  async def infer_tensor(self, request_id: str, shard: Shard, input_data: np.ndarray) -> np.ndarray:
-    await self.ensure_shard(shard)
-    return await asyncio.get_running_loop().run_in_executor(self.executor, lambda: self.model(Tensor(input_data), request_id).realize().numpy())
-
-  async def ensure_shard(self, shard: Shard):
-    if self.shard == shard:
-      return
-
-    model_path = await self.shard_downloader.ensure_shard(shard, self.__class__.__name__)
-
-    if self.shard != shard:
-      loop = asyncio.get_running_loop()
-      parameters = "1B" if "1b" in shard.model_id.lower() else "3B" if "3b" in shard.model_id.lower() else "8B" if "8b" in shard.model_id.lower() else "70B"
-      model_shard = await loop.run_in_executor(self.executor, build_transformer, model_path, shard, parameters)
-
-      tokenizer_path = str((model_path if model_path.is_dir() else model_path.parent))
-      self.tokenizer = await resolve_tokenizer(tokenizer_path)
-      self.shard = shard
-      self.model = await loop.run_in_executor(self.executor, StatefulModel, model_shard) 
diff --git a/build/lib/exo/inference/tinygrad/models/__init__.py b/build/lib/exo/inference/tinygrad/models/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/inference/tinygrad/models/llama.py b/build/lib/exo/inference/tinygrad/models/llama.py
deleted file mode 100644
index 52ed3072..00000000
--- a/build/lib/exo/inference/tinygrad/models/llama.py
+++ /dev/null
@@ -1,282 +0,0 @@
-from typing import Tuple, Union, Optional, Dict, Any, List
-from tinygrad import Tensor, Variable, TinyJit, dtypes, nn, Device
-from tinygrad.helpers import getenv
-from collections import OrderedDict
-
-
-# https://github.com/facebookresearch/llama/blob/1076b9c51c77ad06e9d7ba8a4c6df775741732bd/llama/model.py#L47
-def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0, dtype=dtypes.half, rope_scaling: Optional[Dict[str, float]] = None) -> Tensor:
-  freqs = 1.0/(theta**(Tensor.arange(0, dim, 2)[:(dim // 2)]/dim))
-
-  if rope_scaling:
-    factor = rope_scaling.get('factor', 1.0)
-    low_freq_factor = rope_scaling.get('low_freq_factor', 1.0)
-    high_freq_factor = rope_scaling.get('high_freq_factor', 1.0)
-    original_max_pos_emb = rope_scaling.get('original_max_position_embeddings', end)
-
-    freqs[:dim // 4] *= low_freq_factor
-    freqs[dim // 4:] = freqs[dim // 4:].contiguous()*high_freq_factor
-    freqs *= (original_max_pos_emb/end)**(1.0/factor)
-
-  freqs = Tensor.arange(end).unsqueeze(dim=1)*freqs.unsqueeze(dim=0)
-  # TODO: move dtype outside this
-  return Tensor.stack(freqs.cos().cast(dtype), freqs.sin().cast(dtype), dim=-1).reshape(1, end, 1, dim // 2, 2)
-
-
-# (a+i*b) * (c+i*d) = (ac-bd) + i*(ad+bc)
-def complex_mult(A, c, d):
-  a, b = A[..., 0:1], A[..., 1:2]
-  ro = a*c - b*d
-  co = a*d + b*c
-  return ro.cat(co, dim=-1)
-
-
-def apply_rotary_emb(xq: Tensor, xk: Tensor, freqs_cis: Tensor) -> Tuple[Tensor, Tensor]:
-  assert freqs_cis.shape[1] == xq.shape[1] == xk.shape[1], f"freqs_cis shape mismatch {freqs_cis.shape} xq:{xq.shape} xk:{xk.shape}"
-  xq = xq.reshape(*xq.shape[0:-1], -1, 2)
-  xk = xk.reshape(*xk.shape[0:-1], -1, 2)
-  assert len(xq.shape) == len(xk.shape) == len(freqs_cis.shape) == 5
-  c, d = freqs_cis[..., 0:1], freqs_cis[..., 1:2]
-  xq_out = complex_mult(xq, c, d)
-  xk_out = complex_mult(xk, c, d)
-  return xq_out.flatten(3), xk_out.flatten(3)
-
-
-def repeat_kv(x: Tensor, n_rep: int) -> Tensor:
-  bs, seqlen, n_kv_heads, head_dim = x.shape
-  if n_rep == 1: return x
-  # NOTE: this is different from x.repeat((1, 1, n_rep, 1))
-  return x.repeat((1, 1, 1, n_rep)).reshape(bs, seqlen, n_kv_heads*n_rep, head_dim)
-
-class Attention:
-  def __init__(self, dim, n_heads, n_kv_heads, max_context, linear=nn.Linear):
-    self.n_heads = n_heads
-    self.n_kv_heads = n_kv_heads if n_kv_heads is not None else n_heads  # n_kv_heads != n_heads implies MQA [arxiv/2307.09288, A.2.1]
-    self.head_dim = dim // n_heads
-    self.n_rep = self.n_heads // self.n_kv_heads
-    self.max_context = max_context
-
-    self.wq = linear(dim, self.n_heads*self.head_dim, bias=False)
-    self.wk = linear(dim, self.n_kv_heads*self.head_dim, bias=False)
-    self.wv = linear(dim, self.n_kv_heads*self.head_dim, bias=False)
-    self.wo = linear(self.n_heads*self.head_dim, dim, bias=False)
-
-  def __call__(self, x: Tensor, start_pos: Union[Variable, int], freqs_cis: Tensor, mask: Optional[Tensor], cache: Optional[Tensor]=None) -> Tensor:
-    if getenv("WQKV"):
-      if not hasattr(self, 'wqkv'): self.wqkv = Tensor.cat(self.wq.weight, self.wk.weight, self.wv.weight)
-      xqkv = x @ self.wqkv.T
-      xq, xk, xv = xqkv.split([self.wq.weight.shape[0], self.wk.weight.shape[0], self.wv.weight.shape[0]], dim=2)
-    else:
-      xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)
-
-    xq = xq.reshape(xq.shape[0], xq.shape[1], self.n_heads, self.head_dim)
-    xk = xk.reshape(xk.shape[0], xk.shape[1], self.n_kv_heads, self.head_dim)
-    xv = xv.reshape(xv.shape[0], xv.shape[1], self.n_kv_heads, self.head_dim)
-
-    xq, xk = apply_rotary_emb(xq, xk, freqs_cis)
-    bsz, seqlen, _, _ = xq.shape
-
-    if cache is not None:
-      # update the cache
-      assert xk.dtype == xv.dtype == cache.dtype, f"{xk.dtype=}, {xv.dtype=}, {cache.dtype=}"
-      cache.shrink((None, None, (start_pos, start_pos + seqlen), None, None)).assign(Tensor.stack(xk, xv)).realize()
-
-      keys = cache[0].shrink((None, (0, start_pos + seqlen), None, None)) if start_pos > 0 else xk
-      values = cache[1].shrink((None, (0, start_pos + seqlen), None, None)) if start_pos > 0 else xv
-    else:
-      keys = xk
-      values = xv
-
-    keys, values = repeat_kv(keys, self.n_rep), repeat_kv(values, self.n_rep)
-    xq, keys, values = xq.transpose(1, 2), keys.transpose(1, 2), values.transpose(1, 2)
-    attn = xq.scaled_dot_product_attention(keys, values, mask).transpose(1, 2)
-    attn = attn.reshape(bsz, seqlen, -1)
-    return self.wo(attn)
-
-
-class FeedForward:
-  def __init__(self, dim: int, hidden_dim: int, linear=nn.Linear):
-    self.w1 = linear(dim, hidden_dim, bias=False)
-    self.w2 = linear(hidden_dim, dim, bias=False)
-    self.w3 = linear(dim, hidden_dim, bias=False)  # the gate in Gated Linear Unit
-
-  def __call__(self, x: Tensor) -> Tensor:
-    return self.w2(self.w1(x).silu()*self.w3(x))  # SwiGLU [arxiv/2002.05202, eq (5)]
-
-
-class TransformerBlock:
-  def __init__(self, dim: int, hidden_dim: int, n_heads: int, n_kv_heads: int, norm_eps: float, max_context: int, linear=nn.Linear, feed_forward=FeedForward):
-    self.attention = Attention(dim, n_heads, n_kv_heads, max_context, linear)
-    self.feed_forward = feed_forward(dim, hidden_dim, linear)
-    self.attention_norm = nn.RMSNorm(dim, norm_eps)
-    self.ffn_norm = nn.RMSNorm(dim, norm_eps)
-
-  def __call__(self, x: Tensor, start_pos: Union[Variable, int], freqs_cis: Tensor, mask: Optional[Tensor], cache: Optional[Tensor]=None):
-    h = x + self.attention(self.attention_norm(x), start_pos, freqs_cis, mask, cache=cache)
-    return (h + self.feed_forward(self.ffn_norm(h))).contiguous()
-
-
-# standard openai sampling
-def sample_logits(logits: Tensor, temp: float, k: int, p: float, af: float, ap: float):
-  assert logits.ndim == 1, "only works on 1d tensors"
-  assert 0 <= p <= 1, "p must be between 0 and 1"
-  assert 0 <= k <= logits.numel(), "k must be between 0 and numel"
-
-  # if temperature is very low just use argmax
-  if temp < 1e-6: return logits.argmax().reshape(1)
-
-  # alpha sampling
-  if af or ap:
-    if not hasattr(sample, "alpha_counter"):
-      setattr(sample, "alpha_counter", Tensor.zeros_like(logits, dtype=dtypes.int32).contiguous())
-    logits = logits - (sample.alpha_counter*af + (sample.alpha_counter > 0)*ap)
-
-  # replace NaNs with -inf
-  logits = (logits != logits).where(-float("inf"), logits)
-
-  # softmax
-  t = (logits/temp).softmax()
-
-  counter, counter2 = Tensor.arange(t.numel(), device=logits.device).contiguous(), Tensor.arange(t.numel() - 1, -1, -1, device=logits.device).contiguous()
-  # top k
-  if k:
-    output, output_indices = Tensor.zeros(k, device=logits.device).contiguous(), Tensor.zeros(k, device=logits.device, dtype=dtypes.int32).contiguous()
-    for i in range(k):
-      t_argmax = (t.numel() - ((t == (t_max := t.max()))*counter2).max() - 1).cast(dtypes.default_int)
-      output = output + t_max.unsqueeze(0).pad(((i, k - i - 1),))
-      output_indices = output_indices + t_argmax.unsqueeze(0).pad(((i, k - i - 1),))
-      t = (counter == t_argmax).where(0, t)
-
-    # approximate top p
-    # because we are already limited to top k elements we can do top p "without sorting"
-    output_cumsum = output[::-1]._cumsum()[::-1] + t.sum()
-    output = (output_cumsum >= (1 - p))*output
-    output_indices = (output_cumsum >= (1 - p))*output_indices
-
-    # sample
-    output_idx = output.multinomial()
-    output_token = output_indices[output_idx]
-  else:
-    output_token = t.multinomial()
-
-  # increase alpha counter
-  if af or ap:
-    sample.alpha_counter = (counter == output_token).where(sample.alpha_counter + 1, sample.alpha_counter)
-
-  return output_token
-
-
-from exo.inference.shard import Shard
-
-
-class Transformer:
-  def __init__(
-    self,
-    dim: int,
-    hidden_dim: int,
-    n_heads: int,
-    n_layers: int,
-    norm_eps: float,
-    vocab_size,
-    shard: Shard = None,
-    linear=nn.Linear,
-    n_kv_heads=None,
-    rope_theta=10000,
-    max_context=1024,
-    jit=True,
-    feed_forward=FeedForward,
-    rope_scaling: Optional[Dict[str, float]] = None,
-    tie_word_embeddings=False,
-  ):
-    self.layers = [TransformerBlock(dim, hidden_dim, n_heads, n_kv_heads, norm_eps, max_context, linear, feed_forward=feed_forward) for _ in range(n_layers)]
-    self.norm = nn.RMSNorm(dim, norm_eps)
-    self.tok_embeddings = nn.Embedding(vocab_size, dim)
-    self.output = nn.Linear(dim, vocab_size, bias=False)
-    if tie_word_embeddings:
-      self.output.weight = self.tok_embeddings.weight
-    self.max_context = max_context
-    self.freqs_cis = precompute_freqs_cis(dim // n_heads, self.max_context*2, rope_theta, rope_scaling=rope_scaling).contiguous()
-    self.forward_jit = TinyJit(self.forward_base) if jit else None
-    self.shard = shard
-
-  def forward_base(self, x: Tensor, start_pos: Union[Variable, int], cache: Optional[List[Tensor]] = None):
-    seqlen = x.shape[1]
-    freqs_cis = self.freqs_cis.shrink((None, (start_pos, start_pos + seqlen), None, None, None))
-    mask = Tensor.full((1, 1, seqlen, start_pos + seqlen), float("-100000000"), dtype=x.dtype, device=x.device).triu(start_pos + 1).realize() if seqlen > 1 else None
-
-    h = x
-
-    if cache is None:
-      cache = [None for _ in range(self.shard.start_layer, self.shard.end_layer + 1)]  
-    for i, c in zip(range(self.shard.start_layer, self.shard.end_layer + 1), cache):
-      layer = self.layers[i]
-      h = layer(h, start_pos, freqs_cis, mask, cache=c)
-
-    if self.shard.is_last_layer():
-      logits = self.output(self.norm(h)).float().realize()
-      return logits
-    else:
-      return h
-
-  def embed(self, inputs: Tensor):
-    if self.shard.is_first_layer():
-      h = self.tok_embeddings(inputs)
-    else:
-      h = inputs
-    return h
-
-  def forward(self, x: Tensor, start_pos: int, cache: Optional[List[Tensor]] = None):
-    if x.shape[0:2] == (1, 1) and self.forward_jit is not None and start_pos != 0:
-      return self.forward_jit(x, Variable("start_pos", 1, self.max_context).bind(start_pos), cache=cache)
-    return self.forward_base(x, start_pos, cache=cache)
-
-  def __call__(self, tokens: Tensor, start_pos: Variable, cache: Optional[List[Tensor]] = None):
-    # TODO: better way to handle the first call v.s. the rest?
-    h = self.embed(x)
-    return self.forward(h, start_pos, cache=cache)
-
-
-# *** helpers ***
-
-
-def convert_from_huggingface(weights: Dict[str, Tensor], model: Transformer, n_heads: int, n_kv_heads: int):
-  def permute(v: Tensor, n_heads: int):
-    return v.reshape(n_heads, 2, v.shape[0] // n_heads // 2, v.shape[1]).transpose(1, 2).reshape(*v.shape[:2])
-
-  keymap = {
-    "model.embed_tokens.weight": "tok_embeddings.weight",
-    **{f"model.layers.{l}.input_layernorm.weight": f"layers.{l}.attention_norm.weight"
-       for l in range(len(model.layers))},
-    **{f"model.layers.{l}.self_attn.{x}_proj.weight": f"layers.{l}.attention.w{x}.weight"
-       for x in ["q", "k", "v", "o"]
-       for l in range(len(model.layers))},
-    **{f"model.layers.{l}.post_attention_layernorm.weight": f"layers.{l}.ffn_norm.weight"
-       for l in range(len(model.layers))},
-    **{f"model.layers.{l}.mlp.{x}_proj.weight": f"layers.{l}.feed_forward.w{y}.weight"
-       for x, y in {"gate": "1", "down": "2", "up": "3"}.items()
-       for l in range(len(model.layers))},
-    "model.norm.weight": "norm.weight",
-    "lm_head.weight": "output.weight",
-  }
-  sd = {}
-  for k, v in weights.items():
-    if ".rotary_emb." in k: continue
-    v = v.to(Device.DEFAULT)
-    if "model.layers" in k:
-      if "q_proj" in k:
-        v = permute(v, n_heads)
-      elif "k_proj" in k:
-        v = permute(v, n_kv_heads)
-    if k in keymap:
-      sd[keymap[k]] = v
-    else:
-      sd[k] = v
-  return sd
-
-
-def fix_bf16(weights: Dict[Any, Tensor]):
-  if getenv("SUPPORT_BF16", 1):
-    # TODO: without casting to float16, 70B llama OOM on tinybox.
-    return {k: v.cast(dtypes.float16) if v.dtype == dtypes.bfloat16 else v for k, v in weights.items()}
-  # TODO: check if device supports bf16
-  return {k: v.llvm_bf16_cast(dtypes.half).to(v.device) if v.dtype == dtypes.bfloat16 else v for k, v in weights.items()}
diff --git a/build/lib/exo/inference/tinygrad/stateful_model.py b/build/lib/exo/inference/tinygrad/stateful_model.py
deleted file mode 100644
index e9178a38..00000000
--- a/build/lib/exo/inference/tinygrad/stateful_model.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from tinygrad import Tensor, Variable 
-from collections import OrderedDict
-from typing import List
-
-def create_kv_cache(x: Tensor, max_context: int, n_kv_heads: int, head_dim: int):
-  cache_kv = Tensor.zeros(2, x.shape[0], max_context, n_kv_heads, head_dim, dtype=x.dtype).contiguous().realize()
-  if isinstance(x.device, tuple):
-    # TODO: instead of specifying how to shard, it can follow how xk and xv are being sharded
-    cache_kv.shard_((x.device), axis=3 if getenv("SHARD_KVCACHE") else None).realize()
-  return cache_kv.realize()
-
-class ModelState:
-  cache: List[Tensor]
-  start: int 
-  def __init__(self, cache: List[Tensor], start: int = 0):
-    self.cache = cache
-    self.start = start
-
-class StatefulModel:
-  def __init__(self, model, max_states: int = 2):
-    super().__init__()
-    self.model = model
-    self.max_states = max_states
-    self.states = OrderedDict()
- 
-  def init_cache(self, x: Tensor, request_id: str):
-    cache = [create_kv_cache(x, self.model.layers[i].attention.max_context, self.model.layers[i].attention.n_kv_heads, self.model.layers[i].attention.head_dim) for i in range(self.model.shard.start_layer, self.model.shard.end_layer + 1)]
-    if len(self.states) >= self.max_states:
-      self.states.popitem(last=False)
-
-    self.states[request_id] = ModelState(cache)
-
-  def __call__(self, x: Tensor, request_id: str): 
-    h = self.model.embed(x)
-    if request_id not in self.states:
-      self.init_cache(h, request_id)
-    else:
-      self.states.move_to_end(request_id)
-    out = self.model.forward(h, self.states[request_id].start, cache=self.states[request_id].cache)
-    self.states[request_id].start += h.shape[1]
-    return out
-
diff --git a/build/lib/exo/inference/tinygrad/tinygrad_helpers.py b/build/lib/exo/inference/tinygrad/tinygrad_helpers.py
deleted file mode 100644
index a33b01ae..00000000
--- a/build/lib/exo/inference/tinygrad/tinygrad_helpers.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from tinygrad.nn.state import safe_load, torch_load
-from tinygrad import Tensor
-from pathlib import Path
-import json
-from typing import List
-from exo.inference.shard import Shard
-from exo.helpers import DEBUG
-from exo.download.hf.hf_helpers import get_allow_patterns
-from fnmatch import fnmatch
-import re
-
-
-# **** helper functions ****
-def concat_weights(models, device=None):
-  def convert(name) -> Tensor:
-    disk_tensors: List[Tensor] = [model[name] for model in models]
-    if len(disk_tensors) == 1 or len(disk_tensors[0].shape) == 1:
-      return disk_tensors[0].to(device=device)
-    axis = 1 if name.endswith(".attention.wo.weight") or name.endswith(".feed_forward.w2.weight") else 0
-    lazy_tensors = [data.to(device=device) for data in disk_tensors]
-    return lazy_tensors[0].cat(*lazy_tensors[1:], dim=axis)
-
-  return {name: convert(name) for name in {name: None for model in models for name in model}}
-
-
-def load(fn: str, shard: Shard):
-  if fn.endswith('.index.json'):
-    with open(fn) as fp:
-      weight_map = json.load(fp)['weight_map']
-    parts = {}
-    filtered_weight_map = {}
-    allow_patterns = get_allow_patterns(weight_map, shard)
-    for k, n in weight_map.items():
-      if allow_patterns is not None and not any(fnmatch(n, r) for r in allow_patterns):
-        continue
-      if k.startswith("model.layers."):
-        layer_num = int(k.split('.')[2])
-        if layer_num < shard.start_layer or layer_num > shard.end_layer:
-          continue
-
-      parts[n] = load(str(Path(fn).parent/Path(n).name), shard)
-      filtered_weight_map[k] = n
-    if DEBUG >= 2: print(f"Excluded model param keys for {shard=}: {sorted(set(weight_map.keys()) - set(filtered_weight_map.keys()))}")
-    return {k: parts[n][k] for k, n in filtered_weight_map.items()}
-  elif fn.endswith(".safetensors"):
-    weight_map = safe_load(fn)
-    for k in list(weight_map):
-      if (n := re.search(r"\.(\d+)\.", k)) and not (shard.start_layer <= int(n.group(1)) <= shard.end_layer):
-          del weight_map[k]
-    return weight_map
-  else:
-    return torch_load(fn)
diff --git a/build/lib/exo/inference/tokenizers.py b/build/lib/exo/inference/tokenizers.py
deleted file mode 100644
index 6b1439fc..00000000
--- a/build/lib/exo/inference/tokenizers.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import traceback
-from aiofiles import os as aios
-from os import PathLike
-from pathlib import Path
-from typing import Union
-from transformers import AutoTokenizer, AutoProcessor
-import numpy as np
-from exo.download.hf.hf_helpers import get_local_snapshot_dir
-from exo.helpers import DEBUG
-
-
-class DummyTokenizer:
-  def __init__(self):
-    self.eos_token_id = 69
-    self.vocab_size = 1000
-
-  def apply_chat_template(self, messages, tokenize=True, add_generation_prompt=True):
-    return "dummy_tokenized_prompt"
-
-  def encode(self, text):
-    return np.array([1])
-
-  def decode(self, tokens):
-    return "dummy" * len(tokens)
-
-
-async def resolve_tokenizer(model_id: str):
-  if model_id == "dummy":
-    return DummyTokenizer()
-  local_path = await get_local_snapshot_dir(model_id)
-  if DEBUG >= 2: print(f"Checking if local path exists to load tokenizer from local {local_path=}")
-  try:
-    if local_path and await aios.path.exists(local_path):
-      if DEBUG >= 2: print(f"Resolving tokenizer for {model_id=} from {local_path=}")
-      return await _resolve_tokenizer(local_path)
-  except:
-    if DEBUG >= 5: print(f"Local check for {local_path=} failed. Resolving tokenizer for {model_id=} normally...")
-    if DEBUG >= 5: traceback.print_exc()
-  return await _resolve_tokenizer(model_id)
-
-
-async def _resolve_tokenizer(model_id_or_local_path: Union[str, PathLike]):
-  try:
-    if DEBUG >= 4: print(f"Trying AutoProcessor for {model_id_or_local_path}")
-    processor = AutoProcessor.from_pretrained(model_id_or_local_path, use_fast=True if "Mistral-Large" in f"{model_id_or_local_path}" else False, trust_remote_code=True)
-    if not hasattr(processor, 'eos_token_id'):
-      processor.eos_token_id = getattr(processor, 'tokenizer', getattr(processor, '_tokenizer', processor)).eos_token_id
-    if not hasattr(processor, 'encode'):
-      processor.encode = getattr(processor, 'tokenizer', getattr(processor, '_tokenizer', processor)).encode
-    if not hasattr(processor, 'decode'):
-      processor.decode = getattr(processor, 'tokenizer', getattr(processor, '_tokenizer', processor)).decode
-    return processor
-  except Exception as e:
-    if DEBUG >= 4: print(f"Failed to load processor for {model_id_or_local_path}. Error: {e}")
-    if DEBUG >= 4: print(traceback.format_exc())
-
-  try:
-    if DEBUG >= 4: print(f"Trying AutoTokenizer for {model_id_or_local_path}")
-    return AutoTokenizer.from_pretrained(model_id_or_local_path, trust_remote_code=True)
-  except Exception as e:
-    if DEBUG >= 4: print(f"Failed to load tokenizer for {model_id_or_local_path}. Falling back to tinygrad tokenizer. Error: {e}")
-    if DEBUG >= 4: print(traceback.format_exc())
-
-  raise ValueError(f"[TODO] Unsupported model: {model_id_or_local_path}")
diff --git a/build/lib/exo/main.py b/build/lib/exo/main.py
deleted file mode 100644
index 569fb289..00000000
--- a/build/lib/exo/main.py
+++ /dev/null
@@ -1,274 +0,0 @@
-import argparse
-import asyncio
-import atexit
-import signal
-import json
-import logging
-import platform
-import os
-import sys
-import time
-import traceback
-import uuid
-from exo.networking.manual.manual_discovery import ManualDiscovery
-from exo.networking.manual.network_topology_config import NetworkTopology
-from exo.orchestration.standard_node import StandardNode
-from exo.networking.grpc.grpc_server import GRPCServer
-from exo.networking.udp.udp_discovery import UDPDiscovery
-from exo.networking.tailscale.tailscale_discovery import TailscaleDiscovery
-from exo.networking.grpc.grpc_peer_handle import GRPCPeerHandle
-from exo.topology.ring_memory_weighted_partitioning_strategy import RingMemoryWeightedPartitioningStrategy
-from exo.api import ChatGPTAPI
-from exo.download.shard_download import ShardDownloader, RepoProgressEvent, NoopShardDownloader
-from exo.download.hf.hf_shard_download import HFShardDownloader
-from exo.helpers import print_yellow_exo, find_available_port, DEBUG, get_system_info, get_or_create_node_id, get_all_ip_addresses, terminal_link, shutdown
-from exo.inference.shard import Shard
-from exo.inference.inference_engine import get_inference_engine, InferenceEngine
-from exo.inference.tokenizers import resolve_tokenizer
-from exo.orchestration.node import Node
-from exo.models import build_base_shard, get_repo
-from exo.viz.topology_viz import TopologyViz
-from exo.download.hf.hf_helpers import has_hf_home_read_access, has_hf_home_write_access, get_hf_home, move_models_to_hf
-
-# parse args
-parser = argparse.ArgumentParser(description="Initialize GRPC Discovery")
-parser.add_argument("command", nargs="?", choices=["run"], help="Command to run")
-parser.add_argument("model_name", nargs="?", help="Model name to run")
-parser.add_argument("--default-model", type=str, default=None, help="Default model")
-parser.add_argument("--node-id", type=str, default=None, help="Node ID")
-parser.add_argument("--node-host", type=str, default="0.0.0.0", help="Node host")
-parser.add_argument("--node-port", type=int, default=None, help="Node port")
-parser.add_argument("--models-seed-dir", type=str, default=None, help="Model seed directory")
-parser.add_argument("--listen-port", type=int, default=5678, help="Listening port for discovery")
-parser.add_argument("--download-quick-check", action="store_true", help="Quick check local path for model shards download")
-parser.add_argument("--max-parallel-downloads", type=int, default=4, help="Max parallel downloads for model shards download")
-parser.add_argument("--prometheus-client-port", type=int, default=None, help="Prometheus client port")
-parser.add_argument("--broadcast-port", type=int, default=5678, help="Broadcast port for discovery")
-parser.add_argument("--discovery-module", type=str, choices=["udp", "tailscale", "manual"], default="udp", help="Discovery module to use")
-parser.add_argument("--discovery-timeout", type=int, default=30, help="Discovery timeout in seconds")
-parser.add_argument("--discovery-config-path", type=str, default=None, help="Path to discovery config json file")
-parser.add_argument("--wait-for-peers", type=int, default=0, help="Number of peers to wait to connect to before starting")
-parser.add_argument("--chatgpt-api-port", type=int, default=52415, help="ChatGPT API port")
-parser.add_argument("--chatgpt-api-response-timeout", type=int, default=90, help="ChatGPT API response timeout in seconds")
-parser.add_argument("--max-generate-tokens", type=int, default=10000, help="Max tokens to generate in each request")
-parser.add_argument("--inference-engine", type=str, default=None, help="Inference engine to use (mlx, tinygrad, or dummy)")
-parser.add_argument("--disable-tui", action=argparse.BooleanOptionalAction, help="Disable TUI")
-parser.add_argument("--run-model", type=str, help="Specify a model to run directly")
-parser.add_argument("--prompt", type=str, help="Prompt for the model when using --run-model", default="Who are you?")
-parser.add_argument("--default-temp", type=float, help="Default token sampling temperature", default=0.0)
-parser.add_argument("--tailscale-api-key", type=str, default=None, help="Tailscale API key")
-parser.add_argument("--tailnet-name", type=str, default=None, help="Tailnet name")
-parser.add_argument("--node-id-filter", type=str, default=None, help="Comma separated list of allowed node IDs (only for UDP and Tailscale discovery)")
-args = parser.parse_args()
-print(f"Selected inference engine: {args.inference_engine}")
-
-print_yellow_exo()
-
-system_info = get_system_info()
-print(f"Detected system: {system_info}")
-
-shard_downloader: ShardDownloader = HFShardDownloader(quick_check=args.download_quick_check,
-                                                      max_parallel_downloads=args.max_parallel_downloads) if args.inference_engine != "dummy" else NoopShardDownloader()
-inference_engine_name = args.inference_engine or ("mlx" if system_info == "Apple Silicon Mac" else "tinygrad")
-print(f"Inference engine name after selection: {inference_engine_name}")
-
-inference_engine = get_inference_engine(inference_engine_name, shard_downloader)
-print(f"Using inference engine: {inference_engine.__class__.__name__} with shard downloader: {shard_downloader.__class__.__name__}")
-
-if args.node_port is None:
-  args.node_port = find_available_port(args.node_host)
-  if DEBUG >= 1: print(f"Using available port: {args.node_port}")
-
-args.node_id = args.node_id or get_or_create_node_id()
-chatgpt_api_endpoints = [f"http://{ip}:{args.chatgpt_api_port}/v1/chat/completions" for ip in get_all_ip_addresses()]
-web_chat_urls = [f"http://{ip}:{args.chatgpt_api_port}" for ip in get_all_ip_addresses()]
-if DEBUG >= 0:
-  print("Chat interface started:")
-  for web_chat_url in web_chat_urls:
-    print(f" - {terminal_link(web_chat_url)}")
-  print("ChatGPT API endpoint served at:")
-  for chatgpt_api_endpoint in chatgpt_api_endpoints:
-    print(f" - {terminal_link(chatgpt_api_endpoint)}")
-
-# Convert node-id-filter to list if provided
-allowed_node_ids = args.node_id_filter.split(',') if args.node_id_filter else None
-
-if args.discovery_module == "udp":
-  discovery = UDPDiscovery(
-    args.node_id,
-    args.node_port,
-    args.listen_port,
-    args.broadcast_port,
-    lambda peer_id, address, device_capabilities: GRPCPeerHandle(peer_id, address, device_capabilities),
-    discovery_timeout=args.discovery_timeout,
-    allowed_node_ids=allowed_node_ids
-  )
-elif args.discovery_module == "tailscale":
-  discovery = TailscaleDiscovery(
-    args.node_id,
-    args.node_port,
-    lambda peer_id, address, device_capabilities: GRPCPeerHandle(peer_id, address, device_capabilities),
-    discovery_timeout=args.discovery_timeout,
-    tailscale_api_key=args.tailscale_api_key,
-    tailnet=args.tailnet_name,
-    allowed_node_ids=allowed_node_ids
-  )
-elif args.discovery_module == "manual":
-  if not args.discovery_config_path:
-    raise ValueError(f"--discovery-config-path is required when using manual discovery. Please provide a path to a config json file.")
-  discovery = ManualDiscovery(args.discovery_config_path, args.node_id, create_peer_handle=lambda peer_id, address, device_capabilities: GRPCPeerHandle(peer_id, address, device_capabilities))
-topology_viz = TopologyViz(chatgpt_api_endpoints=chatgpt_api_endpoints, web_chat_urls=web_chat_urls) if not args.disable_tui else None
-node = StandardNode(
-  args.node_id,
-  None,
-  inference_engine,
-  discovery,
-  partitioning_strategy=RingMemoryWeightedPartitioningStrategy(),
-  max_generate_tokens=args.max_generate_tokens,
-  topology_viz=topology_viz,
-  shard_downloader=shard_downloader,
-  default_sample_temperature=args.default_temp
-)
-server = GRPCServer(node, args.node_host, args.node_port)
-node.server = server
-api = ChatGPTAPI(
-  node,
-  inference_engine.__class__.__name__,
-  response_timeout=args.chatgpt_api_response_timeout,
-  on_chat_completion_request=lambda req_id, __, prompt: topology_viz.update_prompt(req_id, prompt) if topology_viz else None,
-  default_model=args.default_model
-)
-node.on_token.register("update_topology_viz").on_next(
-  lambda req_id, tokens, __: topology_viz.update_prompt_output(req_id, inference_engine.tokenizer.decode(tokens)) if topology_viz and hasattr(inference_engine, "tokenizer") and inference_engine.shard.model_id != 'stable-diffusion-2-1-base' else None
-)
-
-def preemptively_start_download(request_id: str, opaque_status: str):
-  try:
-    status = json.loads(opaque_status)
-    if status.get("type") == "node_status" and status.get("status") == "start_process_prompt":
-      current_shard = node.get_current_shard(Shard.from_dict(status.get("shard")))
-      if DEBUG >= 2: print(f"Preemptively starting download for {current_shard}")
-      asyncio.create_task(shard_downloader.ensure_shard(current_shard, inference_engine.__class__.__name__))
-  except Exception as e:
-    if DEBUG >= 2:
-      print(f"Failed to preemptively start download: {e}")
-      traceback.print_exc()
-
-
-node.on_opaque_status.register("start_download").on_next(preemptively_start_download)
-
-if args.prometheus_client_port:
-  from exo.stats.metrics import start_metrics_server
-  start_metrics_server(node, args.prometheus_client_port)
-
-last_broadcast_time = 0
-
-
-def throttled_broadcast(shard: Shard, event: RepoProgressEvent):
-  global last_broadcast_time
-  current_time = time.time()
-  if event.status == "complete" or current_time - last_broadcast_time >= 0.1:
-    last_broadcast_time = current_time
-    asyncio.create_task(node.broadcast_opaque_status("", json.dumps({"type": "download_progress", "node_id": node.id, "progress": event.to_dict()})))
-
-
-shard_downloader.on_progress.register("broadcast").on_next(throttled_broadcast)
-
-async def run_model_cli(node: Node, inference_engine: InferenceEngine, model_name: str, prompt: str):
-  inference_class = inference_engine.__class__.__name__
-  shard = build_base_shard(model_name, inference_class)
-  if not shard:
-    print(f"Error: Unsupported model '{model_name}' for inference engine {inference_engine.__class__.__name__}")
-    return
-  tokenizer = await resolve_tokenizer(get_repo(shard.model_id, inference_class))
-  request_id = str(uuid.uuid4())
-  callback_id = f"cli-wait-response-{request_id}"
-  callback = node.on_token.register(callback_id)
-  if topology_viz:
-    topology_viz.update_prompt(request_id, prompt)
-  prompt = tokenizer.apply_chat_template([{"role": "user", "content": prompt}], tokenize=False, add_generation_prompt=True)
-
-  try:
-    print(f"Processing prompt: {prompt}")
-    await node.process_prompt(shard, prompt, request_id=request_id)
-
-    _, tokens, _ = await callback.wait(lambda _request_id, tokens, is_finished: _request_id == request_id and is_finished, timeout=300)
-
-    print("\nGenerated response:")
-    print(tokenizer.decode(tokens))
-  except Exception as e:
-    print(f"Error processing prompt: {str(e)}")
-    traceback.print_exc()
-  finally:
-    node.on_token.deregister(callback_id)
-
-def clean_path(path):
-    """Clean and resolve path"""
-    if path.startswith("Optional("):
-        path = path.strip('Optional("').rstrip('")')
-    return os.path.expanduser(path)
-
-async def main():
-  loop = asyncio.get_running_loop()
-
-  # Check HuggingFace directory permissions
-  hf_home, has_read, has_write = get_hf_home(), await has_hf_home_read_access(), await has_hf_home_write_access()
-  if DEBUG >= 1: print(f"Model storage directory: {hf_home}")
-  print(f"{has_read=}, {has_write=}")
-  if not has_read or not has_write:
-    print(f"""
-          WARNING: Limited permissions for model storage directory: {hf_home}.
-          This may prevent model downloads from working correctly.
-          {"❌ No read access" if not has_read else ""}
-          {"❌ No write access" if not has_write else ""}
-          """)
-    
-  if not args.models_seed_dir is None:
-    try:
-      models_seed_dir = clean_path(args.models_seed_dir)
-      await move_models_to_hf(models_seed_dir)
-    except Exception as e:
-      print(f"Error moving models to .cache/huggingface: {e}")
-
-  def restore_cursor():
-    if platform.system() != "Windows":
-        os.system("tput cnorm")  # Show cursor
-
-  # Restore the cursor when the program exits
-  atexit.register(restore_cursor)
-
-  # Use a more direct approach to handle signals
-  def handle_exit():
-    asyncio.ensure_future(shutdown(signal.SIGTERM, loop, node.server))
-
-  if platform.system() != "Windows":
-    for s in [signal.SIGINT, signal.SIGTERM]:
-      loop.add_signal_handler(s, handle_exit)
-
-  await node.start(wait_for_peers=args.wait_for_peers)
-
-  if args.command == "run" or args.run_model:
-    model_name = args.model_name or args.run_model
-    if not model_name:
-      print("Error: Model name is required when using 'run' command or --run-model")
-      return
-    await run_model_cli(node, inference_engine, model_name, args.prompt)
-  else:
-    asyncio.create_task(api.run(port=args.chatgpt_api_port))  # Start the API server as a non-blocking task
-    await asyncio.Event().wait()
-
-
-def run():
-  loop = asyncio.new_event_loop()
-  asyncio.set_event_loop(loop)
-  try:
-    loop.run_until_complete(main())
-  except KeyboardInterrupt:
-    print("Received keyboard interrupt. Shutting down...")
-  finally:
-    loop.run_until_complete(shutdown(signal.SIGTERM, loop, node.server))
-    loop.close()
-
-
-if __name__ == "__main__":
-  run()
diff --git a/build/lib/exo/models.py b/build/lib/exo/models.py
deleted file mode 100644
index 2f34e257..00000000
--- a/build/lib/exo/models.py
+++ /dev/null
@@ -1,151 +0,0 @@
-from exo.inference.shard import Shard
-from typing import Optional, List
-
-model_cards = {
-  ### llama
-  "llama-3.2-1b": {
-    "layers": 16,
-    "repo": {
-      "MLXDynamicShardInferenceEngine": "mlx-community/Llama-3.2-1B-Instruct-4bit",
-      "TinygradDynamicShardInferenceEngine": "unsloth/Llama-3.2-1B-Instruct",
-    },
-  },
-  "llama-3.2-3b": {
-    "layers": 28,
-    "repo": {
-       "MLXDynamicShardInferenceEngine": "mlx-community/Llama-3.2-3B-Instruct-4bit",
-       "TinygradDynamicShardInferenceEngine": "unsloth/Llama-3.2-3B-Instruct",
-    },
-  },
-  "llama-3.1-8b": {
-    "layers": 32,
-    "repo": {
-       "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3.1-8B-Instruct-4bit",
-       "TinygradDynamicShardInferenceEngine": "mlabonne/Meta-Llama-3.1-8B-Instruct-abliterated",
-    },
-  },
-  "llama-3.1-70b": {
-    "layers": 80,
-    "repo": {
-       "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3.1-70B-Instruct-4bit",
-       "TinygradDynamicShardInferenceEngine": "NousResearch/Meta-Llama-3.1-70B-Instruct",
-    },
-  },
-  "llama-3.1-70b-bf16": {
-    "layers": 80,
-    "repo": {
-       "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3.1-70B-Instruct-bf16-CORRECTED",
-       "TinygradDynamicShardInferenceEngine": "NousResearch/Meta-Llama-3.1-70B-Instruct",
-    },
-  },
-  "llama-3-8b": {
-    "layers": 32,
-    "repo": {
-       "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3-8B-Instruct-4bit",
-       "TinygradDynamicShardInferenceEngine": "TriAiExperiments/SFR-Iterative-DPO-LLaMA-3-8B-R",
-    },
-  },
-  "llama-3-70b": {
-    "layers": 80,
-    "repo": {
-       "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3-70B-Instruct-4bit",
-       "TinygradDynamicShardInferenceEngine": "TriAiExperiments/SFR-Iterative-DPO-LLaMA-3-70B-R",
-    },
-  },
-  "llama-3.1-405b": { "layers": 126, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3.1-405B-4bit", }, },
-  "llama-3.1-405b-8bit": { "layers": 126, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3.1-405B-Instruct-8bit", }, },
-  ### mistral
-  "mistral-nemo": { "layers": 40, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Mistral-Nemo-Instruct-2407-4bit", }, },
-  "mistral-large": { "layers": 88, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Mistral-Large-Instruct-2407-4bit", }, },
-  ### deepseek
-  "deepseek-coder-v2-lite": { "layers": 27, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-Coder-V2-Lite-Instruct-4bit-mlx", }, },
-  "deepseek-coder-v2.5": { "layers": 60, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-V2.5-MLX-AQ4_1_64", }, },
-  ### llava
-  "llava-1.5-7b-hf": { "layers": 32, "repo": { "MLXDynamicShardInferenceEngine": "llava-hf/llava-1.5-7b-hf", }, },
-  ### qwen
-  "qwen-2.5-0.5b": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-0.5B-Instruct-4bit", }, },
-  "qwen-2.5-coder-1.5b": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Coder-1.5B-Instruct-4bit", }, },
-  "qwen-2.5-coder-3b": { "layers": 36, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Coder-3B-Instruct-4bit", }, },
-  "qwen-2.5-coder-7b": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Coder-7B-Instruct-4bit", }, },
-  "qwen-2.5-coder-14b": { "layers": 48, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Coder-14B-Instruct-4bit", }, },
-  "qwen-2.5-coder-32b": { "layers": 64, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Coder-32B-Instruct-4bit", }, },
-  "qwen-2.5-7b": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-7B-Instruct-4bit", }, },
-  "qwen-2.5-math-7b": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Math-7B-Instruct-4bit", }, },
-  "qwen-2.5-14b": { "layers": 48, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-14B-Instruct-4bit", }, },
-  "qwen-2.5-72b": { "layers": 80, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-72B-Instruct-4bit", }, },
-  "qwen-2.5-math-72b": { "layers": 80, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Math-72B-Instruct-4bit", }, },
-  ### nemotron
-  "nemotron-70b": { "layers": 80, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/nvidia_Llama-3.1-Nemotron-70B-Instruct-HF_4bit", }, },
-  "nemotron-70b-bf16": { "layers": 80, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Llama-3.1-Nemotron-70B-Instruct-HF-bf16", }, },
-  # gemma
-  "gemma2-9b": { "layers": 42, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/gemma-2-9b-it-4bit", }, },
-  "gemma2-27b": { "layers": 46, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/gemma-2-27b-it-4bit", }, },
-  # stable diffusion
-  "stable-diffusion-2-1-base": { "layers": 31, "repo": { "MLXDynamicShardInferenceEngine": "stabilityai/stable-diffusion-2-1-base" } },
-  # dummy
-  "dummy": { "layers": 8, "repo": { "DummyInferenceEngine": "dummy", }, },
-}
-
-pretty_name = {
-  "llama-3.2-1b": "Llama 3.2 1B",
-  "llama-3.2-3b": "Llama 3.2 3B",
-  "llama-3.1-8b": "Llama 3.1 8B",
-  "llama-3.1-70b": "Llama 3.1 70B",
-  "llama-3.1-70b-bf16": "Llama 3.1 70B (BF16)",
-  "llama-3.1-405b": "Llama 3.1 405B",
-  "llama-3.1-405b-8bit": "Llama 3.1 405B (8-bit)",
-  "gemma2-9b": "Gemma2 9B",
-  "gemma2-27b": "Gemma2 27B",
-  "nemotron-70b": "Nemotron 70B",
-  "nemotron-70b-bf16": "Nemotron 70B (BF16)",
-  "mistral-nemo": "Mistral Nemo",
-  "mistral-large": "Mistral Large",
-  "deepseek-coder-v2-lite": "Deepseek Coder V2 Lite",
-  "deepseek-coder-v2.5": "Deepseek Coder V2.5",
-  "llava-1.5-7b-hf": "LLaVa 1.5 7B (Vision Model)",
-  "qwen-2.5-coder-1.5b": "Qwen 2.5 Coder 1.5B",
-  "qwen-2.5-coder-3b": "Qwen 2.5 Coder 3B",
-  "qwen-2.5-coder-7b": "Qwen 2.5 Coder 7B",
-  "qwen-2.5-coder-14b": "Qwen 2.5 Coder 14B",
-  "qwen-2.5-coder-32b": "Qwen 2.5 Coder 32B",
-  "qwen-2.5-7b": "Qwen 2.5 7B",
-  "qwen-2.5-math-7b": "Qwen 2.5 7B (Math)",
-  "qwen-2.5-14b": "Qwen 2.5 14B",
-  "qwen-2.5-72b": "Qwen 2.5 72B",
-  "qwen-2.5-math-72b": "Qwen 2.5 72B (Math)",
-  "llama-3-8b": "Llama 3 8B",
-  "llama-3-70b": "Llama 3 70B",
-  "stable-diffusion-2-1-base": "Stable Diffusion 2.1",
-}
-
-def get_repo(model_id: str, inference_engine_classname: str) -> Optional[str]:
-  return model_cards.get(model_id, {}).get("repo", {}).get(inference_engine_classname, None)
-
-def build_base_shard(model_id: str, inference_engine_classname: str) -> Optional[Shard]:
-  repo = get_repo(model_id, inference_engine_classname)
-  n_layers = model_cards.get(model_id, {}).get("layers", 0)
-  if repo is None or n_layers < 1:
-    return None
-  return Shard(model_id, 0, 0, n_layers)
-
-def get_supported_models(supported_inference_engine_lists: List[List[str]]) -> List[str]:
-  if not supported_inference_engine_lists:
-    return list(model_cards.keys())
-
-  from exo.inference.inference_engine import inference_engine_classes
-  supported_inference_engine_lists = [
-    [inference_engine_classes[engine] if engine in inference_engine_classes else engine for engine in engine_list]
-    for engine_list in supported_inference_engine_lists
-  ]
-
-  def has_any_engine(model_info: dict, engine_list: List[str]) -> bool:
-    return any(engine in model_info.get("repo", {}) for engine in engine_list)
-
-  def supports_all_engine_lists(model_info: dict) -> bool:
-    return all(has_any_engine(model_info, engine_list)
-              for engine_list in supported_inference_engine_lists)
-
-  return [
-    model_id for model_id, model_info in model_cards.items()
-    if supports_all_engine_lists(model_info)
-  ]
diff --git a/build/lib/exo/networking/__init__.py b/build/lib/exo/networking/__init__.py
deleted file mode 100644
index 44a10a30..00000000
--- a/build/lib/exo/networking/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from .discovery import Discovery
-from .peer_handle import PeerHandle
-from .server import Server
-
-__all__ = ["Discovery", "PeerHandle", "Server"]
diff --git a/build/lib/exo/networking/discovery.py b/build/lib/exo/networking/discovery.py
deleted file mode 100644
index cdcbfabc..00000000
--- a/build/lib/exo/networking/discovery.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from abc import ABC, abstractmethod
-from typing import List
-from .peer_handle import PeerHandle
-
-
-class Discovery(ABC):
-  @abstractmethod
-  async def start(self) -> None:
-    pass
-
-  @abstractmethod
-  async def stop(self) -> None:
-    pass
-
-  @abstractmethod
-  async def discover_peers(self, wait_for_peers: int = 0) -> List[PeerHandle]:
-    pass
diff --git a/build/lib/exo/networking/grpc/__init__.py b/build/lib/exo/networking/grpc/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/networking/grpc/grpc_peer_handle.py b/build/lib/exo/networking/grpc/grpc_peer_handle.py
deleted file mode 100644
index 25d441f2..00000000
--- a/build/lib/exo/networking/grpc/grpc_peer_handle.py
+++ /dev/null
@@ -1,173 +0,0 @@
-import grpc
-import numpy as np
-import asyncio
-from typing import Optional, Tuple, List
-
-from . import node_service_pb2
-from . import node_service_pb2_grpc
-
-from ..peer_handle import PeerHandle
-from exo.inference.shard import Shard
-from exo.topology.topology import Topology
-from exo.topology.device_capabilities import DeviceCapabilities, DeviceFlops
-from exo.helpers import DEBUG
-import json
-import mlx.core as mx
-
-class GRPCPeerHandle(PeerHandle):
-  def __init__(self, _id: str, address: str, device_capabilities: DeviceCapabilities):
-    self._id = _id
-    self.address = address
-    self._device_capabilities = device_capabilities
-    self.channel = None
-    self.stub = None
-
-  def id(self) -> str:
-    return self._id
-
-  def addr(self) -> str:
-    return self.address
-
-  def device_capabilities(self) -> DeviceCapabilities:
-    return self._device_capabilities
-
-  async def connect(self):
-    if self.channel is None:
-      self.channel = grpc.aio.insecure_channel(self.address, options=[
-        ("grpc.max_metadata_size", 32*1024*1024),
-        ('grpc.max_receive_message_length', 32*1024*1024),
-        ('grpc.max_send_message_length', 32*1024*1024)
-      ])
-      self.stub = node_service_pb2_grpc.NodeServiceStub(self.channel)
-    await self.channel.channel_ready()
-
-  async def is_connected(self) -> bool:
-    return self.channel is not None and self.channel.get_state() == grpc.ChannelConnectivity.READY
-
-  async def disconnect(self):
-    if self.channel:
-      await self.channel.close()
-    self.channel = None
-    self.stub = None
-
-  async def _ensure_connected(self):
-    if not await self.is_connected(): await asyncio.wait_for(self.connect(), timeout=5)
-
-  async def health_check(self) -> bool:
-    try:
-      await self._ensure_connected()
-      request = node_service_pb2.HealthCheckRequest()
-      response = await asyncio.wait_for(self.stub.HealthCheck(request), timeout=5)
-      return response.is_healthy
-    except asyncio.TimeoutError:
-      return False
-    except Exception:
-      if DEBUG >= 4:
-        print(f"Health check failed for {self._id}@{self.address}.")
-        import traceback
-        traceback.print_exc()
-      return False
-
-  async def send_prompt(self, shard: Shard, prompt: str, inference_state: Optional[dict] = None, request_id: Optional[str] = None) -> Optional[np.array]:
-    request = node_service_pb2.PromptRequest(
-      prompt=prompt,
-      shard=node_service_pb2.Shard(
-        model_id=shard.model_id,
-        start_layer=shard.start_layer,
-        end_layer=shard.end_layer,
-        n_layers=shard.n_layers,
-      ),
-      request_id=request_id,
-      inference_state=self.serialize_inference_state(inference_state)
-    )
-    response = await self.stub.SendPrompt(request)
-
-    if not response.tensor_data or not response.shape or not response.dtype:
-      return None
-
-    return np.frombuffer(response.tensor_data, dtype=np.dtype(response.dtype)).reshape(response.shape)
-
-  async def send_tensor(self, shard: Shard, tensor: np.ndarray, inference_state: Optional[dict] = None, request_id: Optional[str] = None) -> Optional[np.array]:
-    request = node_service_pb2.TensorRequest(
-      shard=node_service_pb2.Shard(
-        model_id=shard.model_id,
-        start_layer=shard.start_layer,
-        end_layer=shard.end_layer,
-        n_layers=shard.n_layers,
-      ),
-      tensor=node_service_pb2.Tensor(tensor_data=tensor.tobytes(), shape=tensor.shape, dtype=str(tensor.dtype)),
-      request_id=request_id,
-      inference_state=self.serialize_inference_state(inference_state)
-    )
-    response = await self.stub.SendTensor(request)
-
-    if not response.tensor_data or not response.shape or not response.dtype:
-      return None
-
-    return np.frombuffer(response.tensor_data, dtype=np.dtype(response.dtype)).reshape(response.shape)
-
-  async def get_inference_result(self, request_id: str) -> Tuple[Optional[np.ndarray], bool]:
-    request = node_service_pb2.GetInferenceResultRequest(request_id=request_id)
-    response = await self.stub.GetInferenceResult(request)
-    if response.tensor is None:
-      return None, response.is_finished
-    return (
-      np.frombuffer(response.tensor.tensor_data, dtype=np.dtype(response.tensor.dtype)).reshape(response.tensor.shape),
-      response.is_finished,
-    )
-
-  async def collect_topology(self, visited: set[str], max_depth: int) -> Topology:
-    request = node_service_pb2.CollectTopologyRequest(visited=visited, max_depth=max_depth)
-    response = await self.stub.CollectTopology(request)
-    topology = Topology()
-    for node_id, capabilities in response.nodes.items():
-      device_capabilities = DeviceCapabilities(
-        model=capabilities.model, chip=capabilities.chip, memory=capabilities.memory, flops=DeviceFlops(fp16=capabilities.flops.fp16, fp32=capabilities.flops.fp32, int8=capabilities.flops.int8)
-      )
-      topology.update_node(node_id, device_capabilities)
-    for node_id, peers in response.peer_graph.items():
-      for peer_id in peers.peer_ids:
-        topology.add_edge(node_id, peer_id)
-    return topology
-
-  async def send_result(self, request_id: str, result: List[int], is_finished: bool) -> None:
-    tensor = None
-    if isinstance(result, np.ndarray):
-      tensor = node_service_pb2.Tensor(tensor_data=result.tobytes(), shape=result.shape, dtype=str(result.dtype))
-      result = []
-    request = node_service_pb2.SendResultRequest(request_id=request_id, result=result, tensor=tensor, is_finished=is_finished)
-    await self.stub.SendResult(request)
-
-  async def send_opaque_status(self, request_id: str, status: str) -> None:
-    request = node_service_pb2.SendOpaqueStatusRequest(request_id=request_id, status=status)
-    await self.stub.SendOpaqueStatus(request)
-
-  def serialize_inference_state(self, inference_state: dict) -> node_service_pb2.InferenceState:
-    proto_inference_state = node_service_pb2.InferenceState()
-    other_data = {}
-    for k, v in inference_state.items():
-        if isinstance(v, mx.array):
-            np_array = np.array(v)
-            tensor_data = node_service_pb2.Tensor(
-                tensor_data=np_array.tobytes(),
-                shape=list(np_array.shape),
-                dtype=str(np_array.dtype)
-            )
-            proto_inference_state.tensor_data[k].CopyFrom(tensor_data)
-        elif isinstance(v, list) and all(isinstance(item, mx.array) for item in v):
-            tensor_list = node_service_pb2.TensorList()
-            for tensor in v:
-                np_array = np.array(tensor)
-                tensor_data = node_service_pb2.Tensor(
-                    tensor_data=np_array.tobytes(),
-                    shape=list(np_array.shape),
-                    dtype=str(np_array.dtype)
-                )
-                tensor_list.tensors.append(tensor_data)
-            proto_inference_state.tensor_list_data[k].CopyFrom(tensor_list)
-        else:
-            # For non-tensor data, we'll still use JSON
-            other_data[k] = v
-    if other_data:
-      proto_inference_state.other_data_json = json.dumps(other_data)
-    return proto_inference_state
diff --git a/build/lib/exo/networking/grpc/grpc_server.py b/build/lib/exo/networking/grpc/grpc_server.py
deleted file mode 100644
index 8155e642..00000000
--- a/build/lib/exo/networking/grpc/grpc_server.py
+++ /dev/null
@@ -1,147 +0,0 @@
-import grpc
-from concurrent import futures
-import numpy as np
-from asyncio import CancelledError
-
-from . import node_service_pb2
-from . import node_service_pb2_grpc
-from exo import DEBUG
-from exo.inference.shard import Shard
-from exo.orchestration import Node
-import json
-import mlx.core as mx
-
-
-class GRPCServer(node_service_pb2_grpc.NodeServiceServicer):
-  def __init__(self, node: Node, host: str, port: int):
-    self.node = node
-    self.host = host
-    self.port = port
-    self.server = None
-
-  async def start(self) -> None:
-    self.server = grpc.aio.server(
-      futures.ThreadPoolExecutor(max_workers=10),
-      options=[
-        ("grpc.max_metadata_size", 32*1024*1024),
-        ("grpc.max_send_message_length", 128*1024*1024),
-        ("grpc.max_receive_message_length", 128*1024*1024),
-      ],
-    )
-    node_service_pb2_grpc.add_NodeServiceServicer_to_server(self, self.server)
-    listen_addr = f"{self.host}:{self.port}"
-    self.server.add_insecure_port(listen_addr)
-    await self.server.start()
-    if DEBUG >= 1: print(f"Server started, listening on {listen_addr}")
-
-  async def stop(self) -> None:
-    if self.server:
-      try:
-        await self.server.stop(grace=5)
-        await self.server.wait_for_termination()
-      except CancelledError:
-        pass
-      if DEBUG >= 1: print("Server stopped and all connections are closed")
-
-  async def SendPrompt(self, request, context):
-    shard = Shard(
-      model_id=request.shard.model_id,
-      start_layer=request.shard.start_layer,
-      end_layer=request.shard.end_layer,
-      n_layers=request.shard.n_layers,
-    )
-    prompt = request.prompt
-    request_id = request.request_id
-    inference_state = self.deserialize_inference_state(request.inference_state)
-    result = await self.node.process_prompt(shard, prompt, request_id, inference_state)
-    if DEBUG >= 5: print(f"SendPrompt {shard=} {prompt=} {request_id=} result: {result}")
-    tensor_data = result.tobytes() if result is not None else None
-    return node_service_pb2.Tensor(tensor_data=tensor_data, shape=result.shape, dtype=str(result.dtype)) if result is not None else node_service_pb2.Tensor()
-
-  async def SendTensor(self, request, context):
-    shard = Shard(
-      model_id=request.shard.model_id,
-      start_layer=request.shard.start_layer,
-      end_layer=request.shard.end_layer,
-      n_layers=request.shard.n_layers,
-    )
-    tensor = np.frombuffer(request.tensor.tensor_data, dtype=np.dtype(request.tensor.dtype)).reshape(request.tensor.shape)
-    request_id = request.request_id
-
-    inference_state = self.deserialize_inference_state(request.inference_state)
-
-    result = await self.node.process_tensor(shard, tensor, request_id, inference_state)
-    if DEBUG >= 5: print(f"SendTensor tensor {shard=} {tensor=} {request_id=} result: {result}")
-    tensor_data = result.tobytes() if result is not None else None
-    return node_service_pb2.Tensor(tensor_data=tensor_data, shape=result.shape, dtype=str(result.dtype)) if result is not None else node_service_pb2.Tensor()
-
-  async def GetInferenceResult(self, request, context):
-    request_id = request.request_id
-    result = await self.node.get_inference_result(request_id)
-    if DEBUG >= 5: print(f"GetInferenceResult {request_id=}: {result}")
-    tensor_data = result[0].tobytes() if result[0] is not None else None
-    return (
-      node_service_pb2.InferenceResult(
-        tensor=node_service_pb2.Tensor(tensor_data=tensor_data, shape=result[0].shape, dtype=str(result[0].dtype)),
-        is_finished=result[1],
-      ) if result[0] is not None else node_service_pb2.InferenceResult(is_finished=result[1])
-    )
-
-  async def CollectTopology(self, request, context):
-    max_depth = request.max_depth
-    visited = set(request.visited)
-    topology = await self.node.collect_topology(visited, max_depth)
-    nodes = {
-      node_id:
-        node_service_pb2.DeviceCapabilities(
-          model=cap.model,
-          chip=cap.chip,
-          memory=cap.memory,
-          flops=node_service_pb2.DeviceFlops(fp32=cap.flops.fp32, fp16=cap.flops.fp16, int8=cap.flops.int8),
-        )
-      for node_id, cap in topology.nodes.items()
-    }
-    peer_graph = {node_id: node_service_pb2.Peers(peer_ids=peers) for node_id, peers in topology.peer_graph.items()}
-    if DEBUG >= 5: print(f"CollectTopology {max_depth=} {visited=} {nodes=} {peer_graph=}")
-    return node_service_pb2.Topology(nodes=nodes, peer_graph=peer_graph)
-
-  async def SendResult(self, request, context):
-    request_id = request.request_id
-    result = request.result
-    is_finished = request.is_finished
-    img = request.tensor
-    if DEBUG >= 5: print(f"Received SendResult request: {request_id=} {result=} {is_finished=}")
-    result = list(result)
-    if len(img.tensor_data) > 0:
-      result=np.frombuffer(img.tensor_data, dtype=np.dtype(img.dtype)).reshape(img.shape)
-    self.node.on_token.trigger_all(request_id, result, is_finished)
-    return node_service_pb2.Empty()
-
-  async def SendOpaqueStatus(self, request, context):
-    request_id = request.request_id
-    status = request.status
-    if DEBUG >= 8: print(f"Received SendOpaqueStatus request: {request_id=} {status=}")
-    self.node.on_opaque_status.trigger_all(request_id, status)
-    return node_service_pb2.Empty()
-
-  async def HealthCheck(self, request, context):
-    return node_service_pb2.HealthCheckResponse(is_healthy=True)
-
-  def deserialize_inference_state(self,inference_state_proto: node_service_pb2.InferenceState) -> dict:
-    inference_state = {}
-    
-    for k, tensor_data in inference_state_proto.tensor_data.items():
-        np_array = np.frombuffer(tensor_data.tensor_data, dtype=tensor_data.dtype).reshape(tensor_data.shape)
-        inference_state[k] = mx.array(np_array)
-    
-    for k, tensor_list in inference_state_proto.tensor_list_data.items():
-        inference_state[k] = [
-            mx.array(np.frombuffer(tensor.tensor_data, dtype=tensor.dtype).reshape(tensor.shape))
-            for tensor in tensor_list.tensors
-        ]
-    
-    if inference_state_proto.other_data_json:
-        other_data = json.loads(inference_state_proto.other_data_json)
-        inference_state.update(other_data)
-    
-    return inference_state
diff --git a/build/lib/exo/networking/grpc/node_service_pb2.py b/build/lib/exo/networking/grpc/node_service_pb2.py
deleted file mode 100644
index deb9074e..00000000
--- a/build/lib/exo/networking/grpc/node_service_pb2.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by the protocol buffer compiler.  DO NOT EDIT!
-# source: node_service.proto
-# Protobuf Python Version: 5.26.1
-"""Generated protocol buffer code."""
-from google.protobuf import descriptor as _descriptor
-from google.protobuf import descriptor_pool as _descriptor_pool
-from google.protobuf import symbol_database as _symbol_database
-from google.protobuf.internal import builder as _builder
-# @@protoc_insertion_point(imports)
-
-_sym_db = _symbol_database.Default()
-
-
-
-
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12node_service.proto\x12\x0cnode_service\"S\n\x05Shard\x12\x10\n\x08model_id\x18\x01 \x01(\t\x12\x13\n\x0bstart_layer\x18\x02 \x01(\x05\x12\x11\n\tend_layer\x18\x03 \x01(\x05\x12\x10\n\x08n_layers\x18\x04 \x01(\x05\"\xbb\x01\n\rPromptRequest\x12\"\n\x05shard\x18\x01 \x01(\x0b\x32\x13.node_service.Shard\x12\x0e\n\x06prompt\x18\x02 \x01(\t\x12\x17\n\nrequest_id\x18\x03 \x01(\tH\x00\x88\x01\x01\x12:\n\x0finference_state\x18\x04 \x01(\x0b\x32\x1c.node_service.InferenceStateH\x01\x88\x01\x01\x42\r\n\x0b_request_idB\x12\n\x10_inference_state\"\xd1\x01\n\rTensorRequest\x12\"\n\x05shard\x18\x01 \x01(\x0b\x32\x13.node_service.Shard\x12$\n\x06tensor\x18\x02 \x01(\x0b\x32\x14.node_service.Tensor\x12\x17\n\nrequest_id\x18\x03 \x01(\tH\x00\x88\x01\x01\x12:\n\x0finference_state\x18\x04 \x01(\x0b\x32\x1c.node_service.InferenceStateH\x01\x88\x01\x01\x42\r\n\x0b_request_idB\x12\n\x10_inference_state\"/\n\x19GetInferenceResultRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\"\\\n\x0fInferenceResult\x12)\n\x06tensor\x18\x01 \x01(\x0b\x32\x14.node_service.TensorH\x00\x88\x01\x01\x12\x13\n\x0bis_finished\x18\x02 \x01(\x08\x42\t\n\x07_tensor\";\n\x06Tensor\x12\x13\n\x0btensor_data\x18\x01 \x01(\x0c\x12\r\n\x05shape\x18\x02 \x03(\x05\x12\r\n\x05\x64type\x18\x03 \x01(\t\"3\n\nTensorList\x12%\n\x07tensors\x18\x01 \x03(\x0b\x32\x14.node_service.Tensor\"\xd2\x02\n\x0eInferenceState\x12\x41\n\x0btensor_data\x18\x01 \x03(\x0b\x32,.node_service.InferenceState.TensorDataEntry\x12J\n\x10tensor_list_data\x18\x02 \x03(\x0b\x32\x30.node_service.InferenceState.TensorListDataEntry\x12\x17\n\x0fother_data_json\x18\x03 \x01(\t\x1aG\n\x0fTensorDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.node_service.Tensor:\x02\x38\x01\x1aO\n\x13TensorListDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\'\n\x05value\x18\x02 \x01(\x0b\x32\x18.node_service.TensorList:\x02\x38\x01\"<\n\x16\x43ollectTopologyRequest\x12\x0f\n\x07visited\x18\x01 \x03(\t\x12\x11\n\tmax_depth\x18\x02 \x01(\x05\"\x8e\x02\n\x08Topology\x12\x30\n\x05nodes\x18\x01 \x03(\x0b\x32!.node_service.Topology.NodesEntry\x12\x39\n\npeer_graph\x18\x02 \x03(\x0b\x32%.node_service.Topology.PeerGraphEntry\x1aN\n\nNodesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12/\n\x05value\x18\x02 \x01(\x0b\x32 .node_service.DeviceCapabilities:\x02\x38\x01\x1a\x45\n\x0ePeerGraphEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\"\n\x05value\x18\x02 \x01(\x0b\x32\x13.node_service.Peers:\x02\x38\x01\"\x19\n\x05Peers\x12\x10\n\x08peer_ids\x18\x01 \x03(\t\"7\n\x0b\x44\x65viceFlops\x12\x0c\n\x04\x66p32\x18\x01 \x01(\x02\x12\x0c\n\x04\x66p16\x18\x02 \x01(\x02\x12\x0c\n\x04int8\x18\x03 \x01(\x02\"k\n\x12\x44\x65viceCapabilities\x12\r\n\x05model\x18\x01 \x01(\t\x12\x0c\n\x04\x63hip\x18\x02 \x01(\t\x12\x0e\n\x06memory\x18\x03 \x01(\x05\x12(\n\x05\x66lops\x18\x04 \x01(\x0b\x32\x19.node_service.DeviceFlops\"\x82\x01\n\x11SendResultRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0e\n\x06result\x18\x02 \x03(\x05\x12)\n\x06tensor\x18\x03 \x01(\x0b\x32\x14.node_service.TensorH\x00\x88\x01\x01\x12\x13\n\x0bis_finished\x18\x04 \x01(\x08\x42\t\n\x07_tensor\"=\n\x17SendOpaqueStatusRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\"\x14\n\x12HealthCheckRequest\")\n\x13HealthCheckResponse\x12\x12\n\nis_healthy\x18\x01 \x01(\x08\"\x07\n\x05\x45mpty2\xb4\x04\n\x0bNodeService\x12\x41\n\nSendPrompt\x12\x1b.node_service.PromptRequest\x1a\x14.node_service.Tensor\"\x00\x12\x41\n\nSendTensor\x12\x1b.node_service.TensorRequest\x1a\x14.node_service.Tensor\"\x00\x12^\n\x12GetInferenceResult\x12\'.node_service.GetInferenceResultRequest\x1a\x1d.node_service.InferenceResult\"\x00\x12Q\n\x0f\x43ollectTopology\x12$.node_service.CollectTopologyRequest\x1a\x16.node_service.Topology\"\x00\x12\x44\n\nSendResult\x12\x1f.node_service.SendResultRequest\x1a\x13.node_service.Empty\"\x00\x12P\n\x10SendOpaqueStatus\x12%.node_service.SendOpaqueStatusRequest\x1a\x13.node_service.Empty\"\x00\x12T\n\x0bHealthCheck\x12 .node_service.HealthCheckRequest\x1a!.node_service.HealthCheckResponse\"\x00\x62\x06proto3')
-
-_globals = globals()
-_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
-_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'node_service_pb2', _globals)
-if not _descriptor._USE_C_DESCRIPTORS:
-  DESCRIPTOR._loaded_options = None
-  _globals['_INFERENCESTATE_TENSORDATAENTRY']._loaded_options = None
-  _globals['_INFERENCESTATE_TENSORDATAENTRY']._serialized_options = b'8\001'
-  _globals['_INFERENCESTATE_TENSORLISTDATAENTRY']._loaded_options = None
-  _globals['_INFERENCESTATE_TENSORLISTDATAENTRY']._serialized_options = b'8\001'
-  _globals['_TOPOLOGY_NODESENTRY']._loaded_options = None
-  _globals['_TOPOLOGY_NODESENTRY']._serialized_options = b'8\001'
-  _globals['_TOPOLOGY_PEERGRAPHENTRY']._loaded_options = None
-  _globals['_TOPOLOGY_PEERGRAPHENTRY']._serialized_options = b'8\001'
-  _globals['_SHARD']._serialized_start=36
-  _globals['_SHARD']._serialized_end=119
-  _globals['_PROMPTREQUEST']._serialized_start=122
-  _globals['_PROMPTREQUEST']._serialized_end=309
-  _globals['_TENSORREQUEST']._serialized_start=312
-  _globals['_TENSORREQUEST']._serialized_end=521
-  _globals['_GETINFERENCERESULTREQUEST']._serialized_start=523
-  _globals['_GETINFERENCERESULTREQUEST']._serialized_end=570
-  _globals['_INFERENCERESULT']._serialized_start=572
-  _globals['_INFERENCERESULT']._serialized_end=664
-  _globals['_TENSOR']._serialized_start=666
-  _globals['_TENSOR']._serialized_end=725
-  _globals['_TENSORLIST']._serialized_start=727
-  _globals['_TENSORLIST']._serialized_end=778
-  _globals['_INFERENCESTATE']._serialized_start=781
-  _globals['_INFERENCESTATE']._serialized_end=1119
-  _globals['_INFERENCESTATE_TENSORDATAENTRY']._serialized_start=967
-  _globals['_INFERENCESTATE_TENSORDATAENTRY']._serialized_end=1038
-  _globals['_INFERENCESTATE_TENSORLISTDATAENTRY']._serialized_start=1040
-  _globals['_INFERENCESTATE_TENSORLISTDATAENTRY']._serialized_end=1119
-  _globals['_COLLECTTOPOLOGYREQUEST']._serialized_start=1121
-  _globals['_COLLECTTOPOLOGYREQUEST']._serialized_end=1181
-  _globals['_TOPOLOGY']._serialized_start=1184
-  _globals['_TOPOLOGY']._serialized_end=1454
-  _globals['_TOPOLOGY_NODESENTRY']._serialized_start=1305
-  _globals['_TOPOLOGY_NODESENTRY']._serialized_end=1383
-  _globals['_TOPOLOGY_PEERGRAPHENTRY']._serialized_start=1385
-  _globals['_TOPOLOGY_PEERGRAPHENTRY']._serialized_end=1454
-  _globals['_PEERS']._serialized_start=1456
-  _globals['_PEERS']._serialized_end=1481
-  _globals['_DEVICEFLOPS']._serialized_start=1483
-  _globals['_DEVICEFLOPS']._serialized_end=1538
-  _globals['_DEVICECAPABILITIES']._serialized_start=1540
-  _globals['_DEVICECAPABILITIES']._serialized_end=1647
-  _globals['_SENDRESULTREQUEST']._serialized_start=1650
-  _globals['_SENDRESULTREQUEST']._serialized_end=1780
-  _globals['_SENDOPAQUESTATUSREQUEST']._serialized_start=1782
-  _globals['_SENDOPAQUESTATUSREQUEST']._serialized_end=1843
-  _globals['_HEALTHCHECKREQUEST']._serialized_start=1845
-  _globals['_HEALTHCHECKREQUEST']._serialized_end=1865
-  _globals['_HEALTHCHECKRESPONSE']._serialized_start=1867
-  _globals['_HEALTHCHECKRESPONSE']._serialized_end=1908
-  _globals['_EMPTY']._serialized_start=1910
-  _globals['_EMPTY']._serialized_end=1917
-  _globals['_NODESERVICE']._serialized_start=1920
-  _globals['_NODESERVICE']._serialized_end=2484
-# @@protoc_insertion_point(module_scope)
diff --git a/build/lib/exo/networking/grpc/node_service_pb2_grpc.py b/build/lib/exo/networking/grpc/node_service_pb2_grpc.py
deleted file mode 100644
index ee6ba63b..00000000
--- a/build/lib/exo/networking/grpc/node_service_pb2_grpc.py
+++ /dev/null
@@ -1,360 +0,0 @@
-# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
-"""Client and server classes corresponding to protobuf-defined services."""
-import grpc
-import warnings
-
-from exo.networking.grpc import node_service_pb2 as node__service__pb2
-
-GRPC_GENERATED_VERSION = '1.64.1'
-GRPC_VERSION = grpc.__version__
-EXPECTED_ERROR_RELEASE = '1.65.0'
-SCHEDULED_RELEASE_DATE = 'June 25, 2024'
-_version_not_supported = False
-
-try:
-    from grpc._utilities import first_version_is_lower
-    _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
-except ImportError:
-    _version_not_supported = True
-
-if _version_not_supported:
-    warnings.warn(
-        f'The grpc package installed is at version {GRPC_VERSION},'
-        + f' but the generated code in node_service_pb2_grpc.py depends on'
-        + f' grpcio>={GRPC_GENERATED_VERSION}.'
-        + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
-        + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
-        + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},'
-        + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.',
-        RuntimeWarning
-    )
-
-
-class NodeServiceStub(object):
-    """Missing associated documentation comment in .proto file."""
-
-    def __init__(self, channel):
-        """Constructor.
-
-        Args:
-            channel: A grpc.Channel.
-        """
-        self.SendPrompt = channel.unary_unary(
-                '/node_service.NodeService/SendPrompt',
-                request_serializer=node__service__pb2.PromptRequest.SerializeToString,
-                response_deserializer=node__service__pb2.Tensor.FromString,
-                _registered_method=True)
-        self.SendTensor = channel.unary_unary(
-                '/node_service.NodeService/SendTensor',
-                request_serializer=node__service__pb2.TensorRequest.SerializeToString,
-                response_deserializer=node__service__pb2.Tensor.FromString,
-                _registered_method=True)
-        self.GetInferenceResult = channel.unary_unary(
-                '/node_service.NodeService/GetInferenceResult',
-                request_serializer=node__service__pb2.GetInferenceResultRequest.SerializeToString,
-                response_deserializer=node__service__pb2.InferenceResult.FromString,
-                _registered_method=True)
-        self.CollectTopology = channel.unary_unary(
-                '/node_service.NodeService/CollectTopology',
-                request_serializer=node__service__pb2.CollectTopologyRequest.SerializeToString,
-                response_deserializer=node__service__pb2.Topology.FromString,
-                _registered_method=True)
-        self.SendResult = channel.unary_unary(
-                '/node_service.NodeService/SendResult',
-                request_serializer=node__service__pb2.SendResultRequest.SerializeToString,
-                response_deserializer=node__service__pb2.Empty.FromString,
-                _registered_method=True)
-        self.SendOpaqueStatus = channel.unary_unary(
-                '/node_service.NodeService/SendOpaqueStatus',
-                request_serializer=node__service__pb2.SendOpaqueStatusRequest.SerializeToString,
-                response_deserializer=node__service__pb2.Empty.FromString,
-                _registered_method=True)
-        self.HealthCheck = channel.unary_unary(
-                '/node_service.NodeService/HealthCheck',
-                request_serializer=node__service__pb2.HealthCheckRequest.SerializeToString,
-                response_deserializer=node__service__pb2.HealthCheckResponse.FromString,
-                _registered_method=True)
-
-
-class NodeServiceServicer(object):
-    """Missing associated documentation comment in .proto file."""
-
-    def SendPrompt(self, request, context):
-        """Missing associated documentation comment in .proto file."""
-        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-        context.set_details('Method not implemented!')
-        raise NotImplementedError('Method not implemented!')
-
-    def SendTensor(self, request, context):
-        """Missing associated documentation comment in .proto file."""
-        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-        context.set_details('Method not implemented!')
-        raise NotImplementedError('Method not implemented!')
-
-    def GetInferenceResult(self, request, context):
-        """Missing associated documentation comment in .proto file."""
-        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-        context.set_details('Method not implemented!')
-        raise NotImplementedError('Method not implemented!')
-
-    def CollectTopology(self, request, context):
-        """Missing associated documentation comment in .proto file."""
-        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-        context.set_details('Method not implemented!')
-        raise NotImplementedError('Method not implemented!')
-
-    def SendResult(self, request, context):
-        """Missing associated documentation comment in .proto file."""
-        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-        context.set_details('Method not implemented!')
-        raise NotImplementedError('Method not implemented!')
-
-    def SendOpaqueStatus(self, request, context):
-        """Missing associated documentation comment in .proto file."""
-        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-        context.set_details('Method not implemented!')
-        raise NotImplementedError('Method not implemented!')
-
-    def HealthCheck(self, request, context):
-        """Missing associated documentation comment in .proto file."""
-        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
-        context.set_details('Method not implemented!')
-        raise NotImplementedError('Method not implemented!')
-
-
-def add_NodeServiceServicer_to_server(servicer, server):
-    rpc_method_handlers = {
-            'SendPrompt': grpc.unary_unary_rpc_method_handler(
-                    servicer.SendPrompt,
-                    request_deserializer=node__service__pb2.PromptRequest.FromString,
-                    response_serializer=node__service__pb2.Tensor.SerializeToString,
-            ),
-            'SendTensor': grpc.unary_unary_rpc_method_handler(
-                    servicer.SendTensor,
-                    request_deserializer=node__service__pb2.TensorRequest.FromString,
-                    response_serializer=node__service__pb2.Tensor.SerializeToString,
-            ),
-            'GetInferenceResult': grpc.unary_unary_rpc_method_handler(
-                    servicer.GetInferenceResult,
-                    request_deserializer=node__service__pb2.GetInferenceResultRequest.FromString,
-                    response_serializer=node__service__pb2.InferenceResult.SerializeToString,
-            ),
-            'CollectTopology': grpc.unary_unary_rpc_method_handler(
-                    servicer.CollectTopology,
-                    request_deserializer=node__service__pb2.CollectTopologyRequest.FromString,
-                    response_serializer=node__service__pb2.Topology.SerializeToString,
-            ),
-            'SendResult': grpc.unary_unary_rpc_method_handler(
-                    servicer.SendResult,
-                    request_deserializer=node__service__pb2.SendResultRequest.FromString,
-                    response_serializer=node__service__pb2.Empty.SerializeToString,
-            ),
-            'SendOpaqueStatus': grpc.unary_unary_rpc_method_handler(
-                    servicer.SendOpaqueStatus,
-                    request_deserializer=node__service__pb2.SendOpaqueStatusRequest.FromString,
-                    response_serializer=node__service__pb2.Empty.SerializeToString,
-            ),
-            'HealthCheck': grpc.unary_unary_rpc_method_handler(
-                    servicer.HealthCheck,
-                    request_deserializer=node__service__pb2.HealthCheckRequest.FromString,
-                    response_serializer=node__service__pb2.HealthCheckResponse.SerializeToString,
-            ),
-    }
-    generic_handler = grpc.method_handlers_generic_handler(
-            'node_service.NodeService', rpc_method_handlers)
-    server.add_generic_rpc_handlers((generic_handler,))
-    server.add_registered_method_handlers('node_service.NodeService', rpc_method_handlers)
-
-
- # This class is part of an EXPERIMENTAL API.
-class NodeService(object):
-    """Missing associated documentation comment in .proto file."""
-
-    @staticmethod
-    def SendPrompt(request,
-            target,
-            options=(),
-            channel_credentials=None,
-            call_credentials=None,
-            insecure=False,
-            compression=None,
-            wait_for_ready=None,
-            timeout=None,
-            metadata=None):
-        return grpc.experimental.unary_unary(
-            request,
-            target,
-            '/node_service.NodeService/SendPrompt',
-            node__service__pb2.PromptRequest.SerializeToString,
-            node__service__pb2.Tensor.FromString,
-            options,
-            channel_credentials,
-            insecure,
-            call_credentials,
-            compression,
-            wait_for_ready,
-            timeout,
-            metadata,
-            _registered_method=True)
-
-    @staticmethod
-    def SendTensor(request,
-            target,
-            options=(),
-            channel_credentials=None,
-            call_credentials=None,
-            insecure=False,
-            compression=None,
-            wait_for_ready=None,
-            timeout=None,
-            metadata=None):
-        return grpc.experimental.unary_unary(
-            request,
-            target,
-            '/node_service.NodeService/SendTensor',
-            node__service__pb2.TensorRequest.SerializeToString,
-            node__service__pb2.Tensor.FromString,
-            options,
-            channel_credentials,
-            insecure,
-            call_credentials,
-            compression,
-            wait_for_ready,
-            timeout,
-            metadata,
-            _registered_method=True)
-
-    @staticmethod
-    def GetInferenceResult(request,
-            target,
-            options=(),
-            channel_credentials=None,
-            call_credentials=None,
-            insecure=False,
-            compression=None,
-            wait_for_ready=None,
-            timeout=None,
-            metadata=None):
-        return grpc.experimental.unary_unary(
-            request,
-            target,
-            '/node_service.NodeService/GetInferenceResult',
-            node__service__pb2.GetInferenceResultRequest.SerializeToString,
-            node__service__pb2.InferenceResult.FromString,
-            options,
-            channel_credentials,
-            insecure,
-            call_credentials,
-            compression,
-            wait_for_ready,
-            timeout,
-            metadata,
-            _registered_method=True)
-
-    @staticmethod
-    def CollectTopology(request,
-            target,
-            options=(),
-            channel_credentials=None,
-            call_credentials=None,
-            insecure=False,
-            compression=None,
-            wait_for_ready=None,
-            timeout=None,
-            metadata=None):
-        return grpc.experimental.unary_unary(
-            request,
-            target,
-            '/node_service.NodeService/CollectTopology',
-            node__service__pb2.CollectTopologyRequest.SerializeToString,
-            node__service__pb2.Topology.FromString,
-            options,
-            channel_credentials,
-            insecure,
-            call_credentials,
-            compression,
-            wait_for_ready,
-            timeout,
-            metadata,
-            _registered_method=True)
-
-    @staticmethod
-    def SendResult(request,
-            target,
-            options=(),
-            channel_credentials=None,
-            call_credentials=None,
-            insecure=False,
-            compression=None,
-            wait_for_ready=None,
-            timeout=None,
-            metadata=None):
-        return grpc.experimental.unary_unary(
-            request,
-            target,
-            '/node_service.NodeService/SendResult',
-            node__service__pb2.SendResultRequest.SerializeToString,
-            node__service__pb2.Empty.FromString,
-            options,
-            channel_credentials,
-            insecure,
-            call_credentials,
-            compression,
-            wait_for_ready,
-            timeout,
-            metadata,
-            _registered_method=True)
-
-    @staticmethod
-    def SendOpaqueStatus(request,
-            target,
-            options=(),
-            channel_credentials=None,
-            call_credentials=None,
-            insecure=False,
-            compression=None,
-            wait_for_ready=None,
-            timeout=None,
-            metadata=None):
-        return grpc.experimental.unary_unary(
-            request,
-            target,
-            '/node_service.NodeService/SendOpaqueStatus',
-            node__service__pb2.SendOpaqueStatusRequest.SerializeToString,
-            node__service__pb2.Empty.FromString,
-            options,
-            channel_credentials,
-            insecure,
-            call_credentials,
-            compression,
-            wait_for_ready,
-            timeout,
-            metadata,
-            _registered_method=True)
-
-    @staticmethod
-    def HealthCheck(request,
-            target,
-            options=(),
-            channel_credentials=None,
-            call_credentials=None,
-            insecure=False,
-            compression=None,
-            wait_for_ready=None,
-            timeout=None,
-            metadata=None):
-        return grpc.experimental.unary_unary(
-            request,
-            target,
-            '/node_service.NodeService/HealthCheck',
-            node__service__pb2.HealthCheckRequest.SerializeToString,
-            node__service__pb2.HealthCheckResponse.FromString,
-            options,
-            channel_credentials,
-            insecure,
-            call_credentials,
-            compression,
-            wait_for_ready,
-            timeout,
-            metadata,
-            _registered_method=True)
diff --git a/build/lib/exo/networking/manual/__init__.py b/build/lib/exo/networking/manual/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/networking/manual/manual_discovery.py b/build/lib/exo/networking/manual/manual_discovery.py
deleted file mode 100644
index f8aba9ed..00000000
--- a/build/lib/exo/networking/manual/manual_discovery.py
+++ /dev/null
@@ -1,71 +0,0 @@
-import asyncio
-from exo.networking.discovery import Discovery
-from typing import Dict, List, Callable
-
-from exo.topology.device_capabilities import DeviceCapabilities
-from exo.networking.manual.network_topology_config import NetworkTopology, PeerConfig
-from exo.helpers import DEBUG_DISCOVERY
-from exo.networking.peer_handle import PeerHandle
-
-
-class ManualDiscovery(Discovery):
-  def __init__(
-    self,
-    network_config_path: str,
-    node_id: str,
-    create_peer_handle: Callable[[str, str, DeviceCapabilities], PeerHandle],
-  ):
-    self.topology = NetworkTopology.from_path(network_config_path)
-    self.create_peer_handle = create_peer_handle
-
-    if node_id not in self.topology.peers:
-      raise ValueError(
-        f"Node ID {node_id} not found in network config file {network_config_path}. Please run with `node_id` set to one of the keys in the config file: {[k for k, _ in self.topology.peers]}"
-      )
-
-    self.listen_task = None
-
-    self.known_peers: Dict[str, PeerHandle] = {}
-    self.peers_in_network: Dict[str, PeerConfig] = self.topology.peers
-    self.peers_in_network.pop(node_id)
-
-  async def start(self) -> None:
-    self.listen_task = asyncio.create_task(self.task_find_peers_from_config())
-
-  async def stop(self) -> None:
-    if self.listen_task:
-      self.listen_task.cancel()
-
-  async def discover_peers(self, wait_for_peers: int = 0) -> List[PeerHandle]:
-    if wait_for_peers > 0:
-      while len(self.known_peers) < wait_for_peers:
-        if DEBUG_DISCOVERY >= 2: print(f"Current peers: {len(self.known_peers)}/{wait_for_peers}. Waiting for more peers...")
-        await asyncio.sleep(0.1)
-    if DEBUG_DISCOVERY >= 2: print(f"Discovered peers: {[peer.id() for peer in self.known_peers.values()]}")
-    return list(self.known_peers.values())
-
-  async def task_find_peers_from_config(self):
-    if DEBUG_DISCOVERY >= 2: print("Starting task to find peers from config...")
-    while True:
-      for peer_id, peer_config in self.peers_in_network.items():
-        try:
-          if DEBUG_DISCOVERY >= 2: print(f"Checking peer {peer_id=} at {peer_config.address}:{peer_config.port}")
-          peer = self.known_peers.get(peer_id)
-          if not peer:
-            if DEBUG_DISCOVERY >= 2: print(f"{peer_id=} not found in known peers. Adding.")
-            peer = self.create_peer_handle(peer_id, f"{peer_config.address}:{peer_config.port}", peer_config.device_capabilities)
-          is_healthy = await peer.health_check()
-          if is_healthy:
-            if DEBUG_DISCOVERY >= 2: print(f"{peer_id=} at {peer_config.address}:{peer_config.port} is healthy.")
-            self.known_peers[peer_id] = peer
-          else:
-            if DEBUG_DISCOVERY >= 2: print(f"{peer_id=} at {peer_config.address}:{peer_config.port} is not healthy.")
-            try:
-              del self.known_peers[peer_id]
-            except KeyError:
-              pass
-        except Exception as e:
-          if DEBUG_DISCOVERY >= 2: print(f"Exception occured when attempting to add {peer_id=}: {e}")
-      await asyncio.sleep(1.0)
-
-      if DEBUG_DISCOVERY >= 2: print(f"Current known peers: {[peer.id() for peer in self.known_peers.values()]}")
diff --git a/build/lib/exo/networking/manual/network_topology_config.py b/build/lib/exo/networking/manual/network_topology_config.py
deleted file mode 100644
index c0246ec3..00000000
--- a/build/lib/exo/networking/manual/network_topology_config.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from typing import Dict
-from pydantic import BaseModel, ValidationError
-
-from exo.topology.device_capabilities import DeviceCapabilities
-
-
-class PeerConfig(BaseModel):
-  address: str
-  port: int
-  device_capabilities: DeviceCapabilities
-
-
-class NetworkTopology(BaseModel):
-  """Configuration of the network. A collection outlining all nodes in the network, including the node this is running from."""
-
-  peers: Dict[str, PeerConfig]
-  """
-  node_id to PeerConfig. The node_id is used to identify the peer in the discovery process. The node that this is running from should be included in this dict.
-  """
-  @classmethod
-  def from_path(cls, path: str) -> "NetworkTopology":
-    try:
-      with open(path, "r") as f:
-        config_data = f.read()
-    except FileNotFoundError as e:
-      raise FileNotFoundError(f"Config file not found at {path}") from e
-
-    try:
-      return cls.model_validate_json(config_data)
-    except ValidationError as e:
-      raise ValueError(f"Error validating network topology config from {path}: {e}") from e
diff --git a/build/lib/exo/networking/manual/test_manual_discovery.py b/build/lib/exo/networking/manual/test_manual_discovery.py
deleted file mode 100644
index 902e20bf..00000000
--- a/build/lib/exo/networking/manual/test_manual_discovery.py
+++ /dev/null
@@ -1,103 +0,0 @@
-import asyncio
-import unittest
-from unittest import mock
-from exo.networking.manual.manual_discovery import ManualDiscovery
-from exo.networking.manual.network_topology_config import NetworkTopology
-from exo.networking.grpc.grpc_peer_handle import GRPCPeerHandle
-from exo.networking.grpc.grpc_server import GRPCServer
-from exo.orchestration.node import Node
-
-root_path = "./exo/networking/manual/test_data/test_config.json"
-
-
-class TestSingleNodeManualDiscovery(unittest.IsolatedAsyncioTestCase):
-  async def asyncSetUp(self):
-    self.peer1 = mock.AsyncMock()
-    self.peer1.connect = mock.AsyncMock()
-    self.discovery1 = ManualDiscovery(root_path, "node1", create_peer_handle=lambda peer_id, address, device_capabilities: self.peer1)
-    _ = self.discovery1.start()
-
-  async def asyncTearDown(self):
-    await self.discovery1.stop()
-
-  async def test_discovery(self):
-    peers1 = await self.discovery1.discover_peers(wait_for_peers=0)
-    assert len(peers1) == 0
-
-    self.peer1.connect.assert_not_called()
-
-
-class TestManualDiscovery(unittest.IsolatedAsyncioTestCase):
-  async def asyncSetUp(self):
-    self.peer1 = mock.AsyncMock()
-    self.peer2 = mock.AsyncMock()
-    self.peer1.connect = mock.AsyncMock()
-    self.peer2.connect = mock.AsyncMock()
-    self.discovery1 = ManualDiscovery(root_path, "node1", create_peer_handle=lambda peer_id, address, device_capabilities: self.peer1)
-    self.discovery2 = ManualDiscovery(root_path, "node2", create_peer_handle=lambda peer_id, address, device_capabilities: self.peer2)
-    await self.discovery1.start()
-    await self.discovery2.start()
-
-  async def asyncTearDown(self):
-    await self.discovery1.stop()
-    await self.discovery2.stop()
-
-  async def test_discovery(self):
-    peers1 = await self.discovery1.discover_peers(wait_for_peers=1)
-    assert len(peers1) == 1
-    peers2 = await self.discovery2.discover_peers(wait_for_peers=1)
-    assert len(peers2) == 1
-
-    # connect has to be explicitly called after discovery
-    self.peer1.connect.assert_not_called()
-    self.peer2.connect.assert_not_called()
-
-
-class TestManualDiscoveryWithGRPCPeerHandle(unittest.IsolatedAsyncioTestCase):
-  async def asyncSetUp(self):
-    config = NetworkTopology.from_path(root_path)
-
-    self.node1 = mock.AsyncMock(spec=Node)
-    self.node2 = mock.AsyncMock(spec=Node)
-    self.server1 = GRPCServer(self.node1, config.peers["node1"].address, config.peers["node1"].port)
-    self.server2 = GRPCServer(self.node2, config.peers["node2"].address, config.peers["node2"].port)
-    await self.server1.start()
-    await self.server2.start()
-    self.discovery1 = ManualDiscovery(root_path, "node1", create_peer_handle=lambda peer_id, address, device_capabilities: GRPCPeerHandle(peer_id, address, device_capabilities))
-    self.discovery2 = ManualDiscovery(root_path, "node2", create_peer_handle=lambda peer_id, address, device_capabilities: GRPCPeerHandle(peer_id, address, device_capabilities))
-    await self.discovery1.start()
-    await self.discovery2.start()
-
-  async def asyncTearDown(self):
-    await self.discovery1.stop()
-    await self.discovery2.stop()
-    await self.server1.stop()
-    await self.server2.stop()
-
-  async def test_grpc_discovery(self):
-    peers1 = await self.discovery1.discover_peers(wait_for_peers=1)
-    assert len(peers1) == 1
-    peers2 = await self.discovery2.discover_peers(wait_for_peers=1)
-    assert len(peers2) == 1
-
-    # Connect
-    await peers1[0].connect()
-    await peers2[0].connect()
-    self.assertTrue(await peers1[0].is_connected())
-    self.assertTrue(await peers2[0].is_connected())
-
-    # Kill server1
-    await self.server1.stop()
-
-    self.assertTrue(await peers1[0].is_connected())
-    self.assertFalse(await peers2[0].is_connected())
-
-    # Kill server2
-    await self.server2.stop()
-
-    self.assertFalse(await peers1[0].is_connected())
-    self.assertFalse(await peers2[0].is_connected())
-
-
-if __name__ == "__main__":
-  asyncio.run(unittest.main())
diff --git a/build/lib/exo/networking/manual/test_network_topology_config.py b/build/lib/exo/networking/manual/test_network_topology_config.py
deleted file mode 100644
index b5b2fbd5..00000000
--- a/build/lib/exo/networking/manual/test_network_topology_config.py
+++ /dev/null
@@ -1,49 +0,0 @@
-import unittest
-
-from exo.networking.manual.network_topology_config import NetworkTopology
-
-root_path = "./exo/networking/manual/test_data/"
-
-
-class TestNetworkTopologyConfig(unittest.TestCase):
-  def test_from_path_invalid_path(self):
-    with self.assertRaises(FileNotFoundError) as e:
-      NetworkTopology.from_path("invalid_path")
-    self.assertEqual(str(e.exception), "Config file not found at invalid_path")
-
-  def test_from_path_invalid_json(self):
-    with self.assertRaises(ValueError) as e:
-      NetworkTopology.from_path(root_path + "invalid_json.json")
-    self.assertIn("Error validating network topology config from", str(e.exception))
-    self.assertIn("1 validation error for NetworkTopology\n  Invalid JSON: EOF while parsing a value at line 1 column 0", str(e.exception))
-
-  def test_from_path_invalid_config(self):
-    with self.assertRaises(ValueError) as e:
-      NetworkTopology.from_path(root_path + "invalid_config.json")
-    self.assertIn("Error validating network topology config from", str(e.exception))
-    self.assertIn("port\n  Field required", str(e.exception))
-
-  def test_from_path_valid(self):
-    config = NetworkTopology.from_path(root_path + "test_config.json")
-
-    self.assertEqual(config.peers["node1"].port, 50051)
-    self.assertEqual(config.peers["node1"].device_capabilities.model, "Unknown Model")
-    self.assertEqual(config.peers["node1"].address, "localhost")
-    self.assertEqual(config.peers["node1"].device_capabilities.chip, "Unknown Chip")
-    self.assertEqual(config.peers["node1"].device_capabilities.memory, 0)
-    self.assertEqual(config.peers["node1"].device_capabilities.flops.fp32, 0)
-    self.assertEqual(config.peers["node1"].device_capabilities.flops.fp16, 0)
-    self.assertEqual(config.peers["node1"].device_capabilities.flops.int8, 0)
-
-    self.assertEqual(config.peers["node2"].port, 50052)
-    self.assertEqual(config.peers["node2"].device_capabilities.model, "Unknown Model")
-    self.assertEqual(config.peers["node2"].address, "localhost")
-    self.assertEqual(config.peers["node2"].device_capabilities.chip, "Unknown Chip")
-    self.assertEqual(config.peers["node2"].device_capabilities.memory, 0)
-    self.assertEqual(config.peers["node2"].device_capabilities.flops.fp32, 0)
-    self.assertEqual(config.peers["node2"].device_capabilities.flops.fp16, 0)
-    self.assertEqual(config.peers["node2"].device_capabilities.flops.int8, 0)
-
-
-if __name__ == "__main__":
-  unittest.main()
diff --git a/build/lib/exo/networking/peer_handle.py b/build/lib/exo/networking/peer_handle.py
deleted file mode 100644
index e5085f0b..00000000
--- a/build/lib/exo/networking/peer_handle.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from abc import ABC, abstractmethod
-from typing import Optional, Tuple, List
-import numpy as np
-from exo.inference.shard import Shard
-from exo.topology.device_capabilities import DeviceCapabilities
-from exo.topology.topology import Topology
-
-
-class PeerHandle(ABC):
-  @abstractmethod
-  def id(self) -> str:
-    pass
-
-  @abstractmethod
-  def addr(self) -> str:
-    pass
-
-  @abstractmethod
-  def device_capabilities(self) -> DeviceCapabilities:
-    pass
-
-  @abstractmethod
-  async def connect(self) -> None:
-    pass
-
-  @abstractmethod
-  async def is_connected(self) -> bool:
-    pass
-
-  @abstractmethod
-  async def disconnect(self) -> None:
-    pass
-
-  @abstractmethod
-  async def health_check(self) -> bool:
-    pass
-
-  @abstractmethod
-  async def send_prompt(self, shard: Shard, prompt: str, request_id: Optional[str] = None) -> Optional[np.array]:
-    pass
-
-  @abstractmethod
-  async def send_tensor(self, shard: Shard, tensor: np.array, request_id: Optional[str] = None) -> Optional[np.array]:
-    pass
-
-  @abstractmethod
-  async def send_result(self, request_id: str, result: List[int], is_finished: bool) -> None:
-    pass
-
-  @abstractmethod
-  async def get_inference_result(self, request_id: str) -> Tuple[Optional[np.ndarray], bool]:
-    pass
-
-  @abstractmethod
-  async def collect_topology(self, visited: set[str], max_depth: int) -> Topology:
-    pass
diff --git a/build/lib/exo/networking/server.py b/build/lib/exo/networking/server.py
deleted file mode 100644
index 8e7f9812..00000000
--- a/build/lib/exo/networking/server.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from abc import ABC, abstractmethod
-
-
-class Server(ABC):
-  @abstractmethod
-  async def start(self) -> None:
-    pass
-
-  @abstractmethod
-  async def stop(self) -> None:
-    pass
diff --git a/build/lib/exo/networking/tailscale/__init__.py b/build/lib/exo/networking/tailscale/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/networking/tailscale/tailscale_discovery.py b/build/lib/exo/networking/tailscale/tailscale_discovery.py
deleted file mode 100644
index a5f21d87..00000000
--- a/build/lib/exo/networking/tailscale/tailscale_discovery.py
+++ /dev/null
@@ -1,178 +0,0 @@
-import asyncio
-import time
-import traceback
-from typing import List, Dict, Callable, Tuple
-from exo.networking.discovery import Discovery
-from exo.networking.peer_handle import PeerHandle
-from exo.topology.device_capabilities import DeviceCapabilities, device_capabilities, UNKNOWN_DEVICE_CAPABILITIES
-from exo.helpers import DEBUG, DEBUG_DISCOVERY
-from .tailscale_helpers import get_device_id, update_device_attributes, get_device_attributes, get_tailscale_devices, Device
-
-
-class TailscaleDiscovery(Discovery):
-  def __init__(
-    self,
-    node_id: str,
-    node_port: int,
-    create_peer_handle: Callable[[str, str, DeviceCapabilities], PeerHandle],
-    discovery_interval: int = 5,
-    discovery_timeout: int = 30,
-    update_interval: int = 15,
-    device_capabilities: DeviceCapabilities = UNKNOWN_DEVICE_CAPABILITIES,
-    tailscale_api_key: str = None,
-    tailnet: str = None,
-    allowed_node_ids: List[str] = None,
-  ):
-    self.node_id = node_id
-    self.node_port = node_port
-    self.create_peer_handle = create_peer_handle
-    self.discovery_interval = discovery_interval
-    self.discovery_timeout = discovery_timeout
-    self.update_interval = update_interval
-    self.device_capabilities = device_capabilities
-    self.known_peers: Dict[str, Tuple[PeerHandle, float, float]] = {}
-    self.discovery_task = None
-    self.cleanup_task = None
-    self.tailscale_api_key = tailscale_api_key
-    self.tailnet = tailnet
-    self.allowed_node_ids = allowed_node_ids
-    self._device_id = None
-    self.update_task = None
-
-  async def start(self):
-    self.device_capabilities = device_capabilities()
-    self.discovery_task = asyncio.create_task(self.task_discover_peers())
-    self.cleanup_task = asyncio.create_task(self.task_cleanup_peers())
-    self.update_task = asyncio.create_task(self.task_update_device_posture_attributes())
-
-  async def task_update_device_posture_attributes(self):
-    while True:
-      try:
-        await self.update_device_posture_attributes()
-        if DEBUG_DISCOVERY >= 2:
-          print(f"Updated device posture attributes")
-      except Exception as e:
-        print(f"Error updating device posture attributes: {e}")
-        print(traceback.format_exc())
-      finally:
-        await asyncio.sleep(self.update_interval)
-
-  async def get_device_id(self):
-    if self._device_id:
-      return self._device_id
-    self._device_id = await get_device_id()
-    return self._device_id
-
-  async def update_device_posture_attributes(self):
-    await update_device_attributes(await self.get_device_id(), self.tailscale_api_key, self.node_id, self.node_port, self.device_capabilities)
-
-  async def task_discover_peers(self):
-    while True:
-      try:
-        devices: dict[str, Device] = await get_tailscale_devices(self.tailscale_api_key, self.tailnet)
-        current_time = time.time()
-
-        active_devices = {name: device for name, device in devices.items() if device.last_seen is not None and (current_time - device.last_seen.timestamp()) < 30}
-
-        if DEBUG_DISCOVERY >= 4: print(f"Found tailscale devices: {devices}")
-        if DEBUG_DISCOVERY >= 2: print(f"Active tailscale devices: {len(active_devices)}/{len(devices)}")
-        if DEBUG_DISCOVERY >= 2: print("Time since last seen tailscale devices", [(current_time - device.last_seen.timestamp()) for device in devices.values()])
-
-        for device in active_devices.values():
-          if device.name == self.node_id: continue
-          peer_host = device.addresses[0]
-          peer_id, peer_port, device_capabilities = await get_device_attributes(device.device_id, self.tailscale_api_key)
-          if not peer_id:
-            if DEBUG_DISCOVERY >= 4: print(f"{device.device_id} does not have exo node attributes. skipping.")
-            continue
-
-          if self.allowed_node_ids and peer_id not in self.allowed_node_ids:
-            if DEBUG_DISCOVERY >= 2: print(f"Ignoring peer {peer_id} as it's not in the allowed node IDs list")
-            continue
-
-          if peer_id not in self.known_peers or self.known_peers[peer_id][0].addr() != f"{peer_host}:{peer_port}":
-            new_peer_handle = self.create_peer_handle(peer_id, f"{peer_host}:{peer_port}", device_capabilities)
-            if not await new_peer_handle.health_check():
-              if DEBUG >= 1: print(f"Peer {peer_id} at {peer_host}:{peer_port} is not healthy. Skipping.")
-              continue
-
-            if DEBUG >= 1: print(f"Adding {peer_id=} at {peer_host}:{peer_port}. Replace existing peer_id: {peer_id in self.known_peers}")
-            self.known_peers[peer_id] = (
-              new_peer_handle,
-              current_time,
-              current_time,
-            )
-          else:
-            if not await self.known_peers[peer_id][0].health_check():
-              if DEBUG >= 1: print(f"Peer {peer_id} at {peer_host}:{peer_port} is not healthy. Removing.")
-              if peer_id in self.known_peers: del self.known_peers[peer_id]
-              continue
-            self.known_peers[peer_id] = (self.known_peers[peer_id][0], self.known_peers[peer_id][1], current_time)
-
-      except Exception as e:
-        print(f"Error in discover peers: {e}")
-        print(traceback.format_exc())
-      finally:
-        await asyncio.sleep(self.discovery_interval)
-
-  async def stop(self):
-    if self.discovery_task:
-      self.discovery_task.cancel()
-    if self.cleanup_task:
-      self.cleanup_task.cancel()
-    if self.update_task:
-      self.update_task.cancel()
-    if self.discovery_task or self.cleanup_task or self.update_task:
-      await asyncio.gather(self.discovery_task, self.cleanup_task, self.update_task, return_exceptions=True)
-
-  async def discover_peers(self, wait_for_peers: int = 0) -> List[PeerHandle]:
-    if wait_for_peers > 0:
-      while len(self.known_peers) < wait_for_peers:
-        if DEBUG_DISCOVERY >= 2:
-          print(f"Current peers: {len(self.known_peers)}/{wait_for_peers}. Waiting for more peers...")
-        await asyncio.sleep(0.1)
-    return [peer_handle for peer_handle, _, _ in self.known_peers.values()]
-
-  async def task_cleanup_peers(self):
-    while True:
-      try:
-        current_time = time.time()
-        peers_to_remove = []
-
-        peer_ids = list(self.known_peers.keys())
-        results = await asyncio.gather(*[self.check_peer(peer_id, current_time) for peer_id in peer_ids], return_exceptions=True)
-
-        for peer_id, should_remove in zip(peer_ids, results):
-          if should_remove: peers_to_remove.append(peer_id)
-
-        if DEBUG_DISCOVERY >= 2:
-          print(
-            "Peer statuses:", {
-              peer_handle.id(): f"is_connected={await peer_handle.is_connected()}, health_check={await peer_handle.health_check()}, connected_at={connected_at}, last_seen={last_seen}"
-              for peer_handle, connected_at, last_seen in self.known_peers.values()
-            }
-          )
-
-        for peer_id in peers_to_remove:
-          if peer_id in self.known_peers:
-            del self.known_peers[peer_id]
-            if DEBUG_DISCOVERY >= 2: print(f"Removed peer {peer_id} due to inactivity or failed health check.")
-      except Exception as e:
-        print(f"Error in cleanup peers: {e}")
-        print(traceback.format_exc())
-      finally:
-        await asyncio.sleep(self.discovery_interval)
-
-  async def check_peer(self, peer_id: str, current_time: float) -> bool:
-    peer_handle, connected_at, last_seen = self.known_peers.get(peer_id, (None, None, None))
-    if peer_handle is None: return False
-
-    try:
-      is_connected = await peer_handle.is_connected()
-      health_ok = await peer_handle.health_check()
-    except Exception as e:
-      if DEBUG_DISCOVERY >= 2: print(f"Error checking peer {peer_id}: {e}")
-      return True
-
-    should_remove = ((not is_connected and current_time - connected_at > self.discovery_timeout) or (current_time - last_seen > self.discovery_timeout) or (not health_ok))
-    return should_remove
diff --git a/build/lib/exo/networking/tailscale/tailscale_helpers.py b/build/lib/exo/networking/tailscale/tailscale_helpers.py
deleted file mode 100644
index 969a72da..00000000
--- a/build/lib/exo/networking/tailscale/tailscale_helpers.py
+++ /dev/null
@@ -1,125 +0,0 @@
-import json
-import asyncio
-import aiohttp
-import re
-from typing import Dict, Any, Tuple, List, Optional
-from exo.helpers import DEBUG_DISCOVERY
-from exo.topology.device_capabilities import DeviceCapabilities, DeviceFlops
-from datetime import datetime, timezone
-
-
-class Device:
-  def __init__(self, device_id: str, name: str, addresses: List[str], last_seen: Optional[datetime] = None):
-    self.device_id = device_id
-    self.name = name
-    self.addresses = addresses
-    self.last_seen = last_seen
-
-  @classmethod
-  def from_dict(cls, data: Dict[str, Any]) -> 'Device':
-    return cls(device_id=data.get('id', ''), name=data.get('name', ''), addresses=data.get('addresses', []), last_seen=cls.parse_datetime(data.get('lastSeen')))
-
-  @staticmethod
-  def parse_datetime(date_string: Optional[str]) -> Optional[datetime]:
-    if not date_string:
-      return None
-    return datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
-
-
-async def get_device_id() -> str:
-  try:
-    process = await asyncio.create_subprocess_exec('tailscale', 'status', '--json', stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
-    stdout, stderr = await process.communicate()
-    if process.returncode != 0:
-      raise Exception(f"Command failed with exit code {process.returncode}: {stderr.decode().strip()}.")
-    if DEBUG_DISCOVERY >= 4: print(f"tailscale status: {stdout.decode()}")
-    data = json.loads(stdout.decode())
-    return data['Self']['ID']
-  except Exception as e:
-    raise Exception(f"{str(e)} Do you have the tailscale cli installed? See: https://tailscale.com/kb/1080/cli")
-
-
-async def update_device_attributes(device_id: str, api_key: str, node_id: str, node_port: int, device_capabilities: DeviceCapabilities):
-  async with aiohttp.ClientSession() as session:
-    base_url = f"https://api.tailscale.com/api/v2/device/{device_id}/attributes"
-    headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}
-
-    attributes = {
-      "custom:exo_node_id": node_id.replace('-', '_'), "custom:exo_node_port": node_port, "custom:exo_device_capability_chip": sanitize_attribute(device_capabilities.chip),
-      "custom:exo_device_capability_model": sanitize_attribute(device_capabilities.model), "custom:exo_device_capability_memory": str(device_capabilities.memory),
-      "custom:exo_device_capability_flops_fp16": str(device_capabilities.flops.fp16), "custom:exo_device_capability_flops_fp32": str(device_capabilities.flops.fp32),
-      "custom:exo_device_capability_flops_int8": str(device_capabilities.flops.int8)
-    }
-
-    for attr_name, attr_value in attributes.items():
-      url = f"{base_url}/{attr_name}"
-      data = {"value": str(attr_value).replace(' ', '_')}  # Ensure all values are strings for JSON
-      async with session.post(url, headers=headers, json=data) as response:
-        if response.status == 200:
-          if DEBUG_DISCOVERY >= 1: print(f"Updated device posture attribute {attr_name} for device {device_id}")
-        else:
-          print(f"Failed to update device posture attribute {attr_name}: {response.status} {await response.text()}")
-
-
-async def get_device_attributes(device_id: str, api_key: str) -> Tuple[str, int, DeviceCapabilities]:
-  async with aiohttp.ClientSession() as session:
-    url = f"https://api.tailscale.com/api/v2/device/{device_id}/attributes"
-    headers = {'Authorization': f'Bearer {api_key}'}
-    async with session.get(url, headers=headers) as response:
-      if response.status == 200:
-        data = await response.json()
-        attributes = data.get("attributes", {})
-        node_id = attributes.get("custom:exo_node_id", "").replace('_', '-')
-        node_port = int(attributes.get("custom:exo_node_port", 0))
-        device_capabilities = DeviceCapabilities(
-          model=attributes.get("custom:exo_device_capability_model", "").replace('_', ' '),
-          chip=attributes.get("custom:exo_device_capability_chip", "").replace('_', ' '),
-          memory=int(attributes.get("custom:exo_device_capability_memory", 0)),
-          flops=DeviceFlops(
-            fp16=float(attributes.get("custom:exo_device_capability_flops_fp16", 0)),
-            fp32=float(attributes.get("custom:exo_device_capability_flops_fp32", 0)),
-            int8=float(attributes.get("custom:exo_device_capability_flops_int8", 0))
-          )
-        )
-        return node_id, node_port, device_capabilities
-      else:
-        print(f"Failed to fetch posture attributes for {device_id}: {response.status}")
-        return "", 0, DeviceCapabilities(model="", chip="", memory=0, flops=DeviceFlops(fp16=0, fp32=0, int8=0))
-
-
-def parse_device_attributes(data: Dict[str, str]) -> Dict[str, Any]:
-  result = {}
-  prefix = "custom:exo_"
-  for key, value in data.items():
-    if key.startswith(prefix):
-      attr_name = key.replace(prefix, "")
-      if attr_name in ["node_id", "node_port", "device_capability_chip", "device_capability_model"]:
-        result[attr_name] = value.replace('_', ' ')
-      elif attr_name in ["device_capability_memory", "device_capability_flops_fp16", "device_capability_flops_fp32", "device_capability_flops_int8"]:
-        result[attr_name] = float(value)
-  return result
-
-
-def sanitize_attribute(value: str) -> str:
-  # Replace invalid characters with underscores
-  sanitized_value = re.sub(r'[^a-zA-Z0-9_.]', '_', value)
-  # Truncate to 50 characters
-  return sanitized_value[:50]
-
-
-async def get_tailscale_devices(api_key: str, tailnet: str) -> Dict[str, Device]:
-  async with aiohttp.ClientSession() as session:
-    url = f"https://api.tailscale.com/api/v2/tailnet/{tailnet}/devices"
-    headers = {"Authorization": f"Bearer {api_key}"}
-
-    async with session.get(url, headers=headers) as response:
-      response.raise_for_status()
-      data = await response.json()
-
-      devices = {}
-      for device_data in data.get("devices", []):
-        print("Device data: ", device_data)
-        device = Device.from_dict(device_data)
-        devices[device.name] = device
-
-      return devices
diff --git a/build/lib/exo/networking/tailscale/test_tailscale_discovery.py b/build/lib/exo/networking/tailscale/test_tailscale_discovery.py
deleted file mode 100644
index a0dab5b3..00000000
--- a/build/lib/exo/networking/tailscale/test_tailscale_discovery.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import os
-import asyncio
-import unittest
-from unittest import mock
-from exo.networking.tailscale.tailscale_discovery import TailscaleDiscovery
-from exo.networking.peer_handle import PeerHandle
-
-
-class TestTailscaleDiscovery(unittest.IsolatedAsyncioTestCase):
-  async def asyncSetUp(self):
-    self.tailscale_api_key = os.environ.get("TAILSCALE_API_KEY", "")
-    self.tailnet = os.environ.get("TAILSCALE_TAILNET", "")
-    self.discovery = TailscaleDiscovery(
-      node_id="test_node",
-      node_port=50051,
-      create_peer_handle=lambda peer_id, address, device_capabilities: unittest.mock.Mock(spec=PeerHandle, id=lambda: peer_id),
-      tailscale_api_key=self.tailscale_api_key,
-      tailnet=self.tailnet
-    )
-    await self.discovery.start()
-
-  async def asyncTearDown(self):
-    await self.discovery.stop()
-
-  async def test_discovery(self):
-    # Wait for a short period to allow discovery to happen
-    await asyncio.sleep(15)
-
-    # Get discovered peers
-    peers = await self.discovery.discover_peers()
-
-    # Check if any peers were discovered
-    self.assertGreater(len(peers), 0, "No peers were discovered")
-
-    # Print discovered peers for debugging
-    print(f"Discovered peers: {[peer.id() for peer in peers]}")
-
-    # Check if discovered peers are instances of GRPCPeerHandle
-    print(peers)
-
-
-if __name__ == '__main__':
-  unittest.main()
diff --git a/build/lib/exo/networking/udp/__init__.py b/build/lib/exo/networking/udp/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/networking/udp/test_udp_discovery.py b/build/lib/exo/networking/udp/test_udp_discovery.py
deleted file mode 100644
index 60ee0b96..00000000
--- a/build/lib/exo/networking/udp/test_udp_discovery.py
+++ /dev/null
@@ -1,77 +0,0 @@
-import asyncio
-import unittest
-from unittest import mock
-from exo.networking.udp.udp_discovery import UDPDiscovery
-from exo.networking.grpc.grpc_peer_handle import GRPCPeerHandle
-from exo.networking.grpc.grpc_server import GRPCServer
-from exo.orchestration.node import Node
-
-
-class TestUDPDiscovery(unittest.IsolatedAsyncioTestCase):
-  async def asyncSetUp(self):
-    self.peer1 = mock.AsyncMock()
-    self.peer2 = mock.AsyncMock()
-    self.peer1.connect = mock.AsyncMock()
-    self.peer2.connect = mock.AsyncMock()
-    self.discovery1 = UDPDiscovery("discovery1", 50051, 5678, 5679, create_peer_handle=lambda peer_id, address, device_capabilities: self.peer1)
-    self.discovery2 = UDPDiscovery("discovery2", 50052, 5679, 5678, create_peer_handle=lambda peer_id, address, device_capabilities: self.peer2)
-    await self.discovery1.start()
-    await self.discovery2.start()
-
-  async def asyncTearDown(self):
-    await self.discovery1.stop()
-    await self.discovery2.stop()
-
-  async def test_discovery(self):
-    peers1 = await self.discovery1.discover_peers(wait_for_peers=1)
-    assert len(peers1) == 1
-    peers2 = await self.discovery2.discover_peers(wait_for_peers=1)
-    assert len(peers2) == 1
-
-    # connect has to be explicitly called after discovery
-    self.peer1.connect.assert_not_called()
-    self.peer2.connect.assert_not_called()
-
-
-class TestUDPDiscoveryWithGRPCPeerHandle(unittest.IsolatedAsyncioTestCase):
-  async def asyncSetUp(self):
-    self.node1 = mock.AsyncMock(spec=Node)
-    self.node2 = mock.AsyncMock(spec=Node)
-    self.server1 = GRPCServer(self.node1, "localhost", 50053)
-    self.server2 = GRPCServer(self.node2, "localhost", 50054)
-    await self.server1.start()
-    await self.server2.start()
-    self.discovery1 = UDPDiscovery("discovery1", 50053, 5678, 5679, lambda peer_id, address, device_capabilities: GRPCPeerHandle(peer_id, address, device_capabilities))
-    self.discovery2 = UDPDiscovery("discovery2", 50054, 5679, 5678, lambda peer_id, address, device_capabilities: GRPCPeerHandle(peer_id, address, device_capabilities))
-    await self.discovery1.start()
-    await self.discovery2.start()
-
-  async def asyncTearDown(self):
-    await self.discovery1.stop()
-    await self.discovery2.stop()
-    await self.server1.stop()
-    await self.server2.stop()
-
-  async def test_grpc_discovery(self):
-    peers1 = await self.discovery1.discover_peers(wait_for_peers=1)
-    assert len(peers1) == 1
-    peers2 = await self.discovery2.discover_peers(wait_for_peers=1)
-    assert len(peers2) == 1
-    assert not await peers1[0].is_connected()
-    assert not await peers2[0].is_connected()
-
-    # Connect
-    await peers1[0].connect()
-    await peers2[0].connect()
-    assert await peers1[0].is_connected()
-    assert await peers2[0].is_connected()
-
-    # Kill server1
-    await self.server1.stop()
-
-    assert await peers1[0].is_connected()
-    assert not await peers2[0].is_connected()
-
-
-if __name__ == "__main__":
-  asyncio.run(unittest.main())
diff --git a/build/lib/exo/networking/udp/udp_discovery.py b/build/lib/exo/networking/udp/udp_discovery.py
deleted file mode 100644
index f24241be..00000000
--- a/build/lib/exo/networking/udp/udp_discovery.py
+++ /dev/null
@@ -1,215 +0,0 @@
-import asyncio
-import json
-import socket
-import time
-import traceback
-from typing import List, Dict, Callable, Tuple, Coroutine
-from exo.networking.discovery import Discovery
-from exo.networking.peer_handle import PeerHandle
-from exo.topology.device_capabilities import DeviceCapabilities, device_capabilities, UNKNOWN_DEVICE_CAPABILITIES
-from exo.helpers import DEBUG, DEBUG_DISCOVERY, get_all_ip_addresses
-
-
-class ListenProtocol(asyncio.DatagramProtocol):
-  def __init__(self, on_message: Callable[[bytes, Tuple[str, int]], Coroutine]):
-    super().__init__()
-    self.on_message = on_message
-    self.loop = asyncio.get_event_loop()
-
-  def connection_made(self, transport):
-    self.transport = transport
-
-  def datagram_received(self, data, addr):
-    asyncio.create_task(self.on_message(data, addr))
-
-
-class BroadcastProtocol(asyncio.DatagramProtocol):
-  def __init__(self, message: str, broadcast_port: int):
-    self.message = message
-    self.broadcast_port = broadcast_port
-
-  def connection_made(self, transport):
-    sock = transport.get_extra_info("socket")
-    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-    transport.sendto(self.message.encode("utf-8"), ("<broadcast>", self.broadcast_port))
-
-
-class UDPDiscovery(Discovery):
-  def __init__(
-    self,
-    node_id: str,
-    node_port: int,
-    listen_port: int,
-    broadcast_port: int,
-    create_peer_handle: Callable[[str, str, DeviceCapabilities], PeerHandle],
-    broadcast_interval: int = 1,
-    discovery_timeout: int = 30,
-    device_capabilities: DeviceCapabilities = UNKNOWN_DEVICE_CAPABILITIES,
-    allowed_node_ids: List[str] = None,
-  ):
-    self.node_id = node_id
-    self.node_port = node_port
-    self.listen_port = listen_port
-    self.broadcast_port = broadcast_port
-    self.create_peer_handle = create_peer_handle
-    self.broadcast_interval = broadcast_interval
-    self.discovery_timeout = discovery_timeout
-    self.device_capabilities = device_capabilities
-    self.allowed_node_ids = allowed_node_ids
-    self.known_peers: Dict[str, Tuple[PeerHandle, float, float, int]] = {}
-    self.broadcast_task = None
-    self.listen_task = None
-    self.cleanup_task = None
-
-  async def start(self):
-    self.device_capabilities = device_capabilities()
-    self.broadcast_task = asyncio.create_task(self.task_broadcast_presence())
-    self.listen_task = asyncio.create_task(self.task_listen_for_peers())
-    self.cleanup_task = asyncio.create_task(self.task_cleanup_peers())
-
-  async def stop(self):
-    if self.broadcast_task: self.broadcast_task.cancel()
-    if self.listen_task: self.listen_task.cancel()
-    if self.cleanup_task: self.cleanup_task.cancel()
-    if self.broadcast_task or self.listen_task or self.cleanup_task:
-      await asyncio.gather(self.broadcast_task, self.listen_task, self.cleanup_task, return_exceptions=True)
-
-  async def discover_peers(self, wait_for_peers: int = 0) -> List[PeerHandle]:
-    if wait_for_peers > 0:
-      while len(self.known_peers) < wait_for_peers:
-        if DEBUG_DISCOVERY >= 2: print(f"Current peers: {len(self.known_peers)}/{wait_for_peers}. Waiting for more peers...")
-        await asyncio.sleep(0.1)
-    return [peer_handle for peer_handle, _, _, _ in self.known_peers.values()]
-
-  async def task_broadcast_presence(self):
-    if DEBUG_DISCOVERY >= 2: print("Starting task_broadcast_presence...")
-
-    while True:
-      # Explicitly broadcasting on all assigned ips since broadcasting on `0.0.0.0` on MacOS does not broadcast over
-      # the Thunderbolt bridge when other connection modalities exist such as WiFi or Ethernet
-      for addr in get_all_ip_addresses():
-        message = json.dumps({
-          "type": "discovery",
-          "node_id": self.node_id,
-          "grpc_port": self.node_port,
-          "device_capabilities": self.device_capabilities.to_dict(),
-          "priority": 1,  # For now, every interface has the same priority. We can make this better by prioriting interfaces based on bandwidth, latency, and jitter e.g. prioritise Thunderbolt over WiFi.
-        })
-        if DEBUG_DISCOVERY >= 3: print(f"Broadcasting presence at ({addr}): {message}")
-
-        transport = None
-        try:
-          transport, _ = await asyncio.get_event_loop().create_datagram_endpoint(lambda: BroadcastProtocol(message, self.broadcast_port), local_addr=(addr, 0), family=socket.AF_INET)
-          if DEBUG_DISCOVERY >= 3:
-            print(f"Broadcasting presence at ({addr})")
-        except Exception as e:
-          print(f"Error in broadcast presence ({addr}): {e}")
-        finally:
-          if transport:
-            try:
-              transport.close()
-            except Exception as e:
-              if DEBUG_DISCOVERY >= 2: print(f"Error closing transport: {e}")
-              if DEBUG_DISCOVERY >= 2: traceback.print_exc()
-      await asyncio.sleep(self.broadcast_interval)
-
-  async def on_listen_message(self, data, addr):
-    if not data:
-      return
-
-    decoded_data = data.decode("utf-8", errors="ignore")
-
-    # Check if the decoded data starts with a valid JSON character
-    if not (decoded_data.strip() and decoded_data.strip()[0] in "{["):
-      if DEBUG_DISCOVERY >= 2: print(f"Received invalid JSON data from {addr}: {decoded_data[:100]}")
-      return
-
-    try:
-      decoder = json.JSONDecoder(strict=False)
-      message = decoder.decode(decoded_data)
-    except json.JSONDecodeError as e:
-      if DEBUG_DISCOVERY >= 2: print(f"Error decoding JSON data from {addr}: {e}")
-      return
-
-    if DEBUG_DISCOVERY >= 2: print(f"received from peer {addr}: {message}")
-
-    if message["type"] == "discovery" and message["node_id"] != self.node_id:
-      peer_id = message["node_id"]
-      
-      # Skip if peer_id is not in allowed list
-      if self.allowed_node_ids and peer_id not in self.allowed_node_ids:
-        if DEBUG_DISCOVERY >= 2: print(f"Ignoring peer {peer_id} as it's not in the allowed node IDs list")
-        return
-
-      peer_host = addr[0]
-      peer_port = message["grpc_port"]
-      peer_prio = message["priority"]
-      device_capabilities = DeviceCapabilities(**message["device_capabilities"])
-
-      if peer_id not in self.known_peers or self.known_peers[peer_id][0].addr() != f"{peer_host}:{peer_port}":
-        if peer_id in self.known_peers:
-          existing_peer_prio = self.known_peers[peer_id][3]
-          if existing_peer_prio >= peer_prio:
-            if DEBUG >= 1:
-              print(f"Ignoring peer {peer_id} at {peer_host}:{peer_port} with priority {peer_prio} because we already know about a peer with higher or equal priority: {existing_peer_prio}")
-            return
-        new_peer_handle = self.create_peer_handle(peer_id, f"{peer_host}:{peer_port}", device_capabilities)
-        if not await new_peer_handle.health_check():
-          if DEBUG >= 1: print(f"Peer {peer_id} at {peer_host}:{peer_port} is not healthy. Skipping.")
-          return
-        if DEBUG >= 1: print(f"Adding {peer_id=} at {peer_host}:{peer_port}. Replace existing peer_id: {peer_id in self.known_peers}")
-        self.known_peers[peer_id] = (new_peer_handle, time.time(), time.time(), peer_prio)
-      else:
-        if not await self.known_peers[peer_id][0].health_check():
-          if DEBUG >= 1: print(f"Peer {peer_id} at {peer_host}:{peer_port} is not healthy. Removing.")
-          if peer_id in self.known_peers: del self.known_peers[peer_id]
-          return
-        if peer_id in self.known_peers: self.known_peers[peer_id] = (self.known_peers[peer_id][0], self.known_peers[peer_id][1], time.time(), peer_prio)
-
-  async def task_listen_for_peers(self):
-    await asyncio.get_event_loop().create_datagram_endpoint(lambda: ListenProtocol(self.on_listen_message), local_addr=("0.0.0.0", self.listen_port))
-    if DEBUG_DISCOVERY >= 2: print("Started listen task")
-
-  async def task_cleanup_peers(self):
-    while True:
-      try:
-        current_time = time.time()
-        peers_to_remove = []
-
-        peer_ids = list(self.known_peers.keys())
-        results = await asyncio.gather(*[self.check_peer(peer_id, current_time) for peer_id in peer_ids], return_exceptions=True)
-
-        for peer_id, should_remove in zip(peer_ids, results):
-          if should_remove: peers_to_remove.append(peer_id)
-
-        if DEBUG_DISCOVERY >= 2:
-          print(
-            "Peer statuses:", {
-              peer_handle.id(): f"is_connected={await peer_handle.is_connected()}, health_check={await peer_handle.health_check()}, connected_at={connected_at}, last_seen={last_seen}, prio={prio}"
-              for peer_handle, connected_at, last_seen, prio in self.known_peers.values()
-            }
-          )
-
-        for peer_id in peers_to_remove:
-          if peer_id in self.known_peers:
-            del self.known_peers[peer_id]
-            if DEBUG_DISCOVERY >= 2: print(f"Removed peer {peer_id} due to inactivity or failed health check.")
-      except Exception as e:
-        print(f"Error in cleanup peers: {e}")
-        print(traceback.format_exc())
-      finally:
-        await asyncio.sleep(self.broadcast_interval)
-
-  async def check_peer(self, peer_id: str, current_time: float) -> bool:
-    peer_handle, connected_at, last_seen, prio = self.known_peers.get(peer_id, (None, None, None, None))
-    if peer_handle is None: return False
-
-    try:
-      is_connected = await peer_handle.is_connected()
-      health_ok = await peer_handle.health_check()
-    except Exception as e:
-      if DEBUG_DISCOVERY >= 2: print(f"Error checking peer {peer_id}: {e}")
-      return True
-
-    should_remove = ((not is_connected and current_time - connected_at > self.discovery_timeout) or (current_time - last_seen > self.discovery_timeout) or (not health_ok))
-    return should_remove
diff --git a/build/lib/exo/orchestration/__init__.py b/build/lib/exo/orchestration/__init__.py
deleted file mode 100644
index 478af537..00000000
--- a/build/lib/exo/orchestration/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .node import Node
-from .standard_node import StandardNode
-
-__all__ = ["Node", "StandardNode"]
diff --git a/build/lib/exo/orchestration/node.py b/build/lib/exo/orchestration/node.py
deleted file mode 100644
index 5fe117e1..00000000
--- a/build/lib/exo/orchestration/node.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from typing import Optional, Tuple, List
-import numpy as np
-from abc import ABC, abstractmethod
-from exo.helpers import AsyncCallbackSystem
-from exo.inference.shard import Shard
-from exo.topology.topology import Topology
-
-
-class Node(ABC):
-  @abstractmethod
-  async def start(self, wait_for_peers: int = 0) -> None:
-    pass
-
-  @abstractmethod
-  async def stop(self) -> None:
-    pass
-
-  @abstractmethod
-  async def process_prompt(self, shard: Shard, prompt: str, request_id: Optional[str] = None, inference_state: Optional[dict] = None) -> Optional[np.ndarray]:
-    pass
-
-  @abstractmethod
-  async def process_tensor(self, shard: Shard, tensor: np.ndarray, request_id: Optional[str] = None, inference_state: Optional[dict] = None) -> Optional[np.ndarray]:
-    pass
-
-  @abstractmethod
-  async def get_inference_result(self, request_id: str) -> Tuple[Optional[np.ndarray], bool]:
-    pass
-
-  @abstractmethod
-  async def collect_topology(self, visited: set[str] = set(), max_depth: int = 2) -> Topology:
-    pass
-
-  @property
-  @abstractmethod
-  def current_topology(self) -> Topology:
-    pass
-
-  @property
-  @abstractmethod
-  def on_token(self) -> AsyncCallbackSystem[str, Tuple[str, List[int], bool]]:
-    pass
-
-  @property
-  @abstractmethod
-  def on_opaque_status(self) -> AsyncCallbackSystem[str, Tuple[str, str]]:
-    pass
diff --git a/build/lib/exo/orchestration/standard_node.py b/build/lib/exo/orchestration/standard_node.py
deleted file mode 100644
index baf8216b..00000000
--- a/build/lib/exo/orchestration/standard_node.py
+++ /dev/null
@@ -1,488 +0,0 @@
-import numpy as np
-import json
-import asyncio
-import uuid
-import time
-import traceback
-from typing import List, Dict, Optional, Tuple, Union, Set
-from exo.networking import Discovery, PeerHandle, Server
-from exo.inference.inference_engine import InferenceEngine, Shard
-from .node import Node
-from exo.topology.topology import Topology
-from exo.topology.device_capabilities import device_capabilities
-from exo.topology.partitioning_strategy import Partition, PartitioningStrategy, map_partitions_to_shards
-from exo import DEBUG
-from exo.helpers import AsyncCallbackSystem
-from exo.viz.topology_viz import TopologyViz
-from exo.download.hf.hf_helpers import RepoProgressEvent
-from exo.inference.inference_engine import get_inference_engine, InferenceEngine
-from exo.download.hf.hf_shard_download import HFShardDownloader
-
-class StandardNode(Node):
-  def __init__(
-    self,
-    _id: str,
-    server: Server,
-    inference_engine: InferenceEngine,
-    discovery: Discovery,
-    partitioning_strategy: PartitioningStrategy = None,
-    max_generate_tokens: int = 1024,
-    default_sample_temperature: float = 0.0,
-    topology_viz: Optional[TopologyViz] = None,
-    shard_downloader: Optional[HFShardDownloader] = None,
-  ):
-    self.id = _id
-    self.inference_engine = inference_engine
-    self.server = server
-    self.discovery = discovery
-    self.partitioning_strategy = partitioning_strategy
-    self.peers: List[PeerHandle] = {}
-    self.topology: Topology = Topology()
-    self.device_capabilities = device_capabilities()
-    self.buffered_token_output: Dict[str, Tuple[List[int], bool]] = {}
-    self.buffered_logits: Dict[str, List[np.ndarray]] = {}
-    self.buffered_inputs: Dict[str, List[np.ndarray]] = {}
-    self.max_generate_tokens = max_generate_tokens
-    self.topology_viz = topology_viz
-    self.default_sample_temperature = default_sample_temperature
-    self._on_token = AsyncCallbackSystem[str, Tuple[str, List[int], bool]]()
-    self._on_opaque_status = AsyncCallbackSystem[str, Tuple[str, str]]()
-    self._on_opaque_status.register("node_status").on_next(self.on_node_status)
-    self.node_download_progress: Dict[str, RepoProgressEvent] = {}
-    self.topology_inference_engines_pool: List[List[str]] = []
-    self.shard_downloader = shard_downloader
-
-  async def start(self, wait_for_peers: int = 0) -> None:
-    await self.server.start()
-    await self.discovery.start()
-    await self.update_peers(wait_for_peers)
-    await self.collect_topology()
-    if DEBUG >= 2: print(f"Collected topology: {self.topology}")
-    asyncio.create_task(self.periodic_topology_collection(1.0))
-
-  async def stop(self) -> None:
-    await self.discovery.stop()
-    await self.server.stop()
-
-  def on_node_status(self, request_id, opaque_status):
-    try:
-      status_data = json.loads(opaque_status)
-      if status_data.get("type", "") == "supported_inference_engines":
-        node_id = status_data.get("node_id")
-        engines = status_data.get("engines", [])
-        self.topology_inference_engines_pool.append(engines)
-      if status_data.get("type", "") == "node_status":
-        if status_data.get("status", "").startswith("start_"):
-          self.current_topology.active_node_id = status_data.get("node_id")
-        elif status_data.get("status", "").startswith("end_"):
-          if status_data.get("node_id") == self.current_topology.active_node_id:
-            self.current_topology.active_node_id = None
-      download_progress = None
-      if status_data.get("type", "") == "download_progress":
-        if DEBUG >= 8: print(f"Download progress from {status_data.get('node_id')}: {status_data.get('progress')}")
-        download_progress = RepoProgressEvent.from_dict(status_data.get('progress'))
-        self.node_download_progress[status_data.get('node_id')] = download_progress
-      if self.topology_viz:
-        self.topology_viz.update_visualization(self.current_topology, self.partitioning_strategy.partition(self.current_topology), self.id, self.node_download_progress)
-    except Exception as e:
-      if DEBUG >= 1: print(f"Error updating visualization: {e}")
-      if DEBUG >= 1: traceback.print_exc()
-
-  def get_supported_inference_engines(self):
-    supported_engine_names = []
-    if self.inference_engine.__class__.__name__ == 'MLXDynamicShardInferenceEngine':
-      supported_engine_names.append('mlx')
-      supported_engine_names.append('tinygrad')
-    else:
-      supported_engine_names.append('tinygrad')
-    return supported_engine_names
-
-  async def broadcast_supported_engines(self, supported_engines_names: List[str]):
-    status_message = json.dumps({"type": "supported_inference_engines", "node_id": self.id, "engines": supported_engines_names})
-    await self.broadcast_opaque_status("", status_message)
-
-  def get_topology_inference_engines(self) -> List[List[str]]:
-    return self.topology_inference_engines_pool
-  
-  async def process_inference_result(
-    self,
-    shard,
-    result: np.ndarray,
-    request_id: Optional[str] = None,
-    inference_state: Optional[dict] = None,
-  ):
-    if shard.model_id != 'stable-diffusion-2-1-base':
-      if request_id not in self.buffered_token_output:
-        self.buffered_token_output[request_id] = ([], False)
-      is_finished = len(self.buffered_token_output[request_id][0]) >= self.max_generate_tokens
-      if shard.is_last_layer() and not is_finished:
-        token = await self.inference_engine.sample(result, temp = self.default_sample_temperature)
-        self.buffered_token_output[request_id][0].append(token.item())
-        intermediate_result = self.buffered_token_output[request_id][0]
-        if DEBUG >= 2: print(f"[{request_id}] result size: {result.size}, is finished: {is_finished}, buffered tokens: {len(self.buffered_token_output[request_id][0])}")
-        is_finished = token.item() == self.inference_engine.tokenizer.eos_token_id
-        forward = token.reshape(1, -1)
-      else:
-        forward = result
-    else:
-      await self.inference_engine.ensure_shard(shard)
-      is_finished = inference_state.get('is_finished', False)
-      intermediate_result, inference_state = self.handle_stable_diffusion(inference_state, result)
-      forward = result
-    if shard.is_last_layer():
-      asyncio.create_task(self.broadcast_result(request_id, intermediate_result, is_finished))
-      self.trigger_on_token_callbacks(request_id, intermediate_result, is_finished)
-    if is_finished:
-      if shard.model_id != 'stable-diffusion-2-1-base':
-          self.buffered_token_output[request_id] = (self.buffered_token_output[request_id][0], True)
-          intermediate_result = self.buffered_token_output[request_id][0]
-    else:
-      asyncio.create_task(self.forward_tensor(shard, forward, request_id, self.get_partition_index(offset = 1), inference_state))
-
-    return np.array(intermediate_result)
-  
-  async def process_prompt(
-    self,
-    base_shard: Shard,
-    prompt: str,
-    request_id: Optional[str] = None,
-    inference_state: Optional[dict] = {},
-  ) -> Optional[np.ndarray]:
-    shard = self.get_current_shard(base_shard)
-    asyncio.create_task(
-      self.broadcast_opaque_status(
-        request_id,
-        json.dumps({
-          "type": "node_status",
-          "node_id": self.id,
-          "status": "start_process_prompt",
-          "base_shard": base_shard.to_dict(),
-          "shard": shard.to_dict(),
-          "prompt": prompt,
-          "request_id": request_id,
-        }),
-      )
-    )
-    start_time = time.perf_counter_ns()
-    resp = await self._process_prompt(base_shard, prompt, request_id, inference_state)
-    end_time = time.perf_counter_ns()
-    elapsed_time_ns = end_time - start_time
-    asyncio.create_task(
-      self.broadcast_opaque_status(
-        request_id,
-        json.dumps({
-          "type": "node_status",
-          "node_id": self.id,
-          "status": "end_process_prompt",
-          "base_shard": base_shard.to_dict(),
-          "shard": shard.to_dict(),
-          "prompt": prompt,
-          "request_id": request_id,
-          "elapsed_time_ns": elapsed_time_ns,
-          "result_size": resp.size if resp is not None else 0,
-        }),
-      )
-    )
-    return resp
-
-  async def _process_prompt(self, base_shard: Shard, prompt: str, request_id: Optional[str] = None, inference_state: Optional[dict] = None) -> Optional[np.ndarray]:
-    if request_id is None:
-      request_id = str(uuid.uuid4())
-    shard = self.get_current_shard(base_shard)
-    if DEBUG >= 2: print(f"[{request_id}] process prompt: {base_shard=} {shard=} {prompt=}")
-    if not shard.is_first_layer():
-      if DEBUG >= 2: print(f"[{request_id}] forwarding to next shard: {base_shard=} {shard=} {prompt=}")
-      resp = await self.forward_prompt(shard, prompt, request_id, 0, inference_state)
-      return None
-    else:
-      result,inference_state = await self.inference_engine.infer_prompt(request_id, shard, prompt, inference_state)
-      ret = await self.process_inference_result(shard, result, request_id, inference_state) 
-      return result
-
-  async def process_tensor(
-    self,
-    base_shard: Shard,
-    tensor: np.ndarray,
-    request_id: Optional[str] = None,
-    inference_state: Optional[dict] = None,
-  ) -> Optional[np.ndarray]:
-    shard = self.get_current_shard(base_shard)
-    asyncio.create_task(
-      self.broadcast_opaque_status(
-        request_id,
-        json.dumps({
-          "type": "node_status",
-          "node_id": self.id,
-          "status": "start_process_tensor",
-          "base_shard": base_shard.to_dict(),
-          "shard": shard.to_dict(),
-          "tensor_size": tensor.size,
-          "tensor_shape": tensor.shape,
-          "request_id": request_id,
-        }),
-      )
-    )
-    start_time = time.perf_counter_ns()
-    resp = await self._process_tensor(shard, tensor, request_id, inference_state)
-    end_time = time.perf_counter_ns()
-    elapsed_time_ns = end_time - start_time
-    asyncio.create_task(
-      self.broadcast_opaque_status(
-        request_id,
-        json.dumps({
-          "type": "node_status",
-          "node_id": self.id,
-          "status": "end_process_tensor",
-          "base_shard": base_shard.to_dict(),
-          "shard": shard.to_dict(),
-          "request_id": request_id,
-          "elapsed_time_ns": elapsed_time_ns,
-          "result_size": resp.size if resp is not None else 0,
-        }),
-      )
-    )
-    return resp
-
-  async def _process_tensor(
-    self,
-    base_shard: Shard,
-    tensor: np.ndarray,
-    request_id: Optional[str] = None,
-    inference_state: Optional[dict] = None,
-  ) -> Optional[np.ndarray]:
-    if request_id is None:
-      request_id = str(uuid.uuid4())
-    shard = self.get_current_shard(base_shard)
-
-    if DEBUG >= 1: print(f"[{request_id}] process_tensor: {tensor.size=} {tensor.shape=}")
-    try:
-      result, inference_state = await self.inference_engine.infer_tensor(request_id, shard, tensor, inference_state)
-      ret = await self.process_inference_result(shard, result, request_id, inference_state) 
-      return ret
-    except Exception as e:
-      print(f"Error processing tensor for shard {shard}: {e}")
-      traceback.print_exc()
-      return None
-
-  async def forward_prompt(
-    self,
-    base_shard: Shard,
-    prompt: str,
-    request_id: str,
-    target_index: int,
-    inference_state: Optional[dict] = None,
-  ) -> None:
-    if DEBUG >= 1: print(f"target partition index: {target_index}")
-    target_id = self.partitioning_strategy.partition(self.topology)[target_index].node_id
-    next_shard = self.get_current_shard(base_shard, target_index)
-    if DEBUG >= 2: print(f"Computed target from: {base_shard} {target_index}, {self.topology}. next shard: {next_shard}")
-    if target_id == self.id:
-      await self.process_prompt(next_shard, prompt, request_id)
-    else:
-      target_peer = next((p for p in self.peers if p.id() == target_id), None)
-      if not target_peer:
-        raise ValueError(f"Peer for {target_index} not found")
-      if DEBUG >= 1: print(f"Sending prompt to {target_peer.id()}: {prompt}")
-      await target_peer.send_prompt(next_shard, prompt, request_id=request_id, inference_state=inference_state)
-  
-  async def forward_tensor(
-    self,
-    base_shard: Shard,
-    tensor: np.ndarray,
-    request_id: str,
-    target_index: int,
-    inference_state: Optional[dict] = None,
-  ) -> None:
-    if DEBUG >= 1: print(f"target partition index: {target_index}")
-    target_id = self.partitioning_strategy.partition(self.topology)[target_index].node_id
-    next_shard = self.get_current_shard(base_shard, target_index)
-    if DEBUG >= 2: print(f"Computed target from: {base_shard} {target_index}, {self.topology}. target shard: {next_shard}")
-    if target_id == self.id:
-      await self.process_tensor(next_shard, tensor, request_id, inference_state)
-    else:
-      target_peer = next((p for p in self.peers if p.id() == target_id), None)
-      if not target_peer:
-        raise ValueError(f"Peer for {target_index} not found")
-      if DEBUG >= 1: print(f"Sending tensor to {target_peer.id()}: {tensor}")
-      await target_peer.send_tensor(next_shard, tensor, request_id=request_id, inference_state=inference_state)
-
-  def get_partition_index(self, offset: int = 0):
-    if not self.partitioning_strategy:
-      if DEBUG >= 1: print("No partitioning strategy found. Skipping forward.")
-      return None
-    partitions = self.partitioning_strategy.partition(self.topology)
-    current_partition_index = next((i for i, p in enumerate(partitions) if p.node_id == self.id), None)
-    if current_partition_index is None:
-      raise ValueError(f"No current partition found for node: {self.id}")
-    return (current_partition_index + offset) % len(partitions)
-
-  def get_current_shard(self, base_shard: Shard, index: Optional[int] = None) -> Shard:
-    if index is None:
-      index = self.get_partition_index()
-    partitions = self.partitioning_strategy.partition(self.topology)
-    shards = map_partitions_to_shards(partitions, base_shard.n_layers, base_shard.model_id)
-    return shards[index]
-
-  async def update_peers(self, wait_for_peers: int = 0) -> bool:
-    next_peers = await self.discovery.discover_peers(wait_for_peers)
-    current_peer_ids = {peer.id() for peer in self.peers}
-    next_peer_ids = {peer.id() for peer in next_peers}
-    peers_added = [peer for peer in next_peers if peer.id() not in current_peer_ids]
-    peers_removed = [peer for peer in self.peers if peer.id() not in next_peer_ids]
-    peers_updated = [peer for peer in next_peers if peer.id() in current_peer_ids and any(p.addr() != peer.addr() for p in self.peers if p.id() == peer.id())]
-    peers_unchanged = [peer for peer in next_peers if peer.id() in current_peer_ids and all(p.addr() == peer.addr() for p in self.peers if p.id() == peer.id())]
-    peers_to_disconnect = [peer for peer in peers_removed if await peer.is_connected()]
-    peers_to_connect = [peer for peer in peers_added + peers_updated + peers_unchanged if not await peer.is_connected()]
-
-    def _pretty(peers: List[PeerHandle]) -> List[str]:
-      return [f"{peer.id()}@{peer.addr()}" for peer in peers]
-
-    if DEBUG >= 2:
-      print(f"update_peers: added={peers_added} removed={peers_removed} updated={peers_updated} unchanged={peers_unchanged} to_disconnect={peers_to_disconnect} to_connect={peers_to_connect}")
-
-    async def disconnect_with_timeout(peer, timeout=5):
-      try:
-        await asyncio.wait_for(peer.disconnect(), timeout)
-        return True
-      except Exception as e:
-        print(f"Error disconnecting peer {peer.id()}@{peer.addr()}: {e}")
-        traceback.print_exc()
-        return False
-
-    async def connect_with_timeout(peer, timeout=5):
-      try:
-        await asyncio.wait_for(peer.connect(), timeout)
-        return True
-      except Exception as e:
-        print(f"Error connecting peer {peer.id()}@{peer.addr()}: {e}")
-        traceback.print_exc()
-        return False
-
-    disconnect_results = await asyncio.gather(*(disconnect_with_timeout(peer) for peer in peers_to_disconnect), return_exceptions=True)
-    connect_results = await asyncio.gather(*(connect_with_timeout(peer) for peer in peers_to_connect), return_exceptions=True)
-
-    successful_disconnects = [peer for peer, result in zip(peers_to_disconnect, disconnect_results) if result is True]
-    failed_disconnects = [peer for peer, result in zip(peers_to_disconnect, disconnect_results) if result is False]
-    successful_connects = [peer for peer, result in zip(peers_to_connect, connect_results) if result is True]
-    failed_connects = [peer for peer, result in zip(peers_to_connect, connect_results) if result is False]
-    if DEBUG >= 1:
-      if successful_disconnects: print(f"Successfully disconnected peers: {_pretty(successful_disconnects)}")
-      if failed_disconnects: print(f"Failed to disconnect peers: {_pretty(failed_disconnects)}")
-      if successful_connects: print(f"Successfully connected peers: {_pretty(successful_connects)}")
-      if failed_connects: print(f"Failed to connect peers: {_pretty(failed_connects)}")
-
-    self.peers = next_peers
-    return len(peers_added) > 0 or len(peers_removed) > 0 or len(peers_updated) > 0
-
-  async def select_best_inference_engine(self):
-    if self.inference_engine.__class__.__name__ == 'DummyInferenceEngine': return
-    supported_engines = self.get_supported_inference_engines()
-    await self.broadcast_supported_engines(supported_engines)
-    if len(self.get_topology_inference_engines()):
-      self.inference_engine = get_inference_engine(supported_engines[0], self.shard_downloader)
-
-  async def periodic_topology_collection(self, interval: int):
-    while True:
-      await asyncio.sleep(interval)
-      try:
-        did_peers_change = await self.update_peers()
-        if DEBUG >= 2: print(f"{did_peers_change=}")
-        if did_peers_change:
-          await self.collect_topology()
-          await self.select_best_inference_engine()
-      except Exception as e:
-        print(f"Error collecting topology: {e}")
-        traceback.print_exc()
-
-  async def get_inference_result(self, request_id: str) -> Tuple[Optional[np.ndarray], bool]:
-    if request_id not in self.buffered_token_output:
-      return None, False
-    return np.array(self.buffered_token_output[request_id][0]), self.buffered_token_output[request_id][1]
-
-  async def collect_topology(self, visited: set[str] = set(), max_depth: int = 4) -> Topology:
-    next_topology = Topology()
-    next_topology.update_node(self.id, self.device_capabilities)
-
-    if DEBUG >= 2: print(f"Collecting topology {max_depth=} {visited=}")
-
-    prev_visited = visited.copy()
-    visited.add(self.id)
-    visited.update(p.id() for p in self.peers)
-
-    for peer in self.peers:
-      next_topology.update_node(peer.id(), peer.device_capabilities())
-      next_topology.add_edge(self.id, peer.id())
-
-      if peer.id() in prev_visited:
-        continue
-
-      if max_depth <= 0:
-        if DEBUG >= 2: print("Max depth reached. Skipping...")
-        continue
-
-      try:
-        other_topology = await asyncio.wait_for(peer.collect_topology(visited, max_depth=max_depth - 1), timeout=5.0)
-        if DEBUG >= 2: print(f"Collected topology from: {peer.id()}: {other_topology}")
-        self.topology.merge(other_topology)
-      except Exception as e:
-        print(f"Error collecting topology from {peer.id()}: {e}")
-        traceback.print_exc()
-
-    next_topology.active_node_id = self.topology.active_node_id  # this is not so clean.
-    self.topology = next_topology
-    if self.topology_viz:
-      self.topology_viz.update_visualization(self.current_topology, self.partitioning_strategy.partition(self.current_topology), self.id)
-    return next_topology
-
-  @property
-  def on_token(self) -> AsyncCallbackSystem[str, Tuple[str, List[int], bool]]:
-    return self._on_token
-
-  @property
-  def on_opaque_status(self) -> AsyncCallbackSystem[str, Tuple[str, str]]:
-    return self._on_opaque_status
-
-  def trigger_on_token_callbacks(self, request_id: str, tokens: List[int], is_finished: bool) -> None:
-    if DEBUG >= 2: print(f"Triggering all on_token callbacks with {request_id=} num_tokens={len(tokens)} {is_finished=}")
-    self.on_token.trigger_all(request_id, tokens, is_finished)
-  
-  async def broadcast_result(self, request_id: str, result: List[int], is_finished: bool) -> None:
-    async def send_result_to_peer(peer):
-      try:
-        await asyncio.wait_for(peer.send_result(request_id, result, is_finished), timeout=15.0)
-      except asyncio.TimeoutError:
-        print(f"Timeout broadcasting result to {peer.id()}")
-      except Exception as e:
-        print(f"Error broadcasting result to {peer.id()}: {e}")
-        traceback.print_exc()
-
-    await asyncio.gather(*[send_result_to_peer(peer) for peer in self.peers], return_exceptions=True)
-
-  async def broadcast_opaque_status(self, request_id: str, status: str) -> None:
-    if DEBUG >= 8: print(f"Broadcasting opaque status: {request_id=} {status=}")
-
-    async def send_status_to_peer(peer):
-      try:
-        await asyncio.wait_for(peer.send_opaque_status(request_id, status), timeout=15.0)
-      except asyncio.TimeoutError:
-        print(f"Timeout sending opaque status to {peer.id()}")
-      except Exception as e:
-        print(f"Error sending opaque status to {peer.id()}: {e}")
-        traceback.print_exc()
-
-    await asyncio.gather(*[send_status_to_peer(peer) for peer in self.peers], return_exceptions=True)
-    # in the case of opaque status, we also want to receive our own opaque statuses
-    self.on_opaque_status.trigger_all(request_id, status)
-
-  @property
-  def current_topology(self) -> Topology:
-    return self.topology
-
-  def handle_stable_diffusion(self, inference_state, result):
-    if inference_state['is_step_finished']:
-      inference_state['step']+=1
-    progress = [inference_state['step'],inference_state['total_steps']]
-    intermediate_result = result
-    if progress[0] == progress[1]:
-      intermediate_result = result
-    return intermediate_result, inference_state
diff --git a/build/lib/exo/orchestration/test_node.py b/build/lib/exo/orchestration/test_node.py
deleted file mode 100644
index 230ef0cf..00000000
--- a/build/lib/exo/orchestration/test_node.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import unittest
-from unittest.mock import Mock, AsyncMock
-import numpy as np
-
-from .standard_node import StandardNode
-from exo.networking.peer_handle import PeerHandle
-
-
-class TestNode(unittest.IsolatedAsyncioTestCase):
-  def setUp(self):
-    self.mock_inference_engine = AsyncMock()
-    self.mock_server = AsyncMock()
-    self.mock_server.start = AsyncMock()
-    self.mock_server.stop = AsyncMock()
-    self.mock_discovery = AsyncMock()
-    self.mock_discovery.start = AsyncMock()
-    self.mock_discovery.stop = AsyncMock()
-    mock_peer1 = Mock(spec=PeerHandle)
-    mock_peer1.id.return_value = "peer1"
-    mock_peer2 = Mock(spec=PeerHandle)
-    mock_peer2.id.return_value = "peer2"
-    self.mock_discovery.discover_peers = AsyncMock(return_value=[mock_peer1, mock_peer2])
-
-    self.node = StandardNode("test_node", self.mock_server, self.mock_inference_engine, "localhost", 50051, self.mock_discovery)
-
-  async def asyncSetUp(self):
-    await self.node.start()
-
-  async def asyncTearDown(self):
-    await self.node.stop()
-
-  async def test_node_initialization(self):
-    self.assertEqual(self.node.node_id, "test_node")
-    self.assertEqual(self.node.host, "localhost")
-    self.assertEqual(self.node.port, 50051)
-
-  async def test_node_start(self):
-    self.mock_server.start.assert_called_once_with("localhost", 50051)
-
-  async def test_node_stop(self):
-    await self.node.stop()
-    self.mock_server.stop.assert_called_once()
-
-  async def test_discover_and_connect_to_peers(self):
-    await self.node.discover_and_connect_to_peers()
-    self.assertEqual(len(self.node.peers), 2)
-    self.assertIn("peer1", map(lambda p: p.id(), self.node.peers))
-    self.assertIn("peer2", map(lambda p: p.id(), self.node.peers))
-
-  async def test_process_tensor_calls_inference_engine(self):
-    mock_peer = Mock()
-    self.node.peers = [mock_peer]
-
-    input_tensor = np.array([69, 1, 2])
-    await self.node.process_tensor(input_tensor, None)
-
-    self.node.inference_engine.process_shard.assert_called_once_with(input_tensor)
diff --git a/build/lib/exo/stats/__init__.py b/build/lib/exo/stats/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/stats/metrics.py b/build/lib/exo/stats/metrics.py
deleted file mode 100644
index f29533ff..00000000
--- a/build/lib/exo/stats/metrics.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from exo.orchestration import Node
-from prometheus_client import start_http_server, Counter, Histogram
-import json
-
-# Create metrics to track time spent and requests made.
-PROCESS_PROMPT_COUNTER = Counter("process_prompt_total", "Total number of prompts processed", ["node_id"])
-PROCESS_TENSOR_COUNTER = Counter("process_tensor_total", "Total number of tensors processed", ["node_id"])
-PROCESS_TENSOR_TIME = Histogram("process_tensor_seconds", "Time spent processing tensor", ["node_id"])
-
-
-def start_metrics_server(node: Node, port: int):
-  start_http_server(port)
-
-  def _on_opaque_status(request_id, opaque_status: str):
-    status_data = json.loads(opaque_status)
-    _type = status_data.get("type", "")
-    node_id = status_data.get("node_id", "")
-    if _type != "node_status":
-      return
-    status = status_data.get("status", "")
-
-    if status == "end_process_prompt":
-      PROCESS_PROMPT_COUNTER.labels(node_id=node_id).inc()
-    elif status == "end_process_tensor":
-      elapsed_time_ns = status_data.get("elapsed_time_ns", 0)
-      PROCESS_TENSOR_COUNTER.labels(node_id=node_id).inc()
-      PROCESS_TENSOR_TIME.labels(node_id=node_id).observe(elapsed_time_ns/1e9)  # Convert ns to seconds
-
-  node.on_opaque_status.register("stats").on_next(_on_opaque_status)
diff --git a/build/lib/exo/test_callbacks.py b/build/lib/exo/test_callbacks.py
deleted file mode 100644
index c10083d6..00000000
--- a/build/lib/exo/test_callbacks.py
+++ /dev/null
@@ -1,50 +0,0 @@
-import asyncio
-from typing import Any, Callable
-from exo.helpers import AsyncCallbackSystem, AsyncCallback
-
-
-# Usage example
-async def main() -> None:
-  callback_system = AsyncCallbackSystem[str, Any]()
-
-  # Register callbacks
-  callback1 = callback_system.register("callback1")
-  callback2 = callback_system.register("callback2")
-
-  def on_next_callback(name: str) -> Callable[..., None]:
-    def callback(*args: Any) -> None:
-      print(f"{name} received values: {args}")
-
-    return callback
-
-  callback1.on_next(on_next_callback("Callback1"))
-  callback2.on_next(on_next_callback("Callback2"))
-
-  async def wait_for_callback(name: str, callback: AsyncCallback[Any], condition: Callable[..., bool]) -> None:
-    try:
-      result = await callback.wait(condition, timeout=2)
-      print(f"{name} wait completed with result: {result}")
-    except asyncio.TimeoutError:
-      print(f"{name} wait timed out")
-
-  # Trigger all callbacks at once
-  callback_system.trigger_all("Hello", 42, True)
-
-  # Wait for all callbacks with different conditions
-  await asyncio.gather(
-    wait_for_callback("Callback1", callback1, lambda msg, num, flag: isinstance(msg, str) and num > 0),
-    wait_for_callback("Callback2", callback2, lambda msg, num, flag: flag is True),
-  )
-
-  # Trigger individual callback
-  callback_system.trigger("callback2", "World", -10, False)
-
-  # Demonstrate timeout
-  new_callback = callback_system.register("new_callback")
-  new_callback.on_next(on_next_callback("NewCallback"))
-  await wait_for_callback("NewCallback", new_callback, lambda msg, num, flag: num > 100)
-
-  callback_system.trigger("callback2", "World", 200, False)
-
-
-asyncio.run(main())
diff --git a/build/lib/exo/tinychat/common.css b/build/lib/exo/tinychat/common.css
deleted file mode 100644
index e654d6a1..00000000
--- a/build/lib/exo/tinychat/common.css
+++ /dev/null
@@ -1,130 +0,0 @@
-/* make it responsive */
-@media(min-width: 852px) {
-  body {
-    font-size: 14px;
-  }
-}
-@media(max-width: 852px) {
-  body {
-    font-size: 12px;
-  }
-}
-
-/* resets */
-html, body {
-  width: 100%;
-  height: 100%;
-}
-
-*::-webkit-scrollbar {
-  display: none;
-}
-
-* {
-  -ms-overflow-style: none;
-  scrollbar-width: none;
-}
-
-* {
-  -moz-box-sizing: border-box;
-  -webkit-box-sizing: border-box;
-  box-sizing: border-box;
-}
-
-/* default */
-body {
-  margin: 0;
-  background-color: var(--primary-bg-color);
-  color: var(--foreground-color);
-}
-
-h1, h2, h3, h4, h5, h6 {
-  margin: 0em;
-}
-
-hr {
-  width: 92%;
-}
-
-button {
-  cursor: pointer;
-  border: none;
-  background-color: transparent;
-}
-button:hover {
-}
-button:active {
-}
-
-/* components */
-.container {
-  margin: 0 auto;
-  padding: 1rem;
-}
-
-.centered {
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
-}
-
-.centered-w-only {
-  position: absolute;
-  left: 50%;
-  transform: translateX(-50%);
-}
-
-.centered-h-only {
-  position: absolute;
-  top: 50%;
-  transform: translateY(-50%);
-}
-
-.card {
-  padding: 0;
-}
-
-.card-header {
-  padding: 0.5rem 1rem;
-}
-
-.card-container {
-  width: 96vw;
-  height: 100%;
-  gap: 1rem;
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  justify-content: center;
-  align-items: center;
-}
-
-.clean-a {
-  text-decoration: underline;
-  text-decoration-color: #006fc1;
-  text-decoration-thickness: 2px;
-  color: inherit;
-}
-
-.hover-underline {
-  text-decoration: underline;
-  text-decoration-color: #228039;
-  text-decoration-thickness: 2px;
-  color: inherit;
-}
-
-.flex-horizontal {
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.vertical-separator {
-  padding: 0 0.5rem;
-}
-
-[x-cloak] {
-  display: none !important;
-}
diff --git a/build/lib/exo/tinychat/favicon.svg b/build/lib/exo/tinychat/favicon.svg
deleted file mode 100644
index 420a33e0..00000000
--- a/build/lib/exo/tinychat/favicon.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -10 150 70" shape-rendering="crispEdges">
-  <g id="logo">
-    <!-- t -->
-    <polygon points="10,40 10,20 0,20 0,10 10,10 10,0 20,0 20,10 30,10 30,20 20,20 20,30 30,30 30,40" />
-    <!-- i -->
-    <polygon points="40,40 40,20 50,20 50,40" />
-    <polygon points="40,10 40,0 50,0 50,10" />
-    <!-- n -->
-    <polygon points="60,40 60,10 80,10 80,40 90,40 90,20 70,20 70,40" />
-    <!-- y -->
-    <polygon points="100,50 100,40 130,40 130,10 120,10 120,20 110,20 110,10 100,10 100,30 120,30 120,50" />
-  </g>
-  <style>
-  @media (prefers-color-scheme: dark) {
-    #logo {
-      fill: #fff;
-    }
-  }
-  @media (prefers-color-scheme: light) {
-    #logo {
-      fill: #000;
-    }
-  }
-  </style>
-</svg>
diff --git a/build/lib/exo/tinychat/index.css b/build/lib/exo/tinychat/index.css
deleted file mode 100644
index eb9669dd..00000000
--- a/build/lib/exo/tinychat/index.css
+++ /dev/null
@@ -1,484 +0,0 @@
-/* define colors */
-:root {
-  --primary-color: #fff;
-  --secondary-color: #2a2a2a;
-  --secondary-color-transparent: #ffffff66;
-  --primary-bg-color: #1a1a1a;
-  --foreground-color: #f0f0f0;
-  --red-color: #a52e4d;
-}
-
-main {
-  width: 100%;
-  height: 100%;
-
-  display: flex;
-  flex-direction: column;
-
-  place-items: center;
-}
-
-.home {
-  width: 100%;
-  height: 90%;
-
-  margin-bottom: 10rem;
-}
-
-.title {
-  font-size: 3rem;
-  margin: 1rem 0;
-  margin-top: 3rem;
-}
-
-.histories-container-container {
-  width: 100%;
-  max-height: 75%;
-
-  position: relative;
-}
-
-.histories-container {
-  overflow-y: auto;
-  overflow-x: hidden;
-  width: 100%;
-  height: 100%;
-
-  display: flex;
-  flex-direction: column;
-  gap: 1rem;
-  align-items: center;
-
-  margin: 0;
-  padding: 3rem 1rem;
-}
-
-.histories-start {
-  height: 3rem;
-  width: 100%;
-
-  z-index: 999;
-  top: 0;
-  position: absolute;
-
-  background: linear-gradient(
-    180deg,
-    var(--primary-bg-color) 0%,
-    transparent 100%
-  );
-}
-.histories-end {
-  height: 3rem;
-  width: 100%;
-
-  z-index: 999;
-  bottom: 0;
-  position: absolute;
-
-  background: linear-gradient(
-    0deg,
-    var(--primary-bg-color) 0%,
-    transparent 100%
-  );
-}
-
-.history {
-  padding: 1rem;
-  width: 100%;
-  max-width: 40rem;
-
-  background-color: var(--secondary-color);
-  border-radius: 10px;
-  border-left: 2px solid var(--primary-color);
-
-  cursor: pointer;
-
-  transform: translateX(calc(1px * var(--tx, 0)));
-  opacity: var(--opacity, 1);
-}
-.history:hover {
-  background-color: var(--secondary-color);
-}
-
-.history-delete-button {
-  position: absolute;
-  top: 0;
-  right: 0;
-  padding: 0.5rem;
-  margin: 0;
-  outline: none;
-  border: none;
-  background-color: var(--secondary-color);
-  color: var(--foreground-color);
-  border-radius: 0 0 0 10px;
-  cursor: pointer;
-  transition: 0.2s;
-}
-.history-delete-button:hover {
-  background-color: var(--secondary-color);
-  padding: 0.75rem;
-}
-
-.messages {
-  overflow-y: auto;
-  height: 100%;
-  width: 100%;
-  max-width: 1200px;
-
-  display: flex;
-  flex-direction: column;
-  gap: 1rem;
-  align-items: center;
-  padding-top: 1rem;
-  padding-bottom: 11rem;
-}
-
-.message {
-  max-width: 75%;
-  padding: 0.5rem 1rem;
-  border-radius: 20px;
-}
-.message-role-assistant {
-  background-color: var(--secondary-color);
-  margin-right: auto;
-  color: #fff;
-}
-.message-role-user {
-  margin-left: auto;
-  background-color: var(--primary-color);
-  color: #000;
-}
-.download-progress {
-  margin-bottom: 12em;
-  overflow-y: auto;
-  min-height: 350px;
-  padding: 2rem;
-}
-.message > pre {
-  white-space: pre-wrap;
-}
-
-.progress-bar-container {
-  width: 100%;
-  background-color: #e0e0e0;
-  border-radius: 4px;
-  margin: 10px 0;
-}
-.progress-bar {
-  height: 20px;
-  border-radius: 4px;
-  transition: width 0.5s ease-in-out;
-}
-.progress-bar.complete {
-  background-color: #4CAF50;
-}
-.progress-bar.in-progress {
-  background-color: #2196F3;
-}
-
-.toast {
-    width: 100%;
-    background-color: #fc2a2a;
-    color: #fff;
-    text-align: left;
-    border-radius: 2px;
-    padding: 16px;
-    position: fixed;
-    z-index: 9999;
-    top: 0;
-    left: 0;
-    right: 0;
-    display: flex;
-    flex-direction: column;
-    white-space: pre-wrap;
-    font-family: monospace;
-}
-
-.toast-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    width: 100%;
-}
-
-.toast-error-message {
-    flex-grow: 1;
-}
-
-.toast-header-buttons {
-    display: flex;
-    align-items: center;
-    gap: 16px;
-    margin-left: 24px;
-}
-
-.toast-expand-button {
-    background: none;
-    border: none;
-    color: white;
-    padding: 4px;
-    cursor: pointer;
-    font-size: 1em;
-}
-
-.toast-close-button {
-    background: none;
-    border: none;
-    color: white;
-    padding: 4px;
-    cursor: pointer;
-    font-size: 1.2em;
-    line-height: 1;
-}
-
-.toast-expand-button:hover,
-.toast-close-button:hover {
-    opacity: 0.8;
-}
-
-.toast-content {
-    margin-top: 10px;
-    padding: 10px;
-    background-color: rgba(0, 0, 0, 0.2);
-    border-radius: 4px;
-}
-
-.hljs {
-  width: 100%;
-  position: relative;
-  border-radius: 10px;
-  /* wrap code blocks */
-  white-space: pre-wrap;
-}
-/* put clipboard button in the top right corner of the code block */
-.clipboard-button {
-  position: absolute;
-  top: 0;
-  right: 0;
-  padding: 0.5rem;
-  margin: 0;
-  outline: none;
-  border: none;
-  background-color: var(--secondary-color);
-  color: var(--foreground-color);
-  border-radius: 0 0 0 10px;
-  cursor: pointer;
-  transition: 0.2s;
-}
-.clipboard-button:hover {
-  background-color: var(--secondary-color);
-  padding: 0.75rem;
-}
-
-.input-container {
-  position: absolute;
-  bottom: 0;
-
-  /* linear gradient from background-color to transparent on the top */
-  background: linear-gradient(
-    0deg,
-    var(--primary-bg-color) 55%,
-    transparent 100%
-  );
-
-  width: 100%;
-  max-width: 1200px;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
-  z-index: 999;
-}
-
-.input-performance {
-  margin-top: 4rem;
-
-  display: flex;
-  flex-direction: row;
-  gap: 1rem;
-}
-
-.input-performance-point {
-  display: flex;
-  flex-direction: row;
-  place-items: center;
-  gap: 0.5rem;
-}
-.input-performance-point > p {
-  height: 1rem;
-  line-height: normal;
-}
-
-.input {
-  width: 90%;
-  min-height: 3rem;
-  flex-shrink: 0;
-
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-  gap: 0.5rem;
-
-  align-items: flex-end;
-  margin-bottom: 2rem;
-}
-
-.input-form {
-  width: 100%;
-  padding: 1rem;
-  min-height: 3rem;
-  max-height: 8rem;
-
-  background-color: var(--secondary-color);
-  color: var(--foreground-color);
-  border-radius: 10px;
-  border: none;
-  resize: none;
-  outline: none;
-}
-
-.input-button {
-  height: 3rem;
-  width: 4rem;
-
-  background-color: var(--primary-color);
-  color: var(--secondary-color);
-  border-radius: 10px;
-  padding: 0.5rem;
-  cursor: pointer;
-}
-.input-button:hover {
-  background-color: var(--secondary-color-transparent);
-}
-.input-button:disabled {
-  background-color: var(--secondary-color);
-  cursor: not-allowed;
-}
-
-/* wrap text */
-p {
-  white-space: pre-wrap;
-}
-
-/* fonts */
-.megrim-regular {
-  font-family: "Megrim", system-ui;
-  font-weight: 400;
-  font-style: normal;
-}
-
-.monospace {
-  font-family: monospace;
-}
-
-.model-selector {
-  display: flex;
-  justify-content: center;
-  padding: 20px 0;
-}
-.model-selector select {
-  padding: 10px 20px;
-  font-size: 16px;
-  border: 1px solid #ccc;
-  border-radius: 5px;
-  background-color: #f8f8f8;
-  cursor: pointer;
-}
-.model-selector select:focus {
-  outline: none;
-  border-color: #007bff;
-  box-shadow: 0 0 0 2px rgba(0,123,255,.25);
-}
-
-/* Image upload button styles */
-.image-input-button {
-  background-color: var(--secondary-color);
-  color: var(--foreground-color);
-  border: none;
-  border-radius: 50%;
-  width: 40px;
-  height: 40px;
-  font-size: 18px;
-  cursor: pointer;
-  transition: all 0.3s ease;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  margin-right: 10px;
-}
-
-.image-input-button:hover {
-  background-color: var(--secondary-color-transparent);
-  transform: scale(1.1);
-}
-
-.image-input-button:focus {
-  outline: none;
-  box-shadow: 0 0 0 3px rgba(var(--secondary-color-rgb), 0.5);
-}
-
-.image-input-button i {
-  transition: all 0.3s ease;
-}
-
-.image-input-button:hover i {
-  transform: scale(1.2);
-}
-
-/* Hidden file input styles */
-#image-upload {
-  display: none;
-}
-
-.image-preview-container {
-  position: relative;
-  display: inline-block;
-  margin-right: 10px;
-}
-
-.image-preview {
-  max-width: 100px;
-  max-height: 100px;
-  object-fit: cover;
-  border-radius: 5px;
-}
-
-.remove-image-button {
-  position: absolute;
-  top: -5px;
-  right: -5px;
-  background-color: rgba(255, 255, 255, 0.8);
-  border: none;
-  border-radius: 50%;
-  padding: 2px 5px;
-  cursor: pointer;
-}
-
-.message > p > img {
-  max-width: 100%;
-  max-height: 100%;
-  object-fit: contain;
-}
-
-.clear-history-button {
-  background-color: var(--red-color);
-  color: white;
-  padding: 10px 20px;
-  border-radius: 5px;
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  transition: all 0.3s ease;
-  margin: 1rem auto;
-  border: none;
-  cursor: pointer;
-}
-
-.clear-history-button:hover {
-  opacity: 0.8;
-  transform: scale(1.05);
-}
-
-.clear-history-button i {
-  font-size: 14px;
-}
\ No newline at end of file
diff --git a/build/lib/exo/tinychat/index.html b/build/lib/exo/tinychat/index.html
deleted file mode 100644
index 3ae855f4..00000000
--- a/build/lib/exo/tinychat/index.html
+++ /dev/null
@@ -1,255 +0,0 @@
-<!DOCTYPE html>
-
-<head>
-<title>tinychat</title>
-<meta content="width=device-width, initial-scale=1" name="viewport"/>
-<link href="favicon.svg" rel="icon" type="image/svg+xml"/>
-<script defer="" src="/static/cdn.jsdelivr.net/npm/@alpine-collective/toolkit@1.0.2/dist/cdn.min.js"></script>
-<script defer="" src="/static/cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
-<script defer="" src="/static/cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
-<script defer="" src="/static/unpkg.com/@marcreichel/alpine-autosize@1.3.x/dist/alpine-autosize.min.js"></script>
-<script defer="" src="/static/unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
-<script src="/static/unpkg.com/dompurify@3.1.5/dist/purify.min.js"></script>
-<script src="/static/unpkg.com/marked@13.0.0/marked.min.js"></script>
-<script src="/static/unpkg.com/marked-highlight@2.1.2/lib/index.umd.js"></script>
-<script src="/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js"></script>
-<script src="/index.js"></script>
-<link href="/static/fonts.googleapis.com" rel="preconnect"/>
-<link crossorigin="" href="/static/fonts.gstatic.com" rel="preconnect"/>
-<link href="/static/fonts.googleapis.com/css2" rel="stylesheet"/>
-<link href="/static/cdn.jsdelivr.net/npm/purecss@3.0.0/build/base-min.css" rel="stylesheet"/>
-<link crossorigin="anonymous" href="/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" referrerpolicy="no-referrer" rel="stylesheet">
-<link href="/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.css" rel="stylesheet"/>
-<link href="/index.css" rel="stylesheet"/>
-<link href="/common.css" rel="stylesheet"/>
-</head>
-<body>
-<main x-data="state" x-init="console.log(endpoint)">
-     <!-- Error Toast -->
-    <div x-show="errorMessage" x-transition.opacity class="toast">
-        <div class="toast-header">
-            <span class="toast-error-message" x-text="errorMessage.basic"></span>
-            <div class="toast-header-buttons">
-                <button @click="errorExpanded = !errorExpanded; if (errorTimeout) { clearTimeout(errorTimeout); errorTimeout = null; }" 
-                        class="toast-expand-button" 
-                        x-show="errorMessage.stack">
-                    <span x-text="errorExpanded ? 'Hide Details' : 'Show Details'"></span>
-                </button>
-                <button @click="errorMessage = null; errorExpanded = false;" class="toast-close-button">
-                    <i class="fas fa-times"></i>
-                </button>
-            </div>
-        </div>
-        <div class="toast-content" x-show="errorExpanded" x-transition>
-            <span x-text="errorMessage.stack"></span>
-        </div>
-    </div>
-<div class="model-selector">
-  <select @change="if (cstate) cstate.selectedModel = $event.target.value" x-model="cstate.selectedModel" x-init="await populateSelector()" class='model-select'>
-  </select>
-</div>
-<div @popstate.window="
-      if (home === 2) {
-        home = -1;
-        cstate = { time: null, messages: [], selectedModel: 'llama-3.1-8b' };
-        time_till_first = 0;
-        tokens_per_second = 0;
-        total_tokens = 0;
-      }
-    " class="home centered" x-effect="
-      $refs.inputForm.focus();
-      if (home === 1) setTimeout(() =&gt; home = 2, 100);
-      if (home === -1) setTimeout(() =&gt; home = 0, 100);
-    " x-show="home === 0" x-transition="">
-<h1 class="title megrim-regular">tinychat</h1>
-<template x-if="histories.length">
-  <button 
-    @click="if(confirm('Are you sure you want to clear all history?')) clearAllHistory();" 
-    class="clear-history-button">
-    <i class="fas fa-trash"></i> Clear All History
-  </button>
-</template>
-<div class="histories-container-container">
-<template x-if="histories.length">
-<div class="histories-start"></div>
-</template>
-<div class="histories-container" x-intersect="
-          $el.scrollTo({ top: 0, behavior: 'smooth' });
-        ">
-<template x-for="_state in histories.toSorted((a, b) =&gt; b.time - a.time)">
-<div @click="
-            cstate = _state;
-            if (cstate) cstate.selectedModel = document.querySelector('.model-selector select').value
-            // updateTotalTokens(cstate.messages);
-            home = 1;
-            // ensure that going back in history will go back to home
-            window.history.pushState({}, '', '/');
-          " @touchend="
-            if (Math.abs($event.changedTouches[0].clientX - otx) &gt; trigger) removeHistory(_state);
-            $el.style.setProperty('--tx', 0);
-            $el.style.setProperty('--opacity', 1);
-          " @touchmove="
-            $el.style.setProperty('--tx', $event.changedTouches[0].clientX - otx);
-            $el.style.setProperty('--opacity', 1 - (Math.abs($event.changedTouches[0].clientX - otx) / trigger));
-          " @touchstart="
-            otx = $event.changedTouches[0].clientX;
-          " class="history" x-data="{ otx: 0, trigger: 75 }">
-<h3 x-text="new Date(_state.time).toLocaleString()"></h3>
-<p x-text="$truncate(_state.messages[0].content, 80)"></p>
-<!-- delete button -->
-<button @click.stop="removeHistory(_state);" class="history-delete-button">
-<i class="fas fa-trash"></i>
-</button>
-</div>
-</template>
-</div>
-<template x-if="histories.length">
-<div class="histories-end"></div>
-</template>
-</div>
-</div>
-<div class="messages" x-init="
-      $watch('cstate', value =&gt; {
-        $el.innerHTML = '';
-        value.messages.forEach(({ role, content }) =&gt; {
-          const div = document.createElement('div');
-          div.className = `message message-role-${role}`;
-          try {
-              if (content.includes('![Generated Image]')) {
-                const imageUrl = content.match(/\((.*?)\)/)[1];
-                const img = document.createElement('img');
-                img.src = imageUrl;
-                img.alt = 'Generated Image';
-                img.onclick = async () => {
-                  try {
-                    const response = await fetch(img.src);
-                    const blob = await response.blob();
-                    const file = new File([blob], 'image.png', { type: 'image/png' });
-                    handleImageUpload({ target: { files: [file] } });
-                  } catch (error) {
-                    console.error('Error fetching image:', error);
-                  }
-                };
-                div.appendChild(img);
-              } else {
-                div.innerHTML = DOMPurify.sanitize(marked.parse(content));
-              }
-          } catch (e) {
-            console.log(content);
-            console.error(e);
-          }
-
-          // add a clipboard button to all code blocks
-          const codeBlocks = div.querySelectorAll('.hljs');
-          codeBlocks.forEach(codeBlock =&gt; {
-            const button = document.createElement('button');
-            button.className = 'clipboard-button';
-            button.innerHTML = '&lt;i class=\'fas fa-clipboard\'&gt;&lt;/i&gt;';
-            button.onclick = () =&gt; {
-              // navigator.clipboard.writeText(codeBlock.textContent);
-              const range = document.createRange();
-              range.setStartBefore(codeBlock);
-              range.setEndAfter(codeBlock);
-              window.getSelection()?.removeAllRanges();
-              window.getSelection()?.addRange(range);
-              document.execCommand('copy');
-              window.getSelection()?.removeAllRanges();
-
-              button.innerHTML = '&lt;i class=\'fas fa-check\'&gt;&lt;/i&gt;';
-              setTimeout(() =&gt; button.innerHTML = '&lt;i class=\'fas fa-clipboard\'&gt;&lt;/i&gt;', 1000);
-            };
-            codeBlock.appendChild(button);
-          });
-
-          $el.appendChild(div);
-        });
-
-        $el.scrollTo({ top: $el.scrollHeight, behavior: 'smooth' });
-      });
-    " x-intersect="
-      $el.scrollTo({ top: $el.scrollHeight, behavior: 'smooth' });
-    " x-ref="messages" x-show="home === 2" x-transition="">
-</div>
-
-<!-- Download Progress Section -->
-<template x-if="downloadProgress && downloadProgress.length > 0">
-  <div class="download-progress message message-role-assistant">
-    <h2>Download Progress</h2>
-    <br>
-    <template x-for="(progress, index) in downloadProgress" :key="index">
-      <div class="download-progress-node">
-        <br>
-        <h3 x-text="`Download ${index + 1}`"></h3>
-        <p><strong>Model:</strong> <span x-text="progress.repo_id + '@' + progress.repo_revision"></span></p>
-        <p><strong>Status:</strong> <span x-text="progress.status"></span></p>
-        <div class="progress-bar-container">
-          <div class="progress-bar" 
-               :class="progress.isComplete ? 'complete' : 'in-progress'"
-               :style="`width: ${progress.percentage}%;`">
-          </div>
-        </div>
-        <template x-if="!progress.isComplete">
-          <div>
-            <p><strong>Progress:</strong> <span x-text="`${progress.downloaded_bytes_display} / ${progress.total_bytes_display} (${progress.percentage}%)`"></span></p>
-            <p><strong>Speed:</strong> <span x-text="progress.overall_speed_display || 'N/A'"></span></p>
-            <p><strong>ETA:</strong> <span x-text="progress.overall_eta_display || 'N/A'"></span></p>
-          </div>
-        </template>
-      </div>
-    </template>
-  </div>
-</template>
-
-
-<div class="input-container">
-<div class="input-performance">
-<span class="input-performance-point">
-<p class="monospace" x-text="(time_till_first / 1000).toFixed(2)"></p>
-<p class="megrim-regular">SEC TO FIRST TOKEN</p>
-</span>
-<span class="input-performance-point">
-<p class="monospace" x-text="tokens_per_second.toFixed(1)"></p>
-<p class="megrim-regular">TOKENS/SEC</p>
-</span>
-<span class="input-performance-point">
-<p class="monospace" x-text="total_tokens"></p>
-<p class="megrim-regular">TOKENS</p>
-</span>
-</div>
-<div class="input">
-<button @click="$refs.imageUpload.click()" class="image-input-button" x-show="cstate.selectedModel === 'llava-1.5-7b-hf' || cstate.selectedModel === 'stable-diffusion-2-1-base'">
-<i class="fas fa-image"></i>
-</button>
-<input @change="$data.handleImageUpload($event)" accept="image/*" id="image-upload" style="display: none;" type="file" x-ref="imageUpload"/>
-<div class="image-preview-container" x-show="imagePreview">
-<img :src="imagePreview" alt="Uploaded Image" class="image-preview"/>
-<button @click="imagePreview = null; imageUrl = null;" class="remove-image-button">
-<i class="fas fa-times"></i>
-</button>
-</div>
-<textarea :disabled="generating" :placeholder="generating ? 'Generating...' : 'Say something'" @input="
-            home = (home === 0) ? 1 : home
-            if (cstate.messages.length === 0 &amp;&amp; $el.value === '') home = -1;
-
-            if ($el.value !== '') {
-              const messages = [...cstate.messages];
-              messages.push({ role: 'user', content: $el.value });
-              // updateTotalTokens(messages);
-            } else {
-              if (cstate.messages.length === 0) total_tokens = 0;
-              // else updateTotalTokens(cstate.messages);
-            }
-          " @keydown.enter="await handleEnter($event)" @keydown.escape.window="$focus.focus($el)" autofocus="" class="input-form" id="input-form" rows="1" x-autosize="" x-effect="
-            console.log(generating);
-            if (!generating) $nextTick(() =&gt; {
-              $el.focus();
-              setTimeout(() =&gt; $refs.messages.scrollTo({ top: $refs.messages.scrollHeight, behavior: 'smooth' }), 100);
-            });
-          " x-ref="inputForm"></textarea>
-<button :disabled="generating" @click="await handleSend()" class="input-button">
-<i :class="generating ? 'fa-spinner fa-spin' : 'fa-paper-plane'" class="fas"></i>
-</button>
-</div>
-</div>
-</main>
-</body>
diff --git a/build/lib/exo/tinychat/index.js b/build/lib/exo/tinychat/index.js
deleted file mode 100644
index c839d3d7..00000000
--- a/build/lib/exo/tinychat/index.js
+++ /dev/null
@@ -1,687 +0,0 @@
-document.addEventListener("alpine:init", () => {
-  Alpine.data("state", () => ({
-    // current state
-    cstate: {
-      time: null,
-      messages: [],
-      selectedModel: 'llama-3.2-1b',
-    },    
-
-    // historical state
-    histories: JSON.parse(localStorage.getItem("histories")) || [],
-
-    home: 0,
-    generating: false,
-    endpoint: `${window.location.origin}/v1`,
-    errorMessage: null,
-    errorExpanded: false,
-    errorTimeout: null,
-
-    // performance tracking
-    time_till_first: 0,
-    tokens_per_second: 0,
-    total_tokens: 0,
-
-    // image handling
-    imagePreview: null,
-
-    // download progress
-    downloadProgress: null,
-    downloadProgressInterval: null, // To keep track of the polling interval
-
-    // Pending message storage
-    pendingMessage: null,
-
-    init() {
-      // Clean up any pending messages
-      localStorage.removeItem("pendingMessage");
-
-      // Start polling for download progress
-      this.startDownloadProgressPolling();
-    },
-
-    removeHistory(cstate) {
-      const index = this.histories.findIndex((state) => {
-        return state.time === cstate.time;
-      });
-      if (index !== -1) {
-        this.histories.splice(index, 1);
-        localStorage.setItem("histories", JSON.stringify(this.histories));
-      }
-    },
-
-    clearAllHistory() {
-      this.histories = [];
-      localStorage.setItem("histories", JSON.stringify([]));
-    },
-
-    // Utility functions
-    formatBytes(bytes) {
-      if (bytes === 0) return '0 B';
-      const k = 1024;
-      const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
-      const i = Math.floor(Math.log(bytes) / Math.log(k));
-      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
-    },
-
-    formatDuration(seconds) {
-      if (seconds === null || seconds === undefined || isNaN(seconds)) return '';
-      const h = Math.floor(seconds / 3600);
-      const m = Math.floor((seconds % 3600) / 60);
-      const s = Math.floor(seconds % 60);
-      if (h > 0) return `${h}h ${m}m ${s}s`;
-      if (m > 0) return `${m}m ${s}s`;
-      return `${s}s`;
-    },
-
-    async populateSelector() {
-      try {
-        const response = await fetch(`${window.location.origin}/modelpool`);
-        const responseText = await response.text(); // Get raw response text first
-        
-        if (!response.ok) {
-          throw new Error(`HTTP error! status: ${response.status}`);
-        }
-        
-        // Try to parse the response text
-        let responseJson;
-        try {
-          responseJson = JSON.parse(responseText);
-        } catch (parseError) {
-          console.error('Failed to parse JSON:', parseError);
-          throw new Error(`Invalid JSON response: ${responseText}`);
-        }
-
-        const sel = document.querySelector(".model-select");
-        if (!sel) {
-          throw new Error("Could not find model selector element");
-        }
-
-        // Clear the current options and add new ones
-        sel.innerHTML = '';
-          
-        const modelDict = responseJson["model pool"];
-        if (!modelDict) {
-          throw new Error("Response missing 'model pool' property");
-        }
-
-        Object.entries(modelDict).forEach(([key, value]) => {
-          const opt = document.createElement("option");
-          opt.value = key;
-          opt.textContent = value;
-          sel.appendChild(opt);
-        });
-
-        // Set initial value to the first model
-        const firstKey = Object.keys(modelDict)[0];
-        if (firstKey) {
-          sel.value = firstKey;
-          this.cstate.selectedModel = firstKey;
-        }
-      } catch (error) {
-        console.error("Error populating model selector:", error);
-        this.errorMessage = `Failed to load models: ${error.message}`;
-      }
-    },
-
-    async handleImageUpload(event) {
-      const file = event.target.files[0];
-      if (file) {
-        const reader = new FileReader();
-        reader.onload = (e) => {
-          this.imagePreview = e.target.result;
-          this.imageUrl = e.target.result; // Store the image URL
-          // Add image preview to the chat
-          this.cstate.messages.push({
-            role: "user",
-            content: `![Uploaded Image](${this.imagePreview})`,
-          });
-        };
-        reader.readAsDataURL(file);
-      }
-    },
-
-
-    async handleSend() {
-      try {
-        const el = document.getElementById("input-form");
-        const value = el.value.trim();
-        if (!value && !this.imagePreview) return;
-
-        if (this.generating) return;
-        this.generating = true;
-        if (this.home === 0) this.home = 1;
-
-        // ensure that going back in history will go back to home
-        window.history.pushState({}, "", "/");
-
-        // add message to list
-        if (value) {
-          this.cstate.messages.push({ role: "user", content: value });
-        }
-
-        // clear textarea
-        el.value = "";
-        el.style.height = "auto";
-        el.style.height = el.scrollHeight + "px";
-
-        localStorage.setItem("pendingMessage", value);
-        this.processMessage(value);
-      } catch (error) {
-        console.error('error', error);
-        const errorDetails = {
-            message: error.message || 'Unknown error',
-            stack: error.stack,
-            name: error.name || 'Error'
-        };
-        
-        this.errorMessage = {
-            basic: `${errorDetails.name}: ${errorDetails.message}`,
-            stack: errorDetails.stack
-        };
-
-        // Clear any existing timeout
-        if (this.errorTimeout) {
-            clearTimeout(this.errorTimeout);
-        }
-
-        // Only set the timeout if the error details aren't expanded
-        if (!this.errorExpanded) {
-            this.errorTimeout = setTimeout(() => {
-                this.errorMessage = null;
-                this.errorExpanded = false;
-            }, 30 * 1000);
-        }
-        this.generating = false;
-      }
-    },
-
-    async processMessage(value) {
-      try {
-        // reset performance tracking
-        const prefill_start = Date.now();
-        let start_time = 0;
-        let tokens = 0;
-        this.tokens_per_second = 0;
-
-        // prepare messages for API request
-        let apiMessages = this.cstate.messages.map(msg => {
-          if (msg.content.startsWith('![Uploaded Image]')) {
-            return {
-              role: "user",
-              content: [
-                {
-                  type: "image_url",
-                  image_url: {
-                    url: this.imageUrl
-                  }
-                },
-                {
-                  type: "text",
-                  text: value // Use the actual text the user typed
-                }
-              ]
-            };
-          } else {
-            return {
-              role: msg.role,
-              content: msg.content
-            };
-          }
-        });
-        
-        if (this.cstate.selectedModel === "stable-diffusion-2-1-base") {
-          // Send a request to the image generation endpoint
-          console.log(apiMessages[apiMessages.length - 1].content)
-          console.log(this.cstate.selectedModel)  
-          console.log(this.endpoint)
-          const response = await fetch(`${this.endpoint}/image/generations`, {
-            method: "POST",
-            headers: {
-              "Content-Type": "application/json",
-            },
-            body: JSON.stringify({
-              "model": 'stable-diffusion-2-1-base',
-              "prompt": apiMessages[apiMessages.length - 1].content,
-              "image_url": this.imageUrl
-            }),
-          });
-      
-          if (!response.ok) {
-            throw new Error("Failed to fetch");
-          }
-          const reader = response.body.getReader();
-          let done = false;
-          let gottenFirstChunk = false;
-  
-          while (!done) {
-            const { value, done: readerDone } = await reader.read();
-            done = readerDone;
-            const decoder = new TextDecoder();
-  
-            if (value) {
-              // Assume non-binary data (text) comes first
-              const chunk = decoder.decode(value, { stream: true });
-              const parsed = JSON.parse(chunk);
-              console.log(parsed)
-  
-              if (parsed.progress) {
-                if (!gottenFirstChunk) {
-                  this.cstate.messages.push({ role: "assistant", content: "" });
-                  gottenFirstChunk = true;
-                }
-                this.cstate.messages[this.cstate.messages.length - 1].content = parsed.progress;
-              }
-              else if (parsed.images) {
-                if (!gottenFirstChunk) {
-                  this.cstate.messages.push({ role: "assistant", content: "" });
-                  gottenFirstChunk = true;
-                }
-                const imageUrl = parsed.images[0].url;
-                console.log(imageUrl)
-                this.cstate.messages[this.cstate.messages.length - 1].content = `![Generated Image](${imageUrl}?t=${Date.now()})`;
-              }
-            }
-          }
-        }
-        
-        else{        
-          const containsImage = apiMessages.some(msg => Array.isArray(msg.content) && msg.content.some(item => item.type === 'image_url'));
-          if (containsImage) {
-            // Map all messages with string content to object with type text
-            apiMessages = apiMessages.map(msg => {
-              if (typeof msg.content === 'string') {
-                return {
-                  ...msg,
-                  content: [
-                    {
-                      type: "text",
-                      text: msg.content
-                    }
-                  ]
-                };
-              }
-              return msg;
-            });
-          }
-
-          console.log(apiMessages)
-          //start receiving server sent events
-          let gottenFirstChunk = false;
-          for await (
-            const chunk of this.openaiChatCompletion(this.cstate.selectedModel, apiMessages)
-          ) {
-            if (!gottenFirstChunk) {
-              this.cstate.messages.push({ role: "assistant", content: "" });
-              gottenFirstChunk = true;
-            }
-
-            // add chunk to the last message
-            this.cstate.messages[this.cstate.messages.length - 1].content += chunk;
-
-            // calculate performance tracking
-            tokens += 1;
-            this.total_tokens += 1;
-            if (start_time === 0) {
-              start_time = Date.now();
-              this.time_till_first = start_time - prefill_start;
-            } else {
-              const diff = Date.now() - start_time;
-              if (diff > 0) {
-                this.tokens_per_second = tokens / (diff / 1000);
-              }
-            }
-          }
-        }
-        // Clean the cstate before adding it to histories
-        const cleanedCstate = JSON.parse(JSON.stringify(this.cstate));
-        cleanedCstate.messages = cleanedCstate.messages.map(msg => {
-          if (Array.isArray(msg.content)) {
-            return {
-              ...msg,
-              content: msg.content.map(item =>
-                item.type === 'image_url' ? { type: 'image_url', image_url: { url: '[IMAGE_PLACEHOLDER]' } } : item
-              )
-            };
-          }
-          return msg;
-        });
-
-        // Update the state in histories or add it if it doesn't exist
-        const index = this.histories.findIndex((cstate) => cstate.time === cleanedCstate.time);
-        cleanedCstate.time = Date.now();
-        if (index !== -1) {
-          // Update the existing entry
-          this.histories[index] = cleanedCstate;
-        } else {
-          // Add a new entry
-          this.histories.push(cleanedCstate);
-        }
-        console.log(this.histories)
-        // update in local storage
-        try {
-          localStorage.setItem("histories", JSON.stringify(this.histories));
-        } catch (error) {
-          console.error("Failed to save histories to localStorage:", error);
-        }
-      } catch (error) {
-        console.error('error', error);
-        const errorDetails = {
-            message: error.message || 'Unknown error',
-            stack: error.stack,
-            name: error.name || 'Error'
-        };
-        
-        this.errorMessage = {
-            basic: `${errorDetails.name}: ${errorDetails.message}`,
-            stack: errorDetails.stack
-        };
-
-        // Clear any existing timeout
-        if (this.errorTimeout) {
-            clearTimeout(this.errorTimeout);
-        }
-
-        // Only set the timeout if the error details aren't expanded
-        if (!this.errorExpanded) {
-            this.errorTimeout = setTimeout(() => {
-                this.errorMessage = null;
-                this.errorExpanded = false;
-            }, 30 * 1000);
-        }
-      } finally {
-        this.generating = false;
-      }
-    },
-
-    async handleEnter(event) {
-      // if shift is not pressed
-      if (!event.shiftKey) {
-        event.preventDefault();
-        await this.handleSend();
-      }
-    },
-
-    updateTotalTokens(messages) {
-      fetch(`${this.endpoint}/chat/token/encode`, {
-        method: "POST",
-        headers: { "Content-Type": "application/json" },
-        body: JSON.stringify({ messages }),
-      }).then((response) => response.json()).then((data) => {
-        this.total_tokens = data.length;
-      }).catch(console.error);
-    },
-
-    async *openaiChatCompletion(model, messages) {
-      // stream response
-      console.log("model", model)
-      const response = await fetch(`${this.endpoint}/chat/completions`, {
-        method: "POST",
-        headers: {
-          "Content-Type": "application/json",
-        },
-        body: JSON.stringify({
-          "model": model,
-          "messages": messages,
-          "stream": true,
-        }),
-      });
-      if (!response.ok) {
-        const errorResBody = await response.json()
-        if (errorResBody?.detail) {
-          throw new Error(`Failed to fetch completions: ${errorResBody.detail}`);
-        } else {
-          throw new Error("Failed to fetch completions: Unknown error");
-        }
-      }
-
-      const reader = response.body.pipeThrough(new TextDecoderStream())
-        .pipeThrough(new EventSourceParserStream()).getReader();
-      while (true) {
-        const { done, value } = await reader.read();
-        if (done) {
-          break;
-        }
-        if (value.type === "event") {
-          const json = JSON.parse(value.data);
-          if (json.choices) {
-            const choice = json.choices[0];
-            if (choice.finish_reason === "stop") {
-              break;
-            }
-            yield choice.delta.content;
-          }
-        }
-      }
-    },
-
-    async fetchDownloadProgress() {
-      try {
-        const response = await fetch(`${this.endpoint}/download/progress`);
-        if (response.ok) {
-          const data = await response.json();
-          const progressArray = Object.values(data);
-          if (progressArray.length > 0) {
-            this.downloadProgress = progressArray.map(progress => {
-              // Check if download is complete
-              if (progress.status === "complete") {
-                return {
-                  ...progress,
-                  isComplete: true,
-                  percentage: 100
-                };
-              } else if (progress.status === "failed") {
-                return {
-                  ...progress,
-                  isComplete: false,
-                  errorMessage: "Download failed"
-                };
-              } else {
-                return {
-                  ...progress,
-                  isComplete: false,
-                  downloaded_bytes_display: this.formatBytes(progress.downloaded_bytes),
-                  total_bytes_display: this.formatBytes(progress.total_bytes),
-                  overall_speed_display: progress.overall_speed ? this.formatBytes(progress.overall_speed) + '/s' : '',
-                  overall_eta_display: progress.overall_eta ? this.formatDuration(progress.overall_eta) : '',
-                  percentage: ((progress.downloaded_bytes / progress.total_bytes) * 100).toFixed(2)
-                };
-              }
-            });
-            const allComplete = this.downloadProgress.every(progress => progress.isComplete);
-            if (allComplete) {
-              // Check for pendingMessage
-              const savedMessage = localStorage.getItem("pendingMessage");
-              if (savedMessage) {
-                // Clear pendingMessage
-                localStorage.removeItem("pendingMessage");
-                // Call processMessage() with savedMessage
-                if (this.lastErrorMessage) {
-                  await this.processMessage(savedMessage);
-                }
-              }
-              this.lastErrorMessage = null;
-              this.downloadProgress = null;
-            }
-          } else {
-            // No ongoing download
-            this.downloadProgress = null;
-          }
-        }
-      } catch (error) {
-        console.error("Error fetching download progress:", error);
-        this.downloadProgress = null;
-      }
-    },
-
-    startDownloadProgressPolling() {
-      if (this.downloadProgressInterval) {
-        // Already polling
-        return;
-      }
-      this.fetchDownloadProgress(); // Fetch immediately
-      this.downloadProgressInterval = setInterval(() => {
-        this.fetchDownloadProgress();
-      }, 1000); // Poll every second
-    },
-  }));
-});
-
-const { markedHighlight } = globalThis.markedHighlight;
-marked.use(markedHighlight({
-  langPrefix: "hljs language-",
-  highlight(code, lang, _info) {
-    const language = hljs.getLanguage(lang) ? lang : "plaintext";
-    return hljs.highlight(code, { language }).value;
-  },
-}));
-
-// **** eventsource-parser ****
-class EventSourceParserStream extends TransformStream {
-  constructor() {
-    let parser;
-
-    super({
-      start(controller) {
-        parser = createParser((event) => {
-          if (event.type === "event") {
-            controller.enqueue(event);
-          }
-        });
-      },
-
-      transform(chunk) {
-        parser.feed(chunk);
-      },
-    });
-  }
-}
-
-function createParser(onParse) {
-  let isFirstChunk;
-  let buffer;
-  let startingPosition;
-  let startingFieldLength;
-  let eventId;
-  let eventName;
-  let data;
-  reset();
-  return {
-    feed,
-    reset,
-  };
-  function reset() {
-    isFirstChunk = true;
-    buffer = "";
-    startingPosition = 0;
-    startingFieldLength = -1;
-    eventId = void 0;
-    eventName = void 0;
-    data = "";
-  }
-  function feed(chunk) {
-    buffer = buffer ? buffer + chunk : chunk;
-    if (isFirstChunk && hasBom(buffer)) {
-      buffer = buffer.slice(BOM.length);
-    }
-    isFirstChunk = false;
-    const length = buffer.length;
-    let position = 0;
-    let discardTrailingNewline = false;
-    while (position < length) {
-      if (discardTrailingNewline) {
-        if (buffer[position] === "\n") {
-          ++position;
-        }
-        discardTrailingNewline = false;
-      }
-      let lineLength = -1;
-      let fieldLength = startingFieldLength;
-      let character;
-      for (
-        let index = startingPosition;
-        lineLength < 0 && index < length;
-        ++index
-      ) {
-        character = buffer[index];
-        if (character === ":" && fieldLength < 0) {
-          fieldLength = index - position;
-        } else if (character === "\r") {
-          discardTrailingNewline = true;
-          lineLength = index - position;
-        } else if (character === "\n") {
-          lineLength = index - position;
-        }
-      }
-      if (lineLength < 0) {
-        startingPosition = length - position;
-        startingFieldLength = fieldLength;
-        break;
-      } else {
-        startingPosition = 0;
-        startingFieldLength = -1;
-      }
-      parseEventStreamLine(buffer, position, fieldLength, lineLength);
-      position += lineLength + 1;
-    }
-    if (position === length) {
-      buffer = "";
-    } else if (position > 0) {
-      buffer = buffer.slice(position);
-    }
-  }
-  function parseEventStreamLine(lineBuffer, index, fieldLength, lineLength) {
-    if (lineLength === 0) {
-      if (data.length > 0) {
-        onParse({
-          type: "event",
-          id: eventId,
-          event: eventName || void 0,
-          data: data.slice(0, -1),
-          // remove trailing newline
-        });
-
-        data = "";
-        eventId = void 0;
-      }
-      eventName = void 0;
-      return;
-    }
-    const noValue = fieldLength < 0;
-    const field = lineBuffer.slice(
-      index,
-      index + (noValue ? lineLength : fieldLength),
-    );
-    let step = 0;
-    if (noValue) {
-      step = lineLength;
-    } else if (lineBuffer[index + fieldLength + 1] === " ") {
-      step = fieldLength + 2;
-    } else {
-      step = fieldLength + 1;
-    }
-    const position = index + step;
-    const valueLength = lineLength - step;
-    const value = lineBuffer.slice(position, position + valueLength).toString();
-    if (field === "data") {
-      data += value ? "".concat(value, "\n") : "\n";
-    } else if (field === "event") {
-      eventName = value;
-    } else if (field === "id" && !value.includes("\0")) {
-      eventId = value;
-    } else if (field === "retry") {
-      const retry = parseInt(value, 10);
-      if (!Number.isNaN(retry)) {
-        onParse({
-          type: "reconnect-interval",
-          value: retry,
-        });
-      }
-    }
-  }
-}
-
-const BOM = [239, 187, 191];
-function hasBom(buffer) {
-  return BOM.every((charCode, index) => buffer.charCodeAt(index) === charCode);
-}
diff --git a/build/lib/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpine-collective/toolkit@1.0.2/dist/cdn.min.js b/build/lib/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpine-collective/toolkit@1.0.2/dist/cdn.min.js
deleted file mode 100644
index a363545c..00000000
--- a/build/lib/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpine-collective/toolkit@1.0.2/dist/cdn.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(()=>{var H=Object.create,v=Object.defineProperty,N=Object.getPrototypeOf,V=Object.prototype.hasOwnProperty,z=Object.getOwnPropertyNames,q=Object.getOwnPropertyDescriptor;var W=n=>v(n,"__esModule",{value:!0});var D=(n,e)=>()=>(e||(e={exports:{}},n(e.exports,e)),e.exports);var F=(n,e,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of z(e))!V.call(n,r)&&r!=="default"&&v(n,r,{get:()=>e[r],enumerable:!(o=q(e,r))||o.enumerable});return n},U=n=>F(W(v(n!=null?H(N(n)):{},"default",n&&n.__esModule&&"default"in n?{get:()=>n.default,enumerable:!0}:{value:n,enumerable:!0})),n);var I=D((E,w)=>{(function(){"use strict";function n(){var e=window,o=document;if("scrollBehavior"in o.documentElement.style&&e.__forceSmoothScrollPolyfill__!==!0)return;var r=e.HTMLElement||e.Element,i=468,f={scroll:e.scroll||e.scrollTo,scrollBy:e.scrollBy,elementScroll:r.prototype.scroll||b,scrollIntoView:r.prototype.scrollIntoView},u=e.performance&&e.performance.now?e.performance.now.bind(e.performance):Date.now;function c(t){var l=["MSIE ","Trident/","Edge/"];return new RegExp(l.join("|")).test(t)}var g=c(e.navigator.userAgent)?1:0;function b(t,l){this.scrollLeft=t,this.scrollTop=l}function M(t){return .5*(1-Math.cos(Math.PI*t))}function m(t){if(t===null||typeof t!="object"||t.behavior===void 0||t.behavior==="auto"||t.behavior==="instant")return!0;if(typeof t=="object"&&t.behavior==="smooth")return!1;throw new TypeError("behavior member of ScrollOptions "+t.behavior+" is not a valid value for enumeration ScrollBehavior.")}function O(t,l){if(l==="Y")return t.clientHeight+g<t.scrollHeight;if(l==="X")return t.clientWidth+g<t.scrollWidth}function T(t,l){var s=e.getComputedStyle(t,null)["overflow"+l];return s==="auto"||s==="scroll"}function R(t){var l=O(t,"Y")&&T(t,"Y"),s=O(t,"X")&&T(t,"X");return l||s}function $(t){for(;t!==o.body&&R(t)===!1;)t=t.parentNode||t.host;return t}function S(t){var l=u(),s,d,p,a=(l-t.startTime)/i;a=a>1?1:a,s=M(a),d=t.startX+(t.x-t.startX)*s,p=t.startY+(t.y-t.startY)*s,t.method.call(t.scrollable,d,p),(d!==t.x||p!==t.y)&&e.requestAnimationFrame(S.bind(e,t))}function h(t,l,s){var d,p,a,y,_=u();t===o.body?(d=e,p=e.scrollX||e.pageXOffset,a=e.scrollY||e.pageYOffset,y=f.scroll):(d=t,p=t.scrollLeft,a=t.scrollTop,y=b),S({scrollable:d,method:y,startTime:_,startX:p,startY:a,x:l,y:s})}e.scroll=e.scrollTo=function(){if(arguments[0]!==void 0){if(m(arguments[0])===!0){f.scroll.call(e,arguments[0].left!==void 0?arguments[0].left:typeof arguments[0]!="object"?arguments[0]:e.scrollX||e.pageXOffset,arguments[0].top!==void 0?arguments[0].top:arguments[1]!==void 0?arguments[1]:e.scrollY||e.pageYOffset);return}h.call(e,o.body,arguments[0].left!==void 0?~~arguments[0].left:e.scrollX||e.pageXOffset,arguments[0].top!==void 0?~~arguments[0].top:e.scrollY||e.pageYOffset)}},e.scrollBy=function(){if(arguments[0]!==void 0){if(m(arguments[0])){f.scrollBy.call(e,arguments[0].left!==void 0?arguments[0].left:typeof arguments[0]!="object"?arguments[0]:0,arguments[0].top!==void 0?arguments[0].top:arguments[1]!==void 0?arguments[1]:0);return}h.call(e,o.body,~~arguments[0].left+(e.scrollX||e.pageXOffset),~~arguments[0].top+(e.scrollY||e.pageYOffset))}},r.prototype.scroll=r.prototype.scrollTo=function(){if(arguments[0]!==void 0){if(m(arguments[0])===!0){if(typeof arguments[0]=="number"&&arguments[1]===void 0)throw new SyntaxError("Value could not be converted");f.elementScroll.call(this,arguments[0].left!==void 0?~~arguments[0].left:typeof arguments[0]!="object"?~~arguments[0]:this.scrollLeft,arguments[0].top!==void 0?~~arguments[0].top:arguments[1]!==void 0?~~arguments[1]:this.scrollTop);return}var t=arguments[0].left,l=arguments[0].top;h.call(this,this,typeof t=="undefined"?this.scrollLeft:~~t,typeof l=="undefined"?this.scrollTop:~~l)}},r.prototype.scrollBy=function(){if(arguments[0]!==void 0){if(m(arguments[0])===!0){f.elementScroll.call(this,arguments[0].left!==void 0?~~arguments[0].left+this.scrollLeft:~~arguments[0]+this.scrollLeft,arguments[0].top!==void 0?~~arguments[0].top+this.scrollTop:~~arguments[1]+this.scrollTop);return}this.scroll({left:~~arguments[0].left+this.scrollLeft,top:~~arguments[0].top+this.scrollTop,behavior:arguments[0].behavior})}},r.prototype.scrollIntoView=function(){if(m(arguments[0])===!0){f.scrollIntoView.call(this,arguments[0]===void 0?!0:arguments[0]);return}var t=$(this),l=t.getBoundingClientRect(),s=this.getBoundingClientRect();t!==o.body?(h.call(this,t,t.scrollLeft+s.left-l.left,t.scrollTop+s.top-l.top),e.getComputedStyle(t).position!=="fixed"&&e.scrollBy({left:l.left,top:l.top,behavior:"smooth"})):e.scrollBy({left:s.left,top:s.top,behavior:"smooth"})}}typeof E=="object"&&typeof w!="undefined"?w.exports={polyfill:n}:n()})()});function j(n){n.magic("range",()=>function(e,o,r=1){typeof o=="undefined"&&(o=e,e=e?1:0);let i=e>o;i&&([e,o]=[o,e]);let f=Array.from({length:(o-e)/r+1},(u,c)=>e+c*r);return i?f.reverse():f})}var Y=U(I());function X(n){Y.default.polyfill(),n.magic("scroll",()=>function(e,o={}){let r=e,i=o.offset?parseInt(o.offset,10):0;if(delete o.offset,typeof e=="string"&&/^[0-9]+?/g.test(e)&&(e=parseInt(e,10)),typeof e=="string"&&(e=document.querySelector(e)),e instanceof Element&&(e=Math.floor(e.getBoundingClientRect().top+window.pageYOffset)),Number.isInteger(e)&&(e={top:e-i,behavior:"smooth"}),typeof e!="object")throw Error("Unsupported $scroll target: ",r);Object.assign(e,o),window.scroll(e)})}function B(n){let e=(o,r)=>{if(r[0].length<=o.length)return o;let i="\u2026";return typeof r[2]!="undefined"&&(i=r[2]),Object.prototype.hasOwnProperty.call(r[1],"ellipsis")&&(i=r[1].ellipsis),o+i};n.magic("truncate",()=>function(...o){return typeof o[0]!="string"||!o[1]?o[0]:typeof o[1]!="object"?e(o[0].slice(0,o[1]),o):Object.prototype.hasOwnProperty.call(o[1],"words")&&o[1].words?e(o[0].split(" ").splice(0,o[1].words).join(" "),o):Object.prototype.hasOwnProperty.call(o[1],"characters")&&o[1].characters?e(o[0].slice(0,o[1].characters),o):o[0]})}function L(n){n.magic("dbg",e=>function(...o){let r=o.map(i=>n.raw(i));console.log(...r)})}function x(n){let e=n.reactive({screensize:window.innerWidth}),o={xs:0,sm:640,md:768,lg:1024,xl:1280,"2xl":1536},r=window.AlpineMagicHelpersConfig&&window.AlpineMagicHelpersConfig.breakpoints?window.AlpineMagicHelpersConfig.breakpoints:o,i;window.addEventListener("resize",()=>{clearTimeout(i),i=setTimeout(()=>{e.screensize=window.innerWidth},150)}),n.magic("screen",f=>u=>{let c=e.screensize;if(Number.isInteger(u))return u<=c;if(r[u]===void 0)throw Error("Undefined $screen property: "+u+". Supported properties: "+Object.keys(r).join(", "));return r[u]<=c})}function P(n){n.magic("interval",()=>function(...e){if(typeof e[0]!="function")return e[0];let o=e[1],r=0,i=!1;typeof e[1]=="object"&&(Object.prototype.hasOwnProperty.call(e[1],"timer")&&(o=e[1].timer),Object.prototype.hasOwnProperty.call(e[1],"delay")&&(r=e[1].delay),Object.prototype.hasOwnProperty.call(e[1],"forceInterval")&&(i=e[1].forceInterval));let f=null,u=!0,c=()=>{let g=u?o+r:o;u=!1,f=setTimeout(()=>{e[0].call(this),i?c():requestAnimationFrame(c)},g)};n.effect(()=>{this.autoIntervalTest==null||this.autoIntervalTest?i?c():requestAnimationFrame(c):clearTimeout(f)})})}function C(n){j(n),X(n),B(n),L(n),x(n),P(n)}document.addEventListener("alpine:initializing",()=>{C(window.Alpine)});})();
diff --git a/build/lib/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js b/build/lib/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js
deleted file mode 100644
index 5a2d18be..00000000
--- a/build/lib/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js
+++ /dev/null
@@ -1,15 +0,0 @@
-(()=>{var _=["input","select","textarea","a[href]","button","[tabindex]:not(slot)","audio[controls]","video[controls]",'[contenteditable]:not([contenteditable="false"])',"details>summary:first-of-type","details"],k=_.join(","),K=typeof Element>"u",N=K?function(){}:Element.prototype.matches||Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector,B=!K&&Element.prototype.getRootNode?function(i){return i.getRootNode()}:function(i){return i.ownerDocument},V=function(e,t,a){var n=Array.prototype.slice.apply(e.querySelectorAll(k));return t&&N.call(e,k)&&n.unshift(e),n=n.filter(a),n},$=function i(e,t,a){for(var n=[],r=Array.from(e);r.length;){var s=r.shift();if(s.tagName==="SLOT"){var l=s.assignedElements(),m=l.length?l:s.children,p=i(m,!0,a);a.flatten?n.push.apply(n,p):n.push({scope:s,candidates:p})}else{var v=N.call(s,k);v&&a.filter(s)&&(t||!e.includes(s))&&n.push(s);var h=s.shadowRoot||typeof a.getShadowRoot=="function"&&a.getShadowRoot(s),y=!a.shadowRootFilter||a.shadowRootFilter(s);if(h&&y){var w=i(h===!0?s.children:h.children,!0,a);a.flatten?n.push.apply(n,w):n.push({scope:s,candidates:w})}else r.unshift.apply(r,s.children)}}return n},Y=function(e,t){return e.tabIndex<0&&(t||/^(AUDIO|VIDEO|DETAILS)$/.test(e.tagName)||e.isContentEditable)&&isNaN(parseInt(e.getAttribute("tabindex"),10))?0:e.tabIndex},se=function(e,t){return e.tabIndex===t.tabIndex?e.documentOrder-t.documentOrder:e.tabIndex-t.tabIndex},Z=function(e){return e.tagName==="INPUT"},ce=function(e){return Z(e)&&e.type==="hidden"},le=function(e){var t=e.tagName==="DETAILS"&&Array.prototype.slice.apply(e.children).some(function(a){return a.tagName==="SUMMARY"});return t},fe=function(e,t){for(var a=0;a<e.length;a++)if(e[a].checked&&e[a].form===t)return e[a]},de=function(e){if(!e.name)return!0;var t=e.form||B(e),a=function(l){return t.querySelectorAll('input[type="radio"][name="'+l+'"]')},n;if(typeof window<"u"&&typeof window.CSS<"u"&&typeof window.CSS.escape=="function")n=a(window.CSS.escape(e.name));else try{n=a(e.name)}catch(s){return console.error("Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s",s.message),!1}var r=fe(n,e.form);return!r||r===e},be=function(e){return Z(e)&&e.type==="radio"},ve=function(e){return be(e)&&!de(e)},W=function(e){var t=e.getBoundingClientRect(),a=t.width,n=t.height;return a===0&&n===0},he=function(e,t){var a=t.displayCheck,n=t.getShadowRoot;if(getComputedStyle(e).visibility==="hidden")return!0;var r=N.call(e,"details>summary:first-of-type"),s=r?e.parentElement:e;if(N.call(s,"details:not([open]) *"))return!0;var l=B(e).host,m=l?.ownerDocument.contains(l)||e.ownerDocument.contains(e);if(!a||a==="full"){if(typeof n=="function"){for(var p=e;e;){var v=e.parentElement,h=B(e);if(v&&!v.shadowRoot&&n(v)===!0)return W(e);e.assignedSlot?e=e.assignedSlot:!v&&h!==e.ownerDocument?e=h.host:e=v}e=p}if(m)return!e.getClientRects().length}else if(a==="non-zero-area")return W(e);return!1},pe=function(e){if(/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(e.tagName))for(var t=e.parentElement;t;){if(t.tagName==="FIELDSET"&&t.disabled){for(var a=0;a<t.children.length;a++){var n=t.children.item(a);if(n.tagName==="LEGEND")return N.call(t,"fieldset[disabled] *")?!0:!n.contains(e)}return!0}t=t.parentElement}return!1},I=function(e,t){return!(t.disabled||ce(t)||he(t,e)||le(t)||pe(t))},G=function(e,t){return!(ve(t)||Y(t)<0||!I(e,t))},ge=function(e){var t=parseInt(e.getAttribute("tabindex"),10);return!!(isNaN(t)||t>=0)},me=function i(e){var t=[],a=[];return e.forEach(function(n,r){var s=!!n.scope,l=s?n.scope:n,m=Y(l,s),p=s?i(n.candidates):l;m===0?s?t.push.apply(t,p):t.push(l):a.push({documentOrder:r,tabIndex:m,item:n,isScope:s,content:p})}),a.sort(se).reduce(function(n,r){return r.isScope?n.push.apply(n,r.content):n.push(r.content),n},[]).concat(t)},z=function(e,t){t=t||{};var a;return t.getShadowRoot?a=$([e],t.includeContainer,{filter:G.bind(null,t),flatten:!1,getShadowRoot:t.getShadowRoot,shadowRootFilter:ge}):a=V(e,t.includeContainer,G.bind(null,t)),me(a)},x=function(e,t){t=t||{};var a;return t.getShadowRoot?a=$([e],t.includeContainer,{filter:I.bind(null,t),flatten:!0,getShadowRoot:t.getShadowRoot}):a=V(e,t.includeContainer,I.bind(null,t)),a},A=function(e,t){if(t=t||{},!e)throw new Error("No node provided");return N.call(e,k)===!1?!1:G(t,e)},ye=_.concat("iframe").join(","),R=function(e,t){if(t=t||{},!e)throw new Error("No node provided");return N.call(e,ye)===!1?!1:I(t,e)};function Q(i,e){var t=Object.keys(i);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(i);e&&(a=a.filter(function(n){return Object.getOwnPropertyDescriptor(i,n).enumerable})),t.push.apply(t,a)}return t}function X(i){for(var e=1;e<arguments.length;e++){var t=arguments[e]!=null?arguments[e]:{};e%2?Q(Object(t),!0).forEach(function(a){we(i,a,t[a])}):Object.getOwnPropertyDescriptors?Object.defineProperties(i,Object.getOwnPropertyDescriptors(t)):Q(Object(t)).forEach(function(a){Object.defineProperty(i,a,Object.getOwnPropertyDescriptor(t,a))})}return i}function we(i,e,t){return e in i?Object.defineProperty(i,e,{value:t,enumerable:!0,configurable:!0,writable:!0}):i[e]=t,i}var J=function(){var i=[];return{activateTrap:function(t){if(i.length>0){var a=i[i.length-1];a!==t&&a.pause()}var n=i.indexOf(t);n===-1||i.splice(n,1),i.push(t)},deactivateTrap:function(t){var a=i.indexOf(t);a!==-1&&i.splice(a,1),i.length>0&&i[i.length-1].unpause()}}}(),Te=function(e){return e.tagName&&e.tagName.toLowerCase()==="input"&&typeof e.select=="function"},Fe=function(e){return e.key==="Escape"||e.key==="Esc"||e.keyCode===27},Se=function(e){return e.key==="Tab"||e.keyCode===9},ee=function(e){return setTimeout(e,0)},te=function(e,t){var a=-1;return e.every(function(n,r){return t(n)?(a=r,!1):!0}),a},O=function(e){for(var t=arguments.length,a=new Array(t>1?t-1:0),n=1;n<t;n++)a[n-1]=arguments[n];return typeof e=="function"?e.apply(void 0,a):e},L=function(e){return e.target.shadowRoot&&typeof e.composedPath=="function"?e.composedPath()[0]:e.target},re=function(e,t){var a=t?.document||document,n=X({returnFocusOnDeactivate:!0,escapeDeactivates:!0,delayInitialFocus:!0},t),r={containers:[],containerGroups:[],tabbableGroups:[],nodeFocusedBeforeActivation:null,mostRecentlyFocusedNode:null,active:!1,paused:!1,delayInitialFocusTimer:void 0},s,l=function(o,u,c){return o&&o[u]!==void 0?o[u]:n[c||u]},m=function(o){return r.containerGroups.findIndex(function(u){var c=u.container,b=u.tabbableNodes;return c.contains(o)||b.find(function(f){return f===o})})},p=function(o){var u=n[o];if(typeof u=="function"){for(var c=arguments.length,b=new Array(c>1?c-1:0),f=1;f<c;f++)b[f-1]=arguments[f];u=u.apply(void 0,b)}if(u===!0&&(u=void 0),!u){if(u===void 0||u===!1)return u;throw new Error("`".concat(o,"` was specified but was not a node, or did not return a node"))}var g=u;if(typeof u=="string"&&(g=a.querySelector(u),!g))throw new Error("`".concat(o,"` as selector refers to no known node"));return g},v=function(){var o=p("initialFocus");if(o===!1)return!1;if(o===void 0)if(m(a.activeElement)>=0)o=a.activeElement;else{var u=r.tabbableGroups[0],c=u&&u.firstTabbableNode;o=c||p("fallbackFocus")}if(!o)throw new Error("Your focus-trap needs to have at least one focusable element");return o},h=function(){if(r.containerGroups=r.containers.map(function(o){var u=z(o,n.tabbableOptions),c=x(o,n.tabbableOptions);return{container:o,tabbableNodes:u,focusableNodes:c,firstTabbableNode:u.length>0?u[0]:null,lastTabbableNode:u.length>0?u[u.length-1]:null,nextTabbableNode:function(f){var g=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,F=c.findIndex(function(E){return E===f});if(!(F<0))return g?c.slice(F+1).find(function(E){return A(E,n.tabbableOptions)}):c.slice(0,F).reverse().find(function(E){return A(E,n.tabbableOptions)})}}}),r.tabbableGroups=r.containerGroups.filter(function(o){return o.tabbableNodes.length>0}),r.tabbableGroups.length<=0&&!p("fallbackFocus"))throw new Error("Your focus-trap must have at least one container with at least one tabbable node in it at all times")},y=function d(o){if(o!==!1&&o!==a.activeElement){if(!o||!o.focus){d(v());return}o.focus({preventScroll:!!n.preventScroll}),r.mostRecentlyFocusedNode=o,Te(o)&&o.select()}},w=function(o){var u=p("setReturnFocus",o);return u||(u===!1?!1:o)},S=function(o){var u=L(o);if(!(m(u)>=0)){if(O(n.clickOutsideDeactivates,o)){s.deactivate({returnFocus:n.returnFocusOnDeactivate&&!R(u,n.tabbableOptions)});return}O(n.allowOutsideClick,o)||o.preventDefault()}},D=function(o){var u=L(o),c=m(u)>=0;c||u instanceof Document?c&&(r.mostRecentlyFocusedNode=u):(o.stopImmediatePropagation(),y(r.mostRecentlyFocusedNode||v()))},T=function(o){var u=L(o);h();var c=null;if(r.tabbableGroups.length>0){var b=m(u),f=b>=0?r.containerGroups[b]:void 0;if(b<0)o.shiftKey?c=r.tabbableGroups[r.tabbableGroups.length-1].lastTabbableNode:c=r.tabbableGroups[0].firstTabbableNode;else if(o.shiftKey){var g=te(r.tabbableGroups,function(P){var j=P.firstTabbableNode;return u===j});if(g<0&&(f.container===u||R(u,n.tabbableOptions)&&!A(u,n.tabbableOptions)&&!f.nextTabbableNode(u,!1))&&(g=b),g>=0){var F=g===0?r.tabbableGroups.length-1:g-1,E=r.tabbableGroups[F];c=E.lastTabbableNode}}else{var C=te(r.tabbableGroups,function(P){var j=P.lastTabbableNode;return u===j});if(C<0&&(f.container===u||R(u,n.tabbableOptions)&&!A(u,n.tabbableOptions)&&!f.nextTabbableNode(u))&&(C=b),C>=0){var oe=C===r.tabbableGroups.length-1?0:C+1,ue=r.tabbableGroups[oe];c=ue.firstTabbableNode}}}else c=p("fallbackFocus");c&&(o.preventDefault(),y(c))},M=function(o){if(Fe(o)&&O(n.escapeDeactivates,o)!==!1){o.preventDefault(),s.deactivate();return}if(Se(o)){T(o);return}},q=function(o){var u=L(o);m(u)>=0||O(n.clickOutsideDeactivates,o)||O(n.allowOutsideClick,o)||(o.preventDefault(),o.stopImmediatePropagation())},H=function(){if(r.active)return J.activateTrap(s),r.delayInitialFocusTimer=n.delayInitialFocus?ee(function(){y(v())}):y(v()),a.addEventListener("focusin",D,!0),a.addEventListener("mousedown",S,{capture:!0,passive:!1}),a.addEventListener("touchstart",S,{capture:!0,passive:!1}),a.addEventListener("click",q,{capture:!0,passive:!1}),a.addEventListener("keydown",M,{capture:!0,passive:!1}),s},U=function(){if(r.active)return a.removeEventListener("focusin",D,!0),a.removeEventListener("mousedown",S,!0),a.removeEventListener("touchstart",S,!0),a.removeEventListener("click",q,!0),a.removeEventListener("keydown",M,!0),s};return s={get active(){return r.active},get paused(){return r.paused},activate:function(o){if(r.active)return this;var u=l(o,"onActivate"),c=l(o,"onPostActivate"),b=l(o,"checkCanFocusTrap");b||h(),r.active=!0,r.paused=!1,r.nodeFocusedBeforeActivation=a.activeElement,u&&u();var f=function(){b&&h(),H(),c&&c()};return b?(b(r.containers.concat()).then(f,f),this):(f(),this)},deactivate:function(o){if(!r.active)return this;var u=X({onDeactivate:n.onDeactivate,onPostDeactivate:n.onPostDeactivate,checkCanReturnFocus:n.checkCanReturnFocus},o);clearTimeout(r.delayInitialFocusTimer),r.delayInitialFocusTimer=void 0,U(),r.active=!1,r.paused=!1,J.deactivateTrap(s);var c=l(u,"onDeactivate"),b=l(u,"onPostDeactivate"),f=l(u,"checkCanReturnFocus"),g=l(u,"returnFocus","returnFocusOnDeactivate");c&&c();var F=function(){ee(function(){g&&y(w(r.nodeFocusedBeforeActivation)),b&&b()})};return g&&f?(f(w(r.nodeFocusedBeforeActivation)).then(F,F),this):(F(),this)},pause:function(){return r.paused||!r.active?this:(r.paused=!0,U(),this)},unpause:function(){return!r.paused||!r.active?this:(r.paused=!1,h(),H(),this)},updateContainerElements:function(o){var u=[].concat(o).filter(Boolean);return r.containers=u.map(function(c){return typeof c=="string"?a.querySelector(c):c}),r.active&&h(),this}},s.updateContainerElements(e),s};function ne(i){let e,t;window.addEventListener("focusin",()=>{e=t,t=document.activeElement}),i.magic("focus",a=>{let n=a;return{__noscroll:!1,__wrapAround:!1,within(r){return n=r,this},withoutScrolling(){return this.__noscroll=!0,this},noscroll(){return this.__noscroll=!0,this},withWrapAround(){return this.__wrapAround=!0,this},wrap(){return this.withWrapAround()},focusable(r){return R(r)},previouslyFocused(){return e},lastFocused(){return e},focused(){return t},focusables(){return Array.isArray(n)?n:x(n,{displayCheck:"none"})},all(){return this.focusables()},isFirst(r){let s=this.all();return s[0]&&s[0].isSameNode(r)},isLast(r){let s=this.all();return s.length&&s.slice(-1)[0].isSameNode(r)},getFirst(){return this.all()[0]},getLast(){return this.all().slice(-1)[0]},getNext(){let r=this.all(),s=document.activeElement;if(r.indexOf(s)!==-1)return this.__wrapAround&&r.indexOf(s)===r.length-1?r[0]:r[r.indexOf(s)+1]},getPrevious(){let r=this.all(),s=document.activeElement;if(r.indexOf(s)!==-1)return this.__wrapAround&&r.indexOf(s)===0?r.slice(-1)[0]:r[r.indexOf(s)-1]},first(){this.focus(this.getFirst())},last(){this.focus(this.getLast())},next(){this.focus(this.getNext())},previous(){this.focus(this.getPrevious())},prev(){return this.previous()},focus(r){r&&setTimeout(()=>{r.hasAttribute("tabindex")||r.setAttribute("tabindex","0"),r.focus({preventScroll:this.__noscroll})})}}}),i.directive("trap",i.skipDuringClone((a,{expression:n,modifiers:r},{effect:s,evaluateLater:l,cleanup:m})=>{let p=l(n),v=!1,h={escapeDeactivates:!1,allowOutsideClick:!0,fallbackFocus:()=>a};if(r.includes("noautofocus"))h.initialFocus=!1;else{let T=a.querySelector("[autofocus]");T&&(h.initialFocus=T)}let y=re(a,h),w=()=>{},S=()=>{},D=()=>{w(),w=()=>{},S(),S=()=>{},y.deactivate({returnFocus:!r.includes("noreturn")})};s(()=>p(T=>{v!==T&&(T&&!v&&(r.includes("noscroll")&&(S=Ee()),r.includes("inert")&&(w=ae(a)),setTimeout(()=>{y.activate()},15)),!T&&v&&D(),v=!!T)})),m(D)},(a,{expression:n,modifiers:r},{evaluate:s})=>{r.includes("inert")&&s(n)&&ae(a)}))}function ae(i){let e=[];return ie(i,t=>{let a=t.hasAttribute("aria-hidden");t.setAttribute("aria-hidden","true"),e.push(()=>a||t.removeAttribute("aria-hidden"))}),()=>{for(;e.length;)e.pop()()}}function ie(i,e){i.isSameNode(document.body)||!i.parentNode||Array.from(i.parentNode.children).forEach(t=>{t.isSameNode(i)?ie(i.parentNode,e):e(t)})}function Ee(){let i=document.documentElement.style.overflow,e=document.documentElement.style.paddingRight,t=window.innerWidth-document.documentElement.clientWidth;return document.documentElement.style.overflow="hidden",document.documentElement.style.paddingRight=`${t}px`,()=>{document.documentElement.style.overflow=i,document.documentElement.style.paddingRight=e}}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(ne)});})();
-/*! Bundled license information:
-
-tabbable/dist/index.esm.js:
-  (*!
-  * tabbable 5.3.3
-  * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
-  *)
-
-focus-trap/dist/focus-trap.esm.js:
-  (*!
-  * focus-trap 6.9.4
-  * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
-  *)
-*/
diff --git a/build/lib/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js b/build/lib/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js
deleted file mode 100644
index 2342257f..00000000
--- a/build/lib/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(()=>{function o(e){e.directive("intersect",e.skipDuringClone((t,{value:i,expression:l,modifiers:n},{evaluateLater:r,cleanup:c})=>{let s=r(l),a={rootMargin:x(n),threshold:f(n)},u=new IntersectionObserver(d=>{d.forEach(h=>{h.isIntersecting!==(i==="leave")&&(s(),n.includes("once")&&u.disconnect())})},a);u.observe(t),c(()=>{u.disconnect()})}))}function f(e){if(e.includes("full"))return .99;if(e.includes("half"))return .5;if(!e.includes("threshold"))return 0;let t=e[e.indexOf("threshold")+1];return t==="100"?1:t==="0"?0:Number(`.${t}`)}function p(e){let t=e.match(/^(-?[0-9]+)(px|%)?$/);return t?t[1]+(t[2]||"px"):void 0}function x(e){let t="margin",i="0px 0px 0px 0px",l=e.indexOf(t);if(l===-1)return i;let n=[];for(let r=1;r<5;r++)n.push(p(e[l+r]||""));return n=n.filter(r=>r!==void 0),n.length?n.join(" ").trim():i}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(o)});})();
diff --git a/build/lib/exo/tinychat/static/cdn.jsdelivr.net/npm/purecss@3.0.0/build/base-min.css b/build/lib/exo/tinychat/static/cdn.jsdelivr.net/npm/purecss@3.0.0/build/base-min.css
deleted file mode 100644
index df4fbc05..00000000
--- a/build/lib/exo/tinychat/static/cdn.jsdelivr.net/npm/purecss@3.0.0/build/base-min.css
+++ /dev/null
@@ -1,11 +0,0 @@
-/*!
-Pure v3.0.0
-Copyright 2013 Yahoo!
-Licensed under the BSD License.
-https://github.com/pure-css/pure/blob/master/LICENSE
-*/
-/*!
-normalize.css v | MIT License | https://necolas.github.io/normalize.css/
-Copyright (c) Nicolas Gallagher and Jonathan Neal
-*/
-/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}
\ No newline at end of file
diff --git a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css b/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css
deleted file mode 100644
index 45072b30..00000000
--- a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css
+++ /dev/null
@@ -1,9 +0,0 @@
-/*!
- * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com
- * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
- * Copyright 2024 Fonticons, Inc.
- */
-.fa{font-family:var(--fa-style-family,"Font Awesome 6 Free");font-weight:var(--fa-style,900)}.fa,.fa-brands,.fa-classic,.fa-regular,.fa-sharp,.fa-solid,.fab,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:var(--fa-display,inline-block);font-style:normal;font-variant:normal;line-height:1;text-rendering:auto}.fa-classic,.fa-regular,.fa-solid,.far,.fas{font-family:"Font Awesome 6 Free"}.fa-brands,.fab{font-family:"Font Awesome 6 Brands"}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.08333em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.07143em;vertical-align:.05357em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.04167em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(var(--fa-li-width, 2em)*-1);position:absolute;text-align:center;width:var(--fa-li-width,2em);line-height:inherit}.fa-border{border-radius:var(--fa-border-radius,.1em);border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);padding:var(--fa-border-padding,.2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.fa-beat{-webkit-animation-name:fa-beat;animation-name:fa-beat;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{-webkit-animation-name:fa-bounce;animation-name:fa-bounce;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1))}.fa-fade{-webkit-animation-name:fa-fade;animation-name:fa-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-beat-fade,.fa-fade{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s)}.fa-beat-fade{-webkit-animation-name:fa-beat-fade;animation-name:fa-beat-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-flip{-webkit-animation-name:fa-flip;animation-name:fa-flip;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{-webkit-animation-name:fa-shake;animation-name:fa-shake;-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-shake,.fa-spin{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal)}.fa-spin{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-duration:var(--fa-animation-duration,2s);animation-duration:var(--fa-animation-duration,2s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,steps(8));animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-beat-fade,.fa-bounce,.fa-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{-webkit-animation-delay:-1ms;animation-delay:-1ms;-webkit-animation-duration:1ms;animation-duration:1ms;-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-transition-delay:0s;transition-delay:0s;-webkit-transition-duration:0s;transition-duration:0s}}@-webkit-keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@-webkit-keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@-webkit-keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@-webkit-keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@-webkit-keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@-webkit-keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}.fa-rotate-by{-webkit-transform:rotate(var(--fa-rotate-angle,0));transform:rotate(var(--fa-rotate-angle,0))}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%;z-index:var(--fa-stack-z-index,auto)}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:var(--fa-inverse,#fff)}
-
-.fa-0:before{content:"\30"}.fa-1:before{content:"\31"}.fa-2:before{content:"\32"}.fa-3:before{content:"\33"}.fa-4:before{content:"\34"}.fa-5:before{content:"\35"}.fa-6:before{content:"\36"}.fa-7:before{content:"\37"}.fa-8:before{content:"\38"}.fa-9:before{content:"\39"}.fa-fill-drip:before{content:"\f576"}.fa-arrows-to-circle:before{content:"\e4bd"}.fa-chevron-circle-right:before,.fa-circle-chevron-right:before{content:"\f138"}.fa-at:before{content:"\40"}.fa-trash-alt:before,.fa-trash-can:before{content:"\f2ed"}.fa-text-height:before{content:"\f034"}.fa-user-times:before,.fa-user-xmark:before{content:"\f235"}.fa-stethoscope:before{content:"\f0f1"}.fa-comment-alt:before,.fa-message:before{content:"\f27a"}.fa-info:before{content:"\f129"}.fa-compress-alt:before,.fa-down-left-and-up-right-to-center:before{content:"\f422"}.fa-explosion:before{content:"\e4e9"}.fa-file-alt:before,.fa-file-lines:before,.fa-file-text:before{content:"\f15c"}.fa-wave-square:before{content:"\f83e"}.fa-ring:before{content:"\f70b"}.fa-building-un:before{content:"\e4d9"}.fa-dice-three:before{content:"\f527"}.fa-calendar-alt:before,.fa-calendar-days:before{content:"\f073"}.fa-anchor-circle-check:before{content:"\e4aa"}.fa-building-circle-arrow-right:before{content:"\e4d1"}.fa-volleyball-ball:before,.fa-volleyball:before{content:"\f45f"}.fa-arrows-up-to-line:before{content:"\e4c2"}.fa-sort-desc:before,.fa-sort-down:before{content:"\f0dd"}.fa-circle-minus:before,.fa-minus-circle:before{content:"\f056"}.fa-door-open:before{content:"\f52b"}.fa-right-from-bracket:before,.fa-sign-out-alt:before{content:"\f2f5"}.fa-atom:before{content:"\f5d2"}.fa-soap:before{content:"\e06e"}.fa-heart-music-camera-bolt:before,.fa-icons:before{content:"\f86d"}.fa-microphone-alt-slash:before,.fa-microphone-lines-slash:before{content:"\f539"}.fa-bridge-circle-check:before{content:"\e4c9"}.fa-pump-medical:before{content:"\e06a"}.fa-fingerprint:before{content:"\f577"}.fa-hand-point-right:before{content:"\f0a4"}.fa-magnifying-glass-location:before,.fa-search-location:before{content:"\f689"}.fa-forward-step:before,.fa-step-forward:before{content:"\f051"}.fa-face-smile-beam:before,.fa-smile-beam:before{content:"\f5b8"}.fa-flag-checkered:before{content:"\f11e"}.fa-football-ball:before,.fa-football:before{content:"\f44e"}.fa-school-circle-exclamation:before{content:"\e56c"}.fa-crop:before{content:"\f125"}.fa-angle-double-down:before,.fa-angles-down:before{content:"\f103"}.fa-users-rectangle:before{content:"\e594"}.fa-people-roof:before{content:"\e537"}.fa-people-line:before{content:"\e534"}.fa-beer-mug-empty:before,.fa-beer:before{content:"\f0fc"}.fa-diagram-predecessor:before{content:"\e477"}.fa-arrow-up-long:before,.fa-long-arrow-up:before{content:"\f176"}.fa-burn:before,.fa-fire-flame-simple:before{content:"\f46a"}.fa-male:before,.fa-person:before{content:"\f183"}.fa-laptop:before{content:"\f109"}.fa-file-csv:before{content:"\f6dd"}.fa-menorah:before{content:"\f676"}.fa-truck-plane:before{content:"\e58f"}.fa-record-vinyl:before{content:"\f8d9"}.fa-face-grin-stars:before,.fa-grin-stars:before{content:"\f587"}.fa-bong:before{content:"\f55c"}.fa-pastafarianism:before,.fa-spaghetti-monster-flying:before{content:"\f67b"}.fa-arrow-down-up-across-line:before{content:"\e4af"}.fa-spoon:before,.fa-utensil-spoon:before{content:"\f2e5"}.fa-jar-wheat:before{content:"\e517"}.fa-envelopes-bulk:before,.fa-mail-bulk:before{content:"\f674"}.fa-file-circle-exclamation:before{content:"\e4eb"}.fa-circle-h:before,.fa-hospital-symbol:before{content:"\f47e"}.fa-pager:before{content:"\f815"}.fa-address-book:before,.fa-contact-book:before{content:"\f2b9"}.fa-strikethrough:before{content:"\f0cc"}.fa-k:before{content:"\4b"}.fa-landmark-flag:before{content:"\e51c"}.fa-pencil-alt:before,.fa-pencil:before{content:"\f303"}.fa-backward:before{content:"\f04a"}.fa-caret-right:before{content:"\f0da"}.fa-comments:before{content:"\f086"}.fa-file-clipboard:before,.fa-paste:before{content:"\f0ea"}.fa-code-pull-request:before{content:"\e13c"}.fa-clipboard-list:before{content:"\f46d"}.fa-truck-loading:before,.fa-truck-ramp-box:before{content:"\f4de"}.fa-user-check:before{content:"\f4fc"}.fa-vial-virus:before{content:"\e597"}.fa-sheet-plastic:before{content:"\e571"}.fa-blog:before{content:"\f781"}.fa-user-ninja:before{content:"\f504"}.fa-person-arrow-up-from-line:before{content:"\e539"}.fa-scroll-torah:before,.fa-torah:before{content:"\f6a0"}.fa-broom-ball:before,.fa-quidditch-broom-ball:before,.fa-quidditch:before{content:"\f458"}.fa-toggle-off:before{content:"\f204"}.fa-archive:before,.fa-box-archive:before{content:"\f187"}.fa-person-drowning:before{content:"\e545"}.fa-arrow-down-9-1:before,.fa-sort-numeric-desc:before,.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-face-grin-tongue-squint:before,.fa-grin-tongue-squint:before{content:"\f58a"}.fa-spray-can:before{content:"\f5bd"}.fa-truck-monster:before{content:"\f63b"}.fa-w:before{content:"\57"}.fa-earth-africa:before,.fa-globe-africa:before{content:"\f57c"}.fa-rainbow:before{content:"\f75b"}.fa-circle-notch:before{content:"\f1ce"}.fa-tablet-alt:before,.fa-tablet-screen-button:before{content:"\f3fa"}.fa-paw:before{content:"\f1b0"}.fa-cloud:before{content:"\f0c2"}.fa-trowel-bricks:before{content:"\e58a"}.fa-face-flushed:before,.fa-flushed:before{content:"\f579"}.fa-hospital-user:before{content:"\f80d"}.fa-tent-arrow-left-right:before{content:"\e57f"}.fa-gavel:before,.fa-legal:before{content:"\f0e3"}.fa-binoculars:before{content:"\f1e5"}.fa-microphone-slash:before{content:"\f131"}.fa-box-tissue:before{content:"\e05b"}.fa-motorcycle:before{content:"\f21c"}.fa-bell-concierge:before,.fa-concierge-bell:before{content:"\f562"}.fa-pen-ruler:before,.fa-pencil-ruler:before{content:"\f5ae"}.fa-people-arrows-left-right:before,.fa-people-arrows:before{content:"\e068"}.fa-mars-and-venus-burst:before{content:"\e523"}.fa-caret-square-right:before,.fa-square-caret-right:before{content:"\f152"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-sun-plant-wilt:before{content:"\e57a"}.fa-toilets-portable:before{content:"\e584"}.fa-hockey-puck:before{content:"\f453"}.fa-table:before{content:"\f0ce"}.fa-magnifying-glass-arrow-right:before{content:"\e521"}.fa-digital-tachograph:before,.fa-tachograph-digital:before{content:"\f566"}.fa-users-slash:before{content:"\e073"}.fa-clover:before{content:"\e139"}.fa-mail-reply:before,.fa-reply:before{content:"\f3e5"}.fa-star-and-crescent:before{content:"\f699"}.fa-house-fire:before{content:"\e50c"}.fa-minus-square:before,.fa-square-minus:before{content:"\f146"}.fa-helicopter:before{content:"\f533"}.fa-compass:before{content:"\f14e"}.fa-caret-square-down:before,.fa-square-caret-down:before{content:"\f150"}.fa-file-circle-question:before{content:"\e4ef"}.fa-laptop-code:before{content:"\f5fc"}.fa-swatchbook:before{content:"\f5c3"}.fa-prescription-bottle:before{content:"\f485"}.fa-bars:before,.fa-navicon:before{content:"\f0c9"}.fa-people-group:before{content:"\e533"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-heart-broken:before,.fa-heart-crack:before{content:"\f7a9"}.fa-external-link-square-alt:before,.fa-square-up-right:before{content:"\f360"}.fa-face-kiss-beam:before,.fa-kiss-beam:before{content:"\f597"}.fa-film:before{content:"\f008"}.fa-ruler-horizontal:before{content:"\f547"}.fa-people-robbery:before{content:"\e536"}.fa-lightbulb:before{content:"\f0eb"}.fa-caret-left:before{content:"\f0d9"}.fa-circle-exclamation:before,.fa-exclamation-circle:before{content:"\f06a"}.fa-school-circle-xmark:before{content:"\e56d"}.fa-arrow-right-from-bracket:before,.fa-sign-out:before{content:"\f08b"}.fa-chevron-circle-down:before,.fa-circle-chevron-down:before{content:"\f13a"}.fa-unlock-alt:before,.fa-unlock-keyhole:before{content:"\f13e"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-headphones-alt:before,.fa-headphones-simple:before{content:"\f58f"}.fa-sitemap:before{content:"\f0e8"}.fa-circle-dollar-to-slot:before,.fa-donate:before{content:"\f4b9"}.fa-memory:before{content:"\f538"}.fa-road-spikes:before{content:"\e568"}.fa-fire-burner:before{content:"\e4f1"}.fa-flag:before{content:"\f024"}.fa-hanukiah:before{content:"\f6e6"}.fa-feather:before{content:"\f52d"}.fa-volume-down:before,.fa-volume-low:before{content:"\f027"}.fa-comment-slash:before{content:"\f4b3"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-compress:before{content:"\f066"}.fa-wheat-alt:before,.fa-wheat-awn:before{content:"\e2cd"}.fa-ankh:before{content:"\f644"}.fa-hands-holding-child:before{content:"\e4fa"}.fa-asterisk:before{content:"\2a"}.fa-check-square:before,.fa-square-check:before{content:"\f14a"}.fa-peseta-sign:before{content:"\e221"}.fa-header:before,.fa-heading:before{content:"\f1dc"}.fa-ghost:before{content:"\f6e2"}.fa-list-squares:before,.fa-list:before{content:"\f03a"}.fa-phone-square-alt:before,.fa-square-phone-flip:before{content:"\f87b"}.fa-cart-plus:before{content:"\f217"}.fa-gamepad:before{content:"\f11b"}.fa-circle-dot:before,.fa-dot-circle:before{content:"\f192"}.fa-dizzy:before,.fa-face-dizzy:before{content:"\f567"}.fa-egg:before{content:"\f7fb"}.fa-house-medical-circle-xmark:before{content:"\e513"}.fa-campground:before{content:"\f6bb"}.fa-folder-plus:before{content:"\f65e"}.fa-futbol-ball:before,.fa-futbol:before,.fa-soccer-ball:before{content:"\f1e3"}.fa-paint-brush:before,.fa-paintbrush:before{content:"\f1fc"}.fa-lock:before{content:"\f023"}.fa-gas-pump:before{content:"\f52f"}.fa-hot-tub-person:before,.fa-hot-tub:before{content:"\f593"}.fa-map-location:before,.fa-map-marked:before{content:"\f59f"}.fa-house-flood-water:before{content:"\e50e"}.fa-tree:before{content:"\f1bb"}.fa-bridge-lock:before{content:"\e4cc"}.fa-sack-dollar:before{content:"\f81d"}.fa-edit:before,.fa-pen-to-square:before{content:"\f044"}.fa-car-side:before{content:"\f5e4"}.fa-share-alt:before,.fa-share-nodes:before{content:"\f1e0"}.fa-heart-circle-minus:before{content:"\e4ff"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-microscope:before{content:"\f610"}.fa-sink:before{content:"\e06d"}.fa-bag-shopping:before,.fa-shopping-bag:before{content:"\f290"}.fa-arrow-down-z-a:before,.fa-sort-alpha-desc:before,.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-mitten:before{content:"\f7b5"}.fa-person-rays:before{content:"\e54d"}.fa-users:before{content:"\f0c0"}.fa-eye-slash:before{content:"\f070"}.fa-flask-vial:before{content:"\e4f3"}.fa-hand-paper:before,.fa-hand:before{content:"\f256"}.fa-om:before{content:"\f679"}.fa-worm:before{content:"\e599"}.fa-house-circle-xmark:before{content:"\e50b"}.fa-plug:before{content:"\f1e6"}.fa-chevron-up:before{content:"\f077"}.fa-hand-spock:before{content:"\f259"}.fa-stopwatch:before{content:"\f2f2"}.fa-face-kiss:before,.fa-kiss:before{content:"\f596"}.fa-bridge-circle-xmark:before{content:"\e4cb"}.fa-face-grin-tongue:before,.fa-grin-tongue:before{content:"\f589"}.fa-chess-bishop:before{content:"\f43a"}.fa-face-grin-wink:before,.fa-grin-wink:before{content:"\f58c"}.fa-deaf:before,.fa-deafness:before,.fa-ear-deaf:before,.fa-hard-of-hearing:before{content:"\f2a4"}.fa-road-circle-check:before{content:"\e564"}.fa-dice-five:before{content:"\f523"}.fa-rss-square:before,.fa-square-rss:before{content:"\f143"}.fa-land-mine-on:before{content:"\e51b"}.fa-i-cursor:before{content:"\f246"}.fa-stamp:before{content:"\f5bf"}.fa-stairs:before{content:"\e289"}.fa-i:before{content:"\49"}.fa-hryvnia-sign:before,.fa-hryvnia:before{content:"\f6f2"}.fa-pills:before{content:"\f484"}.fa-face-grin-wide:before,.fa-grin-alt:before{content:"\f581"}.fa-tooth:before{content:"\f5c9"}.fa-v:before{content:"\56"}.fa-bangladeshi-taka-sign:before{content:"\e2e6"}.fa-bicycle:before{content:"\f206"}.fa-rod-asclepius:before,.fa-rod-snake:before,.fa-staff-aesculapius:before,.fa-staff-snake:before{content:"\e579"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-ambulance:before,.fa-truck-medical:before{content:"\f0f9"}.fa-wheat-awn-circle-exclamation:before{content:"\e598"}.fa-snowman:before{content:"\f7d0"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-road-barrier:before{content:"\e562"}.fa-school:before{content:"\f549"}.fa-igloo:before{content:"\f7ae"}.fa-joint:before{content:"\f595"}.fa-angle-right:before{content:"\f105"}.fa-horse:before{content:"\f6f0"}.fa-q:before{content:"\51"}.fa-g:before{content:"\47"}.fa-notes-medical:before{content:"\f481"}.fa-temperature-2:before,.fa-temperature-half:before,.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-dong-sign:before{content:"\e169"}.fa-capsules:before{content:"\f46b"}.fa-poo-bolt:before,.fa-poo-storm:before{content:"\f75a"}.fa-face-frown-open:before,.fa-frown-open:before{content:"\f57a"}.fa-hand-point-up:before{content:"\f0a6"}.fa-money-bill:before{content:"\f0d6"}.fa-bookmark:before{content:"\f02e"}.fa-align-justify:before{content:"\f039"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-helmet-un:before{content:"\e503"}.fa-bullseye:before{content:"\f140"}.fa-bacon:before{content:"\f7e5"}.fa-hand-point-down:before{content:"\f0a7"}.fa-arrow-up-from-bracket:before{content:"\e09a"}.fa-folder-blank:before,.fa-folder:before{content:"\f07b"}.fa-file-medical-alt:before,.fa-file-waveform:before{content:"\f478"}.fa-radiation:before{content:"\f7b9"}.fa-chart-simple:before{content:"\e473"}.fa-mars-stroke:before{content:"\f229"}.fa-vial:before{content:"\f492"}.fa-dashboard:before,.fa-gauge-med:before,.fa-gauge:before,.fa-tachometer-alt-average:before{content:"\f624"}.fa-magic-wand-sparkles:before,.fa-wand-magic-sparkles:before{content:"\e2ca"}.fa-e:before{content:"\45"}.fa-pen-alt:before,.fa-pen-clip:before{content:"\f305"}.fa-bridge-circle-exclamation:before{content:"\e4ca"}.fa-user:before{content:"\f007"}.fa-school-circle-check:before{content:"\e56b"}.fa-dumpster:before{content:"\f793"}.fa-shuttle-van:before,.fa-van-shuttle:before{content:"\f5b6"}.fa-building-user:before{content:"\e4da"}.fa-caret-square-left:before,.fa-square-caret-left:before{content:"\f191"}.fa-highlighter:before{content:"\f591"}.fa-key:before{content:"\f084"}.fa-bullhorn:before{content:"\f0a1"}.fa-globe:before{content:"\f0ac"}.fa-synagogue:before{content:"\f69b"}.fa-person-half-dress:before{content:"\e548"}.fa-road-bridge:before{content:"\e563"}.fa-location-arrow:before{content:"\f124"}.fa-c:before{content:"\43"}.fa-tablet-button:before{content:"\f10a"}.fa-building-lock:before{content:"\e4d6"}.fa-pizza-slice:before{content:"\f818"}.fa-money-bill-wave:before{content:"\f53a"}.fa-area-chart:before,.fa-chart-area:before{content:"\f1fe"}.fa-house-flag:before{content:"\e50d"}.fa-person-circle-minus:before{content:"\e540"}.fa-ban:before,.fa-cancel:before{content:"\f05e"}.fa-camera-rotate:before{content:"\e0d8"}.fa-air-freshener:before,.fa-spray-can-sparkles:before{content:"\f5d0"}.fa-star:before{content:"\f005"}.fa-repeat:before{content:"\f363"}.fa-cross:before{content:"\f654"}.fa-box:before{content:"\f466"}.fa-venus-mars:before{content:"\f228"}.fa-arrow-pointer:before,.fa-mouse-pointer:before{content:"\f245"}.fa-expand-arrows-alt:before,.fa-maximize:before{content:"\f31e"}.fa-charging-station:before{content:"\f5e7"}.fa-shapes:before,.fa-triangle-circle-square:before{content:"\f61f"}.fa-random:before,.fa-shuffle:before{content:"\f074"}.fa-person-running:before,.fa-running:before{content:"\f70c"}.fa-mobile-retro:before{content:"\e527"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-spider:before{content:"\f717"}.fa-hands-bound:before{content:"\e4f9"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-plane-circle-exclamation:before{content:"\e556"}.fa-x-ray:before{content:"\f497"}.fa-spell-check:before{content:"\f891"}.fa-slash:before{content:"\f715"}.fa-computer-mouse:before,.fa-mouse:before{content:"\f8cc"}.fa-arrow-right-to-bracket:before,.fa-sign-in:before{content:"\f090"}.fa-shop-slash:before,.fa-store-alt-slash:before{content:"\e070"}.fa-server:before{content:"\f233"}.fa-virus-covid-slash:before{content:"\e4a9"}.fa-shop-lock:before{content:"\e4a5"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-blender-phone:before{content:"\f6b6"}.fa-building-wheat:before{content:"\e4db"}.fa-person-breastfeeding:before{content:"\e53a"}.fa-right-to-bracket:before,.fa-sign-in-alt:before{content:"\f2f6"}.fa-venus:before{content:"\f221"}.fa-passport:before{content:"\f5ab"}.fa-heart-pulse:before,.fa-heartbeat:before{content:"\f21e"}.fa-people-carry-box:before,.fa-people-carry:before{content:"\f4ce"}.fa-temperature-high:before{content:"\f769"}.fa-microchip:before{content:"\f2db"}.fa-crown:before{content:"\f521"}.fa-weight-hanging:before{content:"\f5cd"}.fa-xmarks-lines:before{content:"\e59a"}.fa-file-prescription:before{content:"\f572"}.fa-weight-scale:before,.fa-weight:before{content:"\f496"}.fa-user-friends:before,.fa-user-group:before{content:"\f500"}.fa-arrow-up-a-z:before,.fa-sort-alpha-up:before{content:"\f15e"}.fa-chess-knight:before{content:"\f441"}.fa-face-laugh-squint:before,.fa-laugh-squint:before{content:"\f59b"}.fa-wheelchair:before{content:"\f193"}.fa-arrow-circle-up:before,.fa-circle-arrow-up:before{content:"\f0aa"}.fa-toggle-on:before{content:"\f205"}.fa-person-walking:before,.fa-walking:before{content:"\f554"}.fa-l:before{content:"\4c"}.fa-fire:before{content:"\f06d"}.fa-bed-pulse:before,.fa-procedures:before{content:"\f487"}.fa-shuttle-space:before,.fa-space-shuttle:before{content:"\f197"}.fa-face-laugh:before,.fa-laugh:before{content:"\f599"}.fa-folder-open:before{content:"\f07c"}.fa-heart-circle-plus:before{content:"\e500"}.fa-code-fork:before{content:"\e13b"}.fa-city:before{content:"\f64f"}.fa-microphone-alt:before,.fa-microphone-lines:before{content:"\f3c9"}.fa-pepper-hot:before{content:"\f816"}.fa-unlock:before{content:"\f09c"}.fa-colon-sign:before{content:"\e140"}.fa-headset:before{content:"\f590"}.fa-store-slash:before{content:"\e071"}.fa-road-circle-xmark:before{content:"\e566"}.fa-user-minus:before{content:"\f503"}.fa-mars-stroke-up:before,.fa-mars-stroke-v:before{content:"\f22a"}.fa-champagne-glasses:before,.fa-glass-cheers:before{content:"\f79f"}.fa-clipboard:before{content:"\f328"}.fa-house-circle-exclamation:before{content:"\e50a"}.fa-file-arrow-up:before,.fa-file-upload:before{content:"\f574"}.fa-wifi-3:before,.fa-wifi-strong:before,.fa-wifi:before{content:"\f1eb"}.fa-bath:before,.fa-bathtub:before{content:"\f2cd"}.fa-underline:before{content:"\f0cd"}.fa-user-edit:before,.fa-user-pen:before{content:"\f4ff"}.fa-signature:before{content:"\f5b7"}.fa-stroopwafel:before{content:"\f551"}.fa-bold:before{content:"\f032"}.fa-anchor-lock:before{content:"\e4ad"}.fa-building-ngo:before{content:"\e4d7"}.fa-manat-sign:before{content:"\e1d5"}.fa-not-equal:before{content:"\f53e"}.fa-border-style:before,.fa-border-top-left:before{content:"\f853"}.fa-map-location-dot:before,.fa-map-marked-alt:before{content:"\f5a0"}.fa-jedi:before{content:"\f669"}.fa-poll:before,.fa-square-poll-vertical:before{content:"\f681"}.fa-mug-hot:before{content:"\f7b6"}.fa-battery-car:before,.fa-car-battery:before{content:"\f5df"}.fa-gift:before{content:"\f06b"}.fa-dice-two:before{content:"\f528"}.fa-chess-queen:before{content:"\f445"}.fa-glasses:before{content:"\f530"}.fa-chess-board:before{content:"\f43c"}.fa-building-circle-check:before{content:"\e4d2"}.fa-person-chalkboard:before{content:"\e53d"}.fa-mars-stroke-h:before,.fa-mars-stroke-right:before{content:"\f22b"}.fa-hand-back-fist:before,.fa-hand-rock:before{content:"\f255"}.fa-caret-square-up:before,.fa-square-caret-up:before{content:"\f151"}.fa-cloud-showers-water:before{content:"\e4e4"}.fa-bar-chart:before,.fa-chart-bar:before{content:"\f080"}.fa-hands-bubbles:before,.fa-hands-wash:before{content:"\e05e"}.fa-less-than-equal:before{content:"\f537"}.fa-train:before{content:"\f238"}.fa-eye-low-vision:before,.fa-low-vision:before{content:"\f2a8"}.fa-crow:before{content:"\f520"}.fa-sailboat:before{content:"\e445"}.fa-window-restore:before{content:"\f2d2"}.fa-plus-square:before,.fa-square-plus:before{content:"\f0fe"}.fa-torii-gate:before{content:"\f6a1"}.fa-frog:before{content:"\f52e"}.fa-bucket:before{content:"\e4cf"}.fa-image:before{content:"\f03e"}.fa-microphone:before{content:"\f130"}.fa-cow:before{content:"\f6c8"}.fa-caret-up:before{content:"\f0d8"}.fa-screwdriver:before{content:"\f54a"}.fa-folder-closed:before{content:"\e185"}.fa-house-tsunami:before{content:"\e515"}.fa-square-nfi:before{content:"\e576"}.fa-arrow-up-from-ground-water:before{content:"\e4b5"}.fa-glass-martini-alt:before,.fa-martini-glass:before{content:"\f57b"}.fa-rotate-back:before,.fa-rotate-backward:before,.fa-rotate-left:before,.fa-undo-alt:before{content:"\f2ea"}.fa-columns:before,.fa-table-columns:before{content:"\f0db"}.fa-lemon:before{content:"\f094"}.fa-head-side-mask:before{content:"\e063"}.fa-handshake:before{content:"\f2b5"}.fa-gem:before{content:"\f3a5"}.fa-dolly-box:before,.fa-dolly:before{content:"\f472"}.fa-smoking:before{content:"\f48d"}.fa-compress-arrows-alt:before,.fa-minimize:before{content:"\f78c"}.fa-monument:before{content:"\f5a6"}.fa-snowplow:before{content:"\f7d2"}.fa-angle-double-right:before,.fa-angles-right:before{content:"\f101"}.fa-cannabis:before{content:"\f55f"}.fa-circle-play:before,.fa-play-circle:before{content:"\f144"}.fa-tablets:before{content:"\f490"}.fa-ethernet:before{content:"\f796"}.fa-eur:before,.fa-euro-sign:before,.fa-euro:before{content:"\f153"}.fa-chair:before{content:"\f6c0"}.fa-check-circle:before,.fa-circle-check:before{content:"\f058"}.fa-circle-stop:before,.fa-stop-circle:before{content:"\f28d"}.fa-compass-drafting:before,.fa-drafting-compass:before{content:"\f568"}.fa-plate-wheat:before{content:"\e55a"}.fa-icicles:before{content:"\f7ad"}.fa-person-shelter:before{content:"\e54f"}.fa-neuter:before{content:"\f22c"}.fa-id-badge:before{content:"\f2c1"}.fa-marker:before{content:"\f5a1"}.fa-face-laugh-beam:before,.fa-laugh-beam:before{content:"\f59a"}.fa-helicopter-symbol:before{content:"\e502"}.fa-universal-access:before{content:"\f29a"}.fa-chevron-circle-up:before,.fa-circle-chevron-up:before{content:"\f139"}.fa-lari-sign:before{content:"\e1c8"}.fa-volcano:before{content:"\f770"}.fa-person-walking-dashed-line-arrow-right:before{content:"\e553"}.fa-gbp:before,.fa-pound-sign:before,.fa-sterling-sign:before{content:"\f154"}.fa-viruses:before{content:"\e076"}.fa-square-person-confined:before{content:"\e577"}.fa-user-tie:before{content:"\f508"}.fa-arrow-down-long:before,.fa-long-arrow-down:before{content:"\f175"}.fa-tent-arrow-down-to-line:before{content:"\e57e"}.fa-certificate:before{content:"\f0a3"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-suitcase:before{content:"\f0f2"}.fa-person-skating:before,.fa-skating:before{content:"\f7c5"}.fa-filter-circle-dollar:before,.fa-funnel-dollar:before{content:"\f662"}.fa-camera-retro:before{content:"\f083"}.fa-arrow-circle-down:before,.fa-circle-arrow-down:before{content:"\f0ab"}.fa-arrow-right-to-file:before,.fa-file-import:before{content:"\f56f"}.fa-external-link-square:before,.fa-square-arrow-up-right:before{content:"\f14c"}.fa-box-open:before{content:"\f49e"}.fa-scroll:before{content:"\f70e"}.fa-spa:before{content:"\f5bb"}.fa-location-pin-lock:before{content:"\e51f"}.fa-pause:before{content:"\f04c"}.fa-hill-avalanche:before{content:"\e507"}.fa-temperature-0:before,.fa-temperature-empty:before,.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-bomb:before{content:"\f1e2"}.fa-registered:before{content:"\f25d"}.fa-address-card:before,.fa-contact-card:before,.fa-vcard:before{content:"\f2bb"}.fa-balance-scale-right:before,.fa-scale-unbalanced-flip:before{content:"\f516"}.fa-subscript:before{content:"\f12c"}.fa-diamond-turn-right:before,.fa-directions:before{content:"\f5eb"}.fa-burst:before{content:"\e4dc"}.fa-house-laptop:before,.fa-laptop-house:before{content:"\e066"}.fa-face-tired:before,.fa-tired:before{content:"\f5c8"}.fa-money-bills:before{content:"\e1f3"}.fa-smog:before{content:"\f75f"}.fa-crutch:before{content:"\f7f7"}.fa-cloud-arrow-up:before,.fa-cloud-upload-alt:before,.fa-cloud-upload:before{content:"\f0ee"}.fa-palette:before{content:"\f53f"}.fa-arrows-turn-right:before{content:"\e4c0"}.fa-vest:before{content:"\e085"}.fa-ferry:before{content:"\e4ea"}.fa-arrows-down-to-people:before{content:"\e4b9"}.fa-seedling:before,.fa-sprout:before{content:"\f4d8"}.fa-arrows-alt-h:before,.fa-left-right:before{content:"\f337"}.fa-boxes-packing:before{content:"\e4c7"}.fa-arrow-circle-left:before,.fa-circle-arrow-left:before{content:"\f0a8"}.fa-group-arrows-rotate:before{content:"\e4f6"}.fa-bowl-food:before{content:"\e4c6"}.fa-candy-cane:before{content:"\f786"}.fa-arrow-down-wide-short:before,.fa-sort-amount-asc:before,.fa-sort-amount-down:before{content:"\f160"}.fa-cloud-bolt:before,.fa-thunderstorm:before{content:"\f76c"}.fa-remove-format:before,.fa-text-slash:before{content:"\f87d"}.fa-face-smile-wink:before,.fa-smile-wink:before{content:"\f4da"}.fa-file-word:before{content:"\f1c2"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-arrows-h:before,.fa-arrows-left-right:before{content:"\f07e"}.fa-house-lock:before{content:"\e510"}.fa-cloud-arrow-down:before,.fa-cloud-download-alt:before,.fa-cloud-download:before{content:"\f0ed"}.fa-children:before{content:"\e4e1"}.fa-blackboard:before,.fa-chalkboard:before{content:"\f51b"}.fa-user-alt-slash:before,.fa-user-large-slash:before{content:"\f4fa"}.fa-envelope-open:before{content:"\f2b6"}.fa-handshake-alt-slash:before,.fa-handshake-simple-slash:before{content:"\e05f"}.fa-mattress-pillow:before{content:"\e525"}.fa-guarani-sign:before{content:"\e19a"}.fa-arrows-rotate:before,.fa-refresh:before,.fa-sync:before{content:"\f021"}.fa-fire-extinguisher:before{content:"\f134"}.fa-cruzeiro-sign:before{content:"\e152"}.fa-greater-than-equal:before{content:"\f532"}.fa-shield-alt:before,.fa-shield-halved:before{content:"\f3ed"}.fa-atlas:before,.fa-book-atlas:before{content:"\f558"}.fa-virus:before{content:"\e074"}.fa-envelope-circle-check:before{content:"\e4e8"}.fa-layer-group:before{content:"\f5fd"}.fa-arrows-to-dot:before{content:"\e4be"}.fa-archway:before{content:"\f557"}.fa-heart-circle-check:before{content:"\e4fd"}.fa-house-chimney-crack:before,.fa-house-damage:before{content:"\f6f1"}.fa-file-archive:before,.fa-file-zipper:before{content:"\f1c6"}.fa-square:before{content:"\f0c8"}.fa-glass-martini:before,.fa-martini-glass-empty:before{content:"\f000"}.fa-couch:before{content:"\f4b8"}.fa-cedi-sign:before{content:"\e0df"}.fa-italic:before{content:"\f033"}.fa-table-cells-column-lock:before{content:"\e678"}.fa-church:before{content:"\f51d"}.fa-comments-dollar:before{content:"\f653"}.fa-democrat:before{content:"\f747"}.fa-z:before{content:"\5a"}.fa-person-skiing:before,.fa-skiing:before{content:"\f7c9"}.fa-road-lock:before{content:"\e567"}.fa-a:before{content:"\41"}.fa-temperature-arrow-down:before,.fa-temperature-down:before{content:"\e03f"}.fa-feather-alt:before,.fa-feather-pointed:before{content:"\f56b"}.fa-p:before{content:"\50"}.fa-snowflake:before{content:"\f2dc"}.fa-newspaper:before{content:"\f1ea"}.fa-ad:before,.fa-rectangle-ad:before{content:"\f641"}.fa-arrow-circle-right:before,.fa-circle-arrow-right:before{content:"\f0a9"}.fa-filter-circle-xmark:before{content:"\e17b"}.fa-locust:before{content:"\e520"}.fa-sort:before,.fa-unsorted:before{content:"\f0dc"}.fa-list-1-2:before,.fa-list-numeric:before,.fa-list-ol:before{content:"\f0cb"}.fa-person-dress-burst:before{content:"\e544"}.fa-money-check-alt:before,.fa-money-check-dollar:before{content:"\f53d"}.fa-vector-square:before{content:"\f5cb"}.fa-bread-slice:before{content:"\f7ec"}.fa-language:before{content:"\f1ab"}.fa-face-kiss-wink-heart:before,.fa-kiss-wink-heart:before{content:"\f598"}.fa-filter:before{content:"\f0b0"}.fa-question:before{content:"\3f"}.fa-file-signature:before{content:"\f573"}.fa-arrows-alt:before,.fa-up-down-left-right:before{content:"\f0b2"}.fa-house-chimney-user:before{content:"\e065"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-puzzle-piece:before{content:"\f12e"}.fa-money-check:before{content:"\f53c"}.fa-star-half-alt:before,.fa-star-half-stroke:before{content:"\f5c0"}.fa-code:before{content:"\f121"}.fa-glass-whiskey:before,.fa-whiskey-glass:before{content:"\f7a0"}.fa-building-circle-exclamation:before{content:"\e4d3"}.fa-magnifying-glass-chart:before{content:"\e522"}.fa-arrow-up-right-from-square:before,.fa-external-link:before{content:"\f08e"}.fa-cubes-stacked:before{content:"\e4e6"}.fa-krw:before,.fa-won-sign:before,.fa-won:before{content:"\f159"}.fa-virus-covid:before{content:"\e4a8"}.fa-austral-sign:before{content:"\e0a9"}.fa-f:before{content:"\46"}.fa-leaf:before{content:"\f06c"}.fa-road:before{content:"\f018"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-person-circle-plus:before{content:"\e541"}.fa-chart-pie:before,.fa-pie-chart:before{content:"\f200"}.fa-bolt-lightning:before{content:"\e0b7"}.fa-sack-xmark:before{content:"\e56a"}.fa-file-excel:before{content:"\f1c3"}.fa-file-contract:before{content:"\f56c"}.fa-fish-fins:before{content:"\e4f2"}.fa-building-flag:before{content:"\e4d5"}.fa-face-grin-beam:before,.fa-grin-beam:before{content:"\f582"}.fa-object-ungroup:before{content:"\f248"}.fa-poop:before{content:"\f619"}.fa-location-pin:before,.fa-map-marker:before{content:"\f041"}.fa-kaaba:before{content:"\f66b"}.fa-toilet-paper:before{content:"\f71e"}.fa-hard-hat:before,.fa-hat-hard:before,.fa-helmet-safety:before{content:"\f807"}.fa-eject:before{content:"\f052"}.fa-arrow-alt-circle-right:before,.fa-circle-right:before{content:"\f35a"}.fa-plane-circle-check:before{content:"\e555"}.fa-face-rolling-eyes:before,.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-object-group:before{content:"\f247"}.fa-chart-line:before,.fa-line-chart:before{content:"\f201"}.fa-mask-ventilator:before{content:"\e524"}.fa-arrow-right:before{content:"\f061"}.fa-map-signs:before,.fa-signs-post:before{content:"\f277"}.fa-cash-register:before{content:"\f788"}.fa-person-circle-question:before{content:"\e542"}.fa-h:before{content:"\48"}.fa-tarp:before{content:"\e57b"}.fa-screwdriver-wrench:before,.fa-tools:before{content:"\f7d9"}.fa-arrows-to-eye:before{content:"\e4bf"}.fa-plug-circle-bolt:before{content:"\e55b"}.fa-heart:before{content:"\f004"}.fa-mars-and-venus:before{content:"\f224"}.fa-home-user:before,.fa-house-user:before{content:"\e1b0"}.fa-dumpster-fire:before{content:"\f794"}.fa-house-crack:before{content:"\e3b1"}.fa-cocktail:before,.fa-martini-glass-citrus:before{content:"\f561"}.fa-face-surprise:before,.fa-surprise:before{content:"\f5c2"}.fa-bottle-water:before{content:"\e4c5"}.fa-circle-pause:before,.fa-pause-circle:before{content:"\f28b"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-apple-alt:before,.fa-apple-whole:before{content:"\f5d1"}.fa-kitchen-set:before{content:"\e51a"}.fa-r:before{content:"\52"}.fa-temperature-1:before,.fa-temperature-quarter:before,.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-cube:before{content:"\f1b2"}.fa-bitcoin-sign:before{content:"\e0b4"}.fa-shield-dog:before{content:"\e573"}.fa-solar-panel:before{content:"\f5ba"}.fa-lock-open:before{content:"\f3c1"}.fa-elevator:before{content:"\e16d"}.fa-money-bill-transfer:before{content:"\e528"}.fa-money-bill-trend-up:before{content:"\e529"}.fa-house-flood-water-circle-arrow-right:before{content:"\e50f"}.fa-poll-h:before,.fa-square-poll-horizontal:before{content:"\f682"}.fa-circle:before{content:"\f111"}.fa-backward-fast:before,.fa-fast-backward:before{content:"\f049"}.fa-recycle:before{content:"\f1b8"}.fa-user-astronaut:before{content:"\f4fb"}.fa-plane-slash:before{content:"\e069"}.fa-trademark:before{content:"\f25c"}.fa-basketball-ball:before,.fa-basketball:before{content:"\f434"}.fa-satellite-dish:before{content:"\f7c0"}.fa-arrow-alt-circle-up:before,.fa-circle-up:before{content:"\f35b"}.fa-mobile-alt:before,.fa-mobile-screen-button:before{content:"\f3cd"}.fa-volume-high:before,.fa-volume-up:before{content:"\f028"}.fa-users-rays:before{content:"\e593"}.fa-wallet:before{content:"\f555"}.fa-clipboard-check:before{content:"\f46c"}.fa-file-audio:before{content:"\f1c7"}.fa-burger:before,.fa-hamburger:before{content:"\f805"}.fa-wrench:before{content:"\f0ad"}.fa-bugs:before{content:"\e4d0"}.fa-rupee-sign:before,.fa-rupee:before{content:"\f156"}.fa-file-image:before{content:"\f1c5"}.fa-circle-question:before,.fa-question-circle:before{content:"\f059"}.fa-plane-departure:before{content:"\f5b0"}.fa-handshake-slash:before{content:"\e060"}.fa-book-bookmark:before{content:"\e0bb"}.fa-code-branch:before{content:"\f126"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-bridge:before{content:"\e4c8"}.fa-phone-alt:before,.fa-phone-flip:before{content:"\f879"}.fa-truck-front:before{content:"\e2b7"}.fa-cat:before{content:"\f6be"}.fa-anchor-circle-exclamation:before{content:"\e4ab"}.fa-truck-field:before{content:"\e58d"}.fa-route:before{content:"\f4d7"}.fa-clipboard-question:before{content:"\e4e3"}.fa-panorama:before{content:"\e209"}.fa-comment-medical:before{content:"\f7f5"}.fa-teeth-open:before{content:"\f62f"}.fa-file-circle-minus:before{content:"\e4ed"}.fa-tags:before{content:"\f02c"}.fa-wine-glass:before{content:"\f4e3"}.fa-fast-forward:before,.fa-forward-fast:before{content:"\f050"}.fa-face-meh-blank:before,.fa-meh-blank:before{content:"\f5a4"}.fa-parking:before,.fa-square-parking:before{content:"\f540"}.fa-house-signal:before{content:"\e012"}.fa-bars-progress:before,.fa-tasks-alt:before{content:"\f828"}.fa-faucet-drip:before{content:"\e006"}.fa-cart-flatbed:before,.fa-dolly-flatbed:before{content:"\f474"}.fa-ban-smoking:before,.fa-smoking-ban:before{content:"\f54d"}.fa-terminal:before{content:"\f120"}.fa-mobile-button:before{content:"\f10b"}.fa-house-medical-flag:before{content:"\e514"}.fa-basket-shopping:before,.fa-shopping-basket:before{content:"\f291"}.fa-tape:before{content:"\f4db"}.fa-bus-alt:before,.fa-bus-simple:before{content:"\f55e"}.fa-eye:before{content:"\f06e"}.fa-face-sad-cry:before,.fa-sad-cry:before{content:"\f5b3"}.fa-audio-description:before{content:"\f29e"}.fa-person-military-to-person:before{content:"\e54c"}.fa-file-shield:before{content:"\e4f0"}.fa-user-slash:before{content:"\f506"}.fa-pen:before{content:"\f304"}.fa-tower-observation:before{content:"\e586"}.fa-file-code:before{content:"\f1c9"}.fa-signal-5:before,.fa-signal-perfect:before,.fa-signal:before{content:"\f012"}.fa-bus:before{content:"\f207"}.fa-heart-circle-xmark:before{content:"\e501"}.fa-home-lg:before,.fa-house-chimney:before{content:"\e3af"}.fa-window-maximize:before{content:"\f2d0"}.fa-face-frown:before,.fa-frown:before{content:"\f119"}.fa-prescription:before{content:"\f5b1"}.fa-shop:before,.fa-store-alt:before{content:"\f54f"}.fa-floppy-disk:before,.fa-save:before{content:"\f0c7"}.fa-vihara:before{content:"\f6a7"}.fa-balance-scale-left:before,.fa-scale-unbalanced:before{content:"\f515"}.fa-sort-asc:before,.fa-sort-up:before{content:"\f0de"}.fa-comment-dots:before,.fa-commenting:before{content:"\f4ad"}.fa-plant-wilt:before{content:"\e5aa"}.fa-diamond:before{content:"\f219"}.fa-face-grin-squint:before,.fa-grin-squint:before{content:"\f585"}.fa-hand-holding-dollar:before,.fa-hand-holding-usd:before{content:"\f4c0"}.fa-bacterium:before{content:"\e05a"}.fa-hand-pointer:before{content:"\f25a"}.fa-drum-steelpan:before{content:"\f56a"}.fa-hand-scissors:before{content:"\f257"}.fa-hands-praying:before,.fa-praying-hands:before{content:"\f684"}.fa-arrow-right-rotate:before,.fa-arrow-rotate-forward:before,.fa-arrow-rotate-right:before,.fa-redo:before{content:"\f01e"}.fa-biohazard:before{content:"\f780"}.fa-location-crosshairs:before,.fa-location:before{content:"\f601"}.fa-mars-double:before{content:"\f227"}.fa-child-dress:before{content:"\e59c"}.fa-users-between-lines:before{content:"\e591"}.fa-lungs-virus:before{content:"\e067"}.fa-face-grin-tears:before,.fa-grin-tears:before{content:"\f588"}.fa-phone:before{content:"\f095"}.fa-calendar-times:before,.fa-calendar-xmark:before{content:"\f273"}.fa-child-reaching:before{content:"\e59d"}.fa-head-side-virus:before{content:"\e064"}.fa-user-cog:before,.fa-user-gear:before{content:"\f4fe"}.fa-arrow-up-1-9:before,.fa-sort-numeric-up:before{content:"\f163"}.fa-door-closed:before{content:"\f52a"}.fa-shield-virus:before{content:"\e06c"}.fa-dice-six:before{content:"\f526"}.fa-mosquito-net:before{content:"\e52c"}.fa-bridge-water:before{content:"\e4ce"}.fa-person-booth:before{content:"\f756"}.fa-text-width:before{content:"\f035"}.fa-hat-wizard:before{content:"\f6e8"}.fa-pen-fancy:before{content:"\f5ac"}.fa-digging:before,.fa-person-digging:before{content:"\f85e"}.fa-trash:before{content:"\f1f8"}.fa-gauge-simple-med:before,.fa-gauge-simple:before,.fa-tachometer-average:before{content:"\f629"}.fa-book-medical:before{content:"\f7e6"}.fa-poo:before{content:"\f2fe"}.fa-quote-right-alt:before,.fa-quote-right:before{content:"\f10e"}.fa-shirt:before,.fa-t-shirt:before,.fa-tshirt:before{content:"\f553"}.fa-cubes:before{content:"\f1b3"}.fa-divide:before{content:"\f529"}.fa-tenge-sign:before,.fa-tenge:before{content:"\f7d7"}.fa-headphones:before{content:"\f025"}.fa-hands-holding:before{content:"\f4c2"}.fa-hands-clapping:before{content:"\e1a8"}.fa-republican:before{content:"\f75e"}.fa-arrow-left:before{content:"\f060"}.fa-person-circle-xmark:before{content:"\e543"}.fa-ruler:before{content:"\f545"}.fa-align-left:before{content:"\f036"}.fa-dice-d6:before{content:"\f6d1"}.fa-restroom:before{content:"\f7bd"}.fa-j:before{content:"\4a"}.fa-users-viewfinder:before{content:"\e595"}.fa-file-video:before{content:"\f1c8"}.fa-external-link-alt:before,.fa-up-right-from-square:before{content:"\f35d"}.fa-table-cells:before,.fa-th:before{content:"\f00a"}.fa-file-pdf:before{content:"\f1c1"}.fa-bible:before,.fa-book-bible:before{content:"\f647"}.fa-o:before{content:"\4f"}.fa-medkit:before,.fa-suitcase-medical:before{content:"\f0fa"}.fa-user-secret:before{content:"\f21b"}.fa-otter:before{content:"\f700"}.fa-female:before,.fa-person-dress:before{content:"\f182"}.fa-comment-dollar:before{content:"\f651"}.fa-briefcase-clock:before,.fa-business-time:before{content:"\f64a"}.fa-table-cells-large:before,.fa-th-large:before{content:"\f009"}.fa-book-tanakh:before,.fa-tanakh:before{content:"\f827"}.fa-phone-volume:before,.fa-volume-control-phone:before{content:"\f2a0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-clipboard-user:before{content:"\f7f3"}.fa-child:before{content:"\f1ae"}.fa-lira-sign:before{content:"\f195"}.fa-satellite:before{content:"\f7bf"}.fa-plane-lock:before{content:"\e558"}.fa-tag:before{content:"\f02b"}.fa-comment:before{content:"\f075"}.fa-birthday-cake:before,.fa-cake-candles:before,.fa-cake:before{content:"\f1fd"}.fa-envelope:before{content:"\f0e0"}.fa-angle-double-up:before,.fa-angles-up:before{content:"\f102"}.fa-paperclip:before{content:"\f0c6"}.fa-arrow-right-to-city:before{content:"\e4b3"}.fa-ribbon:before{content:"\f4d6"}.fa-lungs:before{content:"\f604"}.fa-arrow-up-9-1:before,.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-litecoin-sign:before{content:"\e1d3"}.fa-border-none:before{content:"\f850"}.fa-circle-nodes:before{content:"\e4e2"}.fa-parachute-box:before{content:"\f4cd"}.fa-indent:before{content:"\f03c"}.fa-truck-field-un:before{content:"\e58e"}.fa-hourglass-empty:before,.fa-hourglass:before{content:"\f254"}.fa-mountain:before{content:"\f6fc"}.fa-user-doctor:before,.fa-user-md:before{content:"\f0f0"}.fa-circle-info:before,.fa-info-circle:before{content:"\f05a"}.fa-cloud-meatball:before{content:"\f73b"}.fa-camera-alt:before,.fa-camera:before{content:"\f030"}.fa-square-virus:before{content:"\e578"}.fa-meteor:before{content:"\f753"}.fa-car-on:before{content:"\e4dd"}.fa-sleigh:before{content:"\f7cc"}.fa-arrow-down-1-9:before,.fa-sort-numeric-asc:before,.fa-sort-numeric-down:before{content:"\f162"}.fa-hand-holding-droplet:before,.fa-hand-holding-water:before{content:"\f4c1"}.fa-water:before{content:"\f773"}.fa-calendar-check:before{content:"\f274"}.fa-braille:before{content:"\f2a1"}.fa-prescription-bottle-alt:before,.fa-prescription-bottle-medical:before{content:"\f486"}.fa-landmark:before{content:"\f66f"}.fa-truck:before{content:"\f0d1"}.fa-crosshairs:before{content:"\f05b"}.fa-person-cane:before{content:"\e53c"}.fa-tent:before{content:"\e57d"}.fa-vest-patches:before{content:"\e086"}.fa-check-double:before{content:"\f560"}.fa-arrow-down-a-z:before,.fa-sort-alpha-asc:before,.fa-sort-alpha-down:before{content:"\f15d"}.fa-money-bill-wheat:before{content:"\e52a"}.fa-cookie:before{content:"\f563"}.fa-arrow-left-rotate:before,.fa-arrow-rotate-back:before,.fa-arrow-rotate-backward:before,.fa-arrow-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-hard-drive:before,.fa-hdd:before{content:"\f0a0"}.fa-face-grin-squint-tears:before,.fa-grin-squint-tears:before{content:"\f586"}.fa-dumbbell:before{content:"\f44b"}.fa-list-alt:before,.fa-rectangle-list:before{content:"\f022"}.fa-tarp-droplet:before{content:"\e57c"}.fa-house-medical-circle-check:before{content:"\e511"}.fa-person-skiing-nordic:before,.fa-skiing-nordic:before{content:"\f7ca"}.fa-calendar-plus:before{content:"\f271"}.fa-plane-arrival:before{content:"\f5af"}.fa-arrow-alt-circle-left:before,.fa-circle-left:before{content:"\f359"}.fa-subway:before,.fa-train-subway:before{content:"\f239"}.fa-chart-gantt:before{content:"\e0e4"}.fa-indian-rupee-sign:before,.fa-indian-rupee:before,.fa-inr:before{content:"\e1bc"}.fa-crop-alt:before,.fa-crop-simple:before{content:"\f565"}.fa-money-bill-1:before,.fa-money-bill-alt:before{content:"\f3d1"}.fa-left-long:before,.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-dna:before{content:"\f471"}.fa-virus-slash:before{content:"\e075"}.fa-minus:before,.fa-subtract:before{content:"\f068"}.fa-chess:before{content:"\f439"}.fa-arrow-left-long:before,.fa-long-arrow-left:before{content:"\f177"}.fa-plug-circle-check:before{content:"\e55c"}.fa-street-view:before{content:"\f21d"}.fa-franc-sign:before{content:"\e18f"}.fa-volume-off:before{content:"\f026"}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before,.fa-hands-american-sign-language-interpreting:before,.fa-hands-asl-interpreting:before{content:"\f2a3"}.fa-cog:before,.fa-gear:before{content:"\f013"}.fa-droplet-slash:before,.fa-tint-slash:before{content:"\f5c7"}.fa-mosque:before{content:"\f678"}.fa-mosquito:before{content:"\e52b"}.fa-star-of-david:before{content:"\f69a"}.fa-person-military-rifle:before{content:"\e54b"}.fa-cart-shopping:before,.fa-shopping-cart:before{content:"\f07a"}.fa-vials:before{content:"\f493"}.fa-plug-circle-plus:before{content:"\e55f"}.fa-place-of-worship:before{content:"\f67f"}.fa-grip-vertical:before{content:"\f58e"}.fa-arrow-turn-up:before,.fa-level-up:before{content:"\f148"}.fa-u:before{content:"\55"}.fa-square-root-alt:before,.fa-square-root-variable:before{content:"\f698"}.fa-clock-four:before,.fa-clock:before{content:"\f017"}.fa-backward-step:before,.fa-step-backward:before{content:"\f048"}.fa-pallet:before{content:"\f482"}.fa-faucet:before{content:"\e005"}.fa-baseball-bat-ball:before{content:"\f432"}.fa-s:before{content:"\53"}.fa-timeline:before{content:"\e29c"}.fa-keyboard:before{content:"\f11c"}.fa-caret-down:before{content:"\f0d7"}.fa-clinic-medical:before,.fa-house-chimney-medical:before{content:"\f7f2"}.fa-temperature-3:before,.fa-temperature-three-quarters:before,.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-mobile-android-alt:before,.fa-mobile-screen:before{content:"\f3cf"}.fa-plane-up:before{content:"\e22d"}.fa-piggy-bank:before{content:"\f4d3"}.fa-battery-3:before,.fa-battery-half:before{content:"\f242"}.fa-mountain-city:before{content:"\e52e"}.fa-coins:before{content:"\f51e"}.fa-khanda:before{content:"\f66d"}.fa-sliders-h:before,.fa-sliders:before{content:"\f1de"}.fa-folder-tree:before{content:"\f802"}.fa-network-wired:before{content:"\f6ff"}.fa-map-pin:before{content:"\f276"}.fa-hamsa:before{content:"\f665"}.fa-cent-sign:before{content:"\e3f5"}.fa-flask:before{content:"\f0c3"}.fa-person-pregnant:before{content:"\e31e"}.fa-wand-sparkles:before{content:"\f72b"}.fa-ellipsis-v:before,.fa-ellipsis-vertical:before{content:"\f142"}.fa-ticket:before{content:"\f145"}.fa-power-off:before{content:"\f011"}.fa-long-arrow-alt-right:before,.fa-right-long:before{content:"\f30b"}.fa-flag-usa:before{content:"\f74d"}.fa-laptop-file:before{content:"\e51d"}.fa-teletype:before,.fa-tty:before{content:"\f1e4"}.fa-diagram-next:before{content:"\e476"}.fa-person-rifle:before{content:"\e54e"}.fa-house-medical-circle-exclamation:before{content:"\e512"}.fa-closed-captioning:before{content:"\f20a"}.fa-hiking:before,.fa-person-hiking:before{content:"\f6ec"}.fa-venus-double:before{content:"\f226"}.fa-images:before{content:"\f302"}.fa-calculator:before{content:"\f1ec"}.fa-people-pulling:before{content:"\e535"}.fa-n:before{content:"\4e"}.fa-cable-car:before,.fa-tram:before{content:"\f7da"}.fa-cloud-rain:before{content:"\f73d"}.fa-building-circle-xmark:before{content:"\e4d4"}.fa-ship:before{content:"\f21a"}.fa-arrows-down-to-line:before{content:"\e4b8"}.fa-download:before{content:"\f019"}.fa-face-grin:before,.fa-grin:before{content:"\f580"}.fa-backspace:before,.fa-delete-left:before{content:"\f55a"}.fa-eye-dropper-empty:before,.fa-eye-dropper:before,.fa-eyedropper:before{content:"\f1fb"}.fa-file-circle-check:before{content:"\e5a0"}.fa-forward:before{content:"\f04e"}.fa-mobile-android:before,.fa-mobile-phone:before,.fa-mobile:before{content:"\f3ce"}.fa-face-meh:before,.fa-meh:before{content:"\f11a"}.fa-align-center:before{content:"\f037"}.fa-book-dead:before,.fa-book-skull:before{content:"\f6b7"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-heart-circle-exclamation:before{content:"\e4fe"}.fa-home-alt:before,.fa-home-lg-alt:before,.fa-home:before,.fa-house:before{content:"\f015"}.fa-calendar-week:before{content:"\f784"}.fa-laptop-medical:before{content:"\f812"}.fa-b:before{content:"\42"}.fa-file-medical:before{content:"\f477"}.fa-dice-one:before{content:"\f525"}.fa-kiwi-bird:before{content:"\f535"}.fa-arrow-right-arrow-left:before,.fa-exchange:before{content:"\f0ec"}.fa-redo-alt:before,.fa-rotate-forward:before,.fa-rotate-right:before{content:"\f2f9"}.fa-cutlery:before,.fa-utensils:before{content:"\f2e7"}.fa-arrow-up-wide-short:before,.fa-sort-amount-up:before{content:"\f161"}.fa-mill-sign:before{content:"\e1ed"}.fa-bowl-rice:before{content:"\e2eb"}.fa-skull:before{content:"\f54c"}.fa-broadcast-tower:before,.fa-tower-broadcast:before{content:"\f519"}.fa-truck-pickup:before{content:"\f63c"}.fa-long-arrow-alt-up:before,.fa-up-long:before{content:"\f30c"}.fa-stop:before{content:"\f04d"}.fa-code-merge:before{content:"\f387"}.fa-upload:before{content:"\f093"}.fa-hurricane:before{content:"\f751"}.fa-mound:before{content:"\e52d"}.fa-toilet-portable:before{content:"\e583"}.fa-compact-disc:before{content:"\f51f"}.fa-file-arrow-down:before,.fa-file-download:before{content:"\f56d"}.fa-caravan:before{content:"\f8ff"}.fa-shield-cat:before{content:"\e572"}.fa-bolt:before,.fa-zap:before{content:"\f0e7"}.fa-glass-water:before{content:"\e4f4"}.fa-oil-well:before{content:"\e532"}.fa-vault:before{content:"\e2c5"}.fa-mars:before{content:"\f222"}.fa-toilet:before{content:"\f7d8"}.fa-plane-circle-xmark:before{content:"\e557"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen-sign:before,.fa-yen:before{content:"\f157"}.fa-rouble:before,.fa-rub:before,.fa-ruble-sign:before,.fa-ruble:before{content:"\f158"}.fa-sun:before{content:"\f185"}.fa-guitar:before{content:"\f7a6"}.fa-face-laugh-wink:before,.fa-laugh-wink:before{content:"\f59c"}.fa-horse-head:before{content:"\f7ab"}.fa-bore-hole:before{content:"\e4c3"}.fa-industry:before{content:"\f275"}.fa-arrow-alt-circle-down:before,.fa-circle-down:before{content:"\f358"}.fa-arrows-turn-to-dots:before{content:"\e4c1"}.fa-florin-sign:before{content:"\e184"}.fa-arrow-down-short-wide:before,.fa-sort-amount-desc:before,.fa-sort-amount-down-alt:before{content:"\f884"}.fa-less-than:before{content:"\3c"}.fa-angle-down:before{content:"\f107"}.fa-car-tunnel:before{content:"\e4de"}.fa-head-side-cough:before{content:"\e061"}.fa-grip-lines:before{content:"\f7a4"}.fa-thumbs-down:before{content:"\f165"}.fa-user-lock:before{content:"\f502"}.fa-arrow-right-long:before,.fa-long-arrow-right:before{content:"\f178"}.fa-anchor-circle-xmark:before{content:"\e4ac"}.fa-ellipsis-h:before,.fa-ellipsis:before{content:"\f141"}.fa-chess-pawn:before{content:"\f443"}.fa-first-aid:before,.fa-kit-medical:before{content:"\f479"}.fa-person-through-window:before{content:"\e5a9"}.fa-toolbox:before{content:"\f552"}.fa-hands-holding-circle:before{content:"\e4fb"}.fa-bug:before{content:"\f188"}.fa-credit-card-alt:before,.fa-credit-card:before{content:"\f09d"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-hand-holding-hand:before{content:"\e4f7"}.fa-book-open-reader:before,.fa-book-reader:before{content:"\f5da"}.fa-mountain-sun:before{content:"\e52f"}.fa-arrows-left-right-to-line:before{content:"\e4ba"}.fa-dice-d20:before{content:"\f6cf"}.fa-truck-droplet:before{content:"\e58c"}.fa-file-circle-xmark:before{content:"\e5a1"}.fa-temperature-arrow-up:before,.fa-temperature-up:before{content:"\e040"}.fa-medal:before{content:"\f5a2"}.fa-bed:before{content:"\f236"}.fa-h-square:before,.fa-square-h:before{content:"\f0fd"}.fa-podcast:before{content:"\f2ce"}.fa-temperature-4:before,.fa-temperature-full:before,.fa-thermometer-4:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-bell:before{content:"\f0f3"}.fa-superscript:before{content:"\f12b"}.fa-plug-circle-xmark:before{content:"\e560"}.fa-star-of-life:before{content:"\f621"}.fa-phone-slash:before{content:"\f3dd"}.fa-paint-roller:before{content:"\f5aa"}.fa-hands-helping:before,.fa-handshake-angle:before{content:"\f4c4"}.fa-location-dot:before,.fa-map-marker-alt:before{content:"\f3c5"}.fa-file:before{content:"\f15b"}.fa-greater-than:before{content:"\3e"}.fa-person-swimming:before,.fa-swimmer:before{content:"\f5c4"}.fa-arrow-down:before{content:"\f063"}.fa-droplet:before,.fa-tint:before{content:"\f043"}.fa-eraser:before{content:"\f12d"}.fa-earth-america:before,.fa-earth-americas:before,.fa-earth:before,.fa-globe-americas:before{content:"\f57d"}.fa-person-burst:before{content:"\e53b"}.fa-dove:before{content:"\f4ba"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-socks:before{content:"\f696"}.fa-inbox:before{content:"\f01c"}.fa-section:before{content:"\e447"}.fa-gauge-high:before,.fa-tachometer-alt-fast:before,.fa-tachometer-alt:before{content:"\f625"}.fa-envelope-open-text:before{content:"\f658"}.fa-hospital-alt:before,.fa-hospital-wide:before,.fa-hospital:before{content:"\f0f8"}.fa-wine-bottle:before{content:"\f72f"}.fa-chess-rook:before{content:"\f447"}.fa-bars-staggered:before,.fa-reorder:before,.fa-stream:before{content:"\f550"}.fa-dharmachakra:before{content:"\f655"}.fa-hotdog:before{content:"\f80f"}.fa-blind:before,.fa-person-walking-with-cane:before{content:"\f29d"}.fa-drum:before{content:"\f569"}.fa-ice-cream:before{content:"\f810"}.fa-heart-circle-bolt:before{content:"\e4fc"}.fa-fax:before{content:"\f1ac"}.fa-paragraph:before{content:"\f1dd"}.fa-check-to-slot:before,.fa-vote-yea:before{content:"\f772"}.fa-star-half:before{content:"\f089"}.fa-boxes-alt:before,.fa-boxes-stacked:before,.fa-boxes:before{content:"\f468"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-assistive-listening-systems:before,.fa-ear-listen:before{content:"\f2a2"}.fa-tree-city:before{content:"\e587"}.fa-play:before{content:"\f04b"}.fa-font:before{content:"\f031"}.fa-table-cells-row-lock:before{content:"\e67a"}.fa-rupiah-sign:before{content:"\e23d"}.fa-magnifying-glass:before,.fa-search:before{content:"\f002"}.fa-ping-pong-paddle-ball:before,.fa-table-tennis-paddle-ball:before,.fa-table-tennis:before{content:"\f45d"}.fa-diagnoses:before,.fa-person-dots-from-line:before{content:"\f470"}.fa-trash-can-arrow-up:before,.fa-trash-restore-alt:before{content:"\f82a"}.fa-naira-sign:before{content:"\e1f6"}.fa-cart-arrow-down:before{content:"\f218"}.fa-walkie-talkie:before{content:"\f8ef"}.fa-file-edit:before,.fa-file-pen:before{content:"\f31c"}.fa-receipt:before{content:"\f543"}.fa-pen-square:before,.fa-pencil-square:before,.fa-square-pen:before{content:"\f14b"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-person-circle-exclamation:before{content:"\e53f"}.fa-chevron-down:before{content:"\f078"}.fa-battery-5:before,.fa-battery-full:before,.fa-battery:before{content:"\f240"}.fa-skull-crossbones:before{content:"\f714"}.fa-code-compare:before{content:"\e13a"}.fa-list-dots:before,.fa-list-ul:before{content:"\f0ca"}.fa-school-lock:before{content:"\e56f"}.fa-tower-cell:before{content:"\e585"}.fa-down-long:before,.fa-long-arrow-alt-down:before{content:"\f309"}.fa-ranking-star:before{content:"\e561"}.fa-chess-king:before{content:"\f43f"}.fa-person-harassing:before{content:"\e549"}.fa-brazilian-real-sign:before{content:"\e46c"}.fa-landmark-alt:before,.fa-landmark-dome:before{content:"\f752"}.fa-arrow-up:before{content:"\f062"}.fa-television:before,.fa-tv-alt:before,.fa-tv:before{content:"\f26c"}.fa-shrimp:before{content:"\e448"}.fa-list-check:before,.fa-tasks:before{content:"\f0ae"}.fa-jug-detergent:before{content:"\e519"}.fa-circle-user:before,.fa-user-circle:before{content:"\f2bd"}.fa-user-shield:before{content:"\f505"}.fa-wind:before{content:"\f72e"}.fa-car-burst:before,.fa-car-crash:before{content:"\f5e1"}.fa-y:before{content:"\59"}.fa-person-snowboarding:before,.fa-snowboarding:before{content:"\f7ce"}.fa-shipping-fast:before,.fa-truck-fast:before{content:"\f48b"}.fa-fish:before{content:"\f578"}.fa-user-graduate:before{content:"\f501"}.fa-adjust:before,.fa-circle-half-stroke:before{content:"\f042"}.fa-clapperboard:before{content:"\e131"}.fa-circle-radiation:before,.fa-radiation-alt:before{content:"\f7ba"}.fa-baseball-ball:before,.fa-baseball:before{content:"\f433"}.fa-jet-fighter-up:before{content:"\e518"}.fa-diagram-project:before,.fa-project-diagram:before{content:"\f542"}.fa-copy:before{content:"\f0c5"}.fa-volume-mute:before,.fa-volume-times:before,.fa-volume-xmark:before{content:"\f6a9"}.fa-hand-sparkles:before{content:"\e05d"}.fa-grip-horizontal:before,.fa-grip:before{content:"\f58d"}.fa-share-from-square:before,.fa-share-square:before{content:"\f14d"}.fa-child-combatant:before,.fa-child-rifle:before{content:"\e4e0"}.fa-gun:before{content:"\e19b"}.fa-phone-square:before,.fa-square-phone:before{content:"\f098"}.fa-add:before,.fa-plus:before{content:"\2b"}.fa-expand:before{content:"\f065"}.fa-computer:before{content:"\e4e5"}.fa-close:before,.fa-multiply:before,.fa-remove:before,.fa-times:before,.fa-xmark:before{content:"\f00d"}.fa-arrows-up-down-left-right:before,.fa-arrows:before{content:"\f047"}.fa-chalkboard-teacher:before,.fa-chalkboard-user:before{content:"\f51c"}.fa-peso-sign:before{content:"\e222"}.fa-building-shield:before{content:"\e4d8"}.fa-baby:before{content:"\f77c"}.fa-users-line:before{content:"\e592"}.fa-quote-left-alt:before,.fa-quote-left:before{content:"\f10d"}.fa-tractor:before{content:"\f722"}.fa-trash-arrow-up:before,.fa-trash-restore:before{content:"\f829"}.fa-arrow-down-up-lock:before{content:"\e4b0"}.fa-lines-leaning:before{content:"\e51e"}.fa-ruler-combined:before{content:"\f546"}.fa-copyright:before{content:"\f1f9"}.fa-equals:before{content:"\3d"}.fa-blender:before{content:"\f517"}.fa-teeth:before{content:"\f62e"}.fa-ils:before,.fa-shekel-sign:before,.fa-shekel:before,.fa-sheqel-sign:before,.fa-sheqel:before{content:"\f20b"}.fa-map:before{content:"\f279"}.fa-rocket:before{content:"\f135"}.fa-photo-film:before,.fa-photo-video:before{content:"\f87c"}.fa-folder-minus:before{content:"\f65d"}.fa-store:before{content:"\f54e"}.fa-arrow-trend-up:before{content:"\e098"}.fa-plug-circle-minus:before{content:"\e55e"}.fa-sign-hanging:before,.fa-sign:before{content:"\f4d9"}.fa-bezier-curve:before{content:"\f55b"}.fa-bell-slash:before{content:"\f1f6"}.fa-tablet-android:before,.fa-tablet:before{content:"\f3fb"}.fa-school-flag:before{content:"\e56e"}.fa-fill:before{content:"\f575"}.fa-angle-up:before{content:"\f106"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-holly-berry:before{content:"\f7aa"}.fa-chevron-left:before{content:"\f053"}.fa-bacteria:before{content:"\e059"}.fa-hand-lizard:before{content:"\f258"}.fa-notdef:before{content:"\e1fe"}.fa-disease:before{content:"\f7fa"}.fa-briefcase-medical:before{content:"\f469"}.fa-genderless:before{content:"\f22d"}.fa-chevron-right:before{content:"\f054"}.fa-retweet:before{content:"\f079"}.fa-car-alt:before,.fa-car-rear:before{content:"\f5de"}.fa-pump-soap:before{content:"\e06b"}.fa-video-slash:before{content:"\f4e2"}.fa-battery-2:before,.fa-battery-quarter:before{content:"\f243"}.fa-radio:before{content:"\f8d7"}.fa-baby-carriage:before,.fa-carriage-baby:before{content:"\f77d"}.fa-traffic-light:before{content:"\f637"}.fa-thermometer:before{content:"\f491"}.fa-vr-cardboard:before{content:"\f729"}.fa-hand-middle-finger:before{content:"\f806"}.fa-percent:before,.fa-percentage:before{content:"\25"}.fa-truck-moving:before{content:"\f4df"}.fa-glass-water-droplet:before{content:"\e4f5"}.fa-display:before{content:"\e163"}.fa-face-smile:before,.fa-smile:before{content:"\f118"}.fa-thumb-tack:before,.fa-thumbtack:before{content:"\f08d"}.fa-trophy:before{content:"\f091"}.fa-person-praying:before,.fa-pray:before{content:"\f683"}.fa-hammer:before{content:"\f6e3"}.fa-hand-peace:before{content:"\f25b"}.fa-rotate:before,.fa-sync-alt:before{content:"\f2f1"}.fa-spinner:before{content:"\f110"}.fa-robot:before{content:"\f544"}.fa-peace:before{content:"\f67c"}.fa-cogs:before,.fa-gears:before{content:"\f085"}.fa-warehouse:before{content:"\f494"}.fa-arrow-up-right-dots:before{content:"\e4b7"}.fa-splotch:before{content:"\f5bc"}.fa-face-grin-hearts:before,.fa-grin-hearts:before{content:"\f584"}.fa-dice-four:before{content:"\f524"}.fa-sim-card:before{content:"\f7c4"}.fa-transgender-alt:before,.fa-transgender:before{content:"\f225"}.fa-mercury:before{content:"\f223"}.fa-arrow-turn-down:before,.fa-level-down:before{content:"\f149"}.fa-person-falling-burst:before{content:"\e547"}.fa-award:before{content:"\f559"}.fa-ticket-alt:before,.fa-ticket-simple:before{content:"\f3ff"}.fa-building:before{content:"\f1ad"}.fa-angle-double-left:before,.fa-angles-left:before{content:"\f100"}.fa-qrcode:before{content:"\f029"}.fa-clock-rotate-left:before,.fa-history:before{content:"\f1da"}.fa-face-grin-beam-sweat:before,.fa-grin-beam-sweat:before{content:"\f583"}.fa-arrow-right-from-file:before,.fa-file-export:before{content:"\f56e"}.fa-shield-blank:before,.fa-shield:before{content:"\f132"}.fa-arrow-up-short-wide:before,.fa-sort-amount-up-alt:before{content:"\f885"}.fa-house-medical:before{content:"\e3b2"}.fa-golf-ball-tee:before,.fa-golf-ball:before{content:"\f450"}.fa-chevron-circle-left:before,.fa-circle-chevron-left:before{content:"\f137"}.fa-house-chimney-window:before{content:"\e00d"}.fa-pen-nib:before{content:"\f5ad"}.fa-tent-arrow-turn-left:before{content:"\e580"}.fa-tents:before{content:"\e582"}.fa-magic:before,.fa-wand-magic:before{content:"\f0d0"}.fa-dog:before{content:"\f6d3"}.fa-carrot:before{content:"\f787"}.fa-moon:before{content:"\f186"}.fa-wine-glass-alt:before,.fa-wine-glass-empty:before{content:"\f5ce"}.fa-cheese:before{content:"\f7ef"}.fa-yin-yang:before{content:"\f6ad"}.fa-music:before{content:"\f001"}.fa-code-commit:before{content:"\f386"}.fa-temperature-low:before{content:"\f76b"}.fa-biking:before,.fa-person-biking:before{content:"\f84a"}.fa-broom:before{content:"\f51a"}.fa-shield-heart:before{content:"\e574"}.fa-gopuram:before{content:"\f664"}.fa-earth-oceania:before,.fa-globe-oceania:before{content:"\e47b"}.fa-square-xmark:before,.fa-times-square:before,.fa-xmark-square:before{content:"\f2d3"}.fa-hashtag:before{content:"\23"}.fa-expand-alt:before,.fa-up-right-and-down-left-from-center:before{content:"\f424"}.fa-oil-can:before{content:"\f613"}.fa-t:before{content:"\54"}.fa-hippo:before{content:"\f6ed"}.fa-chart-column:before{content:"\e0e3"}.fa-infinity:before{content:"\f534"}.fa-vial-circle-check:before{content:"\e596"}.fa-person-arrow-down-to-line:before{content:"\e538"}.fa-voicemail:before{content:"\f897"}.fa-fan:before{content:"\f863"}.fa-person-walking-luggage:before{content:"\e554"}.fa-arrows-alt-v:before,.fa-up-down:before{content:"\f338"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-calendar:before{content:"\f133"}.fa-trailer:before{content:"\e041"}.fa-bahai:before,.fa-haykal:before{content:"\f666"}.fa-sd-card:before{content:"\f7c2"}.fa-dragon:before{content:"\f6d5"}.fa-shoe-prints:before{content:"\f54b"}.fa-circle-plus:before,.fa-plus-circle:before{content:"\f055"}.fa-face-grin-tongue-wink:before,.fa-grin-tongue-wink:before{content:"\f58b"}.fa-hand-holding:before{content:"\f4bd"}.fa-plug-circle-exclamation:before{content:"\e55d"}.fa-chain-broken:before,.fa-chain-slash:before,.fa-link-slash:before,.fa-unlink:before{content:"\f127"}.fa-clone:before{content:"\f24d"}.fa-person-walking-arrow-loop-left:before{content:"\e551"}.fa-arrow-up-z-a:before,.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-fire-alt:before,.fa-fire-flame-curved:before{content:"\f7e4"}.fa-tornado:before{content:"\f76f"}.fa-file-circle-plus:before{content:"\e494"}.fa-book-quran:before,.fa-quran:before{content:"\f687"}.fa-anchor:before{content:"\f13d"}.fa-border-all:before{content:"\f84c"}.fa-angry:before,.fa-face-angry:before{content:"\f556"}.fa-cookie-bite:before{content:"\f564"}.fa-arrow-trend-down:before{content:"\e097"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-draw-polygon:before{content:"\f5ee"}.fa-balance-scale:before,.fa-scale-balanced:before{content:"\f24e"}.fa-gauge-simple-high:before,.fa-tachometer-fast:before,.fa-tachometer:before{content:"\f62a"}.fa-shower:before{content:"\f2cc"}.fa-desktop-alt:before,.fa-desktop:before{content:"\f390"}.fa-m:before{content:"\4d"}.fa-table-list:before,.fa-th-list:before{content:"\f00b"}.fa-comment-sms:before,.fa-sms:before{content:"\f7cd"}.fa-book:before{content:"\f02d"}.fa-user-plus:before{content:"\f234"}.fa-check:before{content:"\f00c"}.fa-battery-4:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-house-circle-check:before{content:"\e509"}.fa-angle-left:before{content:"\f104"}.fa-diagram-successor:before{content:"\e47a"}.fa-truck-arrow-right:before{content:"\e58b"}.fa-arrows-split-up-and-left:before{content:"\e4bc"}.fa-fist-raised:before,.fa-hand-fist:before{content:"\f6de"}.fa-cloud-moon:before{content:"\f6c3"}.fa-briefcase:before{content:"\f0b1"}.fa-person-falling:before{content:"\e546"}.fa-image-portrait:before,.fa-portrait:before{content:"\f3e0"}.fa-user-tag:before{content:"\f507"}.fa-rug:before{content:"\e569"}.fa-earth-europe:before,.fa-globe-europe:before{content:"\f7a2"}.fa-cart-flatbed-suitcase:before,.fa-luggage-cart:before{content:"\f59d"}.fa-rectangle-times:before,.fa-rectangle-xmark:before,.fa-times-rectangle:before,.fa-window-close:before{content:"\f410"}.fa-baht-sign:before{content:"\e0ac"}.fa-book-open:before{content:"\f518"}.fa-book-journal-whills:before,.fa-journal-whills:before{content:"\f66a"}.fa-handcuffs:before{content:"\e4f8"}.fa-exclamation-triangle:before,.fa-triangle-exclamation:before,.fa-warning:before{content:"\f071"}.fa-database:before{content:"\f1c0"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-bottle-droplet:before{content:"\e4c4"}.fa-mask-face:before{content:"\e1d7"}.fa-hill-rockslide:before{content:"\e508"}.fa-exchange-alt:before,.fa-right-left:before{content:"\f362"}.fa-paper-plane:before{content:"\f1d8"}.fa-road-circle-exclamation:before{content:"\e565"}.fa-dungeon:before{content:"\f6d9"}.fa-align-right:before{content:"\f038"}.fa-money-bill-1-wave:before,.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-life-ring:before{content:"\f1cd"}.fa-hands:before,.fa-sign-language:before,.fa-signing:before{content:"\f2a7"}.fa-calendar-day:before{content:"\f783"}.fa-ladder-water:before,.fa-swimming-pool:before,.fa-water-ladder:before{content:"\f5c5"}.fa-arrows-up-down:before,.fa-arrows-v:before{content:"\f07d"}.fa-face-grimace:before,.fa-grimace:before{content:"\f57f"}.fa-wheelchair-alt:before,.fa-wheelchair-move:before{content:"\e2ce"}.fa-level-down-alt:before,.fa-turn-down:before{content:"\f3be"}.fa-person-walking-arrow-right:before{content:"\e552"}.fa-envelope-square:before,.fa-square-envelope:before{content:"\f199"}.fa-dice:before{content:"\f522"}.fa-bowling-ball:before{content:"\f436"}.fa-brain:before{content:"\f5dc"}.fa-band-aid:before,.fa-bandage:before{content:"\f462"}.fa-calendar-minus:before{content:"\f272"}.fa-circle-xmark:before,.fa-times-circle:before,.fa-xmark-circle:before{content:"\f057"}.fa-gifts:before{content:"\f79c"}.fa-hotel:before{content:"\f594"}.fa-earth-asia:before,.fa-globe-asia:before{content:"\f57e"}.fa-id-card-alt:before,.fa-id-card-clip:before{content:"\f47f"}.fa-magnifying-glass-plus:before,.fa-search-plus:before{content:"\f00e"}.fa-thumbs-up:before{content:"\f164"}.fa-user-clock:before{content:"\f4fd"}.fa-allergies:before,.fa-hand-dots:before{content:"\f461"}.fa-file-invoice:before{content:"\f570"}.fa-window-minimize:before{content:"\f2d1"}.fa-coffee:before,.fa-mug-saucer:before{content:"\f0f4"}.fa-brush:before{content:"\f55d"}.fa-mask:before{content:"\f6fa"}.fa-magnifying-glass-minus:before,.fa-search-minus:before{content:"\f010"}.fa-ruler-vertical:before{content:"\f548"}.fa-user-alt:before,.fa-user-large:before{content:"\f406"}.fa-train-tram:before{content:"\e5b4"}.fa-user-nurse:before{content:"\f82f"}.fa-syringe:before{content:"\f48e"}.fa-cloud-sun:before{content:"\f6c4"}.fa-stopwatch-20:before{content:"\e06f"}.fa-square-full:before{content:"\f45c"}.fa-magnet:before{content:"\f076"}.fa-jar:before{content:"\e516"}.fa-note-sticky:before,.fa-sticky-note:before{content:"\f249"}.fa-bug-slash:before{content:"\e490"}.fa-arrow-up-from-water-pump:before{content:"\e4b6"}.fa-bone:before{content:"\f5d7"}.fa-user-injured:before{content:"\f728"}.fa-face-sad-tear:before,.fa-sad-tear:before{content:"\f5b4"}.fa-plane:before{content:"\f072"}.fa-tent-arrows-down:before{content:"\e581"}.fa-exclamation:before{content:"\21"}.fa-arrows-spin:before{content:"\e4bb"}.fa-print:before{content:"\f02f"}.fa-try:before,.fa-turkish-lira-sign:before,.fa-turkish-lira:before{content:"\e2bb"}.fa-dollar-sign:before,.fa-dollar:before,.fa-usd:before{content:"\24"}.fa-x:before{content:"\58"}.fa-magnifying-glass-dollar:before,.fa-search-dollar:before{content:"\f688"}.fa-users-cog:before,.fa-users-gear:before{content:"\f509"}.fa-person-military-pointing:before{content:"\e54a"}.fa-bank:before,.fa-building-columns:before,.fa-institution:before,.fa-museum:before,.fa-university:before{content:"\f19c"}.fa-umbrella:before{content:"\f0e9"}.fa-trowel:before{content:"\e589"}.fa-d:before{content:"\44"}.fa-stapler:before{content:"\e5af"}.fa-masks-theater:before,.fa-theater-masks:before{content:"\f630"}.fa-kip-sign:before{content:"\e1c4"}.fa-hand-point-left:before{content:"\f0a5"}.fa-handshake-alt:before,.fa-handshake-simple:before{content:"\f4c6"}.fa-fighter-jet:before,.fa-jet-fighter:before{content:"\f0fb"}.fa-share-alt-square:before,.fa-square-share-nodes:before{content:"\f1e1"}.fa-barcode:before{content:"\f02a"}.fa-plus-minus:before{content:"\e43c"}.fa-video-camera:before,.fa-video:before{content:"\f03d"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\f19d"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-person-circle-check:before{content:"\e53e"}.fa-level-up-alt:before,.fa-turn-up:before{content:"\f3bf"}
-.fa-sr-only,.fa-sr-only-focusable:not(:focus),.sr-only,.sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host,:root{--fa-style-family-brands:"Font Awesome 6 Brands";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}@font-face{font-family:"Font Awesome 6 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}.fa-brands,.fab{font-weight:400}.fa-monero:before{content:"\f3d0"}.fa-hooli:before{content:"\f427"}.fa-yelp:before{content:"\f1e9"}.fa-cc-visa:before{content:"\f1f0"}.fa-lastfm:before{content:"\f202"}.fa-shopware:before{content:"\f5b5"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-aws:before{content:"\f375"}.fa-redhat:before{content:"\f7bc"}.fa-yoast:before{content:"\f2b1"}.fa-cloudflare:before{content:"\e07d"}.fa-ups:before{content:"\f7e0"}.fa-pixiv:before{content:"\e640"}.fa-wpexplorer:before{content:"\f2de"}.fa-dyalog:before{content:"\f399"}.fa-bity:before{content:"\f37a"}.fa-stackpath:before{content:"\f842"}.fa-buysellads:before{content:"\f20d"}.fa-first-order:before{content:"\f2b0"}.fa-modx:before{content:"\f285"}.fa-guilded:before{content:"\e07e"}.fa-vnv:before{content:"\f40b"}.fa-js-square:before,.fa-square-js:before{content:"\f3b9"}.fa-microsoft:before{content:"\f3ca"}.fa-qq:before{content:"\f1d6"}.fa-orcid:before{content:"\f8d2"}.fa-java:before{content:"\f4e4"}.fa-invision:before{content:"\f7b0"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-centercode:before{content:"\f380"}.fa-glide-g:before{content:"\f2a6"}.fa-drupal:before{content:"\f1a9"}.fa-jxl:before{content:"\e67b"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-unity:before{content:"\e049"}.fa-whmcs:before{content:"\f40d"}.fa-rocketchat:before{content:"\f3e8"}.fa-vk:before{content:"\f189"}.fa-untappd:before{content:"\f405"}.fa-mailchimp:before{content:"\f59e"}.fa-css3-alt:before{content:"\f38b"}.fa-reddit-square:before,.fa-square-reddit:before{content:"\f1a2"}.fa-vimeo-v:before{content:"\f27d"}.fa-contao:before{content:"\f26d"}.fa-square-font-awesome:before{content:"\e5ad"}.fa-deskpro:before{content:"\f38f"}.fa-brave:before{content:"\e63c"}.fa-sistrix:before{content:"\f3ee"}.fa-instagram-square:before,.fa-square-instagram:before{content:"\e055"}.fa-battle-net:before{content:"\f835"}.fa-the-red-yeti:before{content:"\f69d"}.fa-hacker-news-square:before,.fa-square-hacker-news:before{content:"\f3af"}.fa-edge:before{content:"\f282"}.fa-threads:before{content:"\e618"}.fa-napster:before{content:"\f3d2"}.fa-snapchat-square:before,.fa-square-snapchat:before{content:"\f2ad"}.fa-google-plus-g:before{content:"\f0d5"}.fa-artstation:before{content:"\f77a"}.fa-markdown:before{content:"\f60f"}.fa-sourcetree:before{content:"\f7d3"}.fa-google-plus:before{content:"\f2b3"}.fa-diaspora:before{content:"\f791"}.fa-foursquare:before{content:"\f180"}.fa-stack-overflow:before{content:"\f16c"}.fa-github-alt:before{content:"\f113"}.fa-phoenix-squadron:before{content:"\f511"}.fa-pagelines:before{content:"\f18c"}.fa-algolia:before{content:"\f36c"}.fa-red-river:before{content:"\f3e3"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-safari:before{content:"\f267"}.fa-google:before{content:"\f1a0"}.fa-font-awesome-alt:before,.fa-square-font-awesome-stroke:before{content:"\f35c"}.fa-atlassian:before{content:"\f77b"}.fa-linkedin-in:before{content:"\f0e1"}.fa-digital-ocean:before{content:"\f391"}.fa-nimblr:before{content:"\f5a8"}.fa-chromecast:before{content:"\f838"}.fa-evernote:before{content:"\f839"}.fa-hacker-news:before{content:"\f1d4"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-adversal:before{content:"\f36a"}.fa-creative-commons:before{content:"\f25e"}.fa-watchman-monitoring:before{content:"\e087"}.fa-fonticons:before{content:"\f280"}.fa-weixin:before{content:"\f1d7"}.fa-shirtsinbulk:before{content:"\f214"}.fa-codepen:before{content:"\f1cb"}.fa-git-alt:before{content:"\f841"}.fa-lyft:before{content:"\f3c3"}.fa-rev:before{content:"\f5b2"}.fa-windows:before{content:"\f17a"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-square-viadeo:before,.fa-viadeo-square:before{content:"\f2aa"}.fa-meetup:before{content:"\f2e0"}.fa-centos:before{content:"\f789"}.fa-adn:before{content:"\f170"}.fa-cloudsmith:before{content:"\f384"}.fa-opensuse:before{content:"\e62b"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-dribbble-square:before,.fa-square-dribbble:before{content:"\f397"}.fa-codiepie:before{content:"\f284"}.fa-node:before{content:"\f419"}.fa-mix:before{content:"\f3cb"}.fa-steam:before{content:"\f1b6"}.fa-cc-apple-pay:before{content:"\f416"}.fa-scribd:before{content:"\f28a"}.fa-debian:before{content:"\e60b"}.fa-openid:before{content:"\f19b"}.fa-instalod:before{content:"\e081"}.fa-expeditedssl:before{content:"\f23e"}.fa-sellcast:before{content:"\f2da"}.fa-square-twitter:before,.fa-twitter-square:before{content:"\f081"}.fa-r-project:before{content:"\f4f7"}.fa-delicious:before{content:"\f1a5"}.fa-freebsd:before{content:"\f3a4"}.fa-vuejs:before{content:"\f41f"}.fa-accusoft:before{content:"\f369"}.fa-ioxhost:before{content:"\f208"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-app-store:before{content:"\f36f"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-itunes-note:before{content:"\f3b5"}.fa-golang:before{content:"\e40f"}.fa-kickstarter:before,.fa-square-kickstarter:before{content:"\f3bb"}.fa-grav:before{content:"\f2d6"}.fa-weibo:before{content:"\f18a"}.fa-uncharted:before{content:"\e084"}.fa-firstdraft:before{content:"\f3a1"}.fa-square-youtube:before,.fa-youtube-square:before{content:"\f431"}.fa-wikipedia-w:before{content:"\f266"}.fa-rendact:before,.fa-wpressr:before{content:"\f3e4"}.fa-angellist:before{content:"\f209"}.fa-galactic-republic:before{content:"\f50c"}.fa-nfc-directional:before{content:"\e530"}.fa-skype:before{content:"\f17e"}.fa-joget:before{content:"\f3b7"}.fa-fedora:before{content:"\f798"}.fa-stripe-s:before{content:"\f42a"}.fa-meta:before{content:"\e49b"}.fa-laravel:before{content:"\f3bd"}.fa-hotjar:before{content:"\f3b1"}.fa-bluetooth-b:before{content:"\f294"}.fa-square-letterboxd:before{content:"\e62e"}.fa-sticker-mule:before{content:"\f3f7"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-hips:before{content:"\f452"}.fa-behance:before{content:"\f1b4"}.fa-reddit:before{content:"\f1a1"}.fa-discord:before{content:"\f392"}.fa-chrome:before{content:"\f268"}.fa-app-store-ios:before{content:"\f370"}.fa-cc-discover:before{content:"\f1f2"}.fa-wpbeginner:before{content:"\f297"}.fa-confluence:before{content:"\f78d"}.fa-shoelace:before{content:"\e60c"}.fa-mdb:before{content:"\f8ca"}.fa-dochub:before{content:"\f394"}.fa-accessible-icon:before{content:"\f368"}.fa-ebay:before{content:"\f4f4"}.fa-amazon:before{content:"\f270"}.fa-unsplash:before{content:"\e07c"}.fa-yarn:before{content:"\f7e3"}.fa-square-steam:before,.fa-steam-square:before{content:"\f1b7"}.fa-500px:before{content:"\f26e"}.fa-square-vimeo:before,.fa-vimeo-square:before{content:"\f194"}.fa-asymmetrik:before{content:"\f372"}.fa-font-awesome-flag:before,.fa-font-awesome-logo-full:before,.fa-font-awesome:before{content:"\f2b4"}.fa-gratipay:before{content:"\f184"}.fa-apple:before{content:"\f179"}.fa-hive:before{content:"\e07f"}.fa-gitkraken:before{content:"\f3a6"}.fa-keybase:before{content:"\f4f5"}.fa-apple-pay:before{content:"\f415"}.fa-padlet:before{content:"\e4a0"}.fa-amazon-pay:before{content:"\f42c"}.fa-github-square:before,.fa-square-github:before{content:"\f092"}.fa-stumbleupon:before{content:"\f1a4"}.fa-fedex:before{content:"\f797"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-shopify:before{content:"\e057"}.fa-neos:before{content:"\f612"}.fa-square-threads:before{content:"\e619"}.fa-hackerrank:before{content:"\f5f7"}.fa-researchgate:before{content:"\f4f8"}.fa-swift:before{content:"\f8e1"}.fa-angular:before{content:"\f420"}.fa-speakap:before{content:"\f3f3"}.fa-angrycreative:before{content:"\f36e"}.fa-y-combinator:before{content:"\f23b"}.fa-empire:before{content:"\f1d1"}.fa-envira:before{content:"\f299"}.fa-google-scholar:before{content:"\e63b"}.fa-gitlab-square:before,.fa-square-gitlab:before{content:"\e5ae"}.fa-studiovinari:before{content:"\f3f8"}.fa-pied-piper:before{content:"\f2ae"}.fa-wordpress:before{content:"\f19a"}.fa-product-hunt:before{content:"\f288"}.fa-firefox:before{content:"\f269"}.fa-linode:before{content:"\f2b8"}.fa-goodreads:before{content:"\f3a8"}.fa-odnoklassniki-square:before,.fa-square-odnoklassniki:before{content:"\f264"}.fa-jsfiddle:before{content:"\f1cc"}.fa-sith:before{content:"\f512"}.fa-themeisle:before{content:"\f2b2"}.fa-page4:before{content:"\f3d7"}.fa-hashnode:before{content:"\e499"}.fa-react:before{content:"\f41b"}.fa-cc-paypal:before{content:"\f1f4"}.fa-squarespace:before{content:"\f5be"}.fa-cc-stripe:before{content:"\f1f5"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-bitcoin:before{content:"\f379"}.fa-keycdn:before{content:"\f3ba"}.fa-opera:before{content:"\f26a"}.fa-itch-io:before{content:"\f83a"}.fa-umbraco:before{content:"\f8e8"}.fa-galactic-senate:before{content:"\f50d"}.fa-ubuntu:before{content:"\f7df"}.fa-draft2digital:before{content:"\f396"}.fa-stripe:before{content:"\f429"}.fa-houzz:before{content:"\f27c"}.fa-gg:before{content:"\f260"}.fa-dhl:before{content:"\f790"}.fa-pinterest-square:before,.fa-square-pinterest:before{content:"\f0d3"}.fa-xing:before{content:"\f168"}.fa-blackberry:before{content:"\f37b"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-playstation:before{content:"\f3df"}.fa-quinscape:before{content:"\f459"}.fa-less:before{content:"\f41d"}.fa-blogger-b:before{content:"\f37d"}.fa-opencart:before{content:"\f23d"}.fa-vine:before{content:"\f1ca"}.fa-signal-messenger:before{content:"\e663"}.fa-paypal:before{content:"\f1ed"}.fa-gitlab:before{content:"\f296"}.fa-typo3:before{content:"\f42b"}.fa-reddit-alien:before{content:"\f281"}.fa-yahoo:before{content:"\f19e"}.fa-dailymotion:before{content:"\e052"}.fa-affiliatetheme:before{content:"\f36b"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-bootstrap:before{content:"\f836"}.fa-odnoklassniki:before{content:"\f263"}.fa-nfc-symbol:before{content:"\e531"}.fa-mintbit:before{content:"\e62f"}.fa-ethereum:before{content:"\f42e"}.fa-speaker-deck:before{content:"\f83c"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-patreon:before{content:"\f3d9"}.fa-avianex:before{content:"\f374"}.fa-ello:before{content:"\f5f1"}.fa-gofore:before{content:"\f3a7"}.fa-bimobject:before{content:"\f378"}.fa-brave-reverse:before{content:"\e63d"}.fa-facebook-f:before{content:"\f39e"}.fa-google-plus-square:before,.fa-square-google-plus:before{content:"\f0d4"}.fa-web-awesome:before{content:"\e682"}.fa-mandalorian:before{content:"\f50f"}.fa-first-order-alt:before{content:"\f50a"}.fa-osi:before{content:"\f41a"}.fa-google-wallet:before{content:"\f1ee"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-periscope:before{content:"\f3da"}.fa-fulcrum:before{content:"\f50b"}.fa-cloudscale:before{content:"\f383"}.fa-forumbee:before{content:"\f211"}.fa-mizuni:before{content:"\f3cc"}.fa-schlix:before{content:"\f3ea"}.fa-square-xing:before,.fa-xing-square:before{content:"\f169"}.fa-bandcamp:before{content:"\f2d5"}.fa-wpforms:before{content:"\f298"}.fa-cloudversify:before{content:"\f385"}.fa-usps:before{content:"\f7e1"}.fa-megaport:before{content:"\f5a3"}.fa-magento:before{content:"\f3c4"}.fa-spotify:before{content:"\f1bc"}.fa-optin-monster:before{content:"\f23c"}.fa-fly:before{content:"\f417"}.fa-aviato:before{content:"\f421"}.fa-itunes:before{content:"\f3b4"}.fa-cuttlefish:before{content:"\f38c"}.fa-blogger:before{content:"\f37c"}.fa-flickr:before{content:"\f16e"}.fa-viber:before{content:"\f409"}.fa-soundcloud:before{content:"\f1be"}.fa-digg:before{content:"\f1a6"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-letterboxd:before{content:"\e62d"}.fa-symfony:before{content:"\f83d"}.fa-maxcdn:before{content:"\f136"}.fa-etsy:before{content:"\f2d7"}.fa-facebook-messenger:before{content:"\f39f"}.fa-audible:before{content:"\f373"}.fa-think-peaks:before{content:"\f731"}.fa-bilibili:before{content:"\e3d9"}.fa-erlang:before{content:"\f39d"}.fa-x-twitter:before{content:"\e61b"}.fa-cotton-bureau:before{content:"\f89e"}.fa-dashcube:before{content:"\f210"}.fa-42-group:before,.fa-innosoft:before{content:"\e080"}.fa-stack-exchange:before{content:"\f18d"}.fa-elementor:before{content:"\f430"}.fa-pied-piper-square:before,.fa-square-pied-piper:before{content:"\e01e"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-palfed:before{content:"\f3d8"}.fa-superpowers:before{content:"\f2dd"}.fa-resolving:before{content:"\f3e7"}.fa-xbox:before{content:"\f412"}.fa-square-web-awesome-stroke:before{content:"\e684"}.fa-searchengin:before{content:"\f3eb"}.fa-tiktok:before{content:"\e07b"}.fa-facebook-square:before,.fa-square-facebook:before{content:"\f082"}.fa-renren:before{content:"\f18b"}.fa-linux:before{content:"\f17c"}.fa-glide:before{content:"\f2a5"}.fa-linkedin:before{content:"\f08c"}.fa-hubspot:before{content:"\f3b2"}.fa-deploydog:before{content:"\f38e"}.fa-twitch:before{content:"\f1e8"}.fa-ravelry:before{content:"\f2d9"}.fa-mixer:before{content:"\e056"}.fa-lastfm-square:before,.fa-square-lastfm:before{content:"\f203"}.fa-vimeo:before{content:"\f40a"}.fa-mendeley:before{content:"\f7b3"}.fa-uniregistry:before{content:"\f404"}.fa-figma:before{content:"\f799"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-dropbox:before{content:"\f16b"}.fa-instagram:before{content:"\f16d"}.fa-cmplid:before{content:"\e360"}.fa-upwork:before{content:"\e641"}.fa-facebook:before{content:"\f09a"}.fa-gripfire:before{content:"\f3ac"}.fa-jedi-order:before{content:"\f50e"}.fa-uikit:before{content:"\f403"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-phabricator:before{content:"\f3db"}.fa-ussunnah:before{content:"\f407"}.fa-earlybirds:before{content:"\f39a"}.fa-trade-federation:before{content:"\f513"}.fa-autoprefixer:before{content:"\f41c"}.fa-whatsapp:before{content:"\f232"}.fa-square-upwork:before{content:"\e67c"}.fa-slideshare:before{content:"\f1e7"}.fa-google-play:before{content:"\f3ab"}.fa-viadeo:before{content:"\f2a9"}.fa-line:before{content:"\f3c0"}.fa-google-drive:before{content:"\f3aa"}.fa-servicestack:before{content:"\f3ec"}.fa-simplybuilt:before{content:"\f215"}.fa-bitbucket:before{content:"\f171"}.fa-imdb:before{content:"\f2d8"}.fa-deezer:before{content:"\e077"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-jira:before{content:"\f7b1"}.fa-docker:before{content:"\f395"}.fa-screenpal:before{content:"\e570"}.fa-bluetooth:before{content:"\f293"}.fa-gitter:before{content:"\f426"}.fa-d-and-d:before{content:"\f38d"}.fa-microblog:before{content:"\e01a"}.fa-cc-diners-club:before{content:"\f24c"}.fa-gg-circle:before{content:"\f261"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-yandex:before{content:"\f413"}.fa-readme:before{content:"\f4d5"}.fa-html5:before{content:"\f13b"}.fa-sellsy:before{content:"\f213"}.fa-square-web-awesome:before{content:"\e683"}.fa-sass:before{content:"\f41e"}.fa-wirsindhandwerk:before,.fa-wsh:before{content:"\e2d0"}.fa-buromobelexperte:before{content:"\f37f"}.fa-salesforce:before{content:"\f83b"}.fa-octopus-deploy:before{content:"\e082"}.fa-medapps:before{content:"\f3c6"}.fa-ns8:before{content:"\f3d5"}.fa-pinterest-p:before{content:"\f231"}.fa-apper:before{content:"\f371"}.fa-fort-awesome:before{content:"\f286"}.fa-waze:before{content:"\f83f"}.fa-bluesky:before{content:"\e671"}.fa-cc-jcb:before{content:"\f24b"}.fa-snapchat-ghost:before,.fa-snapchat:before{content:"\f2ab"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-rust:before{content:"\e07a"}.fa-wix:before{content:"\f5cf"}.fa-behance-square:before,.fa-square-behance:before{content:"\f1b5"}.fa-supple:before{content:"\f3f9"}.fa-webflow:before{content:"\e65c"}.fa-rebel:before{content:"\f1d0"}.fa-css3:before{content:"\f13c"}.fa-staylinked:before{content:"\f3f5"}.fa-kaggle:before{content:"\f5fa"}.fa-space-awesome:before{content:"\e5ac"}.fa-deviantart:before{content:"\f1bd"}.fa-cpanel:before{content:"\f388"}.fa-goodreads-g:before{content:"\f3a9"}.fa-git-square:before,.fa-square-git:before{content:"\f1d2"}.fa-square-tumblr:before,.fa-tumblr-square:before{content:"\f174"}.fa-trello:before{content:"\f181"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-get-pocket:before{content:"\f265"}.fa-perbyte:before{content:"\e083"}.fa-grunt:before{content:"\f3ad"}.fa-weebly:before{content:"\f5cc"}.fa-connectdevelop:before{content:"\f20e"}.fa-leanpub:before{content:"\f212"}.fa-black-tie:before{content:"\f27e"}.fa-themeco:before{content:"\f5c6"}.fa-python:before{content:"\f3e2"}.fa-android:before{content:"\f17b"}.fa-bots:before{content:"\e340"}.fa-free-code-camp:before{content:"\f2c5"}.fa-hornbill:before{content:"\f592"}.fa-js:before{content:"\f3b8"}.fa-ideal:before{content:"\e013"}.fa-git:before{content:"\f1d3"}.fa-dev:before{content:"\f6cc"}.fa-sketch:before{content:"\f7c6"}.fa-yandex-international:before{content:"\f414"}.fa-cc-amex:before{content:"\f1f3"}.fa-uber:before{content:"\f402"}.fa-github:before{content:"\f09b"}.fa-php:before{content:"\f457"}.fa-alipay:before{content:"\f642"}.fa-youtube:before{content:"\f167"}.fa-skyatlas:before{content:"\f216"}.fa-firefox-browser:before{content:"\e007"}.fa-replyd:before{content:"\f3e6"}.fa-suse:before{content:"\f7d6"}.fa-jenkins:before{content:"\f3b6"}.fa-twitter:before{content:"\f099"}.fa-rockrms:before{content:"\f3e9"}.fa-pinterest:before{content:"\f0d2"}.fa-buffer:before{content:"\f837"}.fa-npm:before{content:"\f3d4"}.fa-yammer:before{content:"\f840"}.fa-btc:before{content:"\f15a"}.fa-dribbble:before{content:"\f17d"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-internet-explorer:before{content:"\f26b"}.fa-stubber:before{content:"\e5c7"}.fa-telegram-plane:before,.fa-telegram:before{content:"\f2c6"}.fa-old-republic:before{content:"\f510"}.fa-odysee:before{content:"\e5c6"}.fa-square-whatsapp:before,.fa-whatsapp-square:before{content:"\f40c"}.fa-node-js:before{content:"\f3d3"}.fa-edge-legacy:before{content:"\e078"}.fa-slack-hash:before,.fa-slack:before{content:"\f198"}.fa-medrt:before{content:"\f3c8"}.fa-usb:before{content:"\f287"}.fa-tumblr:before{content:"\f173"}.fa-vaadin:before{content:"\f408"}.fa-quora:before{content:"\f2c4"}.fa-square-x-twitter:before{content:"\e61a"}.fa-reacteurope:before{content:"\f75d"}.fa-medium-m:before,.fa-medium:before{content:"\f23a"}.fa-amilia:before{content:"\f36d"}.fa-mixcloud:before{content:"\f289"}.fa-flipboard:before{content:"\f44d"}.fa-viacoin:before{content:"\f237"}.fa-critical-role:before{content:"\f6c9"}.fa-sitrox:before{content:"\e44a"}.fa-discourse:before{content:"\f393"}.fa-joomla:before{content:"\f1aa"}.fa-mastodon:before{content:"\f4f6"}.fa-airbnb:before{content:"\f834"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-buy-n-large:before{content:"\f8a6"}.fa-gulp:before{content:"\f3ae"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-strava:before{content:"\f428"}.fa-ember:before{content:"\f423"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-teamspeak:before{content:"\f4f9"}.fa-pushed:before{content:"\f3e1"}.fa-wordpress-simple:before{content:"\f411"}.fa-nutritionix:before{content:"\f3d6"}.fa-wodu:before{content:"\e088"}.fa-google-pay:before{content:"\e079"}.fa-intercom:before{content:"\f7af"}.fa-zhihu:before{content:"\f63f"}.fa-korvue:before{content:"\f42f"}.fa-pix:before{content:"\e43a"}.fa-steam-symbol:before{content:"\f3f6"}:host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}
\ No newline at end of file
diff --git a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.ttf b/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.ttf
deleted file mode 100644
index 1fbb1f7c..00000000
Binary files a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.ttf and /dev/null differ
diff --git a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.woff2 b/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.woff2
deleted file mode 100644
index 5d280216..00000000
Binary files a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.woff2 and /dev/null differ
diff --git a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.ttf b/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.ttf
deleted file mode 100644
index 549d68dc..00000000
Binary files a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.ttf and /dev/null differ
diff --git a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.woff2 b/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.woff2
deleted file mode 100644
index 18400d7f..00000000
Binary files a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.woff2 and /dev/null differ
diff --git a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.ttf b/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.ttf
deleted file mode 100644
index bb2a8695..00000000
Binary files a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.ttf and /dev/null differ
diff --git a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.woff2 b/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.woff2
deleted file mode 100644
index 758dd4f6..00000000
Binary files a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.woff2 and /dev/null differ
diff --git a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.ttf b/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.ttf
deleted file mode 100644
index 8c5864c4..00000000
Binary files a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.ttf and /dev/null differ
diff --git a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.woff2 b/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.woff2
deleted file mode 100644
index f94bec22..00000000
Binary files a/build/lib/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.woff2 and /dev/null differ
diff --git a/build/lib/exo/tinychat/static/fonts.googleapis.com/css2 b/build/lib/exo/tinychat/static/fonts.googleapis.com/css2
deleted file mode 100644
index 82f55e83..00000000
--- a/build/lib/exo/tinychat/static/fonts.googleapis.com/css2
+++ /dev/null
@@ -1,7 +0,0 @@
-@font-face {
-  font-family: 'Megrim';
-  font-style: normal;
-  font-weight: 400;
-  font-display: swap;
-  src: url(https://fonts.gstatic.com/s/megrim/v16/46kulbz5WjvLqJZlbQ.ttf) format('truetype');
-}
diff --git a/build/lib/exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js b/build/lib/exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js
deleted file mode 100644
index 5d699ae6..00000000
--- a/build/lib/exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js
+++ /dev/null
@@ -1,1213 +0,0 @@
-/*!
-  Highlight.js v11.9.0 (git: f47103d4f1)
-  (c) 2006-2023 undefined and other contributors
-  License: BSD-3-Clause
- */
-var hljs=function(){"use strict";function e(n){
-return n instanceof Map?n.clear=n.delete=n.set=()=>{
-throw Error("map is read-only")}:n instanceof Set&&(n.add=n.clear=n.delete=()=>{
-throw Error("set is read-only")
-}),Object.freeze(n),Object.getOwnPropertyNames(n).forEach((t=>{
-const a=n[t],i=typeof a;"object"!==i&&"function"!==i||Object.isFrozen(a)||e(a)
-})),n}class n{constructor(e){
-void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}
-ignoreMatch(){this.isMatchIgnored=!0}}function t(e){
-return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;")
-}function a(e,...n){const t=Object.create(null);for(const n in e)t[n]=e[n]
-;return n.forEach((e=>{for(const n in e)t[n]=e[n]})),t}const i=e=>!!e.scope
-;class r{constructor(e,n){
-this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){
-this.buffer+=t(e)}openNode(e){if(!i(e))return;const n=((e,{prefix:n})=>{
-if(e.startsWith("language:"))return e.replace("language:","language-")
-;if(e.includes(".")){const t=e.split(".")
-;return[`${n}${t.shift()}`,...t.map(((e,n)=>`${e}${"_".repeat(n+1)}`))].join(" ")
-}return`${n}${e}`})(e.scope,{prefix:this.classPrefix});this.span(n)}
-closeNode(e){i(e)&&(this.buffer+="</span>")}value(){return this.buffer}span(e){
-this.buffer+=`<span class="${e}">`}}const s=(e={})=>{const n={children:[]}
-;return Object.assign(n,e),n};class o{constructor(){
-this.rootNode=s(),this.stack=[this.rootNode]}get top(){
-return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){
-this.top.children.push(e)}openNode(e){const n=s({scope:e})
-;this.add(n),this.stack.push(n)}closeNode(){
-if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){
-for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}
-walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){
-return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n),
-n.children.forEach((n=>this._walk(e,n))),e.closeNode(n)),e}static _collapse(e){
-"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{
-o._collapse(e)})))}}class l extends o{constructor(e){super(),this.options=e}
-addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){
-this.closeNode()}__addSublanguage(e,n){const t=e.root
-;n&&(t.scope="language:"+n),this.add(t)}toHTML(){
-return new r(this,this.options).value()}finalize(){
-return this.closeAllNodes(),!0}}function c(e){
-return e?"string"==typeof e?e:e.source:null}function d(e){return b("(?=",e,")")}
-function g(e){return b("(?:",e,")*")}function u(e){return b("(?:",e,")?")}
-function b(...e){return e.map((e=>c(e))).join("")}function m(...e){const n=(e=>{
-const n=e[e.length-1]
-;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{}
-})(e);return"("+(n.capture?"":"?:")+e.map((e=>c(e))).join("|")+")"}
-function p(e){return RegExp(e.toString()+"|").exec("").length-1}
-const _=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./
-;function h(e,{joinWith:n}){let t=0;return e.map((e=>{t+=1;const n=t
-;let a=c(e),i="";for(;a.length>0;){const e=_.exec(a);if(!e){i+=a;break}
-i+=a.substring(0,e.index),
-a=a.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?i+="\\"+(Number(e[1])+n):(i+=e[0],
-"("===e[0]&&t++)}return i})).map((e=>`(${e})`)).join(n)}
-const f="[a-zA-Z]\\w*",E="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",N="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",w="\\b(0b[01]+)",v={
-begin:"\\\\[\\s\\S]",relevance:0},O={scope:"string",begin:"'",end:"'",
-illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n",
-contains:[v]},x=(e,n,t={})=>{const i=a({scope:"comment",begin:e,end:n,
-contains:[]},t);i.contains.push({scope:"doctag",
-begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",
-end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0})
-;const r=m("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/)
-;return i.contains.push({begin:b(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i
-},M=x("//","$"),S=x("/\\*","\\*/"),A=x("#","$");var C=Object.freeze({
-__proto__:null,APOS_STRING_MODE:O,BACKSLASH_ESCAPE:v,BINARY_NUMBER_MODE:{
-scope:"number",begin:w,relevance:0},BINARY_NUMBER_RE:w,COMMENT:x,
-C_BLOCK_COMMENT_MODE:S,C_LINE_COMMENT_MODE:M,C_NUMBER_MODE:{scope:"number",
-begin:N,relevance:0},C_NUMBER_RE:N,END_SAME_AS_BEGIN:e=>Object.assign(e,{
-"on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{
-n.data._beginMatch!==e[1]&&n.ignoreMatch()}}),HASH_COMMENT_MODE:A,IDENT_RE:f,
-MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+E,relevance:0},
-NUMBER_MODE:{scope:"number",begin:y,relevance:0},NUMBER_RE:y,
-PHRASAL_WORDS_MODE:{
-begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
-},QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/,
-end:/\/[gimuy]*/,contains:[v,{begin:/\[/,end:/\]/,relevance:0,contains:[v]}]},
-RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",
-SHEBANG:(e={})=>{const n=/^#![ ]*\//
-;return e.binary&&(e.begin=b(n,/.*\b/,e.binary,/\b.*/)),a({scope:"meta",begin:n,
-end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)},
-TITLE_MODE:{scope:"title",begin:f,relevance:0},UNDERSCORE_IDENT_RE:E,
-UNDERSCORE_TITLE_MODE:{scope:"title",begin:E,relevance:0}});function T(e,n){
-"."===e.input[e.index-1]&&n.ignoreMatch()}function R(e,n){
-void 0!==e.className&&(e.scope=e.className,delete e.className)}function D(e,n){
-n&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",
-e.__beforeBegin=T,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,
-void 0===e.relevance&&(e.relevance=0))}function I(e,n){
-Array.isArray(e.illegal)&&(e.illegal=m(...e.illegal))}function L(e,n){
-if(e.match){
-if(e.begin||e.end)throw Error("begin & end are not supported with match")
-;e.begin=e.match,delete e.match}}function B(e,n){
-void 0===e.relevance&&(e.relevance=1)}const $=(e,n)=>{if(!e.beforeMatch)return
-;if(e.starts)throw Error("beforeMatch cannot be used with starts")
-;const t=Object.assign({},e);Object.keys(e).forEach((n=>{delete e[n]
-})),e.keywords=t.keywords,e.begin=b(t.beforeMatch,d(t.begin)),e.starts={
-relevance:0,contains:[Object.assign(t,{endsParent:!0})]
-},e.relevance=0,delete t.beforeMatch
-},z=["of","and","for","in","not","or","if","then","parent","list","value"],F="keyword"
-;function U(e,n,t=F){const a=Object.create(null)
-;return"string"==typeof e?i(t,e.split(" ")):Array.isArray(e)?i(t,e):Object.keys(e).forEach((t=>{
-Object.assign(a,U(e[t],n,t))})),a;function i(e,t){
-n&&(t=t.map((e=>e.toLowerCase()))),t.forEach((n=>{const t=n.split("|")
-;a[t[0]]=[e,j(t[0],t[1])]}))}}function j(e,n){
-return n?Number(n):(e=>z.includes(e.toLowerCase()))(e)?0:1}const P={},K=e=>{
-console.error(e)},H=(e,...n)=>{console.log("WARN: "+e,...n)},q=(e,n)=>{
-P[`${e}/${n}`]||(console.log(`Deprecated as of ${e}. ${n}`),P[`${e}/${n}`]=!0)
-},G=Error();function Z(e,n,{key:t}){let a=0;const i=e[t],r={},s={}
-;for(let e=1;e<=n.length;e++)s[e+a]=i[e],r[e+a]=!0,a+=p(n[e-1])
-;e[t]=s,e[t]._emit=r,e[t]._multi=!0}function W(e){(e=>{
-e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope,
-delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={
-_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope
-}),(e=>{if(Array.isArray(e.begin)){
-if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),
-G
-;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"),
-G;Z(e,e.begin,{key:"beginScope"}),e.begin=h(e.begin,{joinWith:""})}})(e),(e=>{
-if(Array.isArray(e.end)){
-if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"),
-G
-;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"),
-G;Z(e,e.end,{key:"endScope"}),e.end=h(e.end,{joinWith:""})}})(e)}function Q(e){
-function n(n,t){
-return RegExp(c(n),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(t?"g":""))
-}class t{constructor(){
-this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}
-addRule(e,n){
-n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]),
-this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null)
-;const e=this.regexes.map((e=>e[1]));this.matcherRe=n(h(e,{joinWith:"|"
-}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex
-;const n=this.matcherRe.exec(e);if(!n)return null
-;const t=n.findIndex(((e,n)=>n>0&&void 0!==e)),a=this.matchIndexes[t]
-;return n.splice(0,t),Object.assign(n,a)}}class i{constructor(){
-this.rules=[],this.multiRegexes=[],
-this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){
-if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t
-;return this.rules.slice(e).forEach((([e,t])=>n.addRule(e,t))),
-n.compile(),this.multiRegexes[e]=n,n}resumingScanAtSamePosition(){
-return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,n){
-this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){
-const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex
-;let t=n.exec(e)
-;if(this.resumingScanAtSamePosition())if(t&&t.index===this.lastIndex);else{
-const n=this.getMatcher(0);n.lastIndex=this.lastIndex+1,t=n.exec(e)}
-return t&&(this.regexIndex+=t.position+1,
-this.regexIndex===this.count&&this.considerAll()),t}}
-if(e.compilerExtensions||(e.compilerExtensions=[]),
-e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language.  See documentation.")
-;return e.classNameAliases=a(e.classNameAliases||{}),function t(r,s){const o=r
-;if(r.isCompiled)return o
-;[R,L,W,$].forEach((e=>e(r,s))),e.compilerExtensions.forEach((e=>e(r,s))),
-r.__beforeBegin=null,[D,I,B].forEach((e=>e(r,s))),r.isCompiled=!0;let l=null
-;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords),
-l=r.keywords.$pattern,
-delete r.keywords.$pattern),l=l||/\w+/,r.keywords&&(r.keywords=U(r.keywords,e.case_insensitive)),
-o.keywordPatternRe=n(l,!0),
-s&&(r.begin||(r.begin=/\B|\b/),o.beginRe=n(o.begin),r.end||r.endsWithParent||(r.end=/\B|\b/),
-r.end&&(o.endRe=n(o.end)),
-o.terminatorEnd=c(o.end)||"",r.endsWithParent&&s.terminatorEnd&&(o.terminatorEnd+=(r.end?"|":"")+s.terminatorEnd)),
-r.illegal&&(o.illegalRe=n(r.illegal)),
-r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((n=>a(e,{
-variants:null},n)))),e.cachedVariants?e.cachedVariants:X(e)?a(e,{
-starts:e.starts?a(e.starts):null
-}):Object.isFrozen(e)?a(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{t(e,o)
-})),r.starts&&t(r.starts,s),o.matcher=(e=>{const n=new i
-;return e.contains.forEach((e=>n.addRule(e.begin,{rule:e,type:"begin"
-}))),e.terminatorEnd&&n.addRule(e.terminatorEnd,{type:"end"
-}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n})(o),o}(e)}function X(e){
-return!!e&&(e.endsWithParent||X(e.starts))}class V extends Error{
-constructor(e,n){super(e),this.name="HTMLInjectionError",this.html=n}}
-const J=t,Y=a,ee=Symbol("nomatch"),ne=t=>{
-const a=Object.create(null),i=Object.create(null),r=[];let s=!0
-;const o="Could not find the language '{}', did you forget to load/include a language module?",c={
-disableAutodetect:!0,name:"Plain text",contains:[]};let p={
-ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,
-languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",
-cssSelector:"pre code",languages:null,__emitter:l};function _(e){
-return p.noHighlightRe.test(e)}function h(e,n,t){let a="",i=""
-;"object"==typeof n?(a=e,
-t=n.ignoreIllegals,i=n.language):(q("10.7.0","highlight(lang, code, ...args) has been deprecated."),
-q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),
-i=e,a=n),void 0===t&&(t=!0);const r={code:a,language:i};x("before:highlight",r)
-;const s=r.result?r.result:f(r.language,r.code,t)
-;return s.code=r.code,x("after:highlight",s),s}function f(e,t,i,r){
-const l=Object.create(null);function c(){if(!x.keywords)return void S.addText(A)
-;let e=0;x.keywordPatternRe.lastIndex=0;let n=x.keywordPatternRe.exec(A),t=""
-;for(;n;){t+=A.substring(e,n.index)
-;const i=w.case_insensitive?n[0].toLowerCase():n[0],r=(a=i,x.keywords[a]);if(r){
-const[e,a]=r
-;if(S.addText(t),t="",l[i]=(l[i]||0)+1,l[i]<=7&&(C+=a),e.startsWith("_"))t+=n[0];else{
-const t=w.classNameAliases[e]||e;g(n[0],t)}}else t+=n[0]
-;e=x.keywordPatternRe.lastIndex,n=x.keywordPatternRe.exec(A)}var a
-;t+=A.substring(e),S.addText(t)}function d(){null!=x.subLanguage?(()=>{
-if(""===A)return;let e=null;if("string"==typeof x.subLanguage){
-if(!a[x.subLanguage])return void S.addText(A)
-;e=f(x.subLanguage,A,!0,M[x.subLanguage]),M[x.subLanguage]=e._top
-}else e=E(A,x.subLanguage.length?x.subLanguage:null)
-;x.relevance>0&&(C+=e.relevance),S.__addSublanguage(e._emitter,e.language)
-})():c(),A=""}function g(e,n){
-""!==e&&(S.startScope(n),S.addText(e),S.endScope())}function u(e,n){let t=1
-;const a=n.length-1;for(;t<=a;){if(!e._emit[t]){t++;continue}
-const a=w.classNameAliases[e[t]]||e[t],i=n[t];a?g(i,a):(A=i,c(),A=""),t++}}
-function b(e,n){
-return e.scope&&"string"==typeof e.scope&&S.openNode(w.classNameAliases[e.scope]||e.scope),
-e.beginScope&&(e.beginScope._wrap?(g(A,w.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),
-A=""):e.beginScope._multi&&(u(e.beginScope,n),A="")),x=Object.create(e,{parent:{
-value:x}}),x}function m(e,t,a){let i=((e,n)=>{const t=e&&e.exec(n)
-;return t&&0===t.index})(e.endRe,a);if(i){if(e["on:end"]){const a=new n(e)
-;e["on:end"](t,a),a.isMatchIgnored&&(i=!1)}if(i){
-for(;e.endsParent&&e.parent;)e=e.parent;return e}}
-if(e.endsWithParent)return m(e.parent,t,a)}function _(e){
-return 0===x.matcher.regexIndex?(A+=e[0],1):(D=!0,0)}function h(e){
-const n=e[0],a=t.substring(e.index),i=m(x,e,a);if(!i)return ee;const r=x
-;x.endScope&&x.endScope._wrap?(d(),
-g(n,x.endScope._wrap)):x.endScope&&x.endScope._multi?(d(),
-u(x.endScope,e)):r.skip?A+=n:(r.returnEnd||r.excludeEnd||(A+=n),
-d(),r.excludeEnd&&(A=n));do{
-x.scope&&S.closeNode(),x.skip||x.subLanguage||(C+=x.relevance),x=x.parent
-}while(x!==i.parent);return i.starts&&b(i.starts,e),r.returnEnd?0:n.length}
-let y={};function N(a,r){const o=r&&r[0];if(A+=a,null==o)return d(),0
-;if("begin"===y.type&&"end"===r.type&&y.index===r.index&&""===o){
-if(A+=t.slice(r.index,r.index+1),!s){const n=Error(`0 width match regex (${e})`)
-;throw n.languageName=e,n.badRule=y.rule,n}return 1}
-if(y=r,"begin"===r.type)return(e=>{
-const t=e[0],a=e.rule,i=new n(a),r=[a.__beforeBegin,a["on:begin"]]
-;for(const n of r)if(n&&(n(e,i),i.isMatchIgnored))return _(t)
-;return a.skip?A+=t:(a.excludeBegin&&(A+=t),
-d(),a.returnBegin||a.excludeBegin||(A=t)),b(a,e),a.returnBegin?0:t.length})(r)
-;if("illegal"===r.type&&!i){
-const e=Error('Illegal lexeme "'+o+'" for mode "'+(x.scope||"<unnamed>")+'"')
-;throw e.mode=x,e}if("end"===r.type){const e=h(r);if(e!==ee)return e}
-if("illegal"===r.type&&""===o)return 1
-;if(R>1e5&&R>3*r.index)throw Error("potential infinite loop, way more iterations than matches")
-;return A+=o,o.length}const w=v(e)
-;if(!w)throw K(o.replace("{}",e)),Error('Unknown language: "'+e+'"')
-;const O=Q(w);let k="",x=r||O;const M={},S=new p.__emitter(p);(()=>{const e=[]
-;for(let n=x;n!==w;n=n.parent)n.scope&&e.unshift(n.scope)
-;e.forEach((e=>S.openNode(e)))})();let A="",C=0,T=0,R=0,D=!1;try{
-if(w.__emitTokens)w.__emitTokens(t,S);else{for(x.matcher.considerAll();;){
-R++,D?D=!1:x.matcher.considerAll(),x.matcher.lastIndex=T
-;const e=x.matcher.exec(t);if(!e)break;const n=N(t.substring(T,e.index),e)
-;T=e.index+n}N(t.substring(T))}return S.finalize(),k=S.toHTML(),{language:e,
-value:k,relevance:C,illegal:!1,_emitter:S,_top:x}}catch(n){
-if(n.message&&n.message.includes("Illegal"))return{language:e,value:J(t),
-illegal:!0,relevance:0,_illegalBy:{message:n.message,index:T,
-context:t.slice(T-100,T+100),mode:n.mode,resultSoFar:k},_emitter:S};if(s)return{
-language:e,value:J(t),illegal:!1,relevance:0,errorRaised:n,_emitter:S,_top:x}
-;throw n}}function E(e,n){n=n||p.languages||Object.keys(a);const t=(e=>{
-const n={value:J(e),illegal:!1,relevance:0,_top:c,_emitter:new p.__emitter(p)}
-;return n._emitter.addText(e),n})(e),i=n.filter(v).filter(k).map((n=>f(n,e,!1)))
-;i.unshift(t);const r=i.sort(((e,n)=>{
-if(e.relevance!==n.relevance)return n.relevance-e.relevance
-;if(e.language&&n.language){if(v(e.language).supersetOf===n.language)return 1
-;if(v(n.language).supersetOf===e.language)return-1}return 0})),[s,o]=r,l=s
-;return l.secondBest=o,l}function y(e){let n=null;const t=(e=>{
-let n=e.className+" ";n+=e.parentNode?e.parentNode.className:""
-;const t=p.languageDetectRe.exec(n);if(t){const n=v(t[1])
-;return n||(H(o.replace("{}",t[1])),
-H("Falling back to no-highlight mode for this block.",e)),n?t[1]:"no-highlight"}
-return n.split(/\s+/).find((e=>_(e)||v(e)))})(e);if(_(t))return
-;if(x("before:highlightElement",{el:e,language:t
-}),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e)
-;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),
-console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),
-console.warn("The element with unescaped HTML:"),
-console.warn(e)),p.throwUnescapedHTML))throw new V("One of your code blocks includes unescaped HTML.",e.innerHTML)
-;n=e;const a=n.textContent,r=t?h(a,{language:t,ignoreIllegals:!0}):E(a)
-;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,n,t)=>{const a=n&&i[n]||t
-;e.classList.add("hljs"),e.classList.add("language-"+a)
-})(e,t,r.language),e.result={language:r.language,re:r.relevance,
-relevance:r.relevance},r.secondBest&&(e.secondBest={
-language:r.secondBest.language,relevance:r.secondBest.relevance
-}),x("after:highlightElement",{el:e,result:r,text:a})}let N=!1;function w(){
-"loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(y):N=!0
-}function v(e){return e=(e||"").toLowerCase(),a[e]||a[i[e]]}
-function O(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach((e=>{
-i[e.toLowerCase()]=n}))}function k(e){const n=v(e)
-;return n&&!n.disableAutodetect}function x(e,n){const t=e;r.forEach((e=>{
-e[t]&&e[t](n)}))}
-"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{
-N&&w()}),!1),Object.assign(t,{highlight:h,highlightAuto:E,highlightAll:w,
-highlightElement:y,
-highlightBlock:e=>(q("10.7.0","highlightBlock will be removed entirely in v12.0"),
-q("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{p=Y(p,e)},
-initHighlighting:()=>{
-w(),q("10.6.0","initHighlighting() deprecated.  Use highlightAll() now.")},
-initHighlightingOnLoad:()=>{
-w(),q("10.6.0","initHighlightingOnLoad() deprecated.  Use highlightAll() now.")
-},registerLanguage:(e,n)=>{let i=null;try{i=n(t)}catch(n){
-if(K("Language definition for '{}' could not be registered.".replace("{}",e)),
-!s)throw n;K(n),i=c}
-i.name||(i.name=e),a[e]=i,i.rawDefinition=n.bind(null,t),i.aliases&&O(i.aliases,{
-languageName:e})},unregisterLanguage:e=>{delete a[e]
-;for(const n of Object.keys(i))i[n]===e&&delete i[n]},
-listLanguages:()=>Object.keys(a),getLanguage:v,registerAliases:O,
-autoDetection:k,inherit:Y,addPlugin:e=>{(e=>{
-e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=n=>{
-e["before:highlightBlock"](Object.assign({block:n.el},n))
-}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=n=>{
-e["after:highlightBlock"](Object.assign({block:n.el},n))})})(e),r.push(e)},
-removePlugin:e=>{const n=r.indexOf(e);-1!==n&&r.splice(n,1)}}),t.debugMode=()=>{
-s=!1},t.safeMode=()=>{s=!0},t.versionString="11.9.0",t.regex={concat:b,
-lookahead:d,either:m,optional:u,anyNumberOfTimes:g}
-;for(const n in C)"object"==typeof C[n]&&e(C[n]);return Object.assign(t,C),t
-},te=ne({});te.newInstance=()=>ne({});var ae=te;const ie=e=>({IMPORTANT:{
-scope:"meta",begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{
-scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},
-FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/},
-ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$",
-contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{
-scope:"number",
-begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",
-relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/}
-}),re=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],se=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],oe=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],le=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],ce=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse(),de=oe.concat(le)
-;var ge="[0-9](_*[0-9])*",ue=`\\.(${ge})`,be="[0-9a-fA-F](_*[0-9a-fA-F])*",me={
-className:"number",variants:[{
-begin:`(\\b(${ge})((${ue})|\\.)?|(${ue}))[eE][+-]?(${ge})[fFdD]?\\b`},{
-begin:`\\b(${ge})((${ue})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{
-begin:`(${ue})[fFdD]?\\b`},{begin:`\\b(${ge})[fFdD]\\b`},{
-begin:`\\b0[xX]((${be})\\.?|(${be})?\\.(${be}))[pP][+-]?(${ge})[fFdD]?\\b`},{
-begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${be})[lL]?\\b`},{
-begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}],
-relevance:0};function pe(e,n,t){return-1===t?"":e.replace(n,(a=>pe(e,n,t-1)))}
-const _e="[A-Za-z$_][0-9A-Za-z$_]*",he=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],fe=["true","false","null","undefined","NaN","Infinity"],Ee=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],ye=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],Ne=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],we=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],ve=[].concat(Ne,Ee,ye)
-;function Oe(e){const n=e.regex,t=_e,a={begin:/<[A-Za-z0-9\\._:-]+/,
-end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{
-const t=e[0].length+e.index,a=e.input[t]
-;if("<"===a||","===a)return void n.ignoreMatch();let i
-;">"===a&&(((e,{after:n})=>{const t="</"+e[0].slice(1)
-;return-1!==e.input.indexOf(t,n)})(e,{after:t})||n.ignoreMatch())
-;const r=e.input.substring(t)
-;((i=r.match(/^\s*=/))||(i=r.match(/^\s+extends\s+/))&&0===i.index)&&n.ignoreMatch()
-}},i={$pattern:_e,keyword:he,literal:fe,built_in:ve,"variable.language":we
-},r="[0-9](_?[0-9])*",s=`\\.(${r})`,o="0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*",l={
-className:"number",variants:[{
-begin:`(\\b(${o})((${s})|\\.)?|(${s}))[eE][+-]?(${r})\\b`},{
-begin:`\\b(${o})\\b((${s})\\b|\\.)?|(${s})\\b`},{
-begin:"\\b(0|[1-9](_?[0-9])*)n\\b"},{
-begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b"},{
-begin:"\\b0[bB][0-1](_?[0-1])*n?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*n?\\b"},{
-begin:"\\b0[0-7]+n?\\b"}],relevance:0},c={className:"subst",begin:"\\$\\{",
-end:"\\}",keywords:i,contains:[]},d={begin:"html`",end:"",starts:{end:"`",
-returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,c],subLanguage:"xml"}},g={
-begin:"css`",end:"",starts:{end:"`",returnEnd:!1,
-contains:[e.BACKSLASH_ESCAPE,c],subLanguage:"css"}},u={begin:"gql`",end:"",
-starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,c],
-subLanguage:"graphql"}},b={className:"string",begin:"`",end:"`",
-contains:[e.BACKSLASH_ESCAPE,c]},m={className:"comment",
-variants:[e.COMMENT(/\/\*\*(?!\/)/,"\\*/",{relevance:0,contains:[{
-begin:"(?=@[A-Za-z]+)",relevance:0,contains:[{className:"doctag",
-begin:"@[A-Za-z]+"},{className:"type",begin:"\\{",end:"\\}",excludeEnd:!0,
-excludeBegin:!0,relevance:0},{className:"variable",begin:t+"(?=\\s*(-)|$)",
-endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]
-}),e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE]
-},p=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,d,g,u,b,{match:/\$\d+/},l]
-;c.contains=p.concat({begin:/\{/,end:/\}/,keywords:i,contains:["self"].concat(p)
-});const _=[].concat(m,c.contains),h=_.concat([{begin:/\(/,end:/\)/,keywords:i,
-contains:["self"].concat(_)}]),f={className:"params",begin:/\(/,end:/\)/,
-excludeBegin:!0,excludeEnd:!0,keywords:i,contains:h},E={variants:[{
-match:[/class/,/\s+/,t,/\s+/,/extends/,/\s+/,n.concat(t,"(",n.concat(/\./,t),")*")],
-scope:{1:"keyword",3:"title.class",5:"keyword",7:"title.class.inherited"}},{
-match:[/class/,/\s+/,t],scope:{1:"keyword",3:"title.class"}}]},y={relevance:0,
-match:n.either(/\bJSON/,/\b[A-Z][a-z]+([A-Z][a-z]*|\d)*/,/\b[A-Z]{2,}([A-Z][a-z]+|\d)+([A-Z][a-z]*)*/,/\b[A-Z]{2,}[a-z]+([A-Z][a-z]+|\d)*([A-Z][a-z]*)*/),
-className:"title.class",keywords:{_:[...Ee,...ye]}},N={variants:[{
-match:[/function/,/\s+/,t,/(?=\s*\()/]},{match:[/function/,/\s*(?=\()/]}],
-className:{1:"keyword",3:"title.function"},label:"func.def",contains:[f],
-illegal:/%/},w={
-match:n.concat(/\b/,(v=[...Ne,"super","import"],n.concat("(?!",v.join("|"),")")),t,n.lookahead(/\(/)),
-className:"title.function",relevance:0};var v;const O={
-begin:n.concat(/\./,n.lookahead(n.concat(t,/(?![0-9A-Za-z$_(])/))),end:t,
-excludeBegin:!0,keywords:"prototype",className:"property",relevance:0},k={
-match:[/get|set/,/\s+/,t,/(?=\()/],className:{1:"keyword",3:"title.function"},
-contains:[{begin:/\(\)/},f]
-},x="(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+e.UNDERSCORE_IDENT_RE+")\\s*=>",M={
-match:[/const|var|let/,/\s+/,t,/\s*/,/=\s*/,/(async\s*)?/,n.lookahead(x)],
-keywords:"async",className:{1:"keyword",3:"title.function"},contains:[f]}
-;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{
-PARAMS_CONTAINS:h,CLASS_REFERENCE:y},illegal:/#(?![$_A-z])/,
-contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),{
-label:"use_strict",className:"meta",relevance:10,
-begin:/^\s*['"]use (strict|asm)['"]/
-},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,d,g,u,b,m,{match:/\$\d+/},l,y,{
-className:"attr",begin:t+n.lookahead(":"),relevance:0},M,{
-begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",
-keywords:"return throw case",relevance:0,contains:[m,e.REGEXP_MODE,{
-className:"function",begin:x,returnBegin:!0,end:"\\s*=>",contains:[{
-className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{
-className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,
-excludeEnd:!0,keywords:i,contains:h}]}]},{begin:/,/,relevance:0},{match:/\s+/,
-relevance:0},{variants:[{begin:"<>",end:"</>"},{
-match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:a.begin,
-"on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{
-begin:a.begin,end:a.end,skip:!0,contains:["self"]}]}]},N,{
-beginKeywords:"while if switch catch for"},{
-begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",
-returnBegin:!0,label:"func.def",contains:[f,e.inherit(e.TITLE_MODE,{begin:t,
-className:"title.function"})]},{match:/\.\.\./,relevance:0},O,{match:"\\$"+t,
-relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"},
-contains:[f]},w,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/,
-className:"variable.constant"},E,k,{match:/\$[(.]/}]}}
-const ke=e=>b(/\b/,e,/\w$/.test(e)?/\b/:/\B/),xe=["Protocol","Type"].map(ke),Me=["init","self"].map(ke),Se=["Any","Self"],Ae=["actor","any","associatedtype","async","await",/as\?/,/as!/,"as","borrowing","break","case","catch","class","consume","consuming","continue","convenience","copy","default","defer","deinit","didSet","distributed","do","dynamic","each","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","isolated","nonisolated","lazy","let","macro","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","switch","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],Ce=["false","nil","true"],Te=["assignment","associativity","higherThan","left","lowerThan","none","right"],Re=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warning"],De=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],Ie=m(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),Le=m(Ie,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),Be=b(Ie,Le,"*"),$e=m(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),ze=m($e,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),Fe=b($e,ze,"*"),Ue=b(/[A-Z]/,ze,"*"),je=["attached","autoclosure",b(/convention\(/,m("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","freestanding","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",b(/objc\(/,Fe,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","resultBuilder","Sendable","testable","UIApplicationMain","unchecked","unknown","usableFromInline","warn_unqualified_access"],Pe=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"]
-;var Ke=Object.freeze({__proto__:null,grmr_bash:e=>{const n=e.regex,t={},a={
-begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]}
-;Object.assign(t,{className:"variable",variants:[{
-begin:n.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},a]});const i={
-className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},r={
-begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,
-end:/(\w+)/,className:"string"})]}},s={className:"string",begin:/"/,end:/"/,
-contains:[e.BACKSLASH_ESCAPE,t,i]};i.contains.push(s);const o={begin:/\$?\(\(/,
-end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t]
-},l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10
-}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,
-contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{
-name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/,
-keyword:["if","then","else","elif","fi","for","while","until","in","do","done","case","esac","function","select"],
-literal:["true","false"],
-built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"]
-},contains:[l,e.SHEBANG(),c,o,e.HASH_COMMENT_MODE,r,{match:/(\/[a-z._-]+)+/},s,{
-match:/\\"/},{className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}},
-grmr_c:e=>{const n=e.regex,t=e.COMMENT("//","$",{contains:[{begin:/\\\n/}]
-}),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={
-className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{
-match:/\batomic_[a-z]{3,6}\b/}]},o={className:"string",variants:[{
-begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{
-begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",
-end:"'",illegal:"."},e.END_SAME_AS_BEGIN({
-begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={
-className:"number",variants:[{begin:"\\b(0b[01']+)"},{
-begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)"
-},{
-begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
-}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{
-keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"
-},contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{
-className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={
-className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0
-},g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={
-keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"],
-type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal128","const","static","complex","bool","imaginary"],
-literal:"true false NULL",
-built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr"
-},b=[c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],m={variants:[{begin:/=/,end:/;/},{
-begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],
-keywords:u,contains:b.concat([{begin:/\(/,end:/\)/,keywords:u,
-contains:b.concat(["self"]),relevance:0}]),relevance:0},p={
-begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,
-keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{
-begin:g,returnBegin:!0,contains:[e.inherit(d,{className:"title.function"})],
-relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/,
-keywords:u,relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/,
-end:/\)/,keywords:u,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s]
-}]},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["h"],keywords:u,
-disableAutodetect:!0,illegal:"</",contains:[].concat(m,p,b,[c,{
-begin:e.IDENT_RE+"::",keywords:u},{className:"class",
-beginKeywords:"enum class struct union",end:/[{;:<>=]/,contains:[{
-beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:c,
-strings:o,keywords:u}}},grmr_cpp:e=>{const n=e.regex,t=e.COMMENT("//","$",{
-contains:[{begin:/\\\n/}]
-}),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="(?!struct)("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={
-className:"type",begin:"\\b[a-z\\d_]*_t\\b"},o={className:"string",variants:[{
-begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{
-begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",
-end:"'",illegal:"."},e.END_SAME_AS_BEGIN({
-begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={
-className:"number",variants:[{begin:"\\b(0b[01']+)"},{
-begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)"
-},{
-begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
-}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{
-keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"
-},contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{
-className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={
-className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0
-},g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={
-type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"],
-keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"],
-literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"],
-_type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"]
-},b={className:"function.dispatch",relevance:0,keywords:{
-_hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"]
-},
-begin:n.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,n.lookahead(/(<[^<>]+>|)\s*\(/))
-},m=[b,c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],p={variants:[{begin:/=/,end:/;/},{
-begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],
-keywords:u,contains:m.concat([{begin:/\(/,end:/\)/,keywords:u,
-contains:m.concat(["self"]),relevance:0}]),relevance:0},_={className:"function",
-begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,
-keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{
-begin:g,returnBegin:!0,contains:[d],relevance:0},{begin:/::/,relevance:0},{
-begin:/:/,endsWithParent:!0,contains:[o,l]},{relevance:0,match:/,/},{
-className:"params",begin:/\(/,end:/\)/,keywords:u,relevance:0,
-contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/,end:/\)/,keywords:u,
-relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s]}]
-},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C++",
-aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:u,illegal:"</",
-classNameAliases:{"function.dispatch":"built_in"},
-contains:[].concat(p,_,b,m,[c,{
-begin:"\\b(deque|list|queue|priority_queue|pair|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array|tuple|optional|variant|function)\\s*<(?!<)",
-end:">",keywords:u,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:u},{
-match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/],
-className:{1:"keyword",3:"title.class"}}])}},grmr_csharp:e=>{const n={
-keyword:["abstract","as","base","break","case","catch","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","scoped","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]),
-built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"],
-literal:["default","false","null","true"]},t=e.inherit(e.TITLE_MODE,{
-begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{
-begin:"\\b(0b[01']+)"},{
-begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{
-begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
-}],relevance:0},i={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]
-},r=e.inherit(i,{illegal:/\n/}),s={className:"subst",begin:/\{/,end:/\}/,
-keywords:n},o=e.inherit(s,{illegal:/\n/}),l={className:"string",begin:/\$"/,
-end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/
-},e.BACKSLASH_ESCAPE,o]},c={className:"string",begin:/\$@"/,end:'"',contains:[{
-begin:/\{\{/},{begin:/\}\}/},{begin:'""'},s]},d=e.inherit(c,{illegal:/\n/,
-contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},o]})
-;s.contains=[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE],
-o.contains=[d,l,r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{
-illegal:/\n/})];const g={variants:[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]
-},u={begin:"<",end:">",contains:[{beginKeywords:"in out"},t]
-},b=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",m={
-begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"],
-keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0,
-contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{
-begin:"\x3c!--|--\x3e"},{begin:"</?",end:">"}]}]
-}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",
-end:"$",keywords:{
-keyword:"if else elif endif define undef warning error line region endregion pragma checksum"
-}},g,a,{beginKeywords:"class interface",relevance:0,end:/[{;=]/,
-illegal:/[^\s:,]/,contains:[{beginKeywords:"where class"
-},t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",
-relevance:0,end:/[{;=]/,illegal:/[^\s:]/,
-contains:[t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{
-beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/,
-contains:[t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta",
-begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{
-className:"string",begin:/"/,end:/"/}]},{
-beginKeywords:"new return throw await else",relevance:0},{className:"function",
-begin:"("+b+"\\s+)+"+e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0,
-end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{
-beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial",
-relevance:0},{begin:e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0,
-contains:[e.TITLE_MODE,u],relevance:0},{match:/\(\)/},{className:"params",
-begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0,
-contains:[g,a,e.C_BLOCK_COMMENT_MODE]
-},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},m]}},grmr_css:e=>{
-const n=e.regex,t=ie(e),a=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return{
-name:"CSS",case_insensitive:!0,illegal:/[=|'\$]/,keywords:{
-keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"},
-contains:[t.BLOCK_COMMENT,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/
-},t.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0
-},{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0
-},t.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{
-begin:":("+oe.join("|")+")"},{begin:":(:)?("+le.join("|")+")"}]
-},t.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b"},{
-begin:/:/,end:/[;}{]/,
-contains:[t.BLOCK_COMMENT,t.HEXCOLOR,t.IMPORTANT,t.CSS_NUMBER_MODE,...a,{
-begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri"
-},contains:[...a,{className:"string",begin:/[^)]/,endsWithParent:!0,
-excludeEnd:!0}]},t.FUNCTION_DISPATCH]},{begin:n.lookahead(/@/),end:"[{;]",
-relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/
-},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{
-$pattern:/[a-z-]+/,keyword:"and or not only",attribute:se.join(" ")},contains:[{
-begin:/[a-z-]+(?=:)/,className:"attribute"},...a,t.CSS_NUMBER_MODE]}]},{
-className:"selector-tag",begin:"\\b("+re.join("|")+")\\b"}]}},grmr_diff:e=>{
-const n=e.regex;return{name:"Diff",aliases:["patch"],contains:[{
-className:"meta",relevance:10,
-match:n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/)
-},{className:"comment",variants:[{
-begin:n.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/),
-end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{
-className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/,
-end:/$/}]}},grmr_go:e=>{const n={
-keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"],
-type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"],
-literal:["true","false","iota","nil"],
-built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"]
-};return{name:"Go",aliases:["golang"],keywords:n,illegal:"</",
-contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"string",
-variants:[e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{begin:"`",end:"`"}]},{
-className:"number",variants:[{begin:e.C_NUMBER_RE+"[i]",relevance:1
-},e.C_NUMBER_MODE]},{begin:/:=/},{className:"function",beginKeywords:"func",
-end:"\\s*(\\{|$)",excludeEnd:!0,contains:[e.TITLE_MODE,{className:"params",
-begin:/\(/,end:/\)/,endsParent:!0,keywords:n,illegal:/["']/}]}]}},
-grmr_graphql:e=>{const n=e.regex;return{name:"GraphQL",aliases:["gql"],
-case_insensitive:!0,disableAutodetect:!1,keywords:{
-keyword:["query","mutation","subscription","type","input","schema","directive","interface","union","scalar","fragment","enum","on"],
-literal:["true","false","null"]},
-contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{
-scope:"punctuation",match:/[.]{3}/,relevance:0},{scope:"punctuation",
-begin:/[\!\(\)\:\=\[\]\{\|\}]{1}/,relevance:0},{scope:"variable",begin:/\$/,
-end:/\W/,excludeEnd:!0,relevance:0},{scope:"meta",match:/@\w+/,excludeEnd:!0},{
-scope:"symbol",begin:n.concat(/[_A-Za-z][_0-9A-Za-z]*/,n.lookahead(/\s*:/)),
-relevance:0}],illegal:[/[;<']/,/BEGIN/]}},grmr_ini:e=>{const n=e.regex,t={
-className:"number",relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{
-begin:e.NUMBER_RE}]},a=e.COMMENT();a.variants=[{begin:/;/,end:/$/},{begin:/#/,
-end:/$/}];const i={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{
-begin:/\$\{(.*?)\}/}]},r={className:"literal",
-begin:/\bon|off|true|false|yes|no\b/},s={className:"string",
-contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{
-begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}]
-},o={begin:/\[/,end:/\]/,contains:[a,r,i,s,t,"self"],relevance:0
-},l=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{
-name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/,
-contains:[a,{className:"section",begin:/\[+/,end:/\]+/},{
-begin:n.concat(l,"(\\s*\\.\\s*",l,")*",n.lookahead(/\s*=\s*[^#\s]/)),
-className:"attr",starts:{end:/$/,contains:[a,o,r,i,s,t]}}]}},grmr_java:e=>{
-const n=e.regex,t="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",a=t+pe("(?:<"+t+"~~~(?:\\s*,\\s*"+t+"~~~)*>)?",/~~~/g,2),i={
-keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed","yield","permits"],
-literal:["false","true","null"],
-type:["char","boolean","long","float","int","byte","short","double"],
-built_in:["super","this"]},r={className:"meta",begin:"@"+t,contains:[{
-begin:/\(/,end:/\)/,contains:["self"]}]},s={className:"params",begin:/\(/,
-end:/\)/,keywords:i,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0}
-;return{name:"Java",aliases:["jsp"],keywords:i,illegal:/<\/|#/,
-contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,
-relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{
-begin:/import java\.[a-z]+\./,keywords:"import",relevance:2
-},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/,
-className:"string",contains:[e.BACKSLASH_ESCAPE]
-},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{
-match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,t],className:{
-1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{
-begin:[n.concat(/(?!else)/,t),/\s+/,t,/\s+/,/=(?!=)/],className:{1:"type",
-3:"variable",5:"operator"}},{begin:[/record/,/\s+/,t],className:{1:"keyword",
-3:"title.class"},contains:[s,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{
-beginKeywords:"new throw return else",relevance:0},{
-begin:["(?:"+a+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{
-2:"title.function"},keywords:i,contains:[{className:"params",begin:/\(/,
-end:/\)/,keywords:i,relevance:0,
-contains:[r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,me,e.C_BLOCK_COMMENT_MODE]
-},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},me,r]}},grmr_javascript:Oe,
-grmr_json:e=>{const n=["true","false","null"],t={scope:"literal",
-beginKeywords:n.join(" ")};return{name:"JSON",keywords:{literal:n},contains:[{
-className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{
-match:/[{}[\],:]/,className:"punctuation",relevance:0
-},e.QUOTE_STRING_MODE,t,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],
-illegal:"\\S"}},grmr_kotlin:e=>{const n={
-keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual",
-built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",
-literal:"true false null"},t={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@"
-},a={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},i={
-className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string",
-variants:[{begin:'"""',end:'"""(?=[^"])',contains:[i,a]},{begin:"'",end:"'",
-illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,
-contains:[e.BACKSLASH_ESCAPE,i,a]}]};a.contains.push(r);const s={
-className:"meta",
-begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?"
-},o={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,
-end:/\)/,contains:[e.inherit(r,{className:"string"}),"self"]}]
-},l=me,c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),d={
-variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/,
-contains:[]}]},g=d;return g.variants[1].contains=[d],d.variants[1].contains=[g],
-{name:"Kotlin",aliases:["kt","kts"],keywords:n,
-contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",
-begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword",
-begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol",
-begin:/@\w+/}]}},t,s,o,{className:"function",beginKeywords:"fun",end:"[(]|$",
-returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{
-begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,
-contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin:/</,end:/>/,
-keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,
-endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,
-endsWithParent:!0,contains:[d,e.C_LINE_COMMENT_MODE,c],relevance:0
-},e.C_LINE_COMMENT_MODE,c,s,o,r,e.C_NUMBER_MODE]},c]},{
-begin:[/class|interface|trait/,/\s+/,e.UNDERSCORE_IDENT_RE],beginScope:{
-3:"title.class"},keywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,
-illegal:"extends implements",contains:[{
-beginKeywords:"public protected internal private constructor"
-},e.UNDERSCORE_TITLE_MODE,{className:"type",begin:/</,end:/>/,excludeBegin:!0,
-excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,){\s]|$/,
-excludeBegin:!0,returnEnd:!0},s,o]},r,{className:"meta",begin:"^#!/usr/bin/env",
-end:"$",illegal:"\n"},l]}},grmr_less:e=>{
-const n=ie(e),t=de,a="[\\w-]+",i="("+a+"|@\\{"+a+"\\})",r=[],s=[],o=e=>({
-className:"string",begin:"~?"+e+".*?"+e}),l=(e,n,t)=>({className:e,begin:n,
-relevance:t}),c={$pattern:/[a-z-]+/,keyword:"and or not only",
-attribute:se.join(" ")},d={begin:"\\(",end:"\\)",contains:s,keywords:c,
-relevance:0}
-;s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,o("'"),o('"'),n.CSS_NUMBER_MODE,{
-begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",
-excludeEnd:!0}
-},n.HEXCOLOR,d,l("variable","@@?"+a,10),l("variable","@\\{"+a+"\\}"),l("built_in","~?`[^`]*?`"),{
-className:"attribute",begin:a+"\\s*:",end:":",returnBegin:!0,excludeEnd:!0
-},n.IMPORTANT,{beginKeywords:"and not"},n.FUNCTION_DISPATCH);const g=s.concat({
-begin:/\{/,end:/\}/,contains:r}),u={beginKeywords:"when",endsWithParent:!0,
-contains:[{beginKeywords:"and not"}].concat(s)},b={begin:i+"\\s*:",
-returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/
-},n.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b",
-end:/(?=:)/,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}]
-},m={className:"keyword",
-begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",
-starts:{end:"[;{}]",keywords:c,returnEnd:!0,contains:s,relevance:0}},p={
-className:"variable",variants:[{begin:"@"+a+"\\s*:",relevance:15},{begin:"@"+a
-}],starts:{end:"[;}]",returnEnd:!0,contains:g}},_={variants:[{
-begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:i,end:/\{/}],returnBegin:!0,
-returnEnd:!0,illegal:"[<='$\"]",relevance:0,
-contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,u,l("keyword","all\\b"),l("variable","@\\{"+a+"\\}"),{
-begin:"\\b("+re.join("|")+")\\b",className:"selector-tag"
-},n.CSS_NUMBER_MODE,l("selector-tag",i,0),l("selector-id","#"+i),l("selector-class","\\."+i,0),l("selector-tag","&",0),n.ATTRIBUTE_SELECTOR_MODE,{
-className:"selector-pseudo",begin:":("+oe.join("|")+")"},{
-className:"selector-pseudo",begin:":(:)?("+le.join("|")+")"},{begin:/\(/,
-end:/\)/,relevance:0,contains:g},{begin:"!important"},n.FUNCTION_DISPATCH]},h={
-begin:a+":(:)?"+`(${t.join("|")})`,returnBegin:!0,contains:[_]}
-;return r.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,m,p,h,b,_,u,n.FUNCTION_DISPATCH),
-{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:r}},
-grmr_lua:e=>{const n="\\[=*\\[",t="\\]=*\\]",a={begin:n,end:t,contains:["self"]
-},i=[e.COMMENT("--(?!"+n+")","$"),e.COMMENT("--"+n,t,{contains:[a],relevance:10
-})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE,
-literal:"true false nil",
-keyword:"and break do else elseif end for goto if in local not or repeat return then until while",
-built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"
-},contains:i.concat([{className:"function",beginKeywords:"function",end:"\\)",
-contains:[e.inherit(e.TITLE_MODE,{
-begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",
-begin:"\\(",endsWithParent:!0,contains:i}].concat(i)
-},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string",
-begin:n,end:t,contains:[a],relevance:5}])}},grmr_makefile:e=>{const n={
-className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)",
-contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%<?\^\+\*]/}]},t={className:"string",
-begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,n]},a={className:"variable",
-begin:/\$\([\w-]+\s/,end:/\)/,keywords:{
-built_in:"subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value"
-},contains:[n]},i={begin:"^"+e.UNDERSCORE_IDENT_RE+"\\s*(?=[:+?]?=)"},r={
-className:"section",begin:/^[^\s]+:/,end:/$/,contains:[n]};return{
-name:"Makefile",aliases:["mk","mak","make"],keywords:{$pattern:/[\w-]+/,
-keyword:"define endef undefine ifdef ifndef ifeq ifneq else endif include -include sinclude override export unexport private vpath"
-},contains:[e.HASH_COMMENT_MODE,n,t,a,i,{className:"meta",begin:/^\.PHONY:/,
-end:/$/,keywords:{$pattern:/[\.\w]+/,keyword:".PHONY"}},r]}},grmr_markdown:e=>{
-const n={begin:/<\/?[A-Za-z_]/,end:">",subLanguage:"xml",relevance:0},t={
-variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0},{
-begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/,
-relevance:2},{
-begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/),
-relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{
-begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/
-},{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0,
-returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)",
-excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[",
-end:"\\]",excludeBegin:!0,excludeEnd:!0}]},a={className:"strong",contains:[],
-variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}]
-},i={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{
-begin:/_(?![_\s])/,end:/_/,relevance:0}]},r=e.inherit(a,{contains:[]
-}),s=e.inherit(i,{contains:[]});a.contains.push(s),i.contains.push(r)
-;let o=[n,t];return[a,i,r,s].forEach((e=>{e.contains=e.contains.concat(o)
-})),o=o.concat(a,i),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{
-className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:o},{
-begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n",
-contains:o}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)",
-end:"\\s+",excludeEnd:!0},a,i,{className:"quote",begin:"^>\\s+",contains:o,
-end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{
-begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{
-begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))",
-contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{
-begin:"^[-\\*]{3,}",end:"$"},t,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{
-className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{
-className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}},grmr_objectivec:e=>{
-const n=/[a-zA-Z@][a-zA-Z0-9_]*/,t={$pattern:n,
-keyword:["@interface","@class","@protocol","@implementation"]};return{
-name:"Objective-C",aliases:["mm","objc","obj-c","obj-c++","objective-c++"],
-keywords:{"variable.language":["this","super"],$pattern:n,
-keyword:["while","export","sizeof","typedef","const","struct","for","union","volatile","static","mutable","if","do","return","goto","enum","else","break","extern","asm","case","default","register","explicit","typename","switch","continue","inline","readonly","assign","readwrite","self","@synchronized","id","typeof","nonatomic","IBOutlet","IBAction","strong","weak","copy","in","out","inout","bycopy","byref","oneway","__strong","__weak","__block","__autoreleasing","@private","@protected","@public","@try","@property","@end","@throw","@catch","@finally","@autoreleasepool","@synthesize","@dynamic","@selector","@optional","@required","@encode","@package","@import","@defs","@compatibility_alias","__bridge","__bridge_transfer","__bridge_retained","__bridge_retain","__covariant","__contravariant","__kindof","_Nonnull","_Nullable","_Null_unspecified","__FUNCTION__","__PRETTY_FUNCTION__","__attribute__","getter","setter","retain","unsafe_unretained","nonnull","nullable","null_unspecified","null_resettable","class","instancetype","NS_DESIGNATED_INITIALIZER","NS_UNAVAILABLE","NS_REQUIRES_SUPER","NS_RETURNS_INNER_POINTER","NS_INLINE","NS_AVAILABLE","NS_DEPRECATED","NS_ENUM","NS_OPTIONS","NS_SWIFT_UNAVAILABLE","NS_ASSUME_NONNULL_BEGIN","NS_ASSUME_NONNULL_END","NS_REFINED_FOR_SWIFT","NS_SWIFT_NAME","NS_SWIFT_NOTHROW","NS_DURING","NS_HANDLER","NS_ENDHANDLER","NS_VALUERETURN","NS_VOIDRETURN"],
-literal:["false","true","FALSE","TRUE","nil","YES","NO","NULL"],
-built_in:["dispatch_once_t","dispatch_queue_t","dispatch_sync","dispatch_async","dispatch_once"],
-type:["int","float","char","unsigned","signed","short","long","double","wchar_t","unichar","void","bool","BOOL","id|0","_Bool"]
-},illegal:"</",contains:[{className:"built_in",
-begin:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"
-},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.C_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{
-className:"string",variants:[{begin:'@"',end:'"',illegal:"\\n",
-contains:[e.BACKSLASH_ESCAPE]}]},{className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,
-keywords:{
-keyword:"if else elif endif define undef warning error line pragma ifdef ifndef include"
-},contains:[{begin:/\\\n/,relevance:0},e.inherit(e.QUOTE_STRING_MODE,{
-className:"string"}),{className:"string",begin:/<.*?>/,end:/$/,illegal:"\\n"
-},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class",
-begin:"("+t.keyword.join("|")+")\\b",end:/(\{|$)/,excludeEnd:!0,keywords:t,
-contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE,
-relevance:0}]}},grmr_perl:e=>{const n=e.regex,t=/[dualxmsipngr]{0,12}/,a={
-$pattern:/[\w.]+/,
-keyword:"abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0"
-},i={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:a},r={begin:/->\{/,
-end:/\}/},s={variants:[{begin:/\$\d/},{
-begin:n.concat(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,"(?![A-Za-z])(?![@$%])")
-},{begin:/[$%@][^\s\w{]/,relevance:0}]
-},o=[e.BACKSLASH_ESCAPE,i,s],l=[/!/,/\//,/\|/,/\?/,/'/,/"/,/#/],c=(e,a,i="\\1")=>{
-const r="\\1"===i?i:n.concat(i,a)
-;return n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,r,/(?:\\.|[^\\\/])*?/,i,t)
-},d=(e,a,i)=>n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,i,t),g=[s,e.HASH_COMMENT_MODE,e.COMMENT(/^=\w/,/=cut/,{
-endsWithParent:!0}),r,{className:"string",contains:o,variants:[{
-begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",
-end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{
-begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*<",end:">",
-relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",
-contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",
-contains:[e.BACKSLASH_ESCAPE]},{begin:/\{\w+\}/,relevance:0},{
-begin:"-?\\w+\\s*=>",relevance:0}]},{className:"number",
-begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",
-relevance:0},{
-begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",
-keywords:"split return print reverse grep",relevance:0,
-contains:[e.HASH_COMMENT_MODE,{className:"regexp",variants:[{
-begin:c("s|tr|y",n.either(...l,{capture:!0}))},{begin:c("s|tr|y","\\(","\\)")},{
-begin:c("s|tr|y","\\[","\\]")},{begin:c("s|tr|y","\\{","\\}")}],relevance:2},{
-className:"regexp",variants:[{begin:/(m|qr)\/\//,relevance:0},{
-begin:d("(?:m|qr)?",/\//,/\//)},{begin:d("m|qr",n.either(...l,{capture:!0
-}),/\1/)},{begin:d("m|qr",/\(/,/\)/)},{begin:d("m|qr",/\[/,/\]/)},{
-begin:d("m|qr",/\{/,/\}/)}]}]},{className:"function",beginKeywords:"sub",
-end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{
-begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",
-subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}]
-}];return i.contains=g,r.contains=g,{name:"Perl",aliases:["pl","pm"],keywords:a,
-contains:g}},grmr_php:e=>{
-const n=e.regex,t=/(?![A-Za-z0-9])(?![$])/,a=n.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,t),i=n.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,t),r={
-scope:"variable",match:"\\$+"+a},s={scope:"subst",variants:[{begin:/\$\w+/},{
-begin:/\{\$/,end:/\}/}]},o=e.inherit(e.APOS_STRING_MODE,{illegal:null
-}),l="[ \t\n]",c={scope:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{
-illegal:null,contains:e.QUOTE_STRING_MODE.contains.concat(s)}),o,{
-begin:/<<<[ \t]*(?:(\w+)|"(\w+)")\n/,end:/[ \t]*(\w+)\b/,
-contains:e.QUOTE_STRING_MODE.contains.concat(s),"on:begin":(e,n)=>{
-n.data._beginMatch=e[1]||e[2]},"on:end":(e,n)=>{
-n.data._beginMatch!==e[1]&&n.ignoreMatch()}},e.END_SAME_AS_BEGIN({
-begin:/<<<[ \t]*'(\w+)'\n/,end:/[ \t]*(\w+)\b/})]},d={scope:"number",variants:[{
-begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{
-begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{
-begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?"
-}],relevance:0
-},g=["false","null","true"],u=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],b=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],m={
-keyword:u,literal:(e=>{const n=[];return e.forEach((e=>{
-n.push(e),e.toLowerCase()===e?n.push(e.toUpperCase()):n.push(e.toLowerCase())
-})),n})(g),built_in:b},p=e=>e.map((e=>e.replace(/\|\d+$/,""))),_={variants:[{
-match:[/new/,n.concat(l,"+"),n.concat("(?!",p(b).join("\\b|"),"\\b)"),i],scope:{
-1:"keyword",4:"title.class"}}]},h=n.concat(a,"\\b(?!\\()"),f={variants:[{
-match:[n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{2:"variable.constant"
-}},{match:[/::/,/class/],scope:{2:"variable.language"}},{
-match:[i,n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{1:"title.class",
-3:"variable.constant"}},{match:[i,n.concat("::",n.lookahead(/(?!class\b)/))],
-scope:{1:"title.class"}},{match:[i,/::/,/class/],scope:{1:"title.class",
-3:"variable.language"}}]},E={scope:"attr",
-match:n.concat(a,n.lookahead(":"),n.lookahead(/(?!::)/))},y={relevance:0,
-begin:/\(/,end:/\)/,keywords:m,contains:[E,r,f,e.C_BLOCK_COMMENT_MODE,c,d,_]
-},N={relevance:0,
-match:[/\b/,n.concat("(?!fn\\b|function\\b|",p(u).join("\\b|"),"|",p(b).join("\\b|"),"\\b)"),a,n.concat(l,"*"),n.lookahead(/(?=\()/)],
-scope:{3:"title.function.invoke"},contains:[y]};y.contains.push(N)
-;const w=[E,f,e.C_BLOCK_COMMENT_MODE,c,d,_];return{case_insensitive:!1,
-keywords:m,contains:[{begin:n.concat(/#\[\s*/,i),beginScope:"meta",end:/]/,
-endScope:"meta",keywords:{literal:g,keyword:["new","array"]},contains:[{
-begin:/\[/,end:/]/,keywords:{literal:g,keyword:["new","array"]},
-contains:["self",...w]},...w,{scope:"meta",match:i}]
-},e.HASH_COMMENT_MODE,e.COMMENT("//","$"),e.COMMENT("/\\*","\\*/",{contains:[{
-scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/,
-keywords:"__halt_compiler",starts:{scope:"comment",end:e.MATCH_NOTHING_RE,
-contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},{scope:"meta",variants:[{
-begin:/<\?php/,relevance:10},{begin:/<\?=/},{begin:/<\?/,relevance:.1},{
-begin:/\?>/}]},{scope:"variable.language",match:/\$this\b/},r,N,f,{
-match:[/const/,/\s/,a],scope:{1:"keyword",3:"variable.constant"}},_,{
-scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/,
-excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use"
-},e.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params",
-begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:m,
-contains:["self",r,f,e.C_BLOCK_COMMENT_MODE,c,d]}]},{scope:"class",variants:[{
-beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait",
-illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{
-beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{
-beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/,
-contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:"title.class"})]},{
-beginKeywords:"use",relevance:0,end:";",contains:[{
-match:/\b(as|const|function)\b/,scope:"keyword"},e.UNDERSCORE_TITLE_MODE]},c,d]}
-},grmr_php_template:e=>({name:"PHP template",subLanguage:"xml",contains:[{
-begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",
-end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0
-},e.inherit(e.APOS_STRING_MODE,{illegal:null,className:null,contains:null,
-skip:!0}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null,className:null,
-contains:null,skip:!0})]}]}),grmr_plaintext:e=>({name:"Plain text",
-aliases:["text","txt"],disableAutodetect:!0}),grmr_python:e=>{
-const n=e.regex,t=/[\p{XID_Start}_]\p{XID_Continue}*/u,a=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],i={
-$pattern:/[A-Za-z]\w+|__\w+__/,keyword:a,
-built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"],
-literal:["__debug__","Ellipsis","False","None","NotImplemented","True"],
-type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"]
-},r={className:"meta",begin:/^(>>>|\.\.\.) /},s={className:"subst",begin:/\{/,
-end:/\}/,keywords:i,illegal:/#/},o={begin:/\{\{/,relevance:0},l={
-className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{
-begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/,
-contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{
-begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/,
-contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{
-begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/,
-contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/,
-end:/"""/,contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([uU]|[rR])'/,end:/'/,
-relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{
-begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/,
-end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/,
-contains:[e.BACKSLASH_ESCAPE,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/,
-contains:[e.BACKSLASH_ESCAPE,o,s]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]
-},c="[0-9](_?[0-9])*",d=`(\\b(${c}))?\\.(${c})|\\b(${c})\\.`,g="\\b|"+a.join("|"),u={
-className:"number",relevance:0,variants:[{
-begin:`(\\b(${c})|(${d}))[eE][+-]?(${c})[jJ]?(?=${g})`},{begin:`(${d})[jJ]?`},{
-begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${g})`},{
-begin:`\\b0[bB](_?[01])+[lL]?(?=${g})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${g})`
-},{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${g})`},{begin:`\\b(${c})[jJ](?=${g})`
-}]},b={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:i,
-contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={
-className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/,
-end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i,
-contains:["self",r,u,l,e.HASH_COMMENT_MODE]}]};return s.contains=[l,u,r],{
-name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:i,
-illegal:/(<\/|\?)|=>/,contains:[r,u,{begin:/\bself\b/},{beginKeywords:"if",
-relevance:0},l,b,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,t],scope:{
-1:"keyword",3:"title.function"},contains:[m]},{variants:[{
-match:[/\bclass/,/\s+/,t,/\s*/,/\(\s*/,t,/\s*\)/]},{match:[/\bclass/,/\s+/,t]}],
-scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{
-className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[u,m,l]}]}},
-grmr_python_repl:e=>({aliases:["pycon"],contains:[{className:"meta.prompt",
-starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{
-begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}),grmr_r:e=>{
-const n=e.regex,t=/(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/,a=n.either(/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/,/0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/,/(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/),i=/[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/,r=n.either(/[()]/,/[{}]/,/\[\[/,/[[\]]/,/\\/,/,/)
-;return{name:"R",keywords:{$pattern:t,
-keyword:"function if in break next repeat else for while",
-literal:"NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10",
-built_in:"LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm"
-},contains:[e.COMMENT(/#'/,/$/,{contains:[{scope:"doctag",match:/@examples/,
-starts:{end:n.lookahead(n.either(/\n^#'\s*(?=@[a-zA-Z]+)/,/\n^(?!#')/)),
-endsParent:!0}},{scope:"doctag",begin:"@param",end:/$/,contains:[{
-scope:"variable",variants:[{match:t},{match:/`(?:\\.|[^`\\])+`/}],endsParent:!0
-}]},{scope:"doctag",match:/@[a-zA-Z]+/},{scope:"keyword",match:/\\[a-zA-Z]+/}]
-}),e.HASH_COMMENT_MODE,{scope:"string",contains:[e.BACKSLASH_ESCAPE],
-variants:[e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\(/,end:/\)(-*)"/
-}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\{/,end:/\}(-*)"/
-}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\[/,end:/\](-*)"/
-}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\(/,end:/\)(-*)'/
-}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\{/,end:/\}(-*)'/
-}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\[/,end:/\](-*)'/}),{begin:'"',end:'"',
-relevance:0},{begin:"'",end:"'",relevance:0}]},{relevance:0,variants:[{scope:{
-1:"operator",2:"number"},match:[i,a]},{scope:{1:"operator",2:"number"},
-match:[/%[^%]*%/,a]},{scope:{1:"punctuation",2:"number"},match:[r,a]},{scope:{
-2:"number"},match:[/[^a-zA-Z0-9._]|^/,a]}]},{scope:{3:"operator"},
-match:[t,/\s+/,/<-/,/\s+/]},{scope:"operator",relevance:0,variants:[{match:i},{
-match:/%[^%]*%/}]},{scope:"punctuation",relevance:0,match:r},{begin:"`",end:"`",
-contains:[{begin:/\\./}]}]}},grmr_ruby:e=>{
-const n=e.regex,t="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",a=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(a,/(::\w+)*/),r={
-"variable.constant":["__FILE__","__LINE__","__ENCODING__"],
-"variable.language":["self","super"],
-keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"],
-built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"],
-literal:["true","false","nil"]},s={className:"doctag",begin:"@[A-Za-z]+"},o={
-begin:"#<",end:">"},l=[e.COMMENT("#","$",{contains:[s]
-}),e.COMMENT("^=begin","^=end",{contains:[s],relevance:10
-}),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],c={className:"subst",begin:/#\{/,
-end:/\}/,keywords:r},d={className:"string",contains:[e.BACKSLASH_ESCAPE,c],
-variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{
-begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{
-begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?</,end:/>/},{begin:/%[qQwWx]?\//,
-end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{
-begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{
-begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{
-begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{
-begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{
-begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)),
-contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,
-contains:[e.BACKSLASH_ESCAPE,c]})]}]},g="[0-9](_?[0-9])*",u={className:"number",
-relevance:0,variants:[{
-begin:`\\b([1-9](_?[0-9])*|0)(\\.(${g}))?([eE][+-]?(${g})|r)?i?\\b`},{
-begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b"
-},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{
-begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{
-begin:"\\b0(_?[0-7])+r?i?\\b"}]},b={variants:[{match:/\(\)/},{
-className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0,
-keywords:r}]},m=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{
-match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class",
-4:"title.class.inherited"},keywords:r},{match:[/(include|extend)\s+/,i],scope:{
-2:"title.class"},keywords:r},{relevance:0,match:[i,/\.new[. (]/],scope:{
-1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/,
-className:"variable.constant"},{relevance:0,match:a,scope:"title.class"},{
-match:[/def/,/\s+/,t],scope:{1:"keyword",3:"title.function"},contains:[b]},{
-begin:e.IDENT_RE+"::"},{className:"symbol",
-begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol",
-begin:":(?!\\s)",contains:[d,{begin:t}],relevance:0},u,{className:"variable",
-begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{
-className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0,
-relevance:0,keywords:r},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*",
-keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c],
-illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{
-begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",
-end:"\\][a-z]*"}]}].concat(o,l),relevance:0}].concat(o,l)
-;c.contains=m,b.contains=m;const p=[{begin:/^\s*=>/,starts:{end:"$",contains:m}
-},{className:"meta.prompt",
-begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])",
-starts:{end:"$",keywords:r,contains:m}}];return l.unshift(o),{name:"Ruby",
-aliases:["rb","gemspec","podspec","thor","irb"],keywords:r,illegal:/\/\*/,
-contains:[e.SHEBANG({binary:"ruby"})].concat(p).concat(l).concat(m)}},
-grmr_rust:e=>{const n=e.regex,t={className:"title.function.invoke",relevance:0,
-begin:n.concat(/\b/,/(?!let|for|while|if|else|match\b)/,e.IDENT_RE,n.lookahead(/\s*\(/))
-},a="([ui](8|16|32|64|128|size)|f(32|64))?",i=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","eprintln!","panic!","file!","format!","format_args!","include_bytes!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"],r=["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"]
-;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:r,
-keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","unsafe","unsized","use","virtual","where","while","yield"],
-literal:["true","false","Some","None","Ok","Err"],built_in:i},illegal:"</",
-contains:[e.C_LINE_COMMENT_MODE,e.COMMENT("/\\*","\\*/",{contains:["self"]
-}),e.inherit(e.QUOTE_STRING_MODE,{begin:/b?"/,illegal:null}),{
-className:"string",variants:[{begin:/b?r(#*)"(.|\n)*?"\1(?!#)/},{
-begin:/b?'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/}]},{className:"symbol",
-begin:/'[a-zA-Z_][a-zA-Z0-9_]*/},{className:"number",variants:[{
-begin:"\\b0b([01_]+)"+a},{begin:"\\b0o([0-7_]+)"+a},{
-begin:"\\b0x([A-Fa-f0-9_]+)"+a},{
-begin:"\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)"+a}],relevance:0},{
-begin:[/fn/,/\s+/,e.UNDERSCORE_IDENT_RE],className:{1:"keyword",
-3:"title.function"}},{className:"meta",begin:"#!?\\[",end:"\\]",contains:[{
-className:"string",begin:/"/,end:/"/}]},{
-begin:[/let/,/\s+/,/(?:mut\s+)?/,e.UNDERSCORE_IDENT_RE],className:{1:"keyword",
-3:"keyword",4:"variable"}},{
-begin:[/for/,/\s+/,e.UNDERSCORE_IDENT_RE,/\s+/,/in/],className:{1:"keyword",
-3:"variable",5:"keyword"}},{begin:[/type/,/\s+/,e.UNDERSCORE_IDENT_RE],
-className:{1:"keyword",3:"title.class"}},{
-begin:[/(?:trait|enum|struct|union|impl|for)/,/\s+/,e.UNDERSCORE_IDENT_RE],
-className:{1:"keyword",3:"title.class"}},{begin:e.IDENT_RE+"::",keywords:{
-keyword:"Self",built_in:i,type:r}},{className:"punctuation",begin:"->"},t]}},
-grmr_scss:e=>{const n=ie(e),t=le,a=oe,i="@[a-z-]+",r={className:"variable",
-begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS",
-case_insensitive:!0,illegal:"[=/|']",
-contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,n.CSS_NUMBER_MODE,{
-className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{
-className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0
-},n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag",
-begin:"\\b("+re.join("|")+")\\b",relevance:0},{className:"selector-pseudo",
-begin:":("+a.join("|")+")"},{className:"selector-pseudo",
-begin:":(:)?("+t.join("|")+")"},r,{begin:/\(/,end:/\)/,
-contains:[n.CSS_NUMBER_MODE]},n.CSS_VARIABLE,{className:"attribute",
-begin:"\\b("+ce.join("|")+")\\b"},{
-begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"
-},{begin:/:/,end:/[;}{]/,relevance:0,
-contains:[n.BLOCK_COMMENT,r,n.HEXCOLOR,n.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.IMPORTANT,n.FUNCTION_DISPATCH]
-},{begin:"@(page|font-face)",keywords:{$pattern:i,keyword:"@page @font-face"}},{
-begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/,
-keyword:"and or not only",attribute:se.join(" ")},contains:[{begin:i,
-className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute"
-},r,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.HEXCOLOR,n.CSS_NUMBER_MODE]
-},n.FUNCTION_DISPATCH]}},grmr_shell:e=>({name:"Shell Session",
-aliases:["console","shellsession"],contains:[{className:"meta.prompt",
-begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/,
-subLanguage:"bash"}}]}),grmr_sql:e=>{
-const n=e.regex,t=e.COMMENT("--","$"),a=["true","false","unknown"],i=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],r=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],s=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],o=r,l=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter((e=>!r.includes(e))),c={
-begin:n.concat(/\b/,n.either(...o),/\s*\(/),relevance:0,keywords:{built_in:o}}
-;return{name:"SQL",case_insensitive:!0,illegal:/[{}]|<\//,keywords:{
-$pattern:/\b[\w\.]+/,keyword:((e,{exceptions:n,when:t}={})=>{const a=t
-;return n=n||[],e.map((e=>e.match(/\|\d+$/)||n.includes(e)?e:a(e)?e+"|0":e))
-})(l,{when:e=>e.length<3}),literal:a,type:i,
-built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"]
-},contains:[{begin:n.either(...s),relevance:0,keywords:{$pattern:/[\w\.]+/,
-keyword:l.concat(s),literal:a,type:i}},{className:"type",
-begin:n.either("double precision","large object","with timezone","without timezone")
-},c,{className:"variable",begin:/@[a-z0-9][a-z0-9_]*/},{className:"string",
-variants:[{begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/,
-contains:[{begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,{
-className:"operator",begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/,
-relevance:0}]}},grmr_swift:e=>{const n={match:/\s+/,relevance:0
-},t=e.COMMENT("/\\*","\\*/",{contains:["self"]}),a=[e.C_LINE_COMMENT_MODE,t],i={
-match:[/\./,m(...xe,...Me)],className:{2:"keyword"}},r={match:b(/\./,m(...Ae)),
-relevance:0},s=Ae.filter((e=>"string"==typeof e)).concat(["_|0"]),o={variants:[{
-className:"keyword",
-match:m(...Ae.filter((e=>"string"!=typeof e)).concat(Se).map(ke),...Me)}]},l={
-$pattern:m(/\b\w+/,/#\w+/),keyword:s.concat(Re),literal:Ce},c=[i,r,o],g=[{
-match:b(/\./,m(...De)),relevance:0},{className:"built_in",
-match:b(/\b/,m(...De),/(?=\()/)}],u={match:/->/,relevance:0},p=[u,{
-className:"operator",relevance:0,variants:[{match:Be},{match:`\\.(\\.|${Le})+`}]
-}],_="([0-9]_*)+",h="([0-9a-fA-F]_*)+",f={className:"number",relevance:0,
-variants:[{match:`\\b(${_})(\\.(${_}))?([eE][+-]?(${_}))?\\b`},{
-match:`\\b0x(${h})(\\.(${h}))?([pP][+-]?(${_}))?\\b`},{match:/\b0o([0-7]_*)+\b/
-},{match:/\b0b([01]_*)+\b/}]},E=(e="")=>({className:"subst",variants:[{
-match:b(/\\/,e,/[0\\tnr"']/)},{match:b(/\\/,e,/u\{[0-9a-fA-F]{1,8}\}/)}]
-}),y=(e="")=>({className:"subst",match:b(/\\/,e,/[\t ]*(?:[\r\n]|\r\n)/)
-}),N=(e="")=>({className:"subst",label:"interpol",begin:b(/\\/,e,/\(/),end:/\)/
-}),w=(e="")=>({begin:b(e,/"""/),end:b(/"""/,e),contains:[E(e),y(e),N(e)]
-}),v=(e="")=>({begin:b(e,/"/),end:b(/"/,e),contains:[E(e),N(e)]}),O={
-className:"string",
-variants:[w(),w("#"),w("##"),w("###"),v(),v("#"),v("##"),v("###")]
-},k=[e.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0,
-contains:[e.BACKSLASH_ESCAPE]}],x={begin:/\/[^\s](?=[^/\n]*\/)/,end:/\//,
-contains:k},M=e=>{const n=b(e,/\//),t=b(/\//,e);return{begin:n,end:t,
-contains:[...k,{scope:"comment",begin:`#(?!.*${t})`,end:/$/}]}},S={
-scope:"regexp",variants:[M("###"),M("##"),M("#"),x]},A={match:b(/`/,Fe,/`/)
-},C=[A,{className:"variable",match:/\$\d+/},{className:"variable",
-match:`\\$${ze}+`}],T=[{match:/(@|#(un)?)available/,scope:"keyword",starts:{
-contains:[{begin:/\(/,end:/\)/,keywords:Pe,contains:[...p,f,O]}]}},{
-scope:"keyword",match:b(/@/,m(...je))},{scope:"meta",match:b(/@/,Fe)}],R={
-match:d(/\b[A-Z]/),relevance:0,contains:[{className:"type",
-match:b(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/,ze,"+")
-},{className:"type",match:Ue,relevance:0},{match:/[?!]+/,relevance:0},{
-match:/\.\.\./,relevance:0},{match:b(/\s+&\s+/,d(Ue)),relevance:0}]},D={
-begin:/</,end:/>/,keywords:l,contains:[...a,...c,...T,u,R]};R.contains.push(D)
-;const I={begin:/\(/,end:/\)/,relevance:0,keywords:l,contains:["self",{
-match:b(Fe,/\s*:/),keywords:"_|0",relevance:0
-},...a,S,...c,...g,...p,f,O,...C,...T,R]},L={begin:/</,end:/>/,
-keywords:"repeat each",contains:[...a,R]},B={begin:/\(/,end:/\)/,keywords:l,
-contains:[{begin:m(d(b(Fe,/\s*:/)),d(b(Fe,/\s+/,Fe,/\s*:/))),end:/:/,
-relevance:0,contains:[{className:"keyword",match:/\b_\b/},{className:"params",
-match:Fe}]},...a,...c,...p,f,O,...T,R,I],endsParent:!0,illegal:/["']/},$={
-match:[/(func|macro)/,/\s+/,m(A.match,Fe,Be)],className:{1:"keyword",
-3:"title.function"},contains:[L,B,n],illegal:[/\[/,/%/]},z={
-match:[/\b(?:subscript|init[?!]?)/,/\s*(?=[<(])/],className:{1:"keyword"},
-contains:[L,B,n],illegal:/\[|%/},F={match:[/operator/,/\s+/,Be],className:{
-1:"keyword",3:"title"}},U={begin:[/precedencegroup/,/\s+/,Ue],className:{
-1:"keyword",3:"title"},contains:[R],keywords:[...Te,...Ce],end:/}/}
-;for(const e of O.variants){const n=e.contains.find((e=>"interpol"===e.label))
-;n.keywords=l;const t=[...c,...g,...p,f,O,...C];n.contains=[...t,{begin:/\(/,
-end:/\)/,contains:["self",...t]}]}return{name:"Swift",keywords:l,
-contains:[...a,$,z,{beginKeywords:"struct protocol class extension enum actor",
-end:"\\{",excludeEnd:!0,keywords:l,contains:[e.inherit(e.TITLE_MODE,{
-className:"title.class",begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}),...c]
-},F,U,{beginKeywords:"import",end:/$/,contains:[...a],relevance:0
-},S,...c,...g,...p,f,O,...C,...T,R,I]}},grmr_typescript:e=>{
-const n=Oe(e),t=_e,a=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],i={
-beginKeywords:"namespace",end:/\{/,excludeEnd:!0,
-contains:[n.exports.CLASS_REFERENCE]},r={beginKeywords:"interface",end:/\{/,
-excludeEnd:!0,keywords:{keyword:"interface extends",built_in:a},
-contains:[n.exports.CLASS_REFERENCE]},s={$pattern:_e,
-keyword:he.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]),
-literal:fe,built_in:ve.concat(a),"variable.language":we},o={className:"meta",
-begin:"@"+t},l=(e,n,t)=>{const a=e.contains.findIndex((e=>e.label===n))
-;if(-1===a)throw Error("can not find mode to replace");e.contains.splice(a,1,t)}
-;return Object.assign(n.keywords,s),
-n.exports.PARAMS_CONTAINS.push(o),n.contains=n.contains.concat([o,i,r]),
-l(n,"shebang",e.SHEBANG()),l(n,"use_strict",{className:"meta",relevance:10,
-begin:/^\s*['"]use strict['"]/
-}),n.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(n,{
-name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),n},grmr_vbnet:e=>{
-const n=e.regex,t=/\d{1,2}\/\d{1,2}\/\d{4}/,a=/\d{4}-\d{1,2}-\d{1,2}/,i=/(\d|1[012])(:\d+){0,2} *(AM|PM)/,r=/\d{1,2}(:\d{1,2}){1,2}/,s={
-className:"literal",variants:[{begin:n.concat(/# */,n.either(a,t),/ *#/)},{
-begin:n.concat(/# */,r,/ *#/)},{begin:n.concat(/# */,i,/ *#/)},{
-begin:n.concat(/# */,n.either(a,t),/ +/,n.either(i,r),/ *#/)}]
-},o=e.COMMENT(/'''/,/$/,{contains:[{className:"doctag",begin:/<\/?/,end:/>/}]
-}),l=e.COMMENT(null,/$/,{variants:[{begin:/'/},{begin:/([\t ]|^)REM(?=\s)/}]})
-;return{name:"Visual Basic .NET",aliases:["vb"],case_insensitive:!0,
-classNameAliases:{label:"symbol"},keywords:{
-keyword:"addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield",
-built_in:"addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort",
-type:"boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort",
-literal:"true false nothing"},
-illegal:"//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",contains:[{
-className:"string",begin:/"(""|[^/n])"C\b/},{className:"string",begin:/"/,
-end:/"/,illegal:/\n/,contains:[{begin:/""/}]},s,{className:"number",relevance:0,
-variants:[{begin:/\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/
-},{begin:/\b\d[\d_]*((U?[SIL])|[%&])?/},{begin:/&H[\dA-F_]+((U?[SIL])|[%&])?/},{
-begin:/&O[0-7_]+((U?[SIL])|[%&])?/},{begin:/&B[01_]+((U?[SIL])|[%&])?/}]},{
-className:"label",begin:/^\w+:/},o,l,{className:"meta",
-begin:/[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/,
-end:/$/,keywords:{
-keyword:"const disable else elseif enable end externalsource if region then"},
-contains:[l]}]}},grmr_wasm:e=>{e.regex;const n=e.COMMENT(/\(;/,/;\)/)
-;return n.contains.push("self"),{name:"WebAssembly",keywords:{$pattern:/[\w.]+/,
-keyword:["anyfunc","block","br","br_if","br_table","call","call_indirect","data","drop","elem","else","end","export","func","global.get","global.set","local.get","local.set","local.tee","get_global","get_local","global","if","import","local","loop","memory","memory.grow","memory.size","module","mut","nop","offset","param","result","return","select","set_global","set_local","start","table","tee_local","then","type","unreachable"]
-},contains:[e.COMMENT(/;;/,/$/),n,{match:[/(?:offset|align)/,/\s*/,/=/],
-className:{1:"keyword",3:"operator"}},{className:"variable",begin:/\$[\w_]+/},{
-match:/(\((?!;)|\))+/,className:"punctuation",relevance:0},{
-begin:[/(?:func|call|call_indirect)/,/\s+/,/\$[^\s)]+/],className:{1:"keyword",
-3:"title.function"}},e.QUOTE_STRING_MODE,{match:/(i32|i64|f32|f64)(?!\.)/,
-className:"type"},{className:"keyword",
-match:/\b(f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|nearest|neg?|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|store(?:8|16|32)?|sqrt|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))\b/
-},{className:"number",relevance:0,
-match:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/
-}]}},grmr_xml:e=>{
-const n=e.regex,t=n.concat(/[\p{L}_]/u,n.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),a={
-className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},i={begin:/\s/,
-contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]
-},r=e.inherit(i,{begin:/\(/,end:/\)/}),s=e.inherit(e.APOS_STRING_MODE,{
-className:"string"}),o=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),l={
-endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",
-begin:/[\p{L}0-9._:-]+/u,relevance:0},{begin:/=\s*/,relevance:0,contains:[{
-className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/,contains:[a]},{
-begin:/'/,end:/'/,contains:[a]},{begin:/[^\s"'=<>`]+/}]}]}]};return{
-name:"HTML, XML",
-aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],
-case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin:/<![a-z]/,
-end:/>/,relevance:10,contains:[i,o,s,r,{begin:/\[/,end:/\]/,contains:[{
-className:"meta",begin:/<![a-z]/,end:/>/,contains:[i,r,o,s]}]}]
-},e.COMMENT(/<!--/,/-->/,{relevance:10}),{begin:/<!\[CDATA\[/,end:/\]\]>/,
-relevance:10},a,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/,
-relevance:10,contains:[o]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag",
-begin:/<style(?=\s|>)/,end:/>/,keywords:{name:"style"},contains:[l],starts:{
-end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",
-begin:/<script(?=\s|>)/,end:/>/,keywords:{name:"script"},contains:[l],starts:{
-end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{
-className:"tag",begin:/<>|<\/>/},{className:"tag",
-begin:n.concat(/</,n.lookahead(n.concat(t,n.either(/\/>/,/>/,/\s/)))),
-end:/\/?>/,contains:[{className:"name",begin:t,relevance:0,starts:l}]},{
-className:"tag",begin:n.concat(/<\//,n.lookahead(n.concat(t,/>/))),contains:[{
-className:"name",begin:t,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}
-},grmr_yaml:e=>{
-const n="true false yes no null",t="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={
-className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/
-},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable",
-variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(a,{
-variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),r={
-end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},s={begin:/\{/,
-end:/\}/,contains:[r],illegal:"\\n",relevance:0},o={begin:"\\[",end:"\\]",
-contains:[r],illegal:"\\n",relevance:0},l=[{className:"attr",variants:[{
-begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{
-begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",
-relevance:10},{className:"string",
-begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{
-begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,
-relevance:0},{className:"type",begin:"!\\w+!"+t},{className:"type",
-begin:"!<"+t+">"},{className:"type",begin:"!"+t},{className:"type",begin:"!!"+t
-},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta",
-begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",
-relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{
-className:"number",
-begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"
-},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},s,o,a],c=[...l]
-;return c.pop(),c.push(i),r.contains=c,{name:"YAML",case_insensitive:!0,
-aliases:["yml"],contains:l}}});const He=ae;for(const e of Object.keys(Ke)){
-const n=e.replace("grmr_","").replace("_","-");He.registerLanguage(n,Ke[e])}
-return He}()
-;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);
\ No newline at end of file
diff --git a/build/lib/exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.css b/build/lib/exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.css
deleted file mode 100644
index 7f6fe11c..00000000
--- a/build/lib/exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.css
+++ /dev/null
@@ -1 +0,0 @@
-pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#1e1e1e;color:#dcdcdc}.hljs-keyword,.hljs-literal,.hljs-name,.hljs-symbol{color:#569cd6}.hljs-link{color:#569cd6;text-decoration:underline}.hljs-built_in,.hljs-type{color:#4ec9b0}.hljs-class,.hljs-number{color:#b8d7a3}.hljs-meta .hljs-string,.hljs-string{color:#d69d85}.hljs-regexp,.hljs-template-tag{color:#9a5334}.hljs-formula,.hljs-function,.hljs-params,.hljs-subst,.hljs-title{color:#dcdcdc}.hljs-comment,.hljs-quote{color:#57a64a;font-style:italic}.hljs-doctag{color:#608b4e}.hljs-meta,.hljs-meta .hljs-keyword,.hljs-tag{color:#9b9b9b}.hljs-template-variable,.hljs-variable{color:#bd63c5}.hljs-attr,.hljs-attribute{color:#9cdcfe}.hljs-section{color:gold}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-bullet,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-selector-tag{color:#d7ba7d}.hljs-addition{background-color:#144212;display:inline-block;width:100%}.hljs-deletion{background-color:#600;display:inline-block;width:100%}
\ No newline at end of file
diff --git a/build/lib/exo/tinychat/static/unpkg.com/@marcreichel/alpine-autosize@1.3.x/dist/alpine-autosize.min.js b/build/lib/exo/tinychat/static/unpkg.com/@marcreichel/alpine-autosize@1.3.x/dist/alpine-autosize.min.js
deleted file mode 100644
index dd119d59..00000000
--- a/build/lib/exo/tinychat/static/unpkg.com/@marcreichel/alpine-autosize@1.3.x/dist/alpine-autosize.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-!function(e){"function"==typeof define&&define.amd?define(e):e()}((function(){"use strict";var e=new Map;function t(t){var o=e.get(t);o&&o.destroy()}function o(t){var o=e.get(t);o&&o.update()}var r=null;"undefined"==typeof window?((r=function(e){return e}).destroy=function(e){return e},r.update=function(e){return e}):((r=function(t,o){return t&&Array.prototype.forEach.call(t.length?t:[t],(function(t){return function(t){if(t&&t.nodeName&&"TEXTAREA"===t.nodeName&&!e.has(t)){var o,r=null,n=window.getComputedStyle(t),i=(o=t.value,function(){s({testForHeightReduction:""===o||!t.value.startsWith(o),restoreTextAlign:null}),o=t.value}),l=function(o){t.removeEventListener("autosize:destroy",l),t.removeEventListener("autosize:update",a),t.removeEventListener("input",i),window.removeEventListener("resize",a),Object.keys(o).forEach((function(e){return t.style[e]=o[e]})),e.delete(t)}.bind(t,{height:t.style.height,resize:t.style.resize,textAlign:t.style.textAlign,overflowY:t.style.overflowY,overflowX:t.style.overflowX,wordWrap:t.style.wordWrap});t.addEventListener("autosize:destroy",l),t.addEventListener("autosize:update",a),t.addEventListener("input",i),window.addEventListener("resize",a),t.style.overflowX="hidden",t.style.wordWrap="break-word",e.set(t,{destroy:l,update:a}),a()}function s(e){var o,i,l=e.restoreTextAlign,a=void 0===l?null:l,d=e.testForHeightReduction,u=void 0===d||d,c=n.overflowY;if(0!==t.scrollHeight&&("vertical"===n.resize?t.style.resize="none":"both"===n.resize&&(t.style.resize="horizontal"),u&&(o=function(e){for(var t=[];e&&e.parentNode&&e.parentNode instanceof Element;)e.parentNode.scrollTop&&t.push([e.parentNode,e.parentNode.scrollTop]),e=e.parentNode;return function(){return t.forEach((function(e){var t=e[0],o=e[1];t.style.scrollBehavior="auto",t.scrollTop=o,t.style.scrollBehavior=null}))}}(t),t.style.height=""),i="content-box"===n.boxSizing?t.scrollHeight-(parseFloat(n.paddingTop)+parseFloat(n.paddingBottom)):t.scrollHeight+parseFloat(n.borderTopWidth)+parseFloat(n.borderBottomWidth),"none"!==n.maxHeight&&i>parseFloat(n.maxHeight)?("hidden"===n.overflowY&&(t.style.overflow="scroll"),i=parseFloat(n.maxHeight)):"hidden"!==n.overflowY&&(t.style.overflow="hidden"),t.style.height=i+"px",a&&(t.style.textAlign=a),o&&o(),r!==i&&(t.dispatchEvent(new Event("autosize:resized",{bubbles:!0})),r=i),c!==n.overflow&&!a)){var f=n.textAlign;"hidden"===n.overflow&&(t.style.textAlign="start"===f?"end":"start"),s({restoreTextAlign:f,testForHeightReduction:!0})}}function a(){s({testForHeightReduction:!0,restoreTextAlign:null})}}(t)})),t}).destroy=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],t),e},r.update=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],o),e});var n=r;document.addEventListener("alpine:init",(()=>{var e;(e=window.Alpine).directive("autosize",((e,{modifiers:t},{cleanup:o})=>{n(e);const r=Array.from(e.attributes);let i=!1;for(let{nodeName:e}of r)if("wire:model"===e||e.startsWith("wire:model.")){i=!0;break}!e.hasAttribute("wire:ignore")&&i&&e.setAttribute("wire:ignore","");const l=()=>{n.update(e)};e.addEventListener("autosize",l),o((()=>{n.destroy(e),e.removeEventListener("autosize",l)}))})),e.magic("autosize",(e=>t=>{(t||e).dispatchEvent(new Event("autosize"))}))}))}));
-//# sourceMappingURL=alpine-autosize.min.js.map
diff --git a/build/lib/exo/tinychat/static/unpkg.com/alpinejs@3.x.x/dist/cdn.min.js b/build/lib/exo/tinychat/static/unpkg.com/alpinejs@3.x.x/dist/cdn.min.js
deleted file mode 100644
index 2ca48278..00000000
--- a/build/lib/exo/tinychat/static/unpkg.com/alpinejs@3.x.x/dist/cdn.min.js
+++ /dev/null
@@ -1,5 +0,0 @@
-(()=>{var rt=!1,nt=!1,U=[],it=-1;function qt(e){Cn(e)}function Cn(e){U.includes(e)||U.push(e),Tn()}function Ee(e){let t=U.indexOf(e);t!==-1&&t>it&&U.splice(t,1)}function Tn(){!nt&&!rt&&(rt=!0,queueMicrotask(Rn))}function Rn(){rt=!1,nt=!0;for(let e=0;e<U.length;e++)U[e](),it=e;U.length=0,it=-1,nt=!1}var R,D,L,st,ot=!0;function Ut(e){ot=!1,e(),ot=!0}function Wt(e){R=e.reactive,L=e.release,D=t=>e.effect(t,{scheduler:r=>{ot?qt(r):r()}}),st=e.raw}function at(e){D=e}function Gt(e){let t=()=>{};return[n=>{let i=D(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),L(i))},i},()=>{t()}]}function ve(e,t){let r=!0,n,i=D(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>L(i)}var Jt=[],Yt=[],Xt=[];function Zt(e){Xt.push(e)}function ee(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Yt.push(t))}function Ae(e){Jt.push(e)}function Oe(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function ct(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function Qt(e){if(e._x_cleanups)for(;e._x_cleanups.length;)e._x_cleanups.pop()()}var lt=new MutationObserver(pt),ut=!1;function le(){lt.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ut=!0}function ft(){Mn(),lt.disconnect(),ut=!1}var ce=[];function Mn(){let e=lt.takeRecords();ce.push(()=>e.length>0&&pt(e));let t=ce.length;queueMicrotask(()=>{if(ce.length===t)for(;ce.length>0;)ce.shift()()})}function _(e){if(!ut)return e();ft();let t=e();return le(),t}var dt=!1,Se=[];function er(){dt=!0}function tr(){dt=!1,pt(Se),Se=[]}function pt(e){if(dt){Se=Se.concat(e);return}let t=new Set,r=new Set,n=new Map,i=new Map;for(let o=0;o<e.length;o++)if(!e[o].target._x_ignoreMutationObserver&&(e[o].type==="childList"&&(e[o].addedNodes.forEach(s=>s.nodeType===1&&t.add(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.add(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{ct(s,o)}),n.forEach((o,s)=>{Jt.forEach(a=>a(s,o))});for(let o of r)t.has(o)||Yt.forEach(s=>s(o));t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.has(o)||o.isConnected&&(delete o._x_ignoreSelf,delete o._x_ignore,Xt.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function Ce(e){return F(j(e))}function P(e,t,r){return e._x_dataStack=[t,...j(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function j(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?j(e.host):e.parentNode?j(e.parentNode):[]}function F(e){return new Proxy({objects:e},Nn)}var Nn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?Dn:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function Dn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Te(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Re(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>Pn(n,i),s=>mt(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function Pn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function mt(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),mt(e[t[0]],t.slice(1),r)}}var rr={};function y(e,t){rr[e]=t}function ue(e,t){return Object.entries(rr).forEach(([r,n])=>{let i=null;function o(){if(i)return i;{let[s,a]=_t(t);return i={interceptor:Re,...s},ee(t,a),i}}Object.defineProperty(e,`$${r}`,{get(){return n(t,o())},enumerable:!1})}),e}function nr(e,t,r,...n){try{return r(...n)}catch(i){te(i,e,t)}}function te(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message}
-
-${r?'Expression: "'+r+`"
-
-`:""}`,t),setTimeout(()=>{throw e},0)}var Me=!0;function De(e){let t=Me;Me=!1;let r=e();return Me=t,r}function M(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return ir(...e)}var ir=gt;function or(e){ir=e}function gt(e,t){let r={};ue(r,e);let n=[r,...j(e)],i=typeof t=="function"?In(n,t):Ln(n,t,e);return nr.bind(null,e,t,i)}function In(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(F([n,...e]),i);Ne(r,o)}}var ht={};function kn(e,t){if(ht[e])return ht[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return te(s,t,e),Promise.resolve()}})();return ht[e]=o,o}function Ln(e,t,r){let n=kn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=F([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>te(l,r,t));n.finished?(Ne(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Ne(i,l,a,s,r)}).catch(l=>te(l,r,t)).finally(()=>n.result=void 0)}}}function Ne(e,t,r,n,i){if(Me&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Ne(e,s,r,n)).catch(s=>te(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var bt="x-";function C(e=""){return bt+e}function sr(e){bt=e}var Pe={};function d(e,t){return Pe[e]=t,{before(r){if(!Pe[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=W.indexOf(r);W.splice(n>=0?n:W.indexOf("DEFAULT"),0,e)}}}function ar(e){return Object.keys(Pe).includes(e)}function de(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=wt(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(ur((o,s)=>n[o]=s)).filter(dr).map(jn(n,r)).sort(Fn).map(o=>$n(e,o))}function wt(e){return Array.from(e).map(ur()).filter(t=>!dr(t))}var xt=!1,fe=new Map,cr=Symbol();function lr(e){xt=!0;let t=Symbol();cr=t,fe.set(t,[]);let r=()=>{for(;fe.get(t).length;)fe.get(t).shift()();fe.delete(t)},n=()=>{xt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Gt(e);return t.push(i),[{Alpine:B,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:M.bind(M,e)},()=>t.forEach(a=>a())]}function $n(e,t){let r=()=>{},n=Pe[t.type]||r,[i,o]=_t(e);Oe(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),xt?fe.get(cr).push(n):n())};return s.runCleanups=o,s}var Ie=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),ke=e=>e;function ur(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=fr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var fr=[];function re(e){fr.push(e)}function dr({name:e}){return pr().test(e)}var pr=()=>new RegExp(`^${bt}([^:^.]+)\\b`);function jn(e,t){return({name:r,value:n})=>{let i=r.match(pr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var yt="DEFAULT",W=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",yt,"teleport"];function Fn(e,t){let r=W.indexOf(e.type)===-1?yt:e.type,n=W.indexOf(t.type)===-1?yt:t.type;return W.indexOf(r)-W.indexOf(n)}function G(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function T(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>T(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)T(n,t,!1),n=n.nextElementSibling}function E(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var mr=!1;function _r(){mr&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),mr=!0,document.body||E("Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine's `<script>` tag?"),G(document,"alpine:init"),G(document,"alpine:initializing"),le(),Zt(t=>S(t,T)),ee(t=>vt(t)),Ae((t,r)=>{de(t,r).forEach(n=>n())});let e=t=>!J(t.parentElement,!0);Array.from(document.querySelectorAll(xr().join(","))).filter(e).forEach(t=>{S(t)}),G(document,"alpine:initialized"),setTimeout(()=>{Bn()})}var Et=[],hr=[];function gr(){return Et.map(e=>e())}function xr(){return Et.concat(hr).map(e=>e())}function Le(e){Et.push(e)}function $e(e){hr.push(e)}function J(e,t=!1){return z(e,r=>{if((t?xr():gr()).some(i=>r.matches(i)))return!0})}function z(e,t){if(e){if(t(e))return e;if(e._x_teleportBack&&(e=e._x_teleportBack),!!e.parentElement)return z(e.parentElement,t)}}function yr(e){return gr().some(t=>e.matches(t))}var br=[];function wr(e){br.push(e)}function S(e,t=T,r=()=>{}){lr(()=>{t(e,(n,i)=>{r(n,i),br.forEach(o=>o(n,i)),de(n,n.attributes).forEach(o=>o()),n._x_ignore&&i()})})}function vt(e,t=T){t(e,r=>{ct(r),Qt(r)})}function Bn(){[["ui","dialog",["[x-dialog], [x-popover]"]],["anchor","anchor",["[x-anchor]"]],["sort","sort",["[x-sort]"]]].forEach(([t,r,n])=>{ar(r)||n.some(i=>{if(document.querySelector(i))return E(`found "${i}", but missing ${t} plugin`),!0})})}var St=[],At=!1;function ne(e=()=>{}){return queueMicrotask(()=>{At||setTimeout(()=>{je()})}),new Promise(t=>{St.push(()=>{e(),t()})})}function je(){for(At=!1;St.length;)St.shift()()}function Er(){At=!0}function pe(e,t){return Array.isArray(t)?vr(e,t.join(" ")):typeof t=="object"&&t!==null?zn(e,t):typeof t=="function"?pe(e,t()):vr(e,t)}function vr(e,t){let r=o=>o.split(" ").filter(Boolean),n=o=>o.split(" ").filter(s=>!e.classList.contains(s)).filter(Boolean),i=o=>(e.classList.add(...o),()=>{e.classList.remove(...o)});return t=t===!0?t="":t||"",i(n(t))}function zn(e,t){let r=a=>a.split(" ").filter(Boolean),n=Object.entries(t).flatMap(([a,c])=>c?r(a):!1).filter(Boolean),i=Object.entries(t).flatMap(([a,c])=>c?!1:r(a)).filter(Boolean),o=[],s=[];return i.forEach(a=>{e.classList.contains(a)&&(e.classList.remove(a),s.push(a))}),n.forEach(a=>{e.classList.contains(a)||(e.classList.add(a),o.push(a))}),()=>{s.forEach(a=>e.classList.add(a)),o.forEach(a=>e.classList.remove(a))}}function Y(e,t){return typeof t=="object"&&t!==null?Kn(e,t):Hn(e,t)}function Kn(e,t){let r={};return Object.entries(t).forEach(([n,i])=>{r[n]=e.style[n],n.startsWith("--")||(n=Vn(n)),e.style.setProperty(n,i)}),setTimeout(()=>{e.style.length===0&&e.removeAttribute("style")}),()=>{Y(e,r)}}function Hn(e,t){let r=e.getAttribute("style",t);return e.setAttribute("style",t),()=>{e.setAttribute("style",r||"")}}function Vn(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}function me(e,t=()=>{}){let r=!1;return function(){r?t.apply(this,arguments):(r=!0,e.apply(this,arguments))}}d("transition",(e,{value:t,modifiers:r,expression:n},{evaluate:i})=>{typeof n=="function"&&(n=i(n)),n!==!1&&(!n||typeof n=="boolean"?Un(e,r,t):qn(e,n,t))});function qn(e,t,r){Sr(e,pe,""),{enter:i=>{e._x_transition.enter.during=i},"enter-start":i=>{e._x_transition.enter.start=i},"enter-end":i=>{e._x_transition.enter.end=i},leave:i=>{e._x_transition.leave.during=i},"leave-start":i=>{e._x_transition.leave.start=i},"leave-end":i=>{e._x_transition.leave.end=i}}[r](t)}function Un(e,t,r){Sr(e,Y);let n=!t.includes("in")&&!t.includes("out")&&!r,i=n||t.includes("in")||["enter"].includes(r),o=n||t.includes("out")||["leave"].includes(r);t.includes("in")&&!n&&(t=t.filter((g,b)=>b<t.indexOf("out"))),t.includes("out")&&!n&&(t=t.filter((g,b)=>b>t.indexOf("out")));let s=!t.includes("opacity")&&!t.includes("scale"),a=s||t.includes("opacity"),c=s||t.includes("scale"),l=a?0:1,u=c?_e(t,"scale",95)/100:1,p=_e(t,"delay",0)/1e3,m=_e(t,"origin","center"),w="opacity, transform",$=_e(t,"duration",150)/1e3,we=_e(t,"duration",75)/1e3,f="cubic-bezier(0.4, 0.0, 0.2, 1)";i&&(e._x_transition.enter.during={transformOrigin:m,transitionDelay:`${p}s`,transitionProperty:w,transitionDuration:`${$}s`,transitionTimingFunction:f},e._x_transition.enter.start={opacity:l,transform:`scale(${u})`},e._x_transition.enter.end={opacity:1,transform:"scale(1)"}),o&&(e._x_transition.leave.during={transformOrigin:m,transitionDelay:`${p}s`,transitionProperty:w,transitionDuration:`${we}s`,transitionTimingFunction:f},e._x_transition.leave.start={opacity:1,transform:"scale(1)"},e._x_transition.leave.end={opacity:l,transform:`scale(${u})`})}function Sr(e,t,r={}){e._x_transition||(e._x_transition={enter:{during:r,start:r,end:r},leave:{during:r,start:r,end:r},in(n=()=>{},i=()=>{}){Fe(e,t,{during:this.enter.during,start:this.enter.start,end:this.enter.end},n,i)},out(n=()=>{},i=()=>{}){Fe(e,t,{during:this.leave.during,start:this.leave.start,end:this.leave.end},n,i)}})}window.Element.prototype._x_toggleAndCascadeWithTransitions=function(e,t,r,n){let i=document.visibilityState==="visible"?requestAnimationFrame:setTimeout,o=()=>i(r);if(t){e._x_transition&&(e._x_transition.enter||e._x_transition.leave)?e._x_transition.enter&&(Object.entries(e._x_transition.enter.during).length||Object.entries(e._x_transition.enter.start).length||Object.entries(e._x_transition.enter.end).length)?e._x_transition.in(r):o():e._x_transition?e._x_transition.in(r):o();return}e._x_hidePromise=e._x_transition?new Promise((s,a)=>{e._x_transition.out(()=>{},()=>s(n)),e._x_transitioning&&e._x_transitioning.beforeCancel(()=>a({isFromCancelledTransition:!0}))}):Promise.resolve(n),queueMicrotask(()=>{let s=Ar(e);s?(s._x_hideChildren||(s._x_hideChildren=[]),s._x_hideChildren.push(e)):i(()=>{let a=c=>{let l=Promise.all([c._x_hidePromise,...(c._x_hideChildren||[]).map(a)]).then(([u])=>u?.());return delete c._x_hidePromise,delete c._x_hideChildren,l};a(e).catch(c=>{if(!c.isFromCancelledTransition)throw c})})})};function Ar(e){let t=e.parentNode;if(t)return t._x_hidePromise?t:Ar(t)}function Fe(e,t,{during:r,start:n,end:i}={},o=()=>{},s=()=>{}){if(e._x_transitioning&&e._x_transitioning.cancel(),Object.keys(r).length===0&&Object.keys(n).length===0&&Object.keys(i).length===0){o(),s();return}let a,c,l;Wn(e,{start(){a=t(e,n)},during(){c=t(e,r)},before:o,end(){a(),l=t(e,i)},after:s,cleanup(){c(),l()}})}function Wn(e,t){let r,n,i,o=me(()=>{_(()=>{r=!0,n||t.before(),i||(t.end(),je()),t.after(),e.isConnected&&t.cleanup(),delete e._x_transitioning})});e._x_transitioning={beforeCancels:[],beforeCancel(s){this.beforeCancels.push(s)},cancel:me(function(){for(;this.beforeCancels.length;)this.beforeCancels.shift()();o()}),finish:o},_(()=>{t.start(),t.during()}),Er(),requestAnimationFrame(()=>{if(r)return;let s=Number(getComputedStyle(e).transitionDuration.replace(/,.*/,"").replace("s",""))*1e3,a=Number(getComputedStyle(e).transitionDelay.replace(/,.*/,"").replace("s",""))*1e3;s===0&&(s=Number(getComputedStyle(e).animationDuration.replace("s",""))*1e3),_(()=>{t.before()}),n=!0,requestAnimationFrame(()=>{r||(_(()=>{t.end()}),je(),setTimeout(e._x_transitioning.finish,s+a),i=!0)})})}function _e(e,t,r){if(e.indexOf(t)===-1)return r;let n=e[e.indexOf(t)+1];if(!n||t==="scale"&&isNaN(n))return r;if(t==="duration"||t==="delay"){let i=n.match(/([0-9]+)ms/);if(i)return i[1]}return t==="origin"&&["top","right","left","center","bottom"].includes(e[e.indexOf(t)+2])?[n,e[e.indexOf(t)+2]].join(" "):n}var I=!1;function A(e,t=()=>{}){return(...r)=>I?t(...r):e(...r)}function Or(e){return(...t)=>I&&e(...t)}var Cr=[];function K(e){Cr.push(e)}function Tr(e,t){Cr.forEach(r=>r(e,t)),I=!0,Mr(()=>{S(t,(r,n)=>{n(r,()=>{})})}),I=!1}var Be=!1;function Rr(e,t){t._x_dataStack||(t._x_dataStack=e._x_dataStack),I=!0,Be=!0,Mr(()=>{Gn(t)}),I=!1,Be=!1}function Gn(e){let t=!1;S(e,(n,i)=>{T(n,(o,s)=>{if(t&&yr(o))return s();t=!0,i(o,s)})})}function Mr(e){let t=D;at((r,n)=>{let i=t(r);return L(i),()=>{}}),e(),at(t)}function he(e,t,r,n=[]){switch(e._x_bindings||(e._x_bindings=R({})),e._x_bindings[t]=r,t=n.includes("camel")?ri(t):t,t){case"value":Jn(e,r);break;case"style":Xn(e,r);break;case"class":Yn(e,r);break;case"selected":case"checked":Zn(e,t,r);break;default:Dr(e,t,r);break}}function Jn(e,t){if(e.type==="radio")e.attributes.value===void 0&&(e.value=t),window.fromModel&&(typeof t=="boolean"?e.checked=ge(e.value)===t:e.checked=Nr(e.value,t));else if(e.type==="checkbox")Number.isInteger(t)?e.value=t:!Array.isArray(t)&&typeof t!="boolean"&&![null,void 0].includes(t)?e.value=String(t):Array.isArray(t)?e.checked=t.some(r=>Nr(r,e.value)):e.checked=!!t;else if(e.tagName==="SELECT")ti(e,t);else{if(e.value===t)return;e.value=t===void 0?"":t}}function Yn(e,t){e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_undoAddedClasses=pe(e,t)}function Xn(e,t){e._x_undoAddedStyles&&e._x_undoAddedStyles(),e._x_undoAddedStyles=Y(e,t)}function Zn(e,t,r){Dr(e,t,r),ei(e,t,r)}function Dr(e,t,r){[null,void 0,!1].includes(r)&&ni(t)?e.removeAttribute(t):(Pr(t)&&(r=t),Qn(e,t,r))}function Qn(e,t,r){e.getAttribute(t)!=r&&e.setAttribute(t,r)}function ei(e,t,r){e[t]!==r&&(e[t]=r)}function ti(e,t){let r=[].concat(t).map(n=>n+"");Array.from(e.options).forEach(n=>{n.selected=r.includes(n.value)})}function ri(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function Nr(e,t){return e==t}function ge(e){return[1,"1","true","on","yes",!0].includes(e)?!0:[0,"0","false","off","no",!1].includes(e)?!1:e?Boolean(e):null}function Pr(e){return["disabled","checked","required","readonly","open","selected","autofocus","itemscope","multiple","novalidate","allowfullscreen","allowpaymentrequest","formnovalidate","autoplay","controls","loop","muted","playsinline","default","ismap","reversed","async","defer","nomodule"].includes(e)}function ni(e){return!["aria-pressed","aria-checked","aria-expanded","aria-selected"].includes(e)}function Ir(e,t,r){return e._x_bindings&&e._x_bindings[t]!==void 0?e._x_bindings[t]:Lr(e,t,r)}function kr(e,t,r,n=!0){if(e._x_bindings&&e._x_bindings[t]!==void 0)return e._x_bindings[t];if(e._x_inlineBindings&&e._x_inlineBindings[t]!==void 0){let i=e._x_inlineBindings[t];return i.extract=n,De(()=>M(e,i.expression))}return Lr(e,t,r)}function Lr(e,t,r){let n=e.getAttribute(t);return n===null?typeof r=="function"?r():r:n===""?!0:Pr(t)?!![t,"true"].includes(n):n}function ze(e,t){var r;return function(){var n=this,i=arguments,o=function(){r=null,e.apply(n,i)};clearTimeout(r),r=setTimeout(o,t)}}function Ke(e,t){let r;return function(){let n=this,i=arguments;r||(e.apply(n,i),r=!0,setTimeout(()=>r=!1,t))}}function He({get:e,set:t},{get:r,set:n}){let i=!0,o,s,a=D(()=>{let c=e(),l=r();if(i)n(Ot(c)),i=!1;else{let u=JSON.stringify(c),p=JSON.stringify(l);u!==o?n(Ot(c)):u!==p&&t(Ot(l))}o=JSON.stringify(e()),s=JSON.stringify(r())});return()=>{L(a)}}function Ot(e){return typeof e=="object"?JSON.parse(JSON.stringify(e)):e}function $r(e){(Array.isArray(e)?e:[e]).forEach(r=>r(B))}var X={},jr=!1;function Fr(e,t){if(jr||(X=R(X),jr=!0),t===void 0)return X[e];X[e]=t,typeof t=="object"&&t!==null&&t.hasOwnProperty("init")&&typeof t.init=="function"&&X[e].init(),Te(X[e])}function Br(){return X}var zr={};function Kr(e,t){let r=typeof t!="function"?()=>t:t;return e instanceof Element?Ct(e,r()):(zr[e]=r,()=>{})}function Hr(e){return Object.entries(zr).forEach(([t,r])=>{Object.defineProperty(e,t,{get(){return(...n)=>r(...n)}})}),e}function Ct(e,t,r){let n=[];for(;n.length;)n.pop()();let i=Object.entries(t).map(([s,a])=>({name:s,value:a})),o=wt(i);return i=i.map(s=>o.find(a=>a.name===s.name)?{name:`x-bind:${s.name}`,value:`"${s.value}"`}:s),de(e,i,r).map(s=>{n.push(s.runCleanups),s()}),()=>{for(;n.length;)n.pop()()}}var Vr={};function qr(e,t){Vr[e]=t}function Ur(e,t){return Object.entries(Vr).forEach(([r,n])=>{Object.defineProperty(e,r,{get(){return(...i)=>n.bind(t)(...i)},enumerable:!1})}),e}var ii={get reactive(){return R},get release(){return L},get effect(){return D},get raw(){return st},version:"3.14.1",flushAndStopDeferringMutations:tr,dontAutoEvaluateFunctions:De,disableEffectScheduling:Ut,startObservingMutations:le,stopObservingMutations:ft,setReactivityEngine:Wt,onAttributeRemoved:Oe,onAttributesAdded:Ae,closestDataStack:j,skipDuringClone:A,onlyDuringClone:Or,addRootSelector:Le,addInitSelector:$e,interceptClone:K,addScopeToNode:P,deferMutations:er,mapAttributes:re,evaluateLater:x,interceptInit:wr,setEvaluator:or,mergeProxies:F,extractProp:kr,findClosest:z,onElRemoved:ee,closestRoot:J,destroyTree:vt,interceptor:Re,transition:Fe,setStyles:Y,mutateDom:_,directive:d,entangle:He,throttle:Ke,debounce:ze,evaluate:M,initTree:S,nextTick:ne,prefixed:C,prefix:sr,plugin:$r,magic:y,store:Fr,start:_r,clone:Rr,cloneNode:Tr,bound:Ir,$data:Ce,watch:ve,walk:T,data:qr,bind:Kr},B=ii;function Tt(e,t){let r=Object.create(null),n=e.split(",");for(let i=0;i<n.length;i++)r[n[i]]=!0;return t?i=>!!r[i.toLowerCase()]:i=>!!r[i]}var oi="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly";var Ms=Tt(oi+",async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");var Wr=Object.freeze({}),Ns=Object.freeze([]);var si=Object.prototype.hasOwnProperty,xe=(e,t)=>si.call(e,t),H=Array.isArray,ie=e=>Gr(e)==="[object Map]";var ai=e=>typeof e=="string",Ve=e=>typeof e=="symbol",ye=e=>e!==null&&typeof e=="object";var ci=Object.prototype.toString,Gr=e=>ci.call(e),Rt=e=>Gr(e).slice(8,-1);var qe=e=>ai(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e;var Ue=e=>{let t=Object.create(null);return r=>t[r]||(t[r]=e(r))},li=/-(\w)/g,Ds=Ue(e=>e.replace(li,(t,r)=>r?r.toUpperCase():"")),ui=/\B([A-Z])/g,Ps=Ue(e=>e.replace(ui,"-$1").toLowerCase()),Mt=Ue(e=>e.charAt(0).toUpperCase()+e.slice(1)),Is=Ue(e=>e?`on${Mt(e)}`:""),Nt=(e,t)=>e!==t&&(e===e||t===t);var Dt=new WeakMap,be=[],k,Z=Symbol("iterate"),Pt=Symbol("Map key iterate");function fi(e){return e&&e._isEffect===!0}function en(e,t=Wr){fi(e)&&(e=e.raw);let r=pi(e,t);return t.lazy||r(),r}function tn(e){e.active&&(rn(e),e.options.onStop&&e.options.onStop(),e.active=!1)}var di=0;function pi(e,t){let r=function(){if(!r.active)return e();if(!be.includes(r)){rn(r);try{return _i(),be.push(r),k=r,e()}finally{be.pop(),nn(),k=be[be.length-1]}}};return r.id=di++,r.allowRecurse=!!t.allowRecurse,r._isEffect=!0,r.active=!0,r.raw=e,r.deps=[],r.options=t,r}function rn(e){let{deps:t}=e;if(t.length){for(let r=0;r<t.length;r++)t[r].delete(e);t.length=0}}var oe=!0,kt=[];function mi(){kt.push(oe),oe=!1}function _i(){kt.push(oe),oe=!0}function nn(){let e=kt.pop();oe=e===void 0?!0:e}function N(e,t,r){if(!oe||k===void 0)return;let n=Dt.get(e);n||Dt.set(e,n=new Map);let i=n.get(r);i||n.set(r,i=new Set),i.has(k)||(i.add(k),k.deps.push(i),k.options.onTrack&&k.options.onTrack({effect:k,target:e,type:t,key:r}))}function q(e,t,r,n,i,o){let s=Dt.get(e);if(!s)return;let a=new Set,c=u=>{u&&u.forEach(p=>{(p!==k||p.allowRecurse)&&a.add(p)})};if(t==="clear")s.forEach(c);else if(r==="length"&&H(e))s.forEach((u,p)=>{(p==="length"||p>=n)&&c(u)});else switch(r!==void 0&&c(s.get(r)),t){case"add":H(e)?qe(r)&&c(s.get("length")):(c(s.get(Z)),ie(e)&&c(s.get(Pt)));break;case"delete":H(e)||(c(s.get(Z)),ie(e)&&c(s.get(Pt)));break;case"set":ie(e)&&c(s.get(Z));break}let l=u=>{u.options.onTrigger&&u.options.onTrigger({effect:u,target:e,key:r,type:t,newValue:n,oldValue:i,oldTarget:o}),u.options.scheduler?u.options.scheduler(u):u()};a.forEach(l)}var hi=Tt("__proto__,__v_isRef,__isVue"),on=new Set(Object.getOwnPropertyNames(Symbol).map(e=>Symbol[e]).filter(Ve)),gi=sn();var xi=sn(!0);var Jr=yi();function yi(){let e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...r){let n=h(this);for(let o=0,s=this.length;o<s;o++)N(n,"get",o+"");let i=n[t](...r);return i===-1||i===!1?n[t](...r.map(h)):i}}),["push","pop","shift","unshift","splice"].forEach(t=>{e[t]=function(...r){mi();let n=h(this)[t].apply(this,r);return nn(),n}}),e}function sn(e=!1,t=!1){return function(n,i,o){if(i==="__v_isReactive")return!e;if(i==="__v_isReadonly")return e;if(i==="__v_raw"&&o===(e?t?ki:un:t?Ii:ln).get(n))return n;let s=H(n);if(!e&&s&&xe(Jr,i))return Reflect.get(Jr,i,o);let a=Reflect.get(n,i,o);return(Ve(i)?on.has(i):hi(i))||(e||N(n,"get",i),t)?a:It(a)?!s||!qe(i)?a.value:a:ye(a)?e?fn(a):Qe(a):a}}var bi=wi();function wi(e=!1){return function(r,n,i,o){let s=r[n];if(!e&&(i=h(i),s=h(s),!H(r)&&It(s)&&!It(i)))return s.value=i,!0;let a=H(r)&&qe(n)?Number(n)<r.length:xe(r,n),c=Reflect.set(r,n,i,o);return r===h(o)&&(a?Nt(i,s)&&q(r,"set",n,i,s):q(r,"add",n,i)),c}}function Ei(e,t){let r=xe(e,t),n=e[t],i=Reflect.deleteProperty(e,t);return i&&r&&q(e,"delete",t,void 0,n),i}function vi(e,t){let r=Reflect.has(e,t);return(!Ve(t)||!on.has(t))&&N(e,"has",t),r}function Si(e){return N(e,"iterate",H(e)?"length":Z),Reflect.ownKeys(e)}var Ai={get:gi,set:bi,deleteProperty:Ei,has:vi,ownKeys:Si},Oi={get:xi,set(e,t){return console.warn(`Set operation on key "${String(t)}" failed: target is readonly.`,e),!0},deleteProperty(e,t){return console.warn(`Delete operation on key "${String(t)}" failed: target is readonly.`,e),!0}};var Lt=e=>ye(e)?Qe(e):e,$t=e=>ye(e)?fn(e):e,jt=e=>e,Ze=e=>Reflect.getPrototypeOf(e);function We(e,t,r=!1,n=!1){e=e.__v_raw;let i=h(e),o=h(t);t!==o&&!r&&N(i,"get",t),!r&&N(i,"get",o);let{has:s}=Ze(i),a=n?jt:r?$t:Lt;if(s.call(i,t))return a(e.get(t));if(s.call(i,o))return a(e.get(o));e!==i&&e.get(t)}function Ge(e,t=!1){let r=this.__v_raw,n=h(r),i=h(e);return e!==i&&!t&&N(n,"has",e),!t&&N(n,"has",i),e===i?r.has(e):r.has(e)||r.has(i)}function Je(e,t=!1){return e=e.__v_raw,!t&&N(h(e),"iterate",Z),Reflect.get(e,"size",e)}function Yr(e){e=h(e);let t=h(this);return Ze(t).has.call(t,e)||(t.add(e),q(t,"add",e,e)),this}function Xr(e,t){t=h(t);let r=h(this),{has:n,get:i}=Ze(r),o=n.call(r,e);o?cn(r,n,e):(e=h(e),o=n.call(r,e));let s=i.call(r,e);return r.set(e,t),o?Nt(t,s)&&q(r,"set",e,t,s):q(r,"add",e,t),this}function Zr(e){let t=h(this),{has:r,get:n}=Ze(t),i=r.call(t,e);i?cn(t,r,e):(e=h(e),i=r.call(t,e));let o=n?n.call(t,e):void 0,s=t.delete(e);return i&&q(t,"delete",e,void 0,o),s}function Qr(){let e=h(this),t=e.size!==0,r=ie(e)?new Map(e):new Set(e),n=e.clear();return t&&q(e,"clear",void 0,void 0,r),n}function Ye(e,t){return function(n,i){let o=this,s=o.__v_raw,a=h(s),c=t?jt:e?$t:Lt;return!e&&N(a,"iterate",Z),s.forEach((l,u)=>n.call(i,c(l),c(u),o))}}function Xe(e,t,r){return function(...n){let i=this.__v_raw,o=h(i),s=ie(o),a=e==="entries"||e===Symbol.iterator&&s,c=e==="keys"&&s,l=i[e](...n),u=r?jt:t?$t:Lt;return!t&&N(o,"iterate",c?Pt:Z),{next(){let{value:p,done:m}=l.next();return m?{value:p,done:m}:{value:a?[u(p[0]),u(p[1])]:u(p),done:m}},[Symbol.iterator](){return this}}}}function V(e){return function(...t){{let r=t[0]?`on key "${t[0]}" `:"";console.warn(`${Mt(e)} operation ${r}failed: target is readonly.`,h(this))}return e==="delete"?!1:this}}function Ci(){let e={get(o){return We(this,o)},get size(){return Je(this)},has:Ge,add:Yr,set:Xr,delete:Zr,clear:Qr,forEach:Ye(!1,!1)},t={get(o){return We(this,o,!1,!0)},get size(){return Je(this)},has:Ge,add:Yr,set:Xr,delete:Zr,clear:Qr,forEach:Ye(!1,!0)},r={get(o){return We(this,o,!0)},get size(){return Je(this,!0)},has(o){return Ge.call(this,o,!0)},add:V("add"),set:V("set"),delete:V("delete"),clear:V("clear"),forEach:Ye(!0,!1)},n={get(o){return We(this,o,!0,!0)},get size(){return Je(this,!0)},has(o){return Ge.call(this,o,!0)},add:V("add"),set:V("set"),delete:V("delete"),clear:V("clear"),forEach:Ye(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(o=>{e[o]=Xe(o,!1,!1),r[o]=Xe(o,!0,!1),t[o]=Xe(o,!1,!0),n[o]=Xe(o,!0,!0)}),[e,r,t,n]}var[Ti,Ri,Mi,Ni]=Ci();function an(e,t){let r=t?e?Ni:Mi:e?Ri:Ti;return(n,i,o)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?n:Reflect.get(xe(r,i)&&i in n?r:n,i,o)}var Di={get:an(!1,!1)};var Pi={get:an(!0,!1)};function cn(e,t,r){let n=h(r);if(n!==r&&t.call(e,n)){let i=Rt(e);console.warn(`Reactive ${i} contains both the raw and reactive versions of the same object${i==="Map"?" as keys":""}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`)}}var ln=new WeakMap,Ii=new WeakMap,un=new WeakMap,ki=new WeakMap;function Li(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function $i(e){return e.__v_skip||!Object.isExtensible(e)?0:Li(Rt(e))}function Qe(e){return e&&e.__v_isReadonly?e:dn(e,!1,Ai,Di,ln)}function fn(e){return dn(e,!0,Oi,Pi,un)}function dn(e,t,r,n,i){if(!ye(e))return console.warn(`value cannot be made reactive: ${String(e)}`),e;if(e.__v_raw&&!(t&&e.__v_isReactive))return e;let o=i.get(e);if(o)return o;let s=$i(e);if(s===0)return e;let a=new Proxy(e,s===2?n:r);return i.set(e,a),a}function h(e){return e&&h(e.__v_raw)||e}function It(e){return Boolean(e&&e.__v_isRef===!0)}y("nextTick",()=>ne);y("dispatch",e=>G.bind(G,e));y("watch",(e,{evaluateLater:t,cleanup:r})=>(n,i)=>{let o=t(n),a=ve(()=>{let c;return o(l=>c=l),c},i);r(a)});y("store",Br);y("data",e=>Ce(e));y("root",e=>J(e));y("refs",e=>(e._x_refs_proxy||(e._x_refs_proxy=F(ji(e))),e._x_refs_proxy));function ji(e){let t=[];return z(e,r=>{r._x_refs&&t.push(r._x_refs)}),t}var Ft={};function Bt(e){return Ft[e]||(Ft[e]=0),++Ft[e]}function pn(e,t){return z(e,r=>{if(r._x_ids&&r._x_ids[t])return!0})}function mn(e,t){e._x_ids||(e._x_ids={}),e._x_ids[t]||(e._x_ids[t]=Bt(t))}y("id",(e,{cleanup:t})=>(r,n=null)=>{let i=`${r}${n?`-${n}`:""}`;return Fi(e,i,t,()=>{let o=pn(e,r),s=o?o._x_ids[r]:Bt(r);return n?`${r}-${s}-${n}`:`${r}-${s}`})});K((e,t)=>{e._x_id&&(t._x_id=e._x_id)});function Fi(e,t,r,n){if(e._x_id||(e._x_id={}),e._x_id[t])return e._x_id[t];let i=n();return e._x_id[t]=i,r(()=>{delete e._x_id[t]}),i}y("el",e=>e);_n("Focus","focus","focus");_n("Persist","persist","persist");function _n(e,t,r){y(t,n=>E(`You can't use [$${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}d("modelable",(e,{expression:t},{effect:r,evaluateLater:n,cleanup:i})=>{let o=n(t),s=()=>{let u;return o(p=>u=p),u},a=n(`${t} = __placeholder`),c=u=>a(()=>{},{scope:{__placeholder:u}}),l=s();c(l),queueMicrotask(()=>{if(!e._x_model)return;e._x_removeModelListeners.default();let u=e._x_model.get,p=e._x_model.set,m=He({get(){return u()},set(w){p(w)}},{get(){return s()},set(w){c(w)}});i(m)})});d("teleport",(e,{modifiers:t,expression:r},{cleanup:n})=>{e.tagName.toLowerCase()!=="template"&&E("x-teleport can only be used on a <template> tag",e);let i=hn(r),o=e.content.cloneNode(!0).firstElementChild;e._x_teleport=o,o._x_teleportBack=e,e.setAttribute("data-teleport-template",!0),o.setAttribute("data-teleport-target",!0),e._x_forwardEvents&&e._x_forwardEvents.forEach(a=>{o.addEventListener(a,c=>{c.stopPropagation(),e.dispatchEvent(new c.constructor(c.type,c))})}),P(o,{},e);let s=(a,c,l)=>{l.includes("prepend")?c.parentNode.insertBefore(a,c):l.includes("append")?c.parentNode.insertBefore(a,c.nextSibling):c.appendChild(a)};_(()=>{s(o,i,t),A(()=>{S(o),o._x_ignore=!0})()}),e._x_teleportPutBack=()=>{let a=hn(r);_(()=>{s(e._x_teleport,a,t)})},n(()=>o.remove())});var Bi=document.createElement("div");function hn(e){let t=A(()=>document.querySelector(e),()=>Bi)();return t||E(`Cannot find x-teleport element for selector: "${e}"`),t}var gn=()=>{};gn.inline=(e,{modifiers:t},{cleanup:r})=>{t.includes("self")?e._x_ignoreSelf=!0:e._x_ignore=!0,r(()=>{t.includes("self")?delete e._x_ignoreSelf:delete e._x_ignore})};d("ignore",gn);d("effect",A((e,{expression:t},{effect:r})=>{r(x(e,t))}));function se(e,t,r,n){let i=e,o=c=>n(c),s={},a=(c,l)=>u=>l(c,u);if(r.includes("dot")&&(t=zi(t)),r.includes("camel")&&(t=Ki(t)),r.includes("passive")&&(s.passive=!0),r.includes("capture")&&(s.capture=!0),r.includes("window")&&(i=window),r.includes("document")&&(i=document),r.includes("debounce")){let c=r[r.indexOf("debounce")+1]||"invalid-wait",l=et(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=ze(o,l)}if(r.includes("throttle")){let c=r[r.indexOf("throttle")+1]||"invalid-wait",l=et(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=Ke(o,l)}return r.includes("prevent")&&(o=a(o,(c,l)=>{l.preventDefault(),c(l)})),r.includes("stop")&&(o=a(o,(c,l)=>{l.stopPropagation(),c(l)})),r.includes("once")&&(o=a(o,(c,l)=>{c(l),i.removeEventListener(t,o,s)})),(r.includes("away")||r.includes("outside"))&&(i=document,o=a(o,(c,l)=>{e.contains(l.target)||l.target.isConnected!==!1&&(e.offsetWidth<1&&e.offsetHeight<1||e._x_isShown!==!1&&c(l))})),r.includes("self")&&(o=a(o,(c,l)=>{l.target===e&&c(l)})),(Vi(t)||yn(t))&&(o=a(o,(c,l)=>{qi(l,r)||c(l)})),i.addEventListener(t,o,s),()=>{i.removeEventListener(t,o,s)}}function zi(e){return e.replace(/-/g,".")}function Ki(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function et(e){return!Array.isArray(e)&&!isNaN(e)}function Hi(e){return[" ","_"].includes(e)?e:e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[_\s]/,"-").toLowerCase()}function Vi(e){return["keydown","keyup"].includes(e)}function yn(e){return["contextmenu","click","mouse"].some(t=>e.includes(t))}function qi(e,t){let r=t.filter(o=>!["window","document","prevent","stop","once","capture","self","away","outside","passive"].includes(o));if(r.includes("debounce")){let o=r.indexOf("debounce");r.splice(o,et((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.includes("throttle")){let o=r.indexOf("throttle");r.splice(o,et((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.length===0||r.length===1&&xn(e.key).includes(r[0]))return!1;let i=["ctrl","shift","alt","meta","cmd","super"].filter(o=>r.includes(o));return r=r.filter(o=>!i.includes(o)),!(i.length>0&&i.filter(s=>((s==="cmd"||s==="super")&&(s="meta"),e[`${s}Key`])).length===i.length&&(yn(e.type)||xn(e.key).includes(r[0])))}function xn(e){if(!e)return[];e=Hi(e);let t={ctrl:"control",slash:"/",space:" ",spacebar:" ",cmd:"meta",esc:"escape",up:"arrow-up",down:"arrow-down",left:"arrow-left",right:"arrow-right",period:".",comma:",",equal:"=",minus:"-",underscore:"_"};return t[e]=e,Object.keys(t).map(r=>{if(t[r]===e)return r}).filter(r=>r)}d("model",(e,{modifiers:t,expression:r},{effect:n,cleanup:i})=>{let o=e;t.includes("parent")&&(o=e.parentNode);let s=x(o,r),a;typeof r=="string"?a=x(o,`${r} = __placeholder`):typeof r=="function"&&typeof r()=="string"?a=x(o,`${r()} = __placeholder`):a=()=>{};let c=()=>{let m;return s(w=>m=w),bn(m)?m.get():m},l=m=>{let w;s($=>w=$),bn(w)?w.set(m):a(()=>{},{scope:{__placeholder:m}})};typeof r=="string"&&e.type==="radio"&&_(()=>{e.hasAttribute("name")||e.setAttribute("name",r)});var u=e.tagName.toLowerCase()==="select"||["checkbox","radio"].includes(e.type)||t.includes("lazy")?"change":"input";let p=I?()=>{}:se(e,u,t,m=>{l(zt(e,t,m,c()))});if(t.includes("fill")&&([void 0,null,""].includes(c())||e.type==="checkbox"&&Array.isArray(c())||e.tagName.toLowerCase()==="select"&&e.multiple)&&l(zt(e,t,{target:e},c())),e._x_removeModelListeners||(e._x_removeModelListeners={}),e._x_removeModelListeners.default=p,i(()=>e._x_removeModelListeners.default()),e.form){let m=se(e.form,"reset",[],w=>{ne(()=>e._x_model&&e._x_model.set(zt(e,t,{target:e},c())))});i(()=>m())}e._x_model={get(){return c()},set(m){l(m)}},e._x_forceModelUpdate=m=>{m===void 0&&typeof r=="string"&&r.match(/\./)&&(m=""),window.fromModel=!0,_(()=>he(e,"value",m)),delete window.fromModel},n(()=>{let m=c();t.includes("unintrusive")&&document.activeElement.isSameNode(e)||e._x_forceModelUpdate(m)})});function zt(e,t,r,n){return _(()=>{if(r instanceof CustomEvent&&r.detail!==void 0)return r.detail!==null&&r.detail!==void 0?r.detail:r.target.value;if(e.type==="checkbox")if(Array.isArray(n)){let i=null;return t.includes("number")?i=Kt(r.target.value):t.includes("boolean")?i=ge(r.target.value):i=r.target.value,r.target.checked?n.includes(i)?n:n.concat([i]):n.filter(o=>!Ui(o,i))}else return r.target.checked;else{if(e.tagName.toLowerCase()==="select"&&e.multiple)return t.includes("number")?Array.from(r.target.selectedOptions).map(i=>{let o=i.value||i.text;return Kt(o)}):t.includes("boolean")?Array.from(r.target.selectedOptions).map(i=>{let o=i.value||i.text;return ge(o)}):Array.from(r.target.selectedOptions).map(i=>i.value||i.text);{let i;return e.type==="radio"?r.target.checked?i=r.target.value:i=n:i=r.target.value,t.includes("number")?Kt(i):t.includes("boolean")?ge(i):t.includes("trim")?i.trim():i}}})}function Kt(e){let t=e?parseFloat(e):null;return Wi(t)?t:e}function Ui(e,t){return e==t}function Wi(e){return!Array.isArray(e)&&!isNaN(e)}function bn(e){return e!==null&&typeof e=="object"&&typeof e.get=="function"&&typeof e.set=="function"}d("cloak",e=>queueMicrotask(()=>_(()=>e.removeAttribute(C("cloak")))));$e(()=>`[${C("init")}]`);d("init",A((e,{expression:t},{evaluate:r})=>typeof t=="string"?!!t.trim()&&r(t,{},!1):r(t,{},!1)));d("text",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{_(()=>{e.textContent=o})})})});d("html",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{_(()=>{e.innerHTML=o,e._x_ignoreSelf=!0,S(e),delete e._x_ignoreSelf})})})});re(Ie(":",ke(C("bind:"))));var wn=(e,{value:t,modifiers:r,expression:n,original:i},{effect:o,cleanup:s})=>{if(!t){let c={};Hr(c),x(e,n)(u=>{Ct(e,u,i)},{scope:c});return}if(t==="key")return Gi(e,n);if(e._x_inlineBindings&&e._x_inlineBindings[t]&&e._x_inlineBindings[t].extract)return;let a=x(e,n);o(()=>a(c=>{c===void 0&&typeof n=="string"&&n.match(/\./)&&(c=""),_(()=>he(e,t,c,r))})),s(()=>{e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_undoAddedStyles&&e._x_undoAddedStyles()})};wn.inline=(e,{value:t,modifiers:r,expression:n})=>{t&&(e._x_inlineBindings||(e._x_inlineBindings={}),e._x_inlineBindings[t]={expression:n,extract:!1})};d("bind",wn);function Gi(e,t){e._x_keyExpression=t}Le(()=>`[${C("data")}]`);d("data",(e,{expression:t},{cleanup:r})=>{if(Ji(e))return;t=t===""?"{}":t;let n={};ue(n,e);let i={};Ur(i,n);let o=M(e,t,{scope:i});(o===void 0||o===!0)&&(o={}),ue(o,e);let s=R(o);Te(s);let a=P(e,s);s.init&&M(e,s.init),r(()=>{s.destroy&&M(e,s.destroy),a()})});K((e,t)=>{e._x_dataStack&&(t._x_dataStack=e._x_dataStack,t.setAttribute("data-has-alpine-state",!0))});function Ji(e){return I?Be?!0:e.hasAttribute("data-has-alpine-state"):!1}d("show",(e,{modifiers:t,expression:r},{effect:n})=>{let i=x(e,r);e._x_doHide||(e._x_doHide=()=>{_(()=>{e.style.setProperty("display","none",t.includes("important")?"important":void 0)})}),e._x_doShow||(e._x_doShow=()=>{_(()=>{e.style.length===1&&e.style.display==="none"?e.removeAttribute("style"):e.style.removeProperty("display")})});let o=()=>{e._x_doHide(),e._x_isShown=!1},s=()=>{e._x_doShow(),e._x_isShown=!0},a=()=>setTimeout(s),c=me(p=>p?s():o(),p=>{typeof e._x_toggleAndCascadeWithTransitions=="function"?e._x_toggleAndCascadeWithTransitions(e,p,s,o):p?a():o()}),l,u=!0;n(()=>i(p=>{!u&&p===l||(t.includes("immediate")&&(p?a():o()),c(p),l=p,u=!1)}))});d("for",(e,{expression:t},{effect:r,cleanup:n})=>{let i=Xi(t),o=x(e,i.items),s=x(e,e._x_keyExpression||"index");e._x_prevKeys=[],e._x_lookup={},r(()=>Yi(e,i,o,s)),n(()=>{Object.values(e._x_lookup).forEach(a=>a.remove()),delete e._x_prevKeys,delete e._x_lookup})});function Yi(e,t,r,n){let i=s=>typeof s=="object"&&!Array.isArray(s),o=e;r(s=>{Zi(s)&&s>=0&&(s=Array.from(Array(s).keys(),f=>f+1)),s===void 0&&(s=[]);let a=e._x_lookup,c=e._x_prevKeys,l=[],u=[];if(i(s))s=Object.entries(s).map(([f,g])=>{let b=En(t,g,f,s);n(v=>{u.includes(v)&&E("Duplicate key on x-for",e),u.push(v)},{scope:{index:f,...b}}),l.push(b)});else for(let f=0;f<s.length;f++){let g=En(t,s[f],f,s);n(b=>{u.includes(b)&&E("Duplicate key on x-for",e),u.push(b)},{scope:{index:f,...g}}),l.push(g)}let p=[],m=[],w=[],$=[];for(let f=0;f<c.length;f++){let g=c[f];u.indexOf(g)===-1&&w.push(g)}c=c.filter(f=>!w.includes(f));let we="template";for(let f=0;f<u.length;f++){let g=u[f],b=c.indexOf(g);if(b===-1)c.splice(f,0,g),p.push([we,f]);else if(b!==f){let v=c.splice(f,1)[0],O=c.splice(b-1,1)[0];c.splice(f,0,O),c.splice(b,0,v),m.push([v,O])}else $.push(g);we=g}for(let f=0;f<w.length;f++){let g=w[f];a[g]._x_effects&&a[g]._x_effects.forEach(Ee),a[g].remove(),a[g]=null,delete a[g]}for(let f=0;f<m.length;f++){let[g,b]=m[f],v=a[g],O=a[b],Q=document.createElement("div");_(()=>{O||E('x-for ":key" is undefined or invalid',o,b,a),O.after(Q),v.after(O),O._x_currentIfEl&&O.after(O._x_currentIfEl),Q.before(v),v._x_currentIfEl&&v.after(v._x_currentIfEl),Q.remove()}),O._x_refreshXForScope(l[u.indexOf(b)])}for(let f=0;f<p.length;f++){let[g,b]=p[f],v=g==="template"?o:a[g];v._x_currentIfEl&&(v=v._x_currentIfEl);let O=l[b],Q=u[b],ae=document.importNode(o.content,!0).firstElementChild,Vt=R(O);P(ae,Vt,o),ae._x_refreshXForScope=Sn=>{Object.entries(Sn).forEach(([An,On])=>{Vt[An]=On})},_(()=>{v.after(ae),A(()=>S(ae))()}),typeof Q=="object"&&E("x-for key cannot be an object, it must be a string or an integer",o),a[Q]=ae}for(let f=0;f<$.length;f++)a[$[f]]._x_refreshXForScope(l[u.indexOf($[f])]);o._x_prevKeys=u})}function Xi(e){let t=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,r=/^\s*\(|\)\s*$/g,n=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,i=e.match(n);if(!i)return;let o={};o.items=i[2].trim();let s=i[1].replace(r,"").trim(),a=s.match(t);return a?(o.item=s.replace(t,"").trim(),o.index=a[1].trim(),a[2]&&(o.collection=a[2].trim())):o.item=s,o}function En(e,t,r,n){let i={};return/^\[.*\]$/.test(e.item)&&Array.isArray(t)?e.item.replace("[","").replace("]","").split(",").map(s=>s.trim()).forEach((s,a)=>{i[s]=t[a]}):/^\{.*\}$/.test(e.item)&&!Array.isArray(t)&&typeof t=="object"?e.item.replace("{","").replace("}","").split(",").map(s=>s.trim()).forEach(s=>{i[s]=t[s]}):i[e.item]=t,e.index&&(i[e.index]=r),e.collection&&(i[e.collection]=n),i}function Zi(e){return!Array.isArray(e)&&!isNaN(e)}function vn(){}vn.inline=(e,{expression:t},{cleanup:r})=>{let n=J(e);n._x_refs||(n._x_refs={}),n._x_refs[t]=e,r(()=>delete n._x_refs[t])};d("ref",vn);d("if",(e,{expression:t},{effect:r,cleanup:n})=>{e.tagName.toLowerCase()!=="template"&&E("x-if can only be used on a <template> tag",e);let i=x(e,t),o=()=>{if(e._x_currentIfEl)return e._x_currentIfEl;let a=e.content.cloneNode(!0).firstElementChild;return P(a,{},e),_(()=>{e.after(a),A(()=>S(a))()}),e._x_currentIfEl=a,e._x_undoIf=()=>{T(a,c=>{c._x_effects&&c._x_effects.forEach(Ee)}),a.remove(),delete e._x_currentIfEl},a},s=()=>{e._x_undoIf&&(e._x_undoIf(),delete e._x_undoIf)};r(()=>i(a=>{a?o():s()})),n(()=>e._x_undoIf&&e._x_undoIf())});d("id",(e,{expression:t},{evaluate:r})=>{r(t).forEach(i=>mn(e,i))});K((e,t)=>{e._x_ids&&(t._x_ids=e._x_ids)});re(Ie("@",ke(C("on:"))));d("on",A((e,{value:t,modifiers:r,expression:n},{cleanup:i})=>{let o=n?x(e,n):()=>{};e.tagName.toLowerCase()==="template"&&(e._x_forwardEvents||(e._x_forwardEvents=[]),e._x_forwardEvents.includes(t)||e._x_forwardEvents.push(t));let s=se(e,t,r,a=>{o(()=>{},{scope:{$event:a},params:[a]})});i(()=>s())}));tt("Collapse","collapse","collapse");tt("Intersect","intersect","intersect");tt("Focus","trap","focus");tt("Mask","mask","mask");function tt(e,t,r){d(t,n=>E(`You can't use [x-${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}B.setEvaluator(gt);B.setReactivityEngine({reactive:Qe,effect:en,release:tn,raw:h});var Ht=B;window.Alpine=Ht;queueMicrotask(()=>{Ht.start()});})();
diff --git a/build/lib/exo/tinychat/static/unpkg.com/dompurify@3.1.5/dist/purify.min.js b/build/lib/exo/tinychat/static/unpkg.com/dompurify@3.1.5/dist/purify.min.js
deleted file mode 100644
index ae1c0836..00000000
--- a/build/lib/exo/tinychat/static/unpkg.com/dompurify@3.1.5/dist/purify.min.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/*! @license DOMPurify 3.1.5 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.5/LICENSE */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=b(Array.prototype.forEach),m=b(Array.prototype.pop),p=b(Array.prototype.push),f=b(String.prototype.toLowerCase),d=b(String.prototype.toString),h=b(String.prototype.match),g=b(String.prototype.replace),T=b(String.prototype.indexOf),y=b(String.prototype.trim),E=b(Object.prototype.hasOwnProperty),_=b(RegExp.prototype.test),A=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return s(N,t)});var N;function b(e){return function(t){for(var n=arguments.length,o=new Array(n>1?n-1:0),r=1;r<n;r++)o[r-1]=arguments[r];return c(e,t,o)}}function S(e,o){let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function R(e){for(let t=0;t<e.length;t++){E(e,t)||(e[t]=null)}return e}function w(t){const n=l(null);for(const[o,r]of e(t)){E(t,o)&&(Array.isArray(r)?n[o]=R(r):r&&"object"==typeof r&&r.constructor===Object?n[o]=w(r):n[o]=r)}return n}function C(e,t){for(;null!==e;){const n=r(e,t);if(n){if(n.get)return b(n.get);if("function"==typeof n.value)return b(n.value)}e=o(e)}return function(){return null}}const L=i(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),D=i(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),v=i(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),O=i(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),x=i(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),k=i(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),M=i(["#text"]),I=i(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns","slot"]),U=i(["accent-height","accumulate","additive","alignment-baseline","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),P=i(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),F=i(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),H=a(/\{\{[\w\W]*|[\w\W]*\}\}/gm),z=a(/<%[\w\W]*|[\w\W]*%>/gm),B=a(/\${[\w\W]*}/gm),W=a(/^data-[\-\w.\u00B7-\uFFFF]/),G=a(/^aria-[\-\w]+$/),Y=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),X=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),q=a(/^html$/i),$=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var K=Object.freeze({__proto__:null,MUSTACHE_EXPR:H,ERB_EXPR:z,TMPLIT_EXPR:B,DATA_ATTR:W,ARIA_ATTR:G,IS_ALLOWED_URI:Y,IS_SCRIPT_OR_DATA:j,ATTR_WHITESPACE:X,DOCTYPE_NAME:q,CUSTOM_ELEMENT:$});const V=1,Z=3,J=7,Q=8,ee=9,te=function(){return"undefined"==typeof window?null:window},ne=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}};var oe=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:te();const o=e=>t(e);if(o.version="3.1.5",o.removed=[],!n||!n.document||n.document.nodeType!==ee)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:b,Element:R,NodeFilter:H,NamedNodeMap:z=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:B,DOMParser:W,trustedTypes:G}=n,j=R.prototype,X=C(j,"cloneNode"),$=C(j,"nextSibling"),oe=C(j,"childNodes"),re=C(j,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ie,ae="";const{implementation:le,createNodeIterator:ce,createDocumentFragment:se,getElementsByTagName:ue}=r,{importNode:me}=a;let pe={};o.isSupported="function"==typeof e&&"function"==typeof re&&le&&void 0!==le.createHTMLDocument;const{MUSTACHE_EXPR:fe,ERB_EXPR:de,TMPLIT_EXPR:he,DATA_ATTR:ge,ARIA_ATTR:Te,IS_SCRIPT_OR_DATA:ye,ATTR_WHITESPACE:Ee,CUSTOM_ELEMENT:_e}=K;let{IS_ALLOWED_URI:Ae}=K,Ne=null;const be=S({},[...L,...D,...v,...x,...M]);let Se=null;const Re=S({},[...I,...U,...P,...F]);let we=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Ce=null,Le=null,De=!0,ve=!0,Oe=!1,xe=!0,ke=!1,Me=!0,Ie=!1,Ue=!1,Pe=!1,Fe=!1,He=!1,ze=!1,Be=!0,We=!1;const Ge="user-content-";let Ye=!0,je=!1,Xe={},qe=null;const $e=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ke=null;const Ve=S({},["audio","video","img","source","image","track"]);let Ze=null;const Je=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Qe="http://www.w3.org/1998/Math/MathML",et="http://www.w3.org/2000/svg",tt="http://www.w3.org/1999/xhtml";let nt=tt,ot=!1,rt=null;const it=S({},[Qe,et,tt],d);let at=null;const lt=["application/xhtml+xml","text/html"],ct="text/html";let st=null,ut=null;const mt=r.createElement("form"),pt=function(e){return e instanceof RegExp||e instanceof Function},ft=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ut||ut!==e){if(e&&"object"==typeof e||(e={}),e=w(e),at=-1===lt.indexOf(e.PARSER_MEDIA_TYPE)?ct:e.PARSER_MEDIA_TYPE,st="application/xhtml+xml"===at?d:f,Ne=E(e,"ALLOWED_TAGS")?S({},e.ALLOWED_TAGS,st):be,Se=E(e,"ALLOWED_ATTR")?S({},e.ALLOWED_ATTR,st):Re,rt=E(e,"ALLOWED_NAMESPACES")?S({},e.ALLOWED_NAMESPACES,d):it,Ze=E(e,"ADD_URI_SAFE_ATTR")?S(w(Je),e.ADD_URI_SAFE_ATTR,st):Je,Ke=E(e,"ADD_DATA_URI_TAGS")?S(w(Ve),e.ADD_DATA_URI_TAGS,st):Ve,qe=E(e,"FORBID_CONTENTS")?S({},e.FORBID_CONTENTS,st):$e,Ce=E(e,"FORBID_TAGS")?S({},e.FORBID_TAGS,st):{},Le=E(e,"FORBID_ATTR")?S({},e.FORBID_ATTR,st):{},Xe=!!E(e,"USE_PROFILES")&&e.USE_PROFILES,De=!1!==e.ALLOW_ARIA_ATTR,ve=!1!==e.ALLOW_DATA_ATTR,Oe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,xe=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,ke=e.SAFE_FOR_TEMPLATES||!1,Me=!1!==e.SAFE_FOR_XML,Ie=e.WHOLE_DOCUMENT||!1,Fe=e.RETURN_DOM||!1,He=e.RETURN_DOM_FRAGMENT||!1,ze=e.RETURN_TRUSTED_TYPE||!1,Pe=e.FORCE_BODY||!1,Be=!1!==e.SANITIZE_DOM,We=e.SANITIZE_NAMED_PROPS||!1,Ye=!1!==e.KEEP_CONTENT,je=e.IN_PLACE||!1,Ae=e.ALLOWED_URI_REGEXP||Y,nt=e.NAMESPACE||tt,we=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&pt(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(we.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&pt(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(we.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(we.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),ke&&(ve=!1),He&&(Fe=!0),Xe&&(Ne=S({},M),Se=[],!0===Xe.html&&(S(Ne,L),S(Se,I)),!0===Xe.svg&&(S(Ne,D),S(Se,U),S(Se,F)),!0===Xe.svgFilters&&(S(Ne,v),S(Se,U),S(Se,F)),!0===Xe.mathMl&&(S(Ne,x),S(Se,P),S(Se,F))),e.ADD_TAGS&&(Ne===be&&(Ne=w(Ne)),S(Ne,e.ADD_TAGS,st)),e.ADD_ATTR&&(Se===Re&&(Se=w(Se)),S(Se,e.ADD_ATTR,st)),e.ADD_URI_SAFE_ATTR&&S(Ze,e.ADD_URI_SAFE_ATTR,st),e.FORBID_CONTENTS&&(qe===$e&&(qe=w(qe)),S(qe,e.FORBID_CONTENTS,st)),Ye&&(Ne["#text"]=!0),Ie&&S(Ne,["html","head","body"]),Ne.table&&(S(Ne,["tbody"]),delete Ce.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ie=e.TRUSTED_TYPES_POLICY,ae=ie.createHTML("")}else void 0===ie&&(ie=ne(G,c)),null!==ie&&"string"==typeof ae&&(ae=ie.createHTML(""));i&&i(e),ut=e}},dt=S({},["mi","mo","mn","ms","mtext"]),ht=S({},["foreignobject","annotation-xml"]),gt=S({},["title","style","font","a","script"]),Tt=S({},[...D,...v,...O]),yt=S({},[...x,...k]),Et=function(e){let t=re(e);t&&t.tagName||(t={namespaceURI:nt,tagName:"template"});const n=f(e.tagName),o=f(t.tagName);return!!rt[e.namespaceURI]&&(e.namespaceURI===et?t.namespaceURI===tt?"svg"===n:t.namespaceURI===Qe?"svg"===n&&("annotation-xml"===o||dt[o]):Boolean(Tt[n]):e.namespaceURI===Qe?t.namespaceURI===tt?"math"===n:t.namespaceURI===et?"math"===n&&ht[o]:Boolean(yt[n]):e.namespaceURI===tt?!(t.namespaceURI===et&&!ht[o])&&(!(t.namespaceURI===Qe&&!dt[o])&&(!yt[n]&&(gt[n]||!Tt[n]))):!("application/xhtml+xml"!==at||!rt[e.namespaceURI]))},_t=function(e){p(o.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){e.remove()}},At=function(e,t){try{p(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Se[e])if(Fe||He)try{_t(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Nt=function(e){let t=null,n=null;if(Pe)e="<remove></remove>"+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===at&&nt===tt&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");const o=ie?ie.createHTML(e):e;if(nt===tt)try{t=(new W).parseFromString(o,at)}catch(e){}if(!t||!t.documentElement){t=le.createDocument(nt,"template",null);try{t.documentElement.innerHTML=ot?ae:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),nt===tt?ue.call(t,Ie?"html":"body")[0]:Ie?t.documentElement:i},bt=function(e){return ce.call(e.ownerDocument||e,e,H.SHOW_ELEMENT|H.SHOW_COMMENT|H.SHOW_TEXT|H.SHOW_PROCESSING_INSTRUCTION|H.SHOW_CDATA_SECTION,null)},St=function(e){return e instanceof B&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof z)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Rt=function(e){return"function"==typeof b&&e instanceof b},wt=function(e,t,n){pe[e]&&u(pe[e],(e=>{e.call(o,t,n,ut)}))},Ct=function(e){let t=null;if(wt("beforeSanitizeElements",e,null),St(e))return _t(e),!0;const n=st(e.nodeName);if(wt("uponSanitizeElement",e,{tagName:n,allowedTags:Ne}),e.hasChildNodes()&&!Rt(e.firstElementChild)&&_(/<[/\w]/g,e.innerHTML)&&_(/<[/\w]/g,e.textContent))return _t(e),!0;if(e.nodeType===J)return _t(e),!0;if(Me&&e.nodeType===Q&&_(/<[/\w]/g,e.data))return _t(e),!0;if(!Ne[n]||Ce[n]){if(!Ce[n]&&Dt(n)){if(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,n))return!1;if(we.tagNameCheck instanceof Function&&we.tagNameCheck(n))return!1}if(Ye&&!qe[n]){const t=re(e)||e.parentNode,n=oe(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=X(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,$(e))}}}return _t(e),!0}return e instanceof R&&!Et(e)?(_t(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!_(/<\/no(script|embed|frames)/i,e.innerHTML)?(ke&&e.nodeType===Z&&(t=e.textContent,u([fe,de,he],(e=>{t=g(t,e," ")})),e.textContent!==t&&(p(o.removed,{element:e.cloneNode()}),e.textContent=t)),wt("afterSanitizeElements",e,null),!1):(_t(e),!0)},Lt=function(e,t,n){if(Be&&("id"===t||"name"===t)&&(n in r||n in mt))return!1;if(ve&&!Le[t]&&_(ge,t));else if(De&&_(Te,t));else if(!Se[t]||Le[t]){if(!(Dt(e)&&(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,e)||we.tagNameCheck instanceof Function&&we.tagNameCheck(e))&&(we.attributeNameCheck instanceof RegExp&&_(we.attributeNameCheck,t)||we.attributeNameCheck instanceof Function&&we.attributeNameCheck(t))||"is"===t&&we.allowCustomizedBuiltInElements&&(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,n)||we.tagNameCheck instanceof Function&&we.tagNameCheck(n))))return!1}else if(Ze[t]);else if(_(Ae,g(n,Ee,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==T(n,"data:")||!Ke[e]){if(Oe&&!_(ye,g(n,Ee,"")));else if(n)return!1}else;return!0},Dt=function(e){return"annotation-xml"!==e&&h(e,_e)},vt=function(e){wt("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Se};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=st(a);let p="value"===a?c:y(c);if(n.attrName=s,n.attrValue=p,n.keepAttr=!0,n.forceKeepAttr=void 0,wt("uponSanitizeAttribute",e,n),p=n.attrValue,n.forceKeepAttr)continue;if(At(a,e),!n.keepAttr)continue;if(!xe&&_(/\/>/i,p)){At(a,e);continue}if(Me&&_(/((--!?|])>)|<\/(style|title)/i,p)){At(a,e);continue}ke&&u([fe,de,he],(e=>{p=g(p,e," ")}));const f=st(e.nodeName);if(Lt(f,s,p)){if(!We||"id"!==s&&"name"!==s||(At(a,e),p=Ge+p),ie&&"object"==typeof G&&"function"==typeof G.getAttributeType)if(l);else switch(G.getAttributeType(f,s)){case"TrustedHTML":p=ie.createHTML(p);break;case"TrustedScriptURL":p=ie.createScriptURL(p)}try{l?e.setAttributeNS(l,a,p):e.setAttribute(a,p),St(e)?_t(e):m(o.removed)}catch(e){}}}wt("afterSanitizeAttributes",e,null)},Ot=function e(t){let n=null;const o=bt(t);for(wt("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)wt("uponSanitizeShadowNode",n,null),Ct(n)||(n.content instanceof s&&e(n.content),vt(n));wt("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(ot=!e,ot&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Rt(e)){if("function"!=typeof e.toString)throw A("toString is not a function");if("string"!=typeof(e=e.toString()))throw A("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Ue||ft(t),o.removed=[],"string"==typeof e&&(je=!1),je){if(e.nodeName){const t=st(e.nodeName);if(!Ne[t]||Ce[t])throw A("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof b)n=Nt("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===V&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!Fe&&!ke&&!Ie&&-1===e.indexOf("<"))return ie&&ze?ie.createHTML(e):e;if(n=Nt(e),!n)return Fe?null:ze?ae:""}n&&Pe&&_t(n.firstChild);const c=bt(je?e:n);for(;i=c.nextNode();)Ct(i)||(i.content instanceof s&&Ot(i.content),vt(i));if(je)return e;if(Fe){if(He)for(l=se.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Se.shadowroot||Se.shadowrootmode)&&(l=me.call(a,l,!0)),l}let m=Ie?n.outerHTML:n.innerHTML;return Ie&&Ne["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&_(q,n.ownerDocument.doctype.name)&&(m="<!DOCTYPE "+n.ownerDocument.doctype.name+">\n"+m),ke&&u([fe,de,he],(e=>{m=g(m,e," ")})),ie&&ze?ie.createHTML(m):m},o.setConfig=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};ft(e),Ue=!0},o.clearConfig=function(){ut=null,Ue=!1},o.isValidAttribute=function(e,t,n){ut||ft({});const o=st(e),r=st(t);return Lt(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&(pe[e]=pe[e]||[],p(pe[e],t))},o.removeHook=function(e){if(pe[e])return m(pe[e])},o.removeHooks=function(e){pe[e]&&(pe[e]=[])},o.removeAllHooks=function(){pe={}},o}();return oe}));
-//# sourceMappingURL=purify.min.js.map
diff --git a/build/lib/exo/tinychat/static/unpkg.com/marked-highlight@2.1.2/lib/index.umd.js b/build/lib/exo/tinychat/static/unpkg.com/marked-highlight@2.1.2/lib/index.umd.js
deleted file mode 100644
index d7af7950..00000000
--- a/build/lib/exo/tinychat/static/unpkg.com/marked-highlight@2.1.2/lib/index.umd.js
+++ /dev/null
@@ -1,97 +0,0 @@
-(function (global, factory) {
-  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
-  typeof define === 'function' && define.amd ? define(['exports'], factory) :
-  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.markedHighlight = {}));
-})(this, (function (exports) { 'use strict';
-
-  function markedHighlight(options) {
-    if (typeof options === 'function') {
-      options = {
-        highlight: options
-      };
-    }
-
-    if (!options || typeof options.highlight !== 'function') {
-      throw new Error('Must provide highlight function');
-    }
-
-    if (typeof options.langPrefix !== 'string') {
-      options.langPrefix = 'language-';
-    }
-
-    return {
-      async: !!options.async,
-      walkTokens(token) {
-        if (token.type !== 'code') {
-          return;
-        }
-
-        const lang = getLang(token.lang);
-
-        if (options.async) {
-          return Promise.resolve(options.highlight(token.text, lang, token.lang || '')).then(updateToken(token));
-        }
-
-        const code = options.highlight(token.text, lang, token.lang || '');
-        if (code instanceof Promise) {
-          throw new Error('markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.');
-        }
-        updateToken(token)(code);
-      },
-      useNewRenderer: true,
-      renderer: {
-        code({ text, lang, escaped }) {
-          const language = getLang(lang);
-          const classAttr = language
-            ? ` class="${options.langPrefix}${escape(language)}"`
-            : '';
-          text = text.replace(/\n$/, '');
-          return `<pre><code${classAttr}>${escaped ? text : escape(text, true)}\n</code></pre>`;
-        }
-      }
-    };
-  }
-
-  function getLang(lang) {
-    return (lang || '').match(/\S*/)[0];
-  }
-
-  function updateToken(token) {
-    return (code) => {
-      if (typeof code === 'string' && code !== token.text) {
-        token.escaped = true;
-        token.text = code;
-      }
-    };
-  }
-
-  // copied from marked helpers
-  const escapeTest = /[&<>"']/;
-  const escapeReplace = new RegExp(escapeTest.source, 'g');
-  const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
-  const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g');
-  const escapeReplacements = {
-    '&': '&amp;',
-    '<': '&lt;',
-    '>': '&gt;',
-    '"': '&quot;',
-    "'": '&#39;'
-  };
-  const getEscapeReplacement = (ch) => escapeReplacements[ch];
-  function escape(html, encode) {
-    if (encode) {
-      if (escapeTest.test(html)) {
-        return html.replace(escapeReplace, getEscapeReplacement);
-      }
-    } else {
-      if (escapeTestNoEncode.test(html)) {
-        return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
-      }
-    }
-
-    return html;
-  }
-
-  exports.markedHighlight = markedHighlight;
-
-}));
diff --git a/build/lib/exo/tinychat/static/unpkg.com/marked@13.0.0/marked.min.js b/build/lib/exo/tinychat/static/unpkg.com/marked@13.0.0/marked.min.js
deleted file mode 100644
index 774998f7..00000000
--- a/build/lib/exo/tinychat/static/unpkg.com/marked@13.0.0/marked.min.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * marked v13.0.0 - a markdown parser
- * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed)
- * https://github.com/markedjs/marked
- */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;const p=/(^|[^\[])\^/g;function u(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(p,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function k(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const g={exec:()=>null};function f(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length<t;)n.push("");for(;s<n.length;s++)n[s]=n[s].trim().replace(/\\\|/g,"|");return n}function d(e,t,n){const s=e.length;if(0===s)return"";let r=0;for(;r<s;){const i=e.charAt(s-r-1);if(i!==t||n){if(i===t||!n)break;r++}else r++}return e.slice(0,s-r)}function x(e,t,n,s){const r=t.href,i=t.title?c(t.title):null,l=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){s.state.inLink=!0;const e={type:"link",raw:n,href:r,title:i,text:l,tokens:s.inlineTokens(l)};return s.state.inLink=!1,e}return{type:"image",raw:n,href:r,title:i,text:c(l)}}class b{options;rules;lexer;constructor(t){this.options=t||e.defaults}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:d(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=d(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:d(t[0],"\n")}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=d(t[0],"\n").split("\n"),n="",s="";const r=[];for(;e.length>0;){let t=!1;const i=[];let l;for(l=0;l<e.length;l++)if(/^ {0,3}>/.test(e[l]))i.push(e[l]),t=!0;else{if(t)break;i.push(e[l])}e=e.slice(l);const o=i.join("\n"),a=o.replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,"\n    $1").replace(/^ {0,3}>[ \t]?/gm,"");n=n?`${n}\n${o}`:o,s=s?`${s}\n${a}`:a;const c=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(a,r,!0),this.lexer.state.top=c,0===e.length)break;const h=r[r.length-1];if("code"===h?.type)break;if("blockquote"===h?.type){const t=h,i=t.raw+"\n"+e.join("\n"),l=this.blockquote(i);r[r.length-1]=l,n=n.substring(0,n.length-t.raw.length)+l.raw,s=s.substring(0,s.length-t.text.length)+l.text;break}if("list"!==h?.type);else{const t=h,i=t.raw+"\n"+e.join("\n"),l=this.list(i);r[r.length-1]=l,n=n.substring(0,n.length-h.raw.length)+l.raw,s=s.substring(0,s.length-t.raw.length)+l.raw,e=i.substring(r[r.length-1].raw.length).split("\n")}}return{type:"blockquote",raw:n,tokens:r,text:s}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g,"  ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(a.length+1),s=c.slice(h)}}r.loose||(a?r.loose=!0:/\n *\n *$/.test(l)&&(a=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:l,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=l}r.items[r.items.length-1].raw=l.trimEnd(),r.items[r.items.length-1].text=o.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e<r.items.length;e++)if(this.lexer.state.top=!1,r.items[e].tokens=this.lexer.blockTokens(r.items[e].text,[]),!r.loose){const t=r.items[e].tokens.filter((e=>"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e<r.items.length;e++)r.items[e].loose=!0;return r}}html(e){const t=this.rules.block.html.exec(e);if(t){return{type:"html",block:!0,raw:t[0],pre:"pre"===t[1]||"script"===t[1]||"style"===t[1],text:t[0]}}}def(e){const t=this.rules.block.def.exec(e);if(t){const e=t[1].toLowerCase().replace(/\s+/g," "),n=t[2]?t[2].replace(/^<(.*)>$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=f(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(let e=0;e<n.length;e++)i.header.push({text:n[e],tokens:this.lexer.inline(n[e]),header:!0,align:i.align[e]});for(const e of r)i.rows.push(f(e,i.header.length).map(((e,t)=>({text:e,tokens:this.lexer.inline(e),header:!1,align:i.align[t]}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^<a /i.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&/^<\/a>/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^</.test(e)){if(!/>$/.test(e))return;const t=d(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s<e.length;s++)if("\\"===e[s])s++;else if(e[s]===t[0])n++;else if(e[s]===t[1]&&(n--,n<0))return s;return-1}(t[2],"()");if(e>-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^</.test(n)&&(n=this.options.pedantic&&!/>$/.test(e)?n.slice(1):n.slice(1,-1)),x(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return x(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const w=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,m=/(?:[*+-]|\d{1,9}[.)])/,y=u(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,m).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),$=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,z=/(?!\s*\])(?:\\.|[^\[\]\\])+/,T=u(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",z).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),R=u(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,m).getRegex(),_="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",A=/<!--(?:-?>|[\s\S]*?(?:-->|$))/,S=u("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",A).replace("tag",_).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),I=u($).replace("hr",w).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",_).getRegex(),E={blockquote:u(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",I).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:T,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:w,html:S,lheading:y,list:R,newline:/^(?: *(?:\n|$))+/,paragraph:I,table:g,text:/^[^\n]+/},q=u("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",w).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",_).getRegex(),Z={...E,table:q,paragraph:u($).replace("hr",w).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",q).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",_).getRegex()},L={...E,html:u("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:\"[^\"]*\"|'[^']*'|\\s[^'\"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",A).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:g,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:u($).replace("hr",w).replace("heading"," *#{1,6} *[^\n]").replace("lheading",y).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},P=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,v=/^( {2,}|\\)\n(?!\s*$)/,Q="\\p{P}\\p{S}",B=u(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,Q).getRegex(),C=u(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,Q).getRegex(),M=u("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,Q).getRegex(),O=u("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,Q).getRegex(),j=u(/\\([punct])/,"gu").replace(/punct/g,Q).getRegex(),D=u(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),H=u(A).replace("(?:--\x3e|$)","--\x3e").getRegex(),F=u("^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>").replace("comment",H).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),U=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,X=u(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",U).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),N=u(/^!?\[(label)\]\[(ref)\]/).replace("label",U).replace("ref",z).getRegex(),G=u(/^!?\[(ref)\](?:\[\])?/).replace("ref",z).getRegex(),J={_backpedal:g,anyPunctuation:j,autolink:D,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:v,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:g,emStrongLDelim:C,emStrongRDelimAst:M,emStrongRDelimUnd:O,escape:P,link:X,nolink:G,punctuation:B,reflink:N,reflinkSearch:u("reflink|nolink(?!\\()","g").replace("reflink",N).replace("nolink",G).getRegex(),tag:F,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,url:g},K={...J,link:u(/^!?\[(label)\]\((.*?)\)/).replace("label",U).getRegex(),reflink:u(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",U).getRegex()},V={...J,escape:u(P).replace("])","~|])").getRegex(),url:u(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/},W={...V,br:u(v).replace("{2,}","*").getRegex(),text:u(V.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()},Y={normal:E,gfm:Z,pedantic:L},ee={normal:J,gfm:V,breaks:W,pedantic:K};class te{tokens;options;state;tokenizer;inlineQueue;constructor(t){this.tokens=[],this.tokens.links=Object.create(null),this.options=t||e.defaults,this.options.tokenizer=this.options.tokenizer||new b,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};const n={block:Y.normal,inline:ee.normal};this.options.pedantic?(n.block=Y.pedantic,n.inline=ee.pedantic):this.options.gfm&&(n.block=Y.gfm,this.options.breaks?n.inline=ee.breaks:n.inline=ee.gfm),this.tokenizer.rules=n}static get rules(){return{block:Y,inline:ee}}static lex(e,t){return new te(t).lex(e)}static lexInline(e,t){return new te(t).inlineTokens(e)}lex(e){e=e.replace(/\r\n|\r/g,"\n"),this.blockTokens(e,this.tokens);for(let e=0;e<this.inlineQueue.length;e++){const t=this.inlineQueue[e];this.inlineTokens(t.src,t.tokens)}return this.inlineQueue=[],this.tokens}blockTokens(e,t=[],n=!1){let s,r,i;for(e=this.options.pedantic?e.replace(/\t/g,"    ").replace(/^ +$/gm,""):e.replace(/^( *)(\t+)/gm,((e,t,n)=>t+"    ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((n=>!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0)))))if(s=this.tokenizer.space(e))e=e.substring(s.raw.length),1===s.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(s);else if(s=this.tokenizer.code(e))e=e.substring(s.raw.length),r=t[t.length-1],!r||"paragraph"!==r.type&&"text"!==r.type?t.push(s):(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue[this.inlineQueue.length-1].src=r.text);else if(s=this.tokenizer.fences(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.heading(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.hr(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.blockquote(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.list(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.html(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.def(e))e=e.substring(s.raw.length),r=t[t.length-1],!r||"paragraph"!==r.type&&"text"!==r.type?this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title}):(r.raw+="\n"+s.raw,r.text+="\n"+s.raw,this.inlineQueue[this.inlineQueue.length-1].src=r.text);else if(s=this.tokenizer.table(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.lheading(e))e=e.substring(s.raw.length),t.push(s);else{if(i=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(i=e.substring(0,t+1))}if(this.state.top&&(s=this.tokenizer.paragraph(i)))r=t[t.length-1],n&&"paragraph"===r?.type?(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):t.push(s),n=i.length!==e.length,e=e.substring(s.raw.length);else if(s=this.tokenizer.text(e))e=e.substring(s.raw.length),r=t[t.length-1],r&&"text"===r.type?(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):t.push(s);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class ne{options;parser;constructor(t){this.options=t||e.defaults}space(e){return""}code({text:e,lang:t,escaped:n}){const s=(t||"").match(/^\S*/)?.[0],r=e.replace(/\n$/,"")+"\n";return s?'<pre><code class="language-'+c(s)+'">'+(n?r:c(r,!0))+"</code></pre>\n":"<pre><code>"+(n?r:c(r,!0))+"</code></pre>\n"}blockquote({tokens:e}){return`<blockquote>\n${this.parser.parse(e)}</blockquote>\n`}html({text:e}){return e}heading({tokens:e,depth:t}){return`<h${t}>${this.parser.parseInline(e)}</h${t}>\n`}hr(e){return"<hr>\n"}list(e){const t=e.ordered,n=e.start;let s="";for(let t=0;t<e.items.length;t++){const n=e.items[t];s+=this.listitem(n)}const r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+s+"</"+r+">\n"}listitem(e){let t="";if(e.task){const n=this.checkbox({checked:!!e.checked});e.loose?e.tokens.length>0&&"paragraph"===e.tokens[0].type?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&"text"===e.tokens[0].tokens[0].type&&(e.tokens[0].tokens[0].text=n+" "+e.tokens[0].tokens[0].text)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" "}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`<li>${t}</li>\n`}checkbox({checked:e}){return"<input "+(e?'checked="" ':"")+'disabled="" type="checkbox">'}paragraph({tokens:e}){return`<p>${this.parser.parseInline(e)}</p>\n`}table(e){let t="",n="";for(let t=0;t<e.header.length;t++)n+=this.tablecell(e.header[t]);t+=this.tablerow({text:n});let s="";for(let t=0;t<e.rows.length;t++){const r=e.rows[t];n="";for(let e=0;e<r.length;e++)n+=this.tablecell(r[e]);s+=this.tablerow({text:n})}return s&&(s=`<tbody>${s}</tbody>`),"<table>\n<thead>\n"+t+"</thead>\n"+s+"</table>\n"}tablerow({text:e}){return`<tr>\n${e}</tr>\n`}tablecell(e){const t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`</${n}>\n`}strong({tokens:e}){return`<strong>${this.parser.parseInline(e)}</strong>`}em({tokens:e}){return`<em>${this.parser.parseInline(e)}</em>`}codespan({text:e}){return`<code>${e}</code>`}br(e){return"<br>"}del({tokens:e}){return`<del>${this.parser.parseInline(e)}</del>`}link({href:e,title:t,tokens:n}){const s=this.parser.parseInline(n),r=k(e);if(null===r)return s;let i='<a href="'+(e=r)+'"';return t&&(i+=' title="'+t+'"'),i+=">"+s+"</a>",i}image({href:e,title:t,text:n}){const s=k(e);if(null===s)return n;let r=`<img src="${e=s}" alt="${n}"`;return t&&(r+=` title="${t}"`),r+=">",r}text(e){return"tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):e.text}}class se{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return""+e}image({text:e}){return""+e}br(){return""}}class re{options;renderer;textRenderer;constructor(t){this.options=t||e.defaults,this.options.renderer=this.options.renderer||new ne,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new se}static parse(e,t){return new re(t).parse(e)}static parseInline(e,t){return new re(t).parseInline(e)}parse(e,t=!0){let n="";for(let s=0;s<e.length;s++){const r=e[s];if(this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[r.type]){const e=r,t=this.options.extensions.renderers[e.type].call({parser:this},e);if(!1!==t||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(e.type)){n+=t||"";continue}}const i=r;switch(i.type){case"space":n+=this.renderer.space(i);continue;case"hr":n+=this.renderer.hr(i);continue;case"heading":n+=this.renderer.heading(i);continue;case"code":n+=this.renderer.code(i);continue;case"table":n+=this.renderer.table(i);continue;case"blockquote":n+=this.renderer.blockquote(i);continue;case"list":n+=this.renderer.list(i);continue;case"html":n+=this.renderer.html(i);continue;case"paragraph":n+=this.renderer.paragraph(i);continue;case"text":{let r=i,l=this.renderer.text(r);for(;s+1<e.length&&"text"===e[s+1].type;)r=e[++s],l+="\n"+this.renderer.text(r);n+=t?this.renderer.paragraph({type:"paragraph",raw:l,text:l,tokens:[{type:"text",raw:l,text:l}]}):l;continue}default:{const e='Token with "'+i.type+'" type was not found.';if(this.options.silent)return console.error(e),"";throw new Error(e)}}}return n}parseInline(e,t){t=t||this.renderer;let n="";for(let s=0;s<e.length;s++){const r=e[s];if(this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[r.type]){const e=this.options.extensions.renderers[r.type].call({parser:this},r);if(!1!==e||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(r.type)){n+=e||"";continue}}const i=r;switch(i.type){case"escape":case"text":n+=t.text(i);break;case"html":n+=t.html(i);break;case"link":n+=t.link(i);break;case"image":n+=t.image(i);break;case"strong":n+=t.strong(i);break;case"em":n+=t.em(i);break;case"codespan":n+=t.codespan(i);break;case"br":n+=t.br(i);break;case"del":n+=t.del(i);break;default:{const e='Token with "'+i.type+'" type was not found.';if(this.options.silent)return console.error(e),"";throw new Error(e)}}}return n}}class ie{options;constructor(t){this.options=t||e.defaults}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}}class le{defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};options=this.setOptions;parse=this.#e(te.lex,re.parse);parseInline=this.#e(te.lexInline,re.parseInline);Parser=re;Renderer=ne;TextRenderer=se;Lexer=te;Tokenizer=b;Hooks=ie;constructor(...e){this.use(...e)}walkTokens(e,t){let n=[];for(const s of e)switch(n=n.concat(t.call(this,s)),s.type){case"table":{const e=s;for(const s of e.header)n=n.concat(this.walkTokens(s.tokens,t));for(const s of e.rows)for(const e of s)n=n.concat(this.walkTokens(e.tokens,t));break}case"list":{const e=s;n=n.concat(this.walkTokens(e.items,t));break}default:{const e=s;this.defaults.extensions?.childTokens?.[e.type]?this.defaults.extensions.childTokens[e.type].forEach((s=>{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new ne(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if(["options","parser"].includes(n))continue;const s=n;let r=e.renderer[s];const i=t[s];t[s]=(...n)=>{e.useNewRenderer||(r=this.#t(r,s,t));let l=r.apply(t,n);return!1===l&&(l=i.apply(t,n)),l||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new b(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new ie;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.hooks[s],i=t[s];ie.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}#t(e,t,n){switch(t){case"heading":return function(s){return s.type&&s.type===t?e(n.parser.parseInline(s.tokens),s.depth,function(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}(n.parser.parseInline(s.tokens,n.parser.textRenderer))):e.apply(this,arguments)};case"code":return function(n){return n.type&&n.type===t?e(n.text,n.lang,!!n.escaped):e.apply(this,arguments)};case"table":return function(n){if(!n.type||n.type!==t)return e.apply(this,arguments);let s="",r="";for(let e=0;e<n.header.length;e++)r+=this.tablecell({text:n.header[e].text,tokens:n.header[e].tokens,header:!0,align:n.align[e]});s+=this.tablerow({text:r});let i="";for(let e=0;e<n.rows.length;e++){const t=n.rows[e];r="";for(let e=0;e<t.length;e++)r+=this.tablecell({text:t[e].text,tokens:t[e].tokens,header:!1,align:n.align[e]});i+=this.tablerow({text:r})}return e(s,i)};case"blockquote":return function(n){if(!n.type||n.type!==t)return e.apply(this,arguments);const s=this.parser.parse(n.tokens);return e(s)};case"list":return function(n){if(!n.type||n.type!==t)return e.apply(this,arguments);const s=n.ordered,r=n.start,i=n.loose;let l="";for(let e=0;e<n.items.length;e++){const t=n.items[e],s=t.checked,r=t.task;let o="";if(t.task){const e=this.checkbox({checked:!!s});i?t.tokens.length>0&&"paragraph"===t.tokens[0].type?(t.tokens[0].text=e+" "+t.tokens[0].text,t.tokens[0].tokens&&t.tokens[0].tokens.length>0&&"text"===t.tokens[0].tokens[0].type&&(t.tokens[0].tokens[0].text=e+" "+t.tokens[0].tokens[0].text)):t.tokens.unshift({type:"text",text:e+" "}):o+=e+" "}o+=this.parser.parse(t.tokens,i),l+=this.listitem({type:"list_item",raw:o,text:o,task:r,checked:!!s,loose:i,tokens:t.tokens})}return e(l,s,r)};case"html":return function(n){return n.type&&n.type===t?e(n.text,n.block):e.apply(this,arguments)};case"paragraph":case"strong":case"em":case"del":return function(n){return n.type&&n.type===t?e(this.parser.parseInline(n.tokens)):e.apply(this,arguments)};case"escape":case"codespan":case"text":return function(n){return n.type&&n.type===t?e(n.text):e.apply(this,arguments)};case"link":return function(n){return n.type&&n.type===t?e(n.href,n.title,this.parser.parseInline(n.tokens)):e.apply(this,arguments)};case"image":return function(n){return n.type&&n.type===t?e(n.href,n.title,n.text):e.apply(this,arguments)}}return e}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return te.lex(e,t??this.defaults)}parser(e,t){return re.parse(e,t??this.defaults)}#e(e,t){return(n,s)=>{const r={...s},i={...this.defaults,...r};!0===this.defaults.async&&!1===r.async&&(i.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),i.async=!0);const l=this.#n(!!i.silent,!!i.async);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i),i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then((t=>e(t,i))).then((e=>i.hooks?i.hooks.processAllTokens(e):e)).then((e=>i.walkTokens?Promise.all(this.walkTokens(e,i.walkTokens)).then((()=>e)):e)).then((e=>t(e,i))).then((e=>i.hooks?i.hooks.postprocess(e):e)).catch(l);try{i.hooks&&(n=i.hooks.preprocess(n));let s=e(n,i);i.hooks&&(s=i.hooks.processAllTokens(s)),i.walkTokens&&this.walkTokens(s,i.walkTokens);let r=t(s,i);return i.hooks&&(r=i.hooks.postprocess(r)),r}catch(e){return l(e)}}}#n(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="<p>An error occurred:</p><pre>"+c(n.message+"",!0)+"</pre>";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const oe=new le;function ae(e,t){return oe.parse(e,t)}ae.options=ae.setOptions=function(e){return oe.setOptions(e),ae.defaults=oe.defaults,n(ae.defaults),ae},ae.getDefaults=t,ae.defaults=e.defaults,ae.use=function(...e){return oe.use(...e),ae.defaults=oe.defaults,n(ae.defaults),ae},ae.walkTokens=function(e,t){return oe.walkTokens(e,t)},ae.parseInline=oe.parseInline,ae.Parser=re,ae.parser=re.parse,ae.Renderer=ne,ae.TextRenderer=se,ae.Lexer=te,ae.lexer=te.lex,ae.Tokenizer=b,ae.Hooks=ie,ae.parse=ae;const ce=ae.options,he=ae.setOptions,pe=ae.use,ue=ae.walkTokens,ke=ae.parseInline,ge=ae,fe=re.parse,de=te.lex;e.Hooks=ie,e.Lexer=te,e.Marked=le,e.Parser=re,e.Renderer=ne,e.TextRenderer=se,e.Tokenizer=b,e.getDefaults=t,e.lexer=de,e.marked=ae,e.options=ce,e.parse=ge,e.parseInline=ke,e.parser=fe,e.setOptions=he,e.use=pe,e.walkTokens=ue}));
diff --git a/build/lib/exo/tinychat/update_deps.py b/build/lib/exo/tinychat/update_deps.py
deleted file mode 100644
index dfacc513..00000000
--- a/build/lib/exo/tinychat/update_deps.py
+++ /dev/null
@@ -1,93 +0,0 @@
-import os
-import requests
-from bs4 import BeautifulSoup
-from urllib.parse import urljoin, urlparse
-import re
-
-
-def download_file(url, local_path):
-  response = requests.get(url)
-  if response.status_code == 200:
-    os.makedirs(os.path.dirname(local_path), exist_ok=True)
-    with open(local_path, 'wb') as f:
-      f.write(response.content)
-    print(f"Downloaded: {local_path}")
-  else:
-    print(response.status_code)
-    print(f"Failed to download: {url}")
-
-
-def update_html(html_content, base_url):
-  soup = BeautifulSoup(html_content, 'html.parser')
-
-  for tag in soup.find_all(['script', 'link']):
-    if tag.has_attr('src'):
-      url = tag['src']
-    elif tag.has_attr('href'):
-      url = tag['href']
-    else:
-      continue
-
-    if url.startswith(('http://', 'https://')):
-      full_url = url
-    else:
-      full_url = urljoin(base_url, url)
-
-    parsed_url = urlparse(full_url)
-    local_path = os.path.join('static', parsed_url.netloc, parsed_url.path.lstrip('/'))
-
-    download_file(full_url, local_path)
-
-    relative_path = os.path.relpath(local_path, '.')
-    if tag.name == 'script':
-      tag['src'] = "/" + relative_path
-    elif tag.name == 'link':
-      tag['href'] = "/" + relative_path
-
-  return str(soup)
-
-
-# Read the HTML file
-with open('./index.html', 'r') as f:
-  html_content = f.read()
-
-# Update HTML and download files
-# updated_html = update_html(html_content, 'https://example.com')
-
-# # Write the updated HTML
-# with open('./index.html', 'w') as f:
-#     f.write(updated_html)
-
-print("HTML file updated with local paths.")
-
-# Download Font Awesome CSS and font files
-base_url = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/"
-css_url = urljoin(base_url, "css/all.min.css")
-output_dir = "static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2"
-
-# Download CSS file
-css_output_path = os.path.join(output_dir, "css", "all.min.css")
-download_file(css_url, css_output_path)
-
-# Parse CSS file for font URLs
-with open(css_output_path, 'r', encoding='utf-8') as f:
-  css_content = f.read()
-
-# Extract font URLs from the CSS content
-font_urls = re.findall(r'url\((.*?\.(?:woff2|ttf))\)', css_content)
-
-print(f"Found {len(font_urls)} font URLs")
-
-# Download font files
-for font_url in font_urls:
-  font_url = font_url.strip('"\'')
-  if font_url.startswith('../'):
-    font_url = font_url[3:]
-
-  # Use base_url instead of urljoin to keep the version number
-  full_url = base_url + font_url
-  relative_path = font_url
-  output_path = os.path.join(output_dir, relative_path)
-  download_file(full_url, output_path)
-
-print("Download complete!")
diff --git a/build/lib/exo/topology/__init__.py b/build/lib/exo/topology/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/topology/device_capabilities.py b/build/lib/exo/topology/device_capabilities.py
deleted file mode 100644
index 4424f15d..00000000
--- a/build/lib/exo/topology/device_capabilities.py
+++ /dev/null
@@ -1,217 +0,0 @@
-from typing import Any
-from pydantic import BaseModel
-from exo import DEBUG
-import subprocess
-import psutil
-
-TFLOPS = 1.00
-
-
-class DeviceFlops(BaseModel):
-  # units of TFLOPS
-  fp32: float
-  fp16: float
-  int8: float
-
-  def __str__(self):
-    return f"fp32: {self.fp32 / TFLOPS:.2f} TFLOPS, fp16: {self.fp16 / TFLOPS:.2f} TFLOPS, int8: {self.int8 / TFLOPS:.2f} TFLOPS"
-
-  def to_dict(self):
-    return self.model_dump()
-
-
-class DeviceCapabilities(BaseModel):
-  model: str
-  chip: str
-  memory: int
-  flops: DeviceFlops
-
-  def __str__(self):
-    return f"Model: {self.model}. Chip: {self.chip}. Memory: {self.memory}MB. Flops: {self.flops}"
-
-  def model_post_init(self, __context: Any) -> None:
-    if isinstance(self.flops, dict):
-      self.flops = DeviceFlops(**self.flops)
-
-  def to_dict(self):
-    return {"model": self.model, "chip": self.chip, "memory": self.memory, "flops": self.flops.to_dict()}
-
-
-UNKNOWN_DEVICE_CAPABILITIES = DeviceCapabilities(model="Unknown Model", chip="Unknown Chip", memory=0, flops=DeviceFlops(fp32=0, fp16=0, int8=0))
-
-CHIP_FLOPS = {
-  # Source: https://www.cpu-monkey.com
-  # Note: currently no distinction between variants of M3 Max and M3 Pro, we pick the lower one to be conservative
-  ### M chips
-  "Apple M1": DeviceFlops(fp32=2.29*TFLOPS, fp16=4.58*TFLOPS, int8=9.16*TFLOPS),
-  "Apple M1 Pro": DeviceFlops(fp32=5.30*TFLOPS, fp16=10.60*TFLOPS, int8=21.20*TFLOPS),
-  "Apple M1 Max": DeviceFlops(fp32=10.60*TFLOPS, fp16=21.20*TFLOPS, int8=42.40*TFLOPS),
-  "Apple M1 Ultra": DeviceFlops(fp32=21.20*TFLOPS, fp16=42.40*TFLOPS, int8=84.80*TFLOPS),
-  "Apple M2": DeviceFlops(fp32=3.55*TFLOPS, fp16=7.10*TFLOPS, int8=14.20*TFLOPS),
-  "Apple M2 Pro": DeviceFlops(fp32=5.68*TFLOPS, fp16=11.36*TFLOPS, int8=22.72*TFLOPS),
-  "Apple M2 Max": DeviceFlops(fp32=13.49*TFLOPS, fp16=26.98*TFLOPS, int8=53.96*TFLOPS),
-  "Apple M2 Ultra": DeviceFlops(fp32=26.98*TFLOPS, fp16=53.96*TFLOPS, int8=107.92*TFLOPS),
-  "Apple M3": DeviceFlops(fp32=3.55*TFLOPS, fp16=7.10*TFLOPS, int8=14.20*TFLOPS),
-  "Apple M3 Pro": DeviceFlops(fp32=4.97*TFLOPS, fp16=9.94*TFLOPS, int8=19.88*TFLOPS),
-  "Apple M3 Max": DeviceFlops(fp32=14.20*TFLOPS, fp16=28.40*TFLOPS, int8=56.80*TFLOPS),
-  "Apple M4": DeviceFlops(fp32=4.26*TFLOPS, fp16=8.52*TFLOPS, int8=17.04*TFLOPS),
-  "Apple M4 Pro": DeviceFlops(fp32=5.72*TFLOPS, fp16=11.44*TFLOPS, int8=22.88*TFLOPS),
-  "Apple M4 Max": DeviceFlops(fp32=18.03*TFLOPS, fp16=36.07*TFLOPS, int8=72.14*TFLOPS),
-  ### A chips
-  "Apple A13 Bionic": DeviceFlops(fp32=0.69*TFLOPS, fp16=1.38*TFLOPS, int8=2.76*TFLOPS),
-  "Apple A14 Bionic": DeviceFlops(fp32=0.75*TFLOPS, fp16=1.50*TFLOPS, int8=3.00*TFLOPS),
-  "Apple A15 Bionic": DeviceFlops(fp32=1.37*TFLOPS, fp16=2.74*TFLOPS, int8=5.48*TFLOPS),
-  "Apple A16 Bionic": DeviceFlops(fp32=1.79*TFLOPS, fp16=3.58*TFLOPS, int8=7.16*TFLOPS),
-  "Apple A17 Pro": DeviceFlops(fp32=2.15*TFLOPS, fp16=4.30*TFLOPS, int8=8.60*TFLOPS),
-  ### NVIDIA GPUs
-  # RTX 40 series
-  "NVIDIA GEFORCE RTX 4090": DeviceFlops(fp32=82.58*TFLOPS, fp16=165.16*TFLOPS, int8=330.32*TFLOPS),
-  "NVIDIA GEFORCE RTX 4080": DeviceFlops(fp32=48.74*TFLOPS, fp16=97.48*TFLOPS, int8=194.96*TFLOPS),
-  "NVIDIA GEFORCE RTX 4080 SUPER": DeviceFlops(fp32=52.0*TFLOPS, fp16=104.0*TFLOPS, int8=208.0*TFLOPS),
-  "NVIDIA GEFORCE RTX 4070 TI SUPER": DeviceFlops(fp32=40.0*TFLOPS, fp16=80.0*TFLOPS, int8=160.0*TFLOPS),
-  "NVIDIA GEFORCE RTX 4070 TI": DeviceFlops(fp32=39.43*TFLOPS, fp16=78.86*TFLOPS, int8=157.72*TFLOPS),
-  "NVIDIA GEFORCE RTX 4070 SUPER": DeviceFlops(fp32=30.0*TFLOPS, fp16=60.0*TFLOPS, int8=120.0*TFLOPS),
-  "NVIDIA GEFORCE RTX 4070": DeviceFlops(fp32=29.0*TFLOPS, fp16=58.0*TFLOPS, int8=116.0*TFLOPS),
-  "NVIDIA GEFORCE RTX 4060 TI 16GB": DeviceFlops(fp32=22.0*TFLOPS, fp16=44.0*TFLOPS, int8=88.0*TFLOPS),
-  "NVIDIA GEFORCE RTX 4060 TI": DeviceFlops(fp32=22.0*TFLOPS, fp16=44.0*TFLOPS, int8=88.0*TFLOPS),
-  # RTX 30 series
-  "NVIDIA GEFORCE RTX 3050": DeviceFlops(fp32=9.11*TFLOPS, fp16=18.22*TFLOPS, int8=36.44*TFLOPS),
-  "NVIDIA GEFORCE RTX 3060": DeviceFlops(fp32=13.0*TFLOPS, fp16=26.0*TFLOPS, int8=52.0*TFLOPS),
-  "NVIDIA GEFORCE RTX 3060 TI": DeviceFlops(fp32=16.2*TFLOPS, fp16=32.4*TFLOPS, int8=64.8*TFLOPS),
-  "NVIDIA GEFORCE RTX 3070": DeviceFlops(fp32=20.3*TFLOPS, fp16=40.6*TFLOPS, int8=81.2*TFLOPS),
-  "NVIDIA GEFORCE RTX 3070 TI": DeviceFlops(fp32=21.8*TFLOPS, fp16=43.6*TFLOPS, int8=87.2*TFLOPS),
-  "NVIDIA GEFORCE RTX 3080 (10 GB)": DeviceFlops(fp32=29.8*TFLOPS, fp16=59.6*TFLOPS, int8=119.2*TFLOPS),
-  "NVIDIA GEFORCE RTX 3080 (12 GB)": DeviceFlops(fp32=30.6*TFLOPS, fp16=61.2*TFLOPS, int8=122.4*TFLOPS),
-  "NVIDIA GEFORCE RTX 3080 TI": DeviceFlops(fp32=34.1*TFLOPS, fp16=68.2*TFLOPS, int8=136.4*TFLOPS),
-  "NVIDIA GEFORCE RTX 3090": DeviceFlops(fp32=35.6*TFLOPS, fp16=71.2*TFLOPS, int8=142.4*TFLOPS),
-  "NVIDIA GEFORCE RTX 3090 TI": DeviceFlops(fp32=40.0*TFLOPS, fp16=80.0*TFLOPS, int8=160.0*TFLOPS),
-  # RTX 20 series
-  "NVIDIA GEFORCE RTX 2060": DeviceFlops(fp32=6.45*TFLOPS, fp16=12.9*TFLOPS, int8=25.8*TFLOPS),
-  "NVIDIA GEFORCE RTX 2060 SUPER": DeviceFlops(fp32=7.2*TFLOPS, fp16=14.4*TFLOPS, int8=28.8*TFLOPS),
-  "NVIDIA GEFORCE RTX 2070": DeviceFlops(fp32=7.46*TFLOPS, fp16=14.93*TFLOPS, int8=29.86*TFLOPS),
-  "NVIDIA GEFORCE RTX 2070 SUPER": DeviceFlops(fp32=9.06*TFLOPS, fp16=18.12*TFLOPS, int8=36.24*TFLOPS),
-  "NVIDIA GEFORCE RTX 2080": DeviceFlops(fp32=10.07*TFLOPS, fp16=20.14*TFLOPS, int8=40.28*TFLOPS),
-  "NVIDIA GEFORCE RTX 2080 TI": DeviceFlops(fp32=13.45*TFLOPS, fp16=26.9*TFLOPS, int8=40.28*TFLOPS),
-  "NVIDIA GEFORCE RTX 2080 SUPER": DeviceFlops(fp32=11.15*TFLOPS, fp16=22.30*TFLOPS, int8=44.60*TFLOPS),
-  "NVIDIA TITAN RTX": DeviceFlops(fp32=16.31*TFLOPS, fp16=32.62*TFLOPS, int8=65.24*TFLOPS),
-  # GTX 10 series
-  "NVIDIA GEFORCE GTX 1050 TI": DeviceFlops(fp32=2.0*TFLOPS, fp16=4.0*TFLOPS, int8=8.0*TFLOPS),
-  "NVIDIA GEFORCE GTX 1070": DeviceFlops(fp32=6.463*TFLOPS, fp16=0.101*TFLOPS, int8=25.852*TFLOPS),
-  "NVIDIA GEFORCE GTX 1080": DeviceFlops(fp32=8.873*TFLOPS, fp16=0.138*TFLOPS, int8=35.492*TFLOPS),
-  "NVIDIA GEFORCE GTX 1080 TI": DeviceFlops(fp32=11.34*TFLOPS, fp16=0.177*TFLOPS, int8=45.36*TFLOPS),
-  # GTX 16 series
-  "NVIDIA GeForce GTX 1660 TI": DeviceFlops(fp32=4.8*TFLOPS, fp16=9.6*TFLOPS, int8=19.2*TFLOPS),
-  # QUADRO RTX Ampere series
-  "NVIDIA RTX A2000": DeviceFlops(fp32=7.99*TFLOPS, fp16=7.99*TFLOPS, int8=31.91*TFLOPS),
-  "NVIDIA RTX A4000": DeviceFlops(fp32=19.17*TFLOPS, fp16=19.17*TFLOPS, int8=76.68*TFLOPS),
-  "NVIDIA RTX A4500": DeviceFlops(fp32=23.65*TFLOPS, fp16=23.65*TFLOPS, int8=94.6*TFLOPS),
-  "NVIDIA RTX A5000": DeviceFlops(fp32=27.8*TFLOPS, fp16=27.8*TFLOPS, int8=111.2*TFLOPS),
-  "NVIDIA RTX A6000": DeviceFlops(fp32=38.71*TFLOPS, fp16=38.71*TFLOPS, int8=154.84*TFLOPS),
-  # NVIDIA Ada Lovelace Architecture-Based
-  "NVIDIA RTX 4000 ADA GENERATION": DeviceFlops(fp32=26.7*TFLOPS, fp16=26.7*TFLOPS, int8=258.0*TFLOPS),
-  # Common Server GPUs
-  "NVIDIA A40 48GB PCIE": DeviceFlops(fp32=37.4*TFLOPS, fp16=149.7*TFLOPS, int8=299.3*TFLOPS),
-  "NVIDIA A100 40GB PCIE": DeviceFlops(fp32=19.5*TFLOPS, fp16=312.0*TFLOPS, int8=624.0*TFLOPS),
-  "NVIDIA A800 40GB PCIE": DeviceFlops(fp32=19.5*TFLOPS, fp16=312.0*TFLOPS, int8=624.0*TFLOPS),
-  "NVIDIA A100 80GB PCIE": DeviceFlops(fp32=19.5*TFLOPS, fp16=312.0*TFLOPS, int8=624.0*TFLOPS),
-  "NVIDIA A800 80GB PCIE": DeviceFlops(fp32=19.5*TFLOPS, fp16=312.0*TFLOPS, int8=624.0*TFLOPS),
-  "NVIDIA A100 80GB SXM": DeviceFlops(fp32=19.5*TFLOPS, fp16=312.0*TFLOPS, int8=624.0*TFLOPS),
-  "NVIDIA A800 80GB SXM": DeviceFlops(fp32=19.5*TFLOPS, fp16=312.0*TFLOPS, int8=624.0*TFLOPS),
-  # ... add more devices if needed ...
-  ### AMD GPUs
-  # RX 6000 series
-  "AMD Radeon RX 6900 XT": DeviceFlops(fp32=23.04*TFLOPS, fp16=46.08*TFLOPS, int8=92.16*TFLOPS),
-  "AMD Radeon RX 6800 XT": DeviceFlops(fp32=20.74*TFLOPS, fp16=41.48*TFLOPS, int8=82.96*TFLOPS),
-  "AMD Radeon RX 6800": DeviceFlops(fp32=16.17*TFLOPS, fp16=32.34*TFLOPS, int8=64.68*TFLOPS),
-  "AMD Radeon RX 6700 XT": DeviceFlops(fp32=13.21*TFLOPS, fp16=26.42*TFLOPS, int8=52.84*TFLOPS),
-  "AMD Radeon RX 6700": DeviceFlops(fp32=11.4*TFLOPS, fp16=22.8*TFLOPS, int8=45.6*TFLOPS),
-  "AMD Radeon RX 6600 XT": DeviceFlops(fp32=10.6*TFLOPS, fp16=21.2*TFLOPS, int8=42.4*TFLOPS),
-  "AMD Radeon RX 6600": DeviceFlops(fp32=8.93*TFLOPS, fp16=17.86*TFLOPS, int8=35.72*TFLOPS),
-  "AMD Radeon RX 6500 XT": DeviceFlops(fp32=5.77*TFLOPS, fp16=11.54*TFLOPS, int8=23.08*TFLOPS),
-  "AMD Radeon RX 6400": DeviceFlops(fp32=3.57*TFLOPS, fp16=7.14*TFLOPS, int8=14.28*TFLOPS),
-  # RX 7000 series
-  "AMD Radeon RX 7900 XTX": DeviceFlops(fp32=61.4*TFLOPS, fp16=122.8*TFLOPS, int8=245.6*TFLOPS),
-  "AMD Radeon RX 7900 XT": DeviceFlops(fp32=53.4*TFLOPS, fp16=106.8*TFLOPS, int8=213.6*TFLOPS),
-  "AMD Radeon RX 7800 XT": DeviceFlops(fp32=42.6*TFLOPS, fp16=85.2*TFLOPS, int8=170.4*TFLOPS),
-  "AMD Radeon RX 7700 XT": DeviceFlops(fp32=34.2*TFLOPS, fp16=68.4*TFLOPS, int8=136.8*TFLOPS),
-  "AMD Radeon RX 7600": DeviceFlops(fp32=21.5*TFLOPS, fp16=43.0*TFLOPS, int8=86.0*TFLOPS),
-  "AMD Radeon RX 7500": DeviceFlops(fp32=16.2*TFLOPS, fp16=32.4*TFLOPS, int8=64.8*TFLOPS),
-  ### Qualcomm embedded chips: TODO
-}
-CHIP_FLOPS.update({f"LAPTOP GPU {key}": value for key, value in CHIP_FLOPS.items()})
-CHIP_FLOPS.update({f"Laptop GPU {key}": value for key, value in CHIP_FLOPS.items()})
-CHIP_FLOPS.update({f"{key} LAPTOP GPU": value for key, value in CHIP_FLOPS.items()})
-CHIP_FLOPS.update({f"{key} Laptop GPU": value for key, value in CHIP_FLOPS.items()})
-
-
-def device_capabilities() -> DeviceCapabilities:
-  if psutil.MACOS:
-    return mac_device_capabilities()
-  elif psutil.LINUX:
-    return linux_device_capabilities()
-  else:
-    return DeviceCapabilities(
-      model="Unknown Device",
-      chip="Unknown Chip",
-      memory=psutil.virtual_memory().total // 2**20,
-      flops=DeviceFlops(fp32=0, fp16=0, int8=0),
-    )
-
-
-def mac_device_capabilities() -> DeviceCapabilities:
-  # Fetch the model of the Mac using system_profiler
-  model = subprocess.check_output(["system_profiler", "SPHardwareDataType"]).decode("utf-8")
-  model_line = next((line for line in model.split("\n") if "Model Name" in line), None)
-  model_id = model_line.split(": ")[1] if model_line else "Unknown Model"
-  chip_line = next((line for line in model.split("\n") if "Chip" in line), None)
-  chip_id = chip_line.split(": ")[1] if chip_line else "Unknown Chip"
-  memory_line = next((line for line in model.split("\n") if "Memory" in line), None)
-  memory_str = memory_line.split(": ")[1] if memory_line else "Unknown Memory"
-  memory_units = memory_str.split()
-  memory_value = int(memory_units[0])
-  if memory_units[1] == "GB":
-    memory = memory_value*1024
-  else:
-    memory = memory_value
-
-  # Assuming static values for other attributes for demonstration
-  return DeviceCapabilities(model=model_id, chip=chip_id, memory=memory, flops=CHIP_FLOPS.get(chip_id, DeviceFlops(fp32=0, fp16=0, int8=0)))
-
-
-def linux_device_capabilities() -> DeviceCapabilities:
-  import psutil
-  from tinygrad import Device
-
-  if DEBUG >= 2: print(f"tinygrad {Device.DEFAULT=}")
-  if Device.DEFAULT == "CUDA" or Device.DEFAULT == "NV" or Device.DEFAULT == "GPU":
-    import pynvml
-
-    pynvml.nvmlInit()
-    handle = pynvml.nvmlDeviceGetHandleByIndex(0)
-    gpu_raw_name = pynvml.nvmlDeviceGetName(handle).upper()
-    gpu_name = gpu_raw_name.rsplit(" ", 1)[0] if gpu_raw_name.endswith("GB") else gpu_raw_name
-    gpu_memory_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
-
-    if DEBUG >= 2: print(f"NVIDIA device {gpu_name=} {gpu_memory_info=}")
-
-    return DeviceCapabilities(
-      model=f"Linux Box ({gpu_name})",
-      chip=gpu_name,
-      memory=gpu_memory_info.total // 2**20,
-      flops=CHIP_FLOPS.get(gpu_name, DeviceFlops(fp32=0, fp16=0, int8=0)),
-    )
-  elif Device.DEFAULT == "AMD":
-    # TODO AMD support
-    return DeviceCapabilities(
-      model="Linux Box (AMD)",
-      chip="Unknown AMD",
-      memory=psutil.virtual_memory().total // 2**20,
-      flops=DeviceFlops(fp32=0, fp16=0, int8=0),
-    )
-  else:
-    return DeviceCapabilities(
-      model=f"Linux Box (Device: {Device.DEFAULT})",
-      chip=f"Unknown Chip (Device: {Device.DEFAULT})",
-      memory=psutil.virtual_memory().total // 2**20,
-      flops=DeviceFlops(fp32=0, fp16=0, int8=0),
-    )
diff --git a/build/lib/exo/topology/partitioning_strategy.py b/build/lib/exo/topology/partitioning_strategy.py
deleted file mode 100644
index 29c3dc6a..00000000
--- a/build/lib/exo/topology/partitioning_strategy.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from abc import ABC, abstractmethod
-from typing import List
-from dataclasses import dataclass
-from .topology import Topology
-from exo.inference.shard import Shard
-
-
-# Partitions shard-space into pieces of contiguous shards, represented by floating point range [start, end) between 0 and 1
-@dataclass
-class Partition:
-  node_id: str
-  start: float
-  end: float
-
-
-class PartitioningStrategy(ABC):
-  @abstractmethod
-  def partition(self, topology: Topology) -> List[Partition]:
-    pass
-
-
-def map_partitions_to_shards(partitions: List[Partition], num_layers: int, model_id: str) -> List[Shard]:
-  shards = []
-  for i, partition in enumerate(partitions):
-    start_layer = int(partition.start*num_layers)
-    end_layer = int(partition.end*num_layers) - 1
-
-    # Ensure the last partition covers up to num_layers - 1
-    if i == len(partitions) - 1:
-      end_layer = num_layers - 1
-
-    # Ensure no empty shards
-    if start_layer <= end_layer:
-      shards.append(Shard(model_id, start_layer, end_layer, num_layers))
-
-  # Ensure full coverage
-  if shards and shards[-1].end_layer < num_layers - 1:
-    shards[-1] = Shard(model_id, shards[-1].start_layer, num_layers - 1, num_layers)
-
-  return shards
diff --git a/build/lib/exo/topology/ring_memory_weighted_partitioning_strategy.py b/build/lib/exo/topology/ring_memory_weighted_partitioning_strategy.py
deleted file mode 100644
index 6550aeb1..00000000
--- a/build/lib/exo/topology/ring_memory_weighted_partitioning_strategy.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from typing import List
-from .partitioning_strategy import PartitioningStrategy
-from .topology import Topology
-from .partitioning_strategy import Partition
-
-
-class RingMemoryWeightedPartitioningStrategy(PartitioningStrategy):
-  def partition(self, topology: Topology) -> List[Partition]:
-    nodes = list(topology.all_nodes())
-    nodes.sort(key=lambda x: (x[1].memory, x[0]), reverse=True)
-    total_memory = sum(node[1].memory for node in nodes)
-    partitions = []
-    start = 0
-    for node in nodes:
-      end = round(start + (node[1].memory/total_memory), 5)
-      partitions.append(Partition(node[0], start, end))
-      start = end
-    return partitions
diff --git a/build/lib/exo/topology/test_device_capabilities.py b/build/lib/exo/topology/test_device_capabilities.py
deleted file mode 100644
index 5f8b4c3a..00000000
--- a/build/lib/exo/topology/test_device_capabilities.py
+++ /dev/null
@@ -1,91 +0,0 @@
-import unittest
-from unittest.mock import patch
-from exo.topology.device_capabilities import mac_device_capabilities, DeviceCapabilities, DeviceFlops, TFLOPS
-
-
-class TestMacDeviceCapabilities(unittest.TestCase):
-  @patch("subprocess.check_output")
-  def test_mac_device_capabilities_pro(self, mock_check_output):
-    # Mock the subprocess output
-    mock_check_output.return_value = b"""
-Hardware:
-
-Hardware Overview:
-
-Model Name: MacBook Pro
-Model Identifier: Mac15,9
-Model Number: Z1CM000EFB/A
-Chip: Apple M3 Max
-Total Number of Cores: 16 (12 performance and 4 efficiency)
-Memory: 128 GB
-System Firmware Version: 10000.000.0
-OS Loader Version: 10000.000.0
-Serial Number (system): XXXXXXXXXX
-Hardware UUID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
-Provisioning UDID: XXXXXXXX-XXXXXXXXXXXXXXXX
-Activation Lock Status: Enabled
-"""
-
-    # Call the function
-    result = mac_device_capabilities()
-
-    # Check the results
-    self.assertIsInstance(result, DeviceCapabilities)
-    self.assertEqual(result.model, "MacBook Pro")
-    self.assertEqual(result.chip, "Apple M3 Max")
-    self.assertEqual(result.memory, 131072)  # 16 GB in MB
-    self.assertEqual(
-      str(result),
-      "Model: MacBook Pro. Chip: Apple M3 Max. Memory: 131072MB. Flops: 14.20 TFLOPS, fp16: 28.40 TFLOPS, int8: 56.80 TFLOPS",
-    )
-
-  @patch("subprocess.check_output")
-  def test_mac_device_capabilities_air(self, mock_check_output):
-    # Mock the subprocess output
-    mock_check_output.return_value = b"""
-Hardware:
-
-Hardware Overview:
-
-Model Name: MacBook Air
-Model Identifier: Mac14,2
-Model Number: MLY33B/A
-Chip: Apple M2
-Total Number of Cores: 8 (4 performance and 4 efficiency)
-Memory: 8 GB
-System Firmware Version: 10000.00.0
-OS Loader Version: 10000.00.0
-Serial Number (system): XXXXXXXXXX
-Hardware UUID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
-Provisioning UDID: XXXXXXXX-XXXXXXXXXXXXXXXX
-Activation Lock Status: Disabled
-"""
-
-    # Call the function
-    result = mac_device_capabilities()
-
-    # Check the results
-    self.assertIsInstance(result, DeviceCapabilities)
-    self.assertEqual(result.model, "MacBook Air")
-    self.assertEqual(result.chip, "Apple M2")
-    self.assertEqual(result.memory, 8192)  # 8 GB in MB
-
-  @unittest.skip("Unskip this test when running on a MacBook Pro, Apple M3 Max, 128GB")
-  def test_mac_device_capabilities_real(self):
-    # Call the function without mocking
-    result = mac_device_capabilities()
-
-    # Check the results
-    self.assertIsInstance(result, DeviceCapabilities)
-    self.assertEqual(result.model, "MacBook Pro")
-    self.assertEqual(result.chip, "Apple M3 Max")
-    self.assertEqual(result.memory, 131072)  # 128 GB in MB
-    self.assertEqual(result.flops, DeviceFlops(fp32=14.20*TFLOPS, fp16=28.40*TFLOPS, int8=56.80*TFLOPS))
-    self.assertEqual(
-      str(result),
-      "Model: MacBook Pro. Chip: Apple M3 Max. Memory: 131072MB. Flops: 14.20 TFLOPS, fp16: 28.40 TFLOPS, int8: 56.80 TFLOPS",
-    )
-
-
-if __name__ == "__main__":
-  unittest.main()
diff --git a/build/lib/exo/topology/test_map_partitions.py b/build/lib/exo/topology/test_map_partitions.py
deleted file mode 100644
index 5254915e..00000000
--- a/build/lib/exo/topology/test_map_partitions.py
+++ /dev/null
@@ -1,81 +0,0 @@
-import unittest
-from typing import List
-from exo.topology.partitioning_strategy import Partition, map_partitions_to_shards
-from exo.inference.shard import Shard
-
-
-class TestRingMemoryWeightedPartitioningStrategy(unittest.TestCase):
-  def test_map_partitions_to_shards(self):
-    partitions = [
-      Partition("node1", 0.0, 0.42857),
-      Partition("node2", 0.42857, 0.71428),
-      Partition("node3", 0.71428, 0.99999),
-    ]
-    shards = map_partitions_to_shards(partitions, 32, "model")
-    self.assertEqual(
-      shards,
-      [
-        Shard("model", 0, 12, 32),
-        Shard("model", 13, 21, 32),
-        Shard("model", 22, 31, 32),
-      ],
-    )
-
-    partitions = [
-      Partition("node1", 0.0, 0.1),
-      Partition("node2", 0.1, 0.2),
-      Partition("node3", 0.2, 1.0),
-    ]
-    shards = map_partitions_to_shards(partitions, 32, "model")
-    self.assertEqual(
-      shards,
-      [
-        Shard("model", 0, 2, 32),
-        Shard("model", 3, 5, 32),
-        Shard("model", 6, 31, 32),
-      ],
-    )
-
-    partitions = [
-      Partition("node1", 0.0, 1.0),
-    ]
-    shards = map_partitions_to_shards(partitions, 32, "model")
-    self.assertEqual(
-      shards,
-      [
-        Shard("model", 0, 31, 32),
-      ],
-    )
-
-    partitions = []
-    shards = map_partitions_to_shards(partitions, 32, "model")
-    self.assertEqual(shards, [])
-
-  def test_broken_map_partitions_to_shards(self):
-    # this was an old broken implementation that sometimes had rounding errors!
-    def _broken_map_partitions_to_shards(partitions: List[Partition], num_layers, model_id: str):
-      shards = []
-      for i, partition in enumerate(partitions):
-        start_layer = int(partition.start*num_layers)
-        end_layer = int(partition.end*num_layers) - 1
-        shards.append(Shard(model_id, start_layer, end_layer, num_layers))
-      return shards
-
-    partitions = [
-      Partition("node1", 0.0, 0.42857),
-      Partition("node2", 0.42857, 0.71428),
-      Partition("node3", 0.71428, 0.99999),
-    ]
-    shards = _broken_map_partitions_to_shards(partitions, 32, "model")
-    self.assertEqual(
-      shards,
-      [
-        Shard("model", 0, 12, 32),
-        Shard("model", 13, 21, 32),
-        Shard("model", 22, 30, 32),
-      ],
-    )
-
-
-if __name__ == "__main__":
-  unittest.main()
diff --git a/build/lib/exo/topology/test_ring_memory_weighted_partitioning_strategy.py b/build/lib/exo/topology/test_ring_memory_weighted_partitioning_strategy.py
deleted file mode 100644
index fd466f36..00000000
--- a/build/lib/exo/topology/test_ring_memory_weighted_partitioning_strategy.py
+++ /dev/null
@@ -1,90 +0,0 @@
-import unittest
-from exo.topology.ring_memory_weighted_partitioning_strategy import RingMemoryWeightedPartitioningStrategy
-from exo.topology.topology import Topology
-from exo.topology.device_capabilities import DeviceCapabilities, DeviceFlops
-from exo.topology.partitioning_strategy import Partition
-
-
-class TestRingMemoryWeightedPartitioningStrategy(unittest.TestCase):
-  def test_partition(self):
-    # triangle
-    # node1 -> node2 -> node3 -> node1
-    topology = Topology()
-    topology.update_node(
-      "node1",
-      DeviceCapabilities(model="test1", chip="test1", memory=3000, flops=DeviceFlops(fp32=0, fp16=0, int8=0)),
-    )
-    topology.update_node(
-      "node2",
-      DeviceCapabilities(model="test2", chip="test2", memory=1000, flops=DeviceFlops(fp32=0, fp16=0, int8=0)),
-    )
-    topology.update_node(
-      "node3",
-      DeviceCapabilities(model="test3", chip="test3", memory=6000, flops=DeviceFlops(fp32=0, fp16=0, int8=0)),
-    )
-    topology.add_edge("node1", "node2")
-    topology.add_edge("node2", "node3")
-    topology.add_edge("node3", "node1")
-    topology.add_edge("node1", "node3")
-
-    strategy = RingMemoryWeightedPartitioningStrategy()
-    partitions = strategy.partition(topology)
-
-    self.assertEqual(len(partitions), 3)
-    self.assertEqual(
-      partitions,
-      [
-        Partition("node3", 0.0, 0.6),
-        Partition("node1", 0.6, 0.9),
-        Partition("node2", 0.9, 1.0),
-      ],
-    )
-
-  def test_partition_rounding(self):
-    # triangle
-    # node1 -> node2 -> node3 -> node1
-    topology = Topology()
-    topology.update_node(
-      "node1",
-      DeviceCapabilities(
-        model="MacBook Pro",
-        chip="test1",
-        memory=128*1024*1024*1024,
-        flops=DeviceFlops(fp32=0, fp16=0, int8=0),
-      ),
-    )
-    topology.update_node(
-      "node2",
-      DeviceCapabilities(
-        model="Mac Studio",
-        chip="test2",
-        memory=192*1024*1024*1024,
-        flops=DeviceFlops(fp32=0, fp16=0, int8=0),
-      ),
-    )
-    topology.update_node(
-      "node3",
-      DeviceCapabilities(
-        model="MacBook Pro",
-        chip="test3",
-        memory=128*1024*1024*1024,
-        flops=DeviceFlops(fp32=0, fp16=0, int8=0),
-      ),
-    )
-
-    strategy = RingMemoryWeightedPartitioningStrategy()
-    partitions = strategy.partition(topology)
-
-    self.assertEqual(len(partitions), 3)
-    self.assertEqual(
-      partitions,
-      [
-        Partition("node3", 0.0, 0.42857),
-        Partition("node1", 0.6, 0.9),
-        Partition("node2", 0.9, 1.0),
-      ],
-    )
-
-
-if __name__ == "__main__":
-  unittest.main()
diff --git a/build/lib/exo/topology/topology.py b/build/lib/exo/topology/topology.py
deleted file mode 100644
index 46b512e5..00000000
--- a/build/lib/exo/topology/topology.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from .device_capabilities import DeviceCapabilities
-from typing import Dict, Set, Optional
-
-
-class Topology:
-  def __init__(self):
-    self.nodes: Dict[str, DeviceCapabilities] = {}  # Maps node IDs to DeviceCapabilities
-    self.peer_graph: Dict[str, Set[str]] = {}  # Adjacency list representing the graph
-    self.active_node_id: Optional[str] = None
-
-  def update_node(self, node_id: str, device_capabilities: DeviceCapabilities):
-    self.nodes[node_id] = device_capabilities
-
-  def get_node(self, node_id: str) -> DeviceCapabilities:
-    return self.nodes.get(node_id)
-
-  def all_nodes(self):
-    return self.nodes.items()
-
-  def add_edge(self, node1_id: str, node2_id: str):
-    if node1_id not in self.peer_graph:
-      self.peer_graph[node1_id] = set()
-    if node2_id not in self.peer_graph:
-      self.peer_graph[node2_id] = set()
-    self.peer_graph[node1_id].add(node2_id)
-    self.peer_graph[node2_id].add(node1_id)
-
-  def get_neighbors(self, node_id: str) -> Set[str]:
-    return self.peer_graph.get(node_id, set())
-
-  def all_edges(self):
-    edges = []
-    for node, neighbors in self.peer_graph.items():
-      for neighbor in neighbors:
-        if (neighbor, node) not in edges:  # Avoid duplicate edges
-          edges.append((node, neighbor))
-    return edges
-
-  def merge(self, other: "Topology"):
-    for node_id, capabilities in other.nodes.items():
-      self.update_node(node_id, capabilities)
-    for node_id, neighbors in other.peer_graph.items():
-      for neighbor in neighbors:
-        self.add_edge(node_id, neighbor)
-
-  def __str__(self):
-    nodes_str = ", ".join(f"{node_id}: {cap}" for node_id, cap in self.nodes.items())
-    edges_str = ", ".join(f"{node}: {neighbors}" for node, neighbors in self.peer_graph.items())
-    return f"Topology(Nodes: {{{nodes_str}}}, Edges: {{{edges_str}}})"
diff --git a/build/lib/exo/viz/__init__.py b/build/lib/exo/viz/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/build/lib/exo/viz/test_topology_viz.py b/build/lib/exo/viz/test_topology_viz.py
deleted file mode 100644
index e57de1ae..00000000
--- a/build/lib/exo/viz/test_topology_viz.py
+++ /dev/null
@@ -1,129 +0,0 @@
-import asyncio
-import unittest
-from datetime import timedelta
-from exo.viz.topology_viz import TopologyViz
-from exo.topology.topology import Topology
-from exo.topology.device_capabilities import DeviceCapabilities, DeviceFlops
-from exo.topology.partitioning_strategy import Partition
-from exo.download.hf.hf_helpers import RepoProgressEvent, RepoFileProgressEvent
-
-
-def create_hf_repo_progress_event(
-  completed_files: int = 5,
-  total_files: int = 10,
-  downloaded_bytes: int = 500000000,
-  downloaded_bytes_this_session: int = 250000000,
-  total_bytes: int = 1000000000,
-  overall_speed: int = 5000000,
-  overall_eta: timedelta = timedelta(seconds=100),
-  file_progress: dict = None,
-  status: str = "in_progress"
-) -> RepoProgressEvent:
-  if file_progress is None:
-    file_progress = {
-      "file1.bin":
-        RepoFileProgressEvent(
-          repo_id="repo_id",
-          repo_revision="repo_revision",
-          file_path="file1.bin",
-          downloaded=100000000,
-          downloaded_this_session=50000000,
-          total=200000000,
-          speed=1000000,
-          eta=timedelta(seconds=100),
-          status="in_progress"
-        ), "file2.bin":
-          RepoFileProgressEvent(
-            repo_id="repo_id",
-            repo_revision="repo_revision",
-            file_path="file2.bin",
-            downloaded=200000000,
-            downloaded_this_session=100000000,
-            total=200000000,
-            speed=2000000,
-            eta=timedelta(seconds=0),
-            status="complete"
-          )
-    }
-
-  return RepoProgressEvent(
-    repo_id="repo_id",
-    repo_revision="repo_revision",
-    completed_files=completed_files,
-    total_files=total_files,
-    downloaded_bytes=downloaded_bytes,
-    downloaded_bytes_this_session=downloaded_bytes_this_session,
-    total_bytes=total_bytes,
-    overall_speed=overall_speed,
-    overall_eta=overall_eta,
-    file_progress=file_progress,
-    status=status
-  )
-
-
-class TestNodeViz(unittest.IsolatedAsyncioTestCase):
-  async def asyncSetUp(self):
-    self.topology = Topology()
-    self.topology.update_node(
-      "node1",
-      DeviceCapabilities(model="ModelA", chip="ChipA", memory=8*1024, flops=DeviceFlops(fp32=1.0, fp16=2.0, int8=4.0)),
-    )
-    self.topology.update_node(
-      "node2",
-      DeviceCapabilities(model="ModelB", chip="ChipB", memory=16*1024, flops=DeviceFlops(fp32=2.0, fp16=4.0, int8=8.0)),
-    )
-    self.topology.update_node(
-      "node3",
-      DeviceCapabilities(model="ModelC", chip="ChipC", memory=32*1024, flops=DeviceFlops(fp32=4.0, fp16=8.0, int8=16.0)),
-    )
-    self.topology.update_node(
-      "node4",
-      DeviceCapabilities(model="ModelD", chip="ChipD", memory=64*1024, flops=DeviceFlops(fp32=8.0, fp16=16.0, int8=32.0)),
-    )
-
-    self.top_viz = TopologyViz()
-    await asyncio.sleep(2)  # Simulate running for a short time
-
-  async def test_layout_generation(self):
-    # self.top_viz._generate_layout()
-    self.top_viz.refresh()
-    import time
-
-    time.sleep(2)
-    self.top_viz.update_visualization(
-      self.topology,
-      [
-        Partition("node1", 0, 0.2),
-        Partition("node4", 0.2, 0.4),
-        Partition("node2", 0.4, 0.8),
-        Partition("node3", 0.8, 0.9),
-      ],
-      "node1",
-      {
-        "node1": create_hf_repo_progress_event(),
-        "node2": create_hf_repo_progress_event(),
-        "node3": create_hf_repo_progress_event(),
-        "node4": create_hf_repo_progress_event(),
-      },
-    )
-    time.sleep(2)
-    self.topology.active_node_id = "node3"
-    self.top_viz.update_visualization(
-      self.topology,
-      [
-        Partition("node1", 0, 0.3),
-        Partition("node5", 0.3, 0.5),
-        Partition("node2", 0.5, 0.7),
-        Partition("node4", 0.7, 0.9),
-      ],
-      "node5",
-      {
-        "node1": create_hf_repo_progress_event(),
-        "node5": create_hf_repo_progress_event(),
-      },
-    )
-    time.sleep(2)
-
-
-if __name__ == "__main__":
-  unittest.main()
diff --git a/build/lib/exo/viz/topology_viz.py b/build/lib/exo/viz/topology_viz.py
deleted file mode 100644
index b9910e9d..00000000
--- a/build/lib/exo/viz/topology_viz.py
+++ /dev/null
@@ -1,307 +0,0 @@
-import math
-from collections import OrderedDict
-from typing import List, Optional, Tuple, Dict
-from exo.helpers import exo_text, pretty_print_bytes, pretty_print_bytes_per_second
-from exo.topology.topology import Topology
-from exo.topology.partitioning_strategy import Partition
-from exo.download.hf.hf_helpers import RepoProgressEvent
-from exo.topology.device_capabilities import UNKNOWN_DEVICE_CAPABILITIES
-from rich.console import Console, Group
-from rich.text import Text
-from rich.live import Live
-from rich.style import Style
-from rich.table import Table
-from rich.layout import Layout
-from rich.syntax import Syntax
-from rich.panel import Panel
-from rich.markdown import Markdown
-
-
-class TopologyViz:
-  def __init__(self, chatgpt_api_endpoints: List[str] = [], web_chat_urls: List[str] = []):
-    self.chatgpt_api_endpoints = chatgpt_api_endpoints
-    self.web_chat_urls = web_chat_urls
-    self.topology = Topology()
-    self.partitions: List[Partition] = []
-    self.node_id = None
-    self.node_download_progress: Dict[str, RepoProgressEvent] = {}
-    self.requests: OrderedDict[str, Tuple[str, str]] = {}
-
-    self.console = Console()
-    self.layout = Layout()
-    self.layout.split(Layout(name="main"), Layout(name="prompt_output", size=15), Layout(name="download", size=25))
-    self.main_panel = Panel(self._generate_main_layout(), title="Exo Cluster (0 nodes)", border_style="bright_yellow")
-    self.prompt_output_panel = Panel("", title="Prompt and Output", border_style="green")
-    self.download_panel = Panel("", title="Download Progress", border_style="cyan")
-    self.layout["main"].update(self.main_panel)
-    self.layout["prompt_output"].update(self.prompt_output_panel)
-    self.layout["download"].update(self.download_panel)
-
-    # Initially hide the prompt_output panel
-    self.layout["prompt_output"].visible = False
-    self.live_panel = Live(self.layout, auto_refresh=False, console=self.console)
-    self.live_panel.start()
-
-  def update_visualization(self, topology: Topology, partitions: List[Partition], node_id: Optional[str] = None, node_download_progress: Dict[str, RepoProgressEvent] = {}):
-    self.topology = topology
-    self.partitions = partitions
-    self.node_id = node_id
-    if node_download_progress:
-      self.node_download_progress = node_download_progress
-    self.refresh()
-
-  def update_prompt(self, request_id: str, prompt: Optional[str] = None):
-    if request_id in self.requests:
-      self.requests[request_id] = [prompt, self.requests[request_id][1]]
-    else:
-      self.requests[request_id] = [prompt, ""]
-    self.refresh()
-
-  def update_prompt_output(self, request_id: str, output: Optional[str] = None):
-    if request_id in self.requests:
-      self.requests[request_id] = [self.requests[request_id][0], output]
-    else:
-      self.requests[request_id] = ["", output]
-    self.refresh()
-
-  def refresh(self):
-    self.main_panel.renderable = self._generate_main_layout()
-    # Update the panel title with the number of nodes and partitions
-    node_count = len(self.topology.nodes)
-    self.main_panel.title = f"Exo Cluster ({node_count} node{'s' if node_count != 1 else ''})"
-
-    # Update and show/hide prompt and output panel
-    if any(r[0] or r[1] for r in self.requests.values()):
-      self.prompt_output_panel = self._generate_prompt_output_layout()
-      self.layout["prompt_output"].update(self.prompt_output_panel)
-      self.layout["prompt_output"].visible = True
-    else:
-      self.layout["prompt_output"].visible = False
-
-    # Only show download_panel if there are in-progress downloads
-    if any(progress.status == "in_progress" for progress in self.node_download_progress.values()):
-      self.download_panel.renderable = self._generate_download_layout()
-      self.layout["download"].visible = True
-    else:
-      self.layout["download"].visible = False
-
-    self.live_panel.update(self.layout, refresh=True)
-
-  def _generate_prompt_output_layout(self) -> Panel:
-    content = []
-    requests = list(self.requests.values())[-3:]  # Get the 3 most recent requests
-    max_width = self.console.width - 6  # Full width minus padding and icon
-    max_lines = 13  # Maximum number of lines for the entire panel content
-
-    for (prompt, output) in reversed(requests):
-      prompt_icon, output_icon = "💬️", "🤖"
-
-      # Process prompt
-      prompt_lines = prompt.split('\n')
-      if len(prompt_lines) > max_lines // 2:
-        prompt_lines = prompt_lines[:max_lines//2 - 1] + ['...']
-      prompt_text = Text(f"{prompt_icon} ", style="bold bright_blue")
-      prompt_text.append('\n'.join(line[:max_width] for line in prompt_lines), style="white")
-
-      # Process output
-      output_lines = output.split('\n')
-      remaining_lines = max_lines - len(prompt_lines) - 2  # -2 for spacing
-      if len(output_lines) > remaining_lines:
-        output_lines = output_lines[:remaining_lines - 1] + ['...']
-      output_text = Text(f"\n{output_icon} ", style="bold bright_magenta")
-      output_text.append('\n'.join(line[:max_width] for line in output_lines), style="white")
-
-      content.append(prompt_text)
-      content.append(output_text)
-      content.append(Text())  # Empty line between entries
-
-    return Panel(
-      Group(*content),
-      title="",
-      border_style="cyan",
-      height=15,  # Increased height to accommodate multiple lines
-      expand=True  # Allow the panel to expand to full width
-    )
-
-  def _generate_main_layout(self) -> str:
-    # Calculate visualization parameters
-    num_partitions = len(self.partitions)
-    radius_x = 30
-    radius_y = 12
-    center_x, center_y = 50, 24  # Increased center_y to add more space
-
-    # Generate visualization
-    visualization = [[" " for _ in range(100)] for _ in range(48)]  # Increased height to 48
-
-    # Add exo_text at the top in bright yellow
-    exo_lines = exo_text.split("\n")
-    yellow_style = Style(color="bright_yellow")
-    max_line_length = max(len(line) for line in exo_lines)
-    for i, line in enumerate(exo_lines):
-      centered_line = line.center(max_line_length)
-      start_x = (100-max_line_length) // 2 + 15
-      colored_line = Text(centered_line, style=yellow_style)
-      for j, char in enumerate(str(colored_line)):
-        if 0 <= start_x + j < 100 and i < len(visualization):
-          visualization[i][start_x + j] = char
-
-    # Display chatgpt_api_endpoints and web_chat_urls
-    info_lines = []
-    if len(self.web_chat_urls) > 0:
-      info_lines.append(f"Web Chat URL (tinychat): {' '.join(self.web_chat_urls[:1])}")
-    if len(self.chatgpt_api_endpoints) > 0:
-      info_lines.append(f"ChatGPT API endpoint: {' '.join(self.chatgpt_api_endpoints[:1])}")
-
-    info_start_y = len(exo_lines) + 1
-    for i, line in enumerate(info_lines):
-      start_x = (100 - len(line)) // 2 + 15
-      for j, char in enumerate(line):
-        if 0 <= start_x + j < 100 and info_start_y + i < 48:
-          visualization[info_start_y + i][start_x + j] = char
-
-    # Calculate total FLOPS and position on the bar
-    total_flops = sum(self.topology.nodes.get(partition.node_id, UNKNOWN_DEVICE_CAPABILITIES).flops.fp16 for partition in self.partitions)
-    bar_pos = (math.tanh(total_flops**(1/3)/2.5 - 2) + 1)
-
-    # Add GPU poor/rich bar
-    bar_width = 30
-    bar_start_x = (100-bar_width) // 2
-    bar_y = info_start_y + len(info_lines) + 1
-
-    # Create a gradient bar using emojis
-    gradient_bar = Text()
-    emojis = ["🟥", "🟧", "🟨", "🟩"]
-    for i in range(bar_width):
-      emoji_index = min(int(i/(bar_width/len(emojis))), len(emojis) - 1)
-      gradient_bar.append(emojis[emoji_index])
-
-    # Add the gradient bar to the visualization
-    visualization[bar_y][bar_start_x - 1] = "["
-    visualization[bar_y][bar_start_x + bar_width] = "]"
-    for i, segment in enumerate(str(gradient_bar)):
-      visualization[bar_y][bar_start_x + i] = segment
-
-    # Add labels
-    visualization[bar_y - 1][bar_start_x - 10:bar_start_x - 3] = "GPU poor"
-    visualization[bar_y - 1][bar_start_x + bar_width*2 + 2:bar_start_x + bar_width*2 + 11] = "GPU rich"
-
-    # Add position indicator and FLOPS value
-    pos_x = bar_start_x + int(bar_pos*bar_width)
-    flops_str = f"{total_flops:.2f} TFLOPS"
-    visualization[bar_y - 1][pos_x] = "▼"
-    visualization[bar_y + 1][pos_x - len(flops_str) // 2:pos_x + len(flops_str) // 2 + len(flops_str) % 2] = flops_str
-    visualization[bar_y + 2][pos_x] = "▲"
-
-    # Add an extra empty line for spacing
-    bar_y += 4
-
-    for i, partition in enumerate(self.partitions):
-      device_capabilities = self.topology.nodes.get(partition.node_id, UNKNOWN_DEVICE_CAPABILITIES)
-
-      angle = 2*math.pi*i/num_partitions
-      x = int(center_x + radius_x*math.cos(angle))
-      y = int(center_y + radius_y*math.sin(angle))
-
-      # Place node with different color for active node and this node
-      if partition.node_id == self.topology.active_node_id:
-        visualization[y][x] = "🔴"
-      elif partition.node_id == self.node_id:
-        visualization[y][x] = "🟢"
-      else:
-        visualization[y][x] = "🔵"
-
-      # Place node info (model, memory, TFLOPS, partition) on three lines
-      node_info = [
-        f"{device_capabilities.model} {device_capabilities.memory // 1024}GB",
-        f"{device_capabilities.flops.fp16}TFLOPS",
-        f"[{partition.start:.2f}-{partition.end:.2f}]",
-      ]
-
-      # Calculate info position based on angle
-      info_distance_x = radius_x + 6
-      info_distance_y = radius_y + 3
-      info_x = int(center_x + info_distance_x*math.cos(angle))
-      info_y = int(center_y + info_distance_y*math.sin(angle))
-
-      # Adjust text position to avoid overwriting the node icon and prevent cutoff
-      if info_x < x:
-        info_x = max(0, x - len(max(node_info, key=len)) - 1)
-      elif info_x > x:
-        info_x = min(99 - len(max(node_info, key=len)), info_x)
-
-      # Adjust for top and bottom nodes
-      if 5*math.pi/4 < angle < 7*math.pi/4:
-        info_x += 4
-      elif math.pi/4 < angle < 3*math.pi/4:
-        info_x += 3
-        info_y -= 2
-
-      for j, line in enumerate(node_info):
-        for k, char in enumerate(line):
-          if 0 <= info_y + j < 48 and 0 <= info_x + k < 100:
-            if info_y + j != y or info_x + k != x:
-              visualization[info_y + j][info_x + k] = char
-
-      # Draw line to next node
-      next_i = (i+1) % num_partitions
-      next_angle = 2*math.pi*next_i/num_partitions
-      next_x = int(center_x + radius_x*math.cos(next_angle))
-      next_y = int(center_y + radius_y*math.sin(next_angle))
-
-      # Simple line drawing
-      steps = max(abs(next_x - x), abs(next_y - y))
-      for step in range(1, steps):
-        line_x = int(x + (next_x-x)*step/steps)
-        line_y = int(y + (next_y-y)*step/steps)
-        if 0 <= line_y < 48 and 0 <= line_x < 100:
-          visualization[line_y][line_x] = "-"
-
-    # Convert to string
-    return "\n".join("".join(str(char) for char in row) for row in visualization)
-
-  def _generate_download_layout(self) -> Table:
-    summary = Table(show_header=False, box=None, padding=(0, 1), expand=True)
-    summary.add_column("Info", style="cyan", no_wrap=True, ratio=50)
-    summary.add_column("Progress", style="cyan", no_wrap=True, ratio=40)
-    summary.add_column("Percentage", style="cyan", no_wrap=True, ratio=10)
-
-    # Current node download progress
-    if self.node_id in self.node_download_progress:
-      download_progress = self.node_download_progress[self.node_id]
-      title = f"Downloading model {download_progress.repo_id}@{download_progress.repo_revision} ({download_progress.completed_files}/{download_progress.total_files}):"
-      summary.add_row(Text(title, style="bold"))
-      progress_info = f"{pretty_print_bytes(download_progress.downloaded_bytes)} / {pretty_print_bytes(download_progress.total_bytes)} ({pretty_print_bytes_per_second(download_progress.overall_speed)})"
-      summary.add_row(progress_info)
-
-      eta_info = f"{download_progress.overall_eta}"
-      summary.add_row(eta_info)
-
-      summary.add_row("")  # Empty row for spacing
-
-      for file_path, file_progress in download_progress.file_progress.items():
-        if file_progress.status != "complete":
-          progress = int(file_progress.downloaded/file_progress.total*30)
-          bar = f"[{'=' * progress}{' ' * (30 - progress)}]"
-          percentage = f"{file_progress.downloaded / file_progress.total * 100:.0f}%"
-          summary.add_row(Text(file_path[:30], style="cyan"), bar, percentage)
-
-    summary.add_row("")  # Empty row for spacing
-
-    # Other nodes download progress summary
-    summary.add_row(Text("Other Nodes Download Progress:", style="bold"))
-    for node_id, progress in self.node_download_progress.items():
-      if node_id != self.node_id:
-        device = self.topology.nodes.get(node_id)
-        partition = next((p for p in self.partitions if p.node_id == node_id), None)
-        partition_info = f"[{partition.start:.2f}-{partition.end:.2f}]" if partition else ""
-        percentage = progress.downloaded_bytes/progress.total_bytes*100 if progress.total_bytes > 0 else 0
-        speed = pretty_print_bytes_per_second(progress.overall_speed)
-        device_info = f"{device.model if device else 'Unknown Device'} {device.memory // 1024 if device else '?'}GB {partition_info}"
-        progress_info = f"{progress.repo_id}@{progress.repo_revision} ({speed})"
-        progress_bar = f"[{'=' * int(percentage // 3.33)}{' ' * (30 - int(percentage // 3.33))}]"
-        percentage_str = f"{percentage:.1f}%"
-        eta_str = f"{progress.overall_eta}"
-        summary.add_row(device_info, progress_info, percentage_str)
-        summary.add_row("", progress_bar, eta_str)
-
-    return summary

← fff8a1a6 fix inference engine for inference state  ·  back to Exo  ·  remove prints and fix download progress for SD 9986fb86 →