diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..18282962
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,10 @@
+# 开发资料不进 Docker 构建上下文。
+# Dockerfile 采用白名单 COPY,这里再加一层保险。
+skills/
+docs/
+front/
+assets/
+.github/
+.git/
+logs/
+*.md
diff --git a/.github/workflows/FNOS.yml b/.github/workflows/FNOS.yml
index 5b1b275e..78a3bf43 100644
--- a/.github/workflows/FNOS.yml
+++ b/.github/workflows/FNOS.yml
@@ -8,40 +8,11 @@ on:
paths:
- fn_version
-jobs:
- build-frontend:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout frontend code
- uses: actions/checkout@v3
- with:
- repository: dongshull/Docker-Copilot-React
- path: './frontend'
- ref: 'master'
-
- - name: Setup Node.js
- uses: actions/setup-node@v3
- with:
- node-version: '22'
- cache: 'npm'
- cache-dependency-path: |
- frontend/package-lock.json
- frontend/package.json
-
- - name: Build Frontend
- run: |
- cd frontend
- npm ci
- npm run build
-
- - name: Upload Frontend Artifacts
- uses: actions/upload-artifact@v4
- with:
- name: my-artifact-front-dist
- path: frontend/dist
+permissions:
+ contents: write
+jobs:
build-backend:
- needs: [ build-frontend ]
runs-on: ubuntu-latest
strategy:
matrix:
@@ -50,15 +21,6 @@ jobs:
- name: Checkout code back end
uses: actions/checkout@v3
- - name: Download Frontend Artifacts
- uses: actions/download-artifact@v4
- with:
- pattern: my-artifact-front-*
- merge-multiple: true
- path: front
-
- - run: ls -R front
-
- name: Get version from file
id: get_version
run: |
@@ -75,7 +37,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
- go-version: '1.23.4'
+ go-version: '1.24.1'
cache: false
- name: Build for ${{ matrix.arch }}
@@ -84,7 +46,10 @@ jobs:
- name: Compress binary
run: |
- tar -czvf dockerCopilot-${{ matrix.arch }}.tar.gz dist/linux/${{ matrix.arch }}/dockerCopilot-new
+ tar -czvf dockerCopilot-${{ matrix.arch }}.tar.gz -C dist/linux/${{ matrix.arch }} dockerCopilot-new
+
+ - name: Generate checksum
+ run: sha256sum dockerCopilot-${{ matrix.arch }}.tar.gz > dockerCopilot-${{ matrix.arch }}.tar.gz.sha256
- name: List current directory
run: ls -la
@@ -108,12 +73,16 @@ jobs:
run: |
gh release create ${{ env.version }} \
--title "${{ env.version }}" \
- --notes "Release of version ${{ env.version }}"
+ --notes "Release of version ${{ env.version }}" \
+ || gh release view ${{ env.version }} >/dev/null
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Asset for ${{ matrix.arch }}
run: |
- gh release upload ${{ env.version }} dockerCopilot-${{ matrix.arch }}.tar.gz
+ gh release upload ${{ env.version }} \
+ dockerCopilot-${{ matrix.arch }}.tar.gz \
+ dockerCopilot-${{ matrix.arch }}.tar.gz.sha256 \
+ --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/UGREEN.yml b/.github/workflows/UGREEN.yml
index 1cc21b95..059d1c74 100644
--- a/.github/workflows/UGREEN.yml
+++ b/.github/workflows/UGREEN.yml
@@ -8,40 +8,11 @@ on:
paths:
- ugreen_version
-jobs:
- build-frontend:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout frontend code
- uses: actions/checkout@v3
- with:
- repository: dongshull/Docker-Copilot-React
- path: './frontend'
- ref: 'master'
-
- - name: Setup Node.js
- uses: actions/setup-node@v3
- with:
- node-version: '22'
- cache: 'npm'
- cache-dependency-path: |
- frontend/package-lock.json
- frontend/package.json
-
- - name: Build Frontend
- run: |
- cd frontend
- npm ci
- npm run build
-
- - name: Upload Frontend Artifacts
- uses: actions/upload-artifact@v4
- with:
- name: my-artifact-front-dist
- path: frontend/dist
+permissions:
+ contents: write
+jobs:
build-backend:
- needs: [ build-frontend ]
runs-on: ubuntu-latest
strategy:
matrix:
@@ -50,15 +21,6 @@ jobs:
- name: Checkout code back end
uses: actions/checkout@v3
- - name: Download Frontend Artifacts
- uses: actions/download-artifact@v4
- with:
- pattern: my-artifact-front-*
- merge-multiple: true
- path: front
-
- - run: ls -R front
-
- name: Get version from file
id: get_version
run: |
@@ -75,7 +37,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
- go-version: '1.23.4'
+ go-version: '1.24.1'
cache: false
- name: Build for ${{ matrix.arch }}
@@ -84,7 +46,10 @@ jobs:
- name: Compress binary
run: |
- tar -czvf dockerCopilot-${{ matrix.arch }}.tar.gz dist/linux/${{ matrix.arch }}/dockerCopilot-new
+ tar -czvf dockerCopilot-${{ matrix.arch }}.tar.gz -C dist/linux/${{ matrix.arch }} dockerCopilot-new
+
+ - name: Generate checksum
+ run: sha256sum dockerCopilot-${{ matrix.arch }}.tar.gz > dockerCopilot-${{ matrix.arch }}.tar.gz.sha256
- name: List current directory
run: ls -la
@@ -108,12 +73,16 @@ jobs:
run: |
gh release create ${{ env.version }} \
--title "${{ env.version }}" \
- --notes "Release of version ${{ env.version }}"
+ --notes "Release of version ${{ env.version }}" \
+ || gh release view ${{ env.version }} >/dev/null
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Asset for ${{ matrix.arch }}
run: |
- gh release upload ${{ env.version }} dockerCopilot-${{ matrix.arch }}.tar.gz
+ gh release upload ${{ env.version }} \
+ dockerCopilot-${{ matrix.arch }}.tar.gz \
+ dockerCopilot-${{ matrix.arch }}.tar.gz.sha256 \
+ --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..a0c8009f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,33 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - latest
+ - dev
+ pull_request:
+ branches:
+ - latest
+ - dev
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ cache: true
+
+ - name: Build
+ run: go build ./...
+
+ - name: Vet
+ run: go vet ./...
+
+ - name: Test
+ run: go test ./... -count=1
diff --git a/.github/workflows/docker-build-push-UGREEN-stable.yml b/.github/workflows/docker-build-push-UGREEN-stable.yml
index 18dd13da..d4401fce 100644
--- a/.github/workflows/docker-build-push-UGREEN-stable.yml
+++ b/.github/workflows/docker-build-push-UGREEN-stable.yml
@@ -9,39 +9,7 @@ on:
- ugreen_version
jobs:
- build-frontend:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout frontend code
- uses: actions/checkout@v3
- with:
- repository: dongshull/Docker-Copilot-React
- path: './frontend'
- ref: 'master'
-
- - name: Setup Node.js
- uses: actions/setup-node@v3
- with:
- node-version: '22'
- cache: 'npm'
- cache-dependency-path: |
- frontend/package-lock.json
- frontend/package.json
-
- - name: Build Frontend
- run: |
- cd frontend
- npm ci
- npm run build
-
- - name: Upload Frontend Artifacts
- uses: actions/upload-artifact@v4
- with:
- name: my-artifact-front-dist
- path: frontend/dist
-
build-backend:
- needs: [ build-frontend ]
runs-on: ubuntu-latest
strategy:
matrix:
@@ -50,15 +18,6 @@ jobs:
- name: Checkout code back end
uses: actions/checkout@v3
- - name: Download Frontend Artifacts
- uses: actions/download-artifact@v4
- with:
- pattern: my-artifact-front-*
- merge-multiple: true
- path: front
-
- - run: ls -R front
-
- name: Get version from file
id: get_version
run: |
@@ -75,7 +34,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
- go-version: '1.23.4'
+ go-version: '1.24.1'
cache: false
- name: Build for ${{ matrix.arch }}
@@ -96,7 +55,7 @@ jobs:
docker-setup:
runs-on: ubuntu-latest
- needs: [ build-frontend, build-backend ]
+ needs: build-backend
steps:
- name: Checkout code Now Code
uses: actions/checkout@v3
diff --git a/.github/workflows/docker-build-push-UGREEN.yml b/.github/workflows/docker-build-push-UGREEN.yml
index 6d27da34..9eec4e2a 100644
--- a/.github/workflows/docker-build-push-UGREEN.yml
+++ b/.github/workflows/docker-build-push-UGREEN.yml
@@ -7,39 +7,7 @@ on:
- latest-dev
jobs:
- build-frontend:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout frontend code
- uses: actions/checkout@v3
- with:
- repository: dongshull/Docker-Copilot-React
- path: './frontend'
- ref: 'master'
-
- - name: Setup Node.js
- uses: actions/setup-node@v3
- with:
- node-version: '22'
- cache: 'npm'
- cache-dependency-path: |
- frontend/package-lock.json
- frontend/package.json
-
- - name: Build Frontend
- run: |
- cd frontend
- npm ci
- npm run build
-
- - name: Upload Frontend Artifacts
- uses: actions/upload-artifact@v4
- with:
- name: my-artifact-front-dist
- path: frontend/dist
-
build-backend:
- needs: [ build-frontend ]
runs-on: ubuntu-latest
strategy:
matrix:
@@ -48,15 +16,6 @@ jobs:
- name: Checkout code back end
uses: actions/checkout@v3
- - name: Download Frontend Artifacts
- uses: actions/download-artifact@v4
- with:
- pattern: my-artifact-front-*
- merge-multiple: true
- path: front
-
- - run: ls -R front
-
- name: Get version from file
id: get_version
run: |
@@ -73,7 +32,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
- go-version: '1.23.4'
+ go-version: '1.24.1'
cache: false
- name: Build for ${{ matrix.arch }}
@@ -94,7 +53,7 @@ jobs:
docker-setup:
runs-on: ubuntu-latest
- needs: [ build-frontend, build-backend ]
+ needs: build-backend
steps:
- name: Checkout code Now Code
uses: actions/checkout@v3
diff --git a/.github/workflows/docker-build-push-latest-stable.yml b/.github/workflows/docker-build-push-latest-stable.yml
index 27753a4b..edf1e7e3 100644
--- a/.github/workflows/docker-build-push-latest-stable.yml
+++ b/.github/workflows/docker-build-push-latest-stable.yml
@@ -9,39 +9,7 @@ on:
- version
jobs:
- build-frontend:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout frontend code
- uses: actions/checkout@v3
- with:
- repository: dongshull/Docker-Copilot-React
- path: './frontend'
- ref: 'master'
-
- - name: Setup Node.js
- uses: actions/setup-node@v3
- with:
- node-version: '22'
- cache: 'npm'
- cache-dependency-path: |
- frontend/package-lock.json
- frontend/package.json
-
- - name: Build Frontend
- run: |
- cd frontend
- npm ci
- npm run build
-
- - name: Upload Frontend Artifacts
- uses: actions/upload-artifact@v4
- with:
- name: my-artifact-front-dist
- path: frontend/dist
-
build-backend:
- needs: [ build-frontend ]
runs-on: ubuntu-latest
strategy:
matrix:
@@ -50,15 +18,6 @@ jobs:
- name: Checkout code back end
uses: actions/checkout@v3
- - name: Download Frontend Artifacts
- uses: actions/download-artifact@v4
- with:
- pattern: my-artifact-front-*
- merge-multiple: true
- path: front
-
- - run: ls -R front
-
- name: Get version from file
id: get_version
run: |
@@ -75,7 +34,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
- go-version: '1.23.4'
+ go-version: '1.24.1'
cache: false
- name: Build for ${{ matrix.arch }}
@@ -96,7 +55,7 @@ jobs:
docker-setup:
runs-on: ubuntu-latest
- needs: [ build-frontend, build-backend ]
+ needs: build-backend
steps:
- name: Checkout code Now Code
uses: actions/checkout@v3
diff --git a/.github/workflows/docker-build-push-latest.yml b/.github/workflows/docker-build-push-latest.yml
index b9f1f705..1d0e0569 100644
--- a/.github/workflows/docker-build-push-latest.yml
+++ b/.github/workflows/docker-build-push-latest.yml
@@ -7,39 +7,7 @@ on:
- latest-dev
jobs:
- build-frontend:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout frontend code
- uses: actions/checkout@v3
- with:
- repository: dongshull/Docker-Copilot-React
- path: './frontend'
- ref: 'master'
-
- - name: Setup Node.js
- uses: actions/setup-node@v3
- with:
- node-version: '22'
- cache: 'npm'
- cache-dependency-path: |
- frontend/package-lock.json
- frontend/package.json
-
- - name: Build Frontend
- run: |
- cd frontend
- npm ci
- npm run build
-
- - name: Upload Frontend Artifacts
- uses: actions/upload-artifact@v4
- with:
- name: my-artifact-front-dist
- path: frontend/dist
-
build-backend:
- needs: [ build-frontend ]
runs-on: ubuntu-latest
strategy:
matrix:
@@ -48,15 +16,6 @@ jobs:
- name: Checkout code back end
uses: actions/checkout@v3
- - name: Download Frontend Artifacts
- uses: actions/download-artifact@v4
- with:
- pattern: my-artifact-front-*
- merge-multiple: true
- path: front
-
- - run: ls -R front
-
- name: Get version from file
id: get_version
run: |
@@ -73,7 +32,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
- go-version: '1.23.4'
+ go-version: '1.24.1'
cache: false
- name: Build for ${{ matrix.arch }}
@@ -94,7 +53,7 @@ jobs:
docker-setup:
runs-on: ubuntu-latest
- needs: [ build-frontend, build-backend ]
+ needs: build-backend
steps:
- name: Checkout code Now Code
uses: actions/checkout@v3
diff --git a/.github/workflows/latest.yml b/.github/workflows/latest.yml
index 58d72eed..a468fb21 100644
--- a/.github/workflows/latest.yml
+++ b/.github/workflows/latest.yml
@@ -8,40 +8,11 @@ on:
paths:
- version
-jobs:
- build-frontend:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout frontend code
- uses: actions/checkout@v3
- with:
- repository: dongshull/Docker-Copilot-React
- path: './frontend'
- ref: 'master'
-
- - name: Setup Node.js
- uses: actions/setup-node@v3
- with:
- node-version: '22'
- cache: 'npm'
- cache-dependency-path: |
- frontend/package-lock.json
- frontend/package.json
-
- - name: Build Frontend
- run: |
- cd frontend
- npm ci
- npm run build
-
- - name: Upload Frontend Artifacts
- uses: actions/upload-artifact@v4
- with:
- name: my-artifact-front-dist
- path: frontend/dist
+permissions:
+ contents: write
+jobs:
build-backend:
- needs: [ build-frontend ]
runs-on: ubuntu-latest
strategy:
matrix:
@@ -50,15 +21,6 @@ jobs:
- name: Checkout code back end
uses: actions/checkout@v3
- - name: Download Frontend Artifacts
- uses: actions/download-artifact@v4
- with:
- pattern: my-artifact-front-*
- merge-multiple: true
- path: front
-
- - run: ls -R front
-
- name: Get version from file
id: get_version
run: |
@@ -75,7 +37,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
- go-version: '1.23.4'
+ go-version: '1.24.1'
cache: false
- name: Build for ${{ matrix.arch }}
@@ -84,7 +46,10 @@ jobs:
- name: Compress binary
run: |
- tar -czvf dockerCopilot-${{ matrix.arch }}.tar.gz dist/linux/${{ matrix.arch }}/dockerCopilot-new
+ tar -czvf dockerCopilot-${{ matrix.arch }}.tar.gz -C dist/linux/${{ matrix.arch }} dockerCopilot-new
+
+ - name: Generate checksum
+ run: sha256sum dockerCopilot-${{ matrix.arch }}.tar.gz > dockerCopilot-${{ matrix.arch }}.tar.gz.sha256
- name: List current directory
run: ls -la
@@ -108,12 +73,16 @@ jobs:
run: |
gh release create ${{ env.version }} \
--title "${{ env.version }}" \
- --notes "Release of version ${{ env.version }}"
+ --notes "Release of version ${{ env.version }}" \
+ || gh release view ${{ env.version }} >/dev/null
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Asset for ${{ matrix.arch }}
run: |
- gh release upload ${{ env.version }} dockerCopilot-${{ matrix.arch }}.tar.gz
+ gh release upload ${{ env.version }} \
+ dockerCopilot-${{ matrix.arch }}.tar.gz \
+ dockerCopilot-${{ matrix.arch }}.tar.gz.sha256 \
+ --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/README.md b/README.md
index bcd86172..e1db1d2c 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,40 @@
# dockerCopilot
+
-
+
+
+一个主打便捷的 Docker 容器管理工具,现已支持所有平台。
+
+> **维护说明**:本项目自 v2.2.0 起由 [DongShull](https://github.com/DongShull) 独立维护(fork 自 onlyLTY/dockerCopilot v2.1.3),
+> 适配飞牛 FnOS(fn_version)与绿联 UGREEN 构建,遵循 AGPLv3 协议分发。
+> 上游自 2026-07 起不再更新。
-# 介绍
+# 功能
-一个主打便捷的docker容器管理工具,现在已经支持所有平台。
-已经实现:
-1. 一键更新容器
-2. 指定镜像和tag更新
+1. 一键更新容器(失败自动回滚,成功自动清理旧镜像)
+2. 指定镜像和 tag 更新
3. 启动、停止、重启容器
4. 重命名容器
-5. 删除无TAG镜像
-6. 删除未使用镜像
-7. 更新进度查看
-8. 备份容器设置
+5. 批量删除无 TAG 镜像(`DELETE /api/images?filter=untagged` / MCP `dc_clean_images`)
+6. 批量删除未使用镜像(`DELETE /api/images?filter=unused` / MCP `dc_clean_images`)
+7. 更新进度查看(SSE 实时推送 / 轮询双通道,支持取消,重启不丢历史)
+8. 备份容器设置(JSON / docker-compose 双格式;每次备份独立文件,超出 `BACKUP_KEEP` 自动清理)
9. 恢复容器设置
+10. 定时自动更新白名单
+11. 操作审计日志与 webhook 通知
+12. 内置 MCP 服务(Streamable HTTP),Agent 可直接调用全部能力;HTTP API 同时保留
+13. 程序自更新(下载 + SHA256 校验;新版连续启动失败自动回退上一版)
## 使用
-docker compose 安装
+docker compose 安装(推荐;无需 privileged,仅挂载 docker.sock)
```
services:
dockercopilot:
container_name: dockercopilot
restart: always
- privileged: true
network_mode: bridge
ports:
- 12712:12712
@@ -36,11 +44,78 @@ services:
environment:
- TZ=Asia/Shanghai
- DOCKER_HOST=unix:///var/run/docker.sock
- - secretKey=密码,不少于八位且非纯数字
- image: 0nlylty/dockercopilot:latest
+ - secretKey=密码,必须大于八位且非纯数字
+ # 改成你的 Docker Hub 账号;或使用 GitHub Release 的二进制自行构建
+ image: your-dockerhub-user/dockercopilot:latest
```
+> 说明:容器内以 root 运行并挂载 `docker.sock` 即可管理 Docker,**不需要** `privileged: true`。
+> 如需进一步降权,可为 docker.sock 配置 `group_add` 并使用非 root 用户运行。
+>
+> 本项目**已移除网页前端**,只提供 HTTP API 与 MCP;建议把端口绑定到回环或内网,
+> 例如 `127.0.0.1:12712:12712`,因为 `/mcp` 等价于 Docker 控制面。
+
+## 环境变量
+
+| 变量 | 默认 | 说明 |
+|---|---|---|
+| `secretKey` | (必填) | 登录密钥(兼作 JWT 签名密钥),必须大于 8 位且非纯数字;缺失会拒绝启动 |
+| `DOCKER_HOST` | unix:///var/run/docker.sock | Docker 守护进程地址 |
+| `BACKUP_DIR` | /data/backups | 容器备份目录(优先级最高;否则用 Settings 页配置值) |
+| `BACKUP_KEEP` | 7 | 每种备份(JSON / compose)保留份数,超出自动清理最旧的 |
+| `DelOldContainer` | true | 更新成功后删除旧容器(设为 `false` 保留) |
+| `CLEANUP_OLD_IMAGE` | true | 更新成功后清理不再被引用的旧镜像(`false` 关闭) |
+| `UPDATE_REPO` | DongShull/dockerCopilot | 版本检查与自更新仓库 |
+| `UPDATE_BRANCH` | latest | 版本检查分支 |
+| `UPDATE_SKIP_CHECKSUM` | false | 设为 `true` 时跳过自更新 SHA256 校验(仅用于过渡旧版本,不推荐) |
+| `githubProxy` | 空 | GitHub 代理前缀,如 `https://ghproxy.example/` |
+| `REGISTRY_INSECURE` | false | 设为 `true` 时 registry 检测跳过 TLS 校验(默认严格校验) |
+| `PROGRESS_STORE` | /data/config/progress.json | 任务进度持久化文件 |
+| `PROGRESS_TTL_HOURS` | 24 | 已完成任务保留小时数,超期自动清理 |
+| `NOTIFY_WEBHOOK_URL` | 空 | 任务完成通知 webhook(POST JSON) |
+| `AUDIT_LOG` | /data/logs/audit.jsonl | 审计日志(JSON Lines) |
+| `AUTOUPDATE_CRON` | 空 | 自动更新周期(cron 表达式,空=关闭),如 `0 4 * * *` |
+| `AUTOUPDATE_WHITELIST` | 空 | 自动更新容器白名单(逗号分隔;`*` 表示全部有更新的容器) |
+| `MCP_TOKEN` | 空 | `/mcp` 的访问令牌;为空时回退到 `secretKey`,设为 `off` 显式关闭鉴权(仅限隔离内网) |
+
+## 接入方式
+
+### MCP(推荐给 Agent)
+
+`http://:12712/mcp`,Streamable HTTP,27 个 `dc_*` 工具覆盖容器、镜像、备份恢复、
+进度、设置与自更新。鉴权支持 `Authorization: Bearer `、`X-MCP-Token` 与 `?token=`。
+详见 [docs/MCP.md](docs/MCP.md)。
+
+### HTTP API
+
+- `POST /api/auth` 登录换 JWT(表单编码:`secretKey=<值>`;注意 `/api/auth` 与 update/rename 等接口只接受 form/query 参数,restore、settings 接口收 JSON body)
+- `GET /api/containers` / `POST /api/container/:id/{start,stop,restart,rename,update}`
+- `GET /api/container/backup` / `GET /api/container/backup2compose` / `POST /api/container/backups/restore`
+ - 备份每次生成独立文件(`backup-<时间戳>.json` / `.yaml`),超出 `BACKUP_KEEP` 的旧备份自动清理;响应里的 `failures` 列出本次备份失败的容器
+ - restore body:`{"filename":"backup-2026-01-01.json","start":true|false,"force":false}`;`start` 省略时按备份记录的状态恢复;`force=true` 会先停止并临时改名同名容器,新容器创建/启动成功后才删除旧容器,失败会自动回滚
+- `GET /api/images` / `DELETE /api/image/:id`
+ - `DELETE /api/images?filter=untagged` 批量删除无 TAG 镜像;`?filter=unused` 批量删除未被容器引用的镜像;响应带 `removed` / `failed` 明细
+- `GET /api/progress` / `GET /api/progress/:taskid` / `POST /api/progress/:taskid/cancel` / `GET /api/progress/:taskid/events`(SSE,请求需带 `Accept: text/event-stream`)
+- `GET /api/settings` / `PUT /api/settings`
+- `GET /api/version` / `PUT /api/program`
+- `POST /api/icons` / `GET /api/icons` 自定义镜像 logo(`imageName` → 上传图片文件的映射,持久化在 `/data/config`)
+
+## 已知限制
+
+- **私有仓库镜像无法自动检查更新**:程序不持有任何 registry 凭证,私有镜像的 digest 检查会静默跳过(日志可见),`haveUpdate` 恒为 false。
+- **FnOS 版不走在线更新链**:`/api/version?type=remote` 与 `dc_check_program_update` 会返回 `fnos=true` 与提示,自更新被明确禁止;请使用 FNOS 专用发布包更新。
+- **自更新回滚依赖启动脚本**:`start.sh` 会在换入新版前保留 `dockerCopilot.bak`,新版连续 3 次启动失败(启动后 30 秒内退出)自动回退;重建容器也能回到镜像自带版本。
+
## 开发环境
-go版本:1.21+
+go 版本:1.23+(go.mod 指定)。项目不再嵌入前端,`go build ./...` 即可直接构建。
+
+```
+go build ./...
+go test ./...
+```
+
+仓库内附开发维护技能:[`skills/dockercopilot-maintainer/SKILL.md`](skills/dockercopilot-maintainer/SKILL.md)。
+该目录仅供开发使用,已通过 `.dockerignore` 排除,不会进入镜像或发布包。
+改造规划与缺陷清单见 [docs/ROADMAP.md](docs/ROADMAP.md)。
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 83592031..59eff54a 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,6 +1,6 @@
# 使用 Alpine 作为基础镜像
FROM alpine
-LABEL authors="onlyLTY"
+LABEL authors="onlyLTY, DongShull"
WORKDIR /app
ARG TARGETPLATFORM
@@ -16,9 +16,13 @@ RUN apk add --no-cache tzdata \
# 设置环境变量
ENV secretKey="" \
DOCKER_HOST="unix:///var/run/docker.sock" \
- BACKUP_DIR="/data/backups" \
TZ="Asia/Shanghai"
VOLUME ["/data"]
+
+# 健康检查:主页可访问即视为健康
+HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
+ CMD wget -q -O /dev/null http://127.0.0.1:12712/ || exit 1
+
# 设置启动命令
-CMD ["./start.sh"]
\ No newline at end of file
+CMD ["./start.sh"]
diff --git a/dockercopilot.api b/dockercopilot.api
index 0716fb6d..e5bbf831 100644
--- a/dockercopilot.api
+++ b/dockercopilot.api
@@ -60,6 +60,8 @@ type IdReq {
type ContainerRestoreReq {
Filename string `json:"filename"`
+ Start *bool `json:"start,optional"`
+ Force bool `json:"force,optional"`
}
type DelContainerBackupReq {
@@ -91,6 +93,10 @@ type VersionReq {
Type string `form:"type"`
}
+type UpdateSettingsReq {
+ BackupDir string `json:"backupDir"`
+}
+
service dockerCopilot {
@handler webindex
get /
@@ -111,8 +117,17 @@ service dockerCopilot {
jwt: Auth
)
service dockerCopilot {
+ @handler ListProgress
+ get /progress returns (Resp)
+
@handler GetProgress
get /progress/:taskid (GetProgressReq) returns (Resp)
+
+ @handler CancelProgress
+ post /progress/:taskid/cancel (GetProgressReq) returns (Resp)
+
+ @handler StreamEvents
+ get /progress/:taskid/events (GetProgressReq)
}
@server(
@@ -168,6 +183,19 @@ service dockerCopilot {
delete /image/:id (RemoveImageReq) returns (Resp)
}
+@server(
+ prefix: /api
+ group: settings
+ jwt: Auth
+)
+service dockerCopilot {
+ @handler GetSettings
+ get /settings returns (Resp)
+
+ @handler UpdateSettings
+ put /settings (UpdateSettingsReq) returns (Resp)
+}
+
@server(
prefix: /api
group: version
diff --git a/dockercopilot.go b/dockercopilot.go
index f9143b9e..4e3c7e45 100644
--- a/dockercopilot.go
+++ b/dockercopilot.go
@@ -1,18 +1,19 @@
package main
import (
- "embed"
"flag"
"fmt"
- "go/types"
- "io/fs"
- "log"
"net/http"
"os"
+ "strings"
+ "unicode"
+ "unicode/utf8"
"github.com/onlyLTY/dockerCopilot/internal/config"
"github.com/onlyLTY/dockerCopilot/internal/handler"
+ "github.com/onlyLTY/dockerCopilot/internal/mcp"
"github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/types"
"github.com/onlyLTY/dockerCopilot/internal/utiles"
"github.com/robfig/cron/v3"
"github.com/zeromicro/go-zero/core/conf"
@@ -23,9 +24,6 @@ import (
xhttp "github.com/zeromicro/x/http"
)
-//go:embed front/*
-var embeddedFront embed.FS
-
var configFile = flag.String("f", "etc/dockerCopilot.yaml", "the config file")
type UnauthorizedResponse struct {
@@ -38,7 +36,7 @@ func main() {
logDir := "./logs"
ErrSetupLog := SetupLog(logDir)
if ErrSetupLog != nil {
- logx.Errorf("failed to setup log: %v", ErrSetupLog)
+ logx.Errorf("初始化日志失败: %v", ErrSetupLog)
os.Exit(1)
}
logx.SetLevel(logx.InfoLevel)
@@ -51,6 +49,10 @@ func main() {
logx.Errorf("请确认secretKey设置正确,要求非纯数字且大于八位")
os.Exit(1)
}
+ if err := validateSecretKey(c.Auth.AccessSecret); err != nil {
+ logx.Errorf("secretKey 配置不合法: %v", err)
+ os.Exit(1)
+ }
server := rest.MustNewServer(c.RestConf, rest.WithCors("*"), rest.WithUnauthorizedCallback(
func(w http.ResponseWriter, r *http.Request, err error) {
response := UnauthorizedResponse{
@@ -62,11 +64,16 @@ func main() {
}))
defer server.Stop()
ctx := svc.NewServiceContext(c)
+ if ctx.DockerInitErr != nil {
+ logx.Errorf("Docker 客户端初始化失败: %v", ctx.DockerInitErr)
+ logx.Errorf("请检查 DOCKER_HOST、DOCKER_TLS_VERIFY、DOCKER_CERT_PATH 等环境变量")
+ os.Exit(1)
+ }
// Ensure data directory and config exist (Auto-init)
dataDir := "/data/config/image"
if err := os.MkdirAll(dataDir, 0755); err != nil {
- logx.Errorf("Failed to create data directory: %v", err)
+ logx.Errorf("创建数据目录失败: %v", err)
}
imageLogosPath := "/data/config/imageLogos.js"
@@ -76,24 +83,30 @@ export const customImageLogos = {
};
`)
if err := os.WriteFile(imageLogosPath, defaultConfig, 0644); err != nil {
- logx.Errorf("Failed to create default imageLogos.js: %v", err)
+ logx.Errorf("创建默认 imageLogos.js 失败: %v", err)
}
}
list, err := utiles.GetImagesList(ctx)
if err != nil {
- logx.Errorf("panic获取镜像列表出错: %v", err)
- panic(err)
+ // Docker 暂不可用时不要杀死进程,交由定时任务稍后重试
+ logx.Errorf("启动时获取镜像列表失败(稍后自动重试): %v", err)
+ } else {
+ go ctx.HubImageInfo.CheckUpdate(list)
}
- go ctx.HubImageInfo.CheckUpdate(list)
corndanmu := cron.New(cron.WithParser(cron.NewParser(
cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow,
)))
_, err = corndanmu.AddFunc("30 * * * *", func() {
- list, err := utiles.GetImagesList(ctx)
- if err != nil {
- logx.Errorf("panic获取镜像列表出错: %v", err)
- panic(err)
+ defer func() {
+ if r := recover(); r != nil {
+ logx.Errorf("镜像更新检查 panic: %v", r)
+ }
+ }()
+ list, listErr := utiles.GetImagesList(ctx)
+ if listErr != nil {
+ logx.Errorf("获取镜像列表出错: %v", listErr)
+ return
}
ctx.HubImageInfo.CheckUpdate(list)
})
@@ -101,6 +114,24 @@ export const customImageLogos = {
logx.Errorf("panic添加定时任务出错: %v", err)
panic(err)
}
+ // 定时自动更新:AUTOUPDATE_CRON 为 cron 表达式,AUTOUPDATE_WHITELIST 为容器白名单
+ if cronExpr := os.Getenv("AUTOUPDATE_CRON"); cronExpr != "" {
+ _, err = corndanmu.AddFunc(cronExpr, func() {
+ defer func() {
+ if r := recover(); r != nil {
+ logx.Errorf("自动更新任务 panic: %v", r)
+ }
+ }()
+ utiles.RunAutoUpdate(ctx)
+ })
+ if err != nil {
+ logx.Errorf("自动更新定时任务表达式非法: %v", err)
+ } else if !utiles.AutoUpdateConfigured() {
+ logx.Errorf("自动更新未完全启用:AUTOUPDATE_WHITELIST 为空,任务将跳过")
+ } else {
+ logx.Infof("自动更新已启用: %s, 白名单: %s", cronExpr, os.Getenv("AUTOUPDATE_WHITELIST"))
+ }
+ }
corndanmu.Start()
defer corndanmu.Stop()
httpx.SetErrorHandler(func(err error) (int, any) {
@@ -118,70 +149,36 @@ export const customImageLogos = {
}
})
handler.RegisterHandlers(server, ctx)
- RegisterHandlers(server)
- fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
+ mcp.Register(server, ctx)
+ fmt.Printf("服务已启动: %s:%d...\n", c.Host, c.Port)
logx.Info("程序版本" + config.Version)
server.Start()
}
-func RegisterHandlers(engine *rest.Server) {
- frontFS, err := fs.Sub(embeddedFront, "front")
- if err != nil {
- log.Fatal(err)
- }
-
- frontFileServer := http.StripPrefix("/manager", http.FileServer(http.FS(frontFS)))
- assetsHandler := http.FileServer(http.FS(frontFS))
-
- // Serve custom icons
- iconFileServer := http.StripPrefix("/src/config/image/", http.FileServer(http.Dir("/data/config/image")))
- engine.AddRoutes(
- []rest.Route{
- {
- Method: http.MethodGet,
- Path: "/src/config/image/:file",
- Handler: func(w http.ResponseWriter, r *http.Request) {
- iconFileServer.ServeHTTP(w, r)
- },
- },
- },
- )
-
- engine.AddRoutes(
- []rest.Route{
- {
- Method: http.MethodGet,
- Path: "/manager",
- Handler: func(w http.ResponseWriter, r *http.Request) {
- frontFileServer.ServeHTTP(w, r)
- },
- },
- {
- Method: http.MethodGet,
- Path: "/manager/:path",
- Handler: func(w http.ResponseWriter, r *http.Request) {
- frontFileServer.ServeHTTP(w, r)
- },
- },
- {
- Method: http.MethodGet,
- Path: "/manager/assets/:path",
- Handler: func(w http.ResponseWriter, r *http.Request) {
- frontFileServer.ServeHTTP(w, r)
- },
- },
- {
- Method: http.MethodGet,
- Path: "/assets/:path",
- Handler: func(w http.ResponseWriter, r *http.Request) {
- assetsHandler.ServeHTTP(w, r)
- },
- },
- },
- )
+// validateSecretKey 校验 secretKey:去掉首尾空白后长度需大于 8 位,且不能是纯数字。
+func validateSecretKey(secret string) error {
+ secret = strings.TrimSpace(secret)
+ // 变量未展开(如 ${secretKey})时长度可能“看起来”合规,必须明确拒绝。
+ if strings.Contains(secret, "${") || strings.Contains(secret, "$") {
+ return fmt.Errorf("secretKey 未展开,请直接填写真实值")
+ }
+ if utf8.RuneCountInString(secret) <= 8 {
+ return fmt.Errorf("长度必须大于 8 位")
+ }
+ allDigits := true
+ for _, r := range secret {
+ if !unicode.IsDigit(r) {
+ allDigits = false
+ break
+ }
+ }
+ if allDigits {
+ return fmt.Errorf("不能是纯数字")
+ }
+ return nil
}
-// 检查并创建日志目录
+// ensureLogDirectory 检查并创建日志目录
func ensureLogDirectory(logDir string) error {
if _, err := os.Stat(logDir); os.IsNotExist(err) {
return os.MkdirAll(logDir, 0755) // 创建目录并设置权限
@@ -202,6 +199,9 @@ func SetupLog(logDir string) error {
KeepDays: 7,
Compress: true,
Mode: "file",
+ // 使用纯文本格式,日志行只保留“时间 + 级别 + 内容”,
+ // 不再输出 JSON 字段或服务名前缀。
+ Encoding: "plain",
}
logx.MustSetup(logConf)
logx.AddWriter(logx.NewWriter(os.Stdout))
diff --git a/dockercopilot_test.go b/dockercopilot_test.go
new file mode 100644
index 00000000..8b26acb8
--- /dev/null
+++ b/dockercopilot_test.go
@@ -0,0 +1,25 @@
+package main
+
+import "testing"
+
+func TestValidateSecretKey(t *testing.T) {
+ tests := []struct {
+ name string
+ secret string
+ wantErr bool
+ }{
+ {name: "empty", secret: " ", wantErr: true},
+ {name: "unexpanded env", secret: "${secretKey}", wantErr: true},
+ {name: "too short", secret: "abc12345", wantErr: true},
+ {name: "pure digits", secret: "123456789", wantErr: true},
+ {name: "valid", secret: "change-me-please", wantErr: false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := validateSecretKey(tt.secret)
+ if (err != nil) != tt.wantErr {
+ t.Fatalf("validateSecretKey(%q) error = %v, wantErr %v", tt.secret, err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/docs/MCP.md b/docs/MCP.md
new file mode 100644
index 00000000..56ecb396
--- /dev/null
+++ b/docs/MCP.md
@@ -0,0 +1,168 @@
+# MCP 接入说明
+
+dockerCopilot 自 v2.2.0 起内置 MCP(Model Context Protocol)服务,Agent 可以直接调用
+容器管理、镜像管理、备份恢复、进度查询、设置与自更新等能力,无需再经过网页前端。
+
+HTTP API(`/api/*`)与 MCP(`/mcp`)**并存**,脚本和旧客户端不受影响。
+
+## 1. 传输与地址
+
+| 项 | 值 |
+|---|---|
+| 协议 | Streamable HTTP(MCP 官方传输,兼容 SSE) |
+| 地址 | `http://:12712/mcp` |
+| 方法 | `POST`(JSON-RPC 消息)、`GET`(SSE 流)、`DELETE`(结束会话) |
+| 版本 | `serverInfo.version` 与程序版本一致 |
+
+## 2. 鉴权
+
+`/mcp` 默认要求 token,取值优先级:
+
+1. 环境变量 `MCP_TOKEN`(若设置)
+2. 否则回退到 `secretKey`(与 `/api` 的 JWT 密钥同源)
+
+三种携带方式都支持,便于不同客户端:
+
+```bash
+# 1) Bearer
+curl -H "Authorization: Bearer " ...
+
+# 2) 自定义头(Claude Code / Cursor 等常用的 headers 配置)
+curl -H "X-MCP-Token: " ...
+
+# 3) URL 参数(仅支持 URL 配置的客户端)
+curl "http://127.0.0.1:12712/mcp?token="
+```
+
+显式关闭鉴权(**仅限完全隔离的内网**):
+
+```yaml
+environment:
+ - MCP_TOKEN=off
+```
+
+关闭时启动日志会打印一条 error 级别告警。
+
+## 3. 客户端配置示例
+
+### Claude Code / Cursor / Cline(Streamable HTTP)
+
+```json
+{
+ "mcpServers": {
+ "dockercopilot": {
+ "type": "streamable-http",
+ "url": "http://127.0.0.1:12712/mcp",
+ "headers": {
+ "Authorization": "Bearer "
+ }
+ }
+ }
+}
+```
+
+### 只支持 URL 的客户端
+
+```json
+{
+ "mcpServers": {
+ "dockercopilot": {
+ "url": "http://127.0.0.1:12712/mcp?token="
+ }
+ }
+}
+```
+
+## 4. 工具清单
+
+共 27 个工具,命名统一以 `dc_` 开头。
+
+### 通用
+
+| 工具 | 说明 |
+|---|---|
+| `dc_ping` | 健康检查 |
+| `dc_info` | 版本、监听地址、Docker daemon 真实连通性、备份目录、MCP 鉴权状态 |
+
+### 容器
+
+| 工具 | 参数 | 说明 |
+|---|---|---|
+| `dc_list_containers` | - | 列出全部容器(含已停止),带 `haveUpdate` / `isCompose` 标记 |
+| `dc_inspect_container` | `containerId` | 单容器完整 inspect |
+| `dc_start_container` | `containerId` | 启动 |
+| `dc_stop_container` | `containerId` | 停止(SIGTERM,10s 宽限) |
+| `dc_restart_container` | `containerId` | 重启 |
+| `dc_rename_container` | `containerId`, `newName` | 重命名 |
+| `dc_update_container` | `containerId`, `containerName`, `image?`, `delOldContainer?` | 一键更新,全链路回滚;`image` 不传或留空时使用容器当前镜像 |
+| `dc_autoupdate_run` | - | 手动触发一轮自动更新;**前置条件**:`AUTOUPDATE_CRON` 与 `AUTOUPDATE_WHITELIST` 均已配置,否则报错 |
+
+### 镜像
+
+| 工具 | 参数 | 说明 |
+|---|---|---|
+| `dc_list_images` | - | 本地镜像列表 |
+| `dc_check_image_update` | - | 立即做一次远端 digest 检查并返回结果 |
+| `dc_remove_image` | `imageId`, `force?` | 删除镜像 |
+| `dc_clean_images` | `filter` | 批量清理:`untagged` 删除无 TAG 镜像,`unused` 删除未使用镜像;返回 `removed` / `failed` 明细 |
+
+### 备份 / 恢复
+
+| 工具 | 参数 | 说明 |
+|---|---|---|
+| `dc_backup_containers` | - | 备份为 JSON |
+| `dc_backup_compose` | - | 导出 docker-compose YAML |
+| `dc_list_backups` | - | 备份文件列表 |
+| `dc_restore_container` | `filename`, `start?`, `force?` | 从 JSON 备份恢复;`force` 走“改名旧容器→新建→成功后删旧容器”,失败回滚 |
+| `dc_delete_backup` | `filename` | 删除备份文件(不动容器) |
+
+### 进度
+
+| 工具 | 参数 | 说明 |
+|---|---|---|
+| `dc_list_progress` | - | 全部任务(含重启恢复的历史任务) |
+| `dc_get_progress` | `taskId` | 单任务进度 |
+| `dc_cancel_progress` | `taskId` | 取消任务(步骤边界生效) |
+
+### 设置 / 版本
+
+| 工具 | 参数 | 说明 |
+|---|---|---|
+| `dc_get_settings` | - | 当前与生效的备份目录 |
+| `dc_update_settings` | `backupDir` | 修改备份目录(绝对路径 + 可写校验) |
+| `dc_get_version` | - | 本地版本与构建时间 |
+| `dc_check_program_update` | - | 检查程序新版本;FnOS 版本返回 `fnos=true` 与提示(不走通用更新链) |
+| `dc_update_program` | - | 自更新并重启(会中断当前服务);FnOS 版本会明确拒绝 |
+
+## 5. 与 HTTP API 的对应关系
+
+MCP 工具直接复用 `internal/logic` 与 `internal/utiles` 的实现,与 HTTP API 共享同一套
+锁、进度、审计与回滚逻辑,因此两条通道行为一致。
+
+- 更新容器:`dc_update_container` ↔ `POST /api/container/:id/update`
+- 恢复容器:`dc_restore_container` ↔ `POST /api/container/backups/restore`
+- 进度:`dc_list_progress` ↔ `GET /api/progress`
+
+## 6. 自检
+
+```bash
+# 未带 token:应返回 401
+curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:12712/mcp \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json, text/event-stream' \
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'
+
+# 带 token:应返回 serverInfo 与 Mcp-Session-Id
+curl -s -D - -X POST http://127.0.0.1:12712/mcp \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json, text/event-stream' \
+ -H 'Authorization: Bearer ' \
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'
+```
+
+## 7. 安全建议
+
+- `/mcp` 等同 root 级 Docker 控制面:能启停、删除容器与镜像。**不要直接暴露公网**。
+- 建议只监听内网/回环:`ports: ["127.0.0.1:12712:12712"]`。
+- 需要给不同 Agent 分配不同凭据时,用 `MCP_TOKEN` 单独设置,避免泄露 `secretKey`。
+- 不需要 MCP 时可不配置任何客户端;端点存在但没有有效 token 无法调用。
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
new file mode 100644
index 00000000..66170fbd
--- /dev/null
+++ b/docs/ROADMAP.md
@@ -0,0 +1,86 @@
+# dockerCopilot 改造规划(ROADMAP)
+
+> 本项目自 v2.2.0 起由 DongShull 独立维护(fork 自 onlyLTY/dockerCopilot v2.1.3,上游已停止更新)。
+> 本文件记录 v2.1.3 基线体检结论与改造路线。
+
+## 一、基线缺陷清单(v2.1.3)
+
+### P0 · 崩溃/数据级
+1. `containerslistlogic.go`:inspect 失败后仍解引用 nil,容器列表接口可被打崩
+2. `updatecontainer.go`:更新失败(create/start)时旧容器停留在改名+停止状态,无回滚
+3. `updatecontainer.go`:更新成功后不清理旧镜像,磁盘慢性泄漏
+
+### P1 · 功能错误
+4. 旧容器改名使用秒级时间戳,同秒二次更新必撞名
+5. `checkupdate.go`:digest 比对循环覆盖,"最后一个说了算",多 RepoDigest 时误报/漏报
+6. `image.go`:`strings.Split(":")[1]` 解析 tag,带端口的私有仓库镜像名/tag 错位
+7. 停止容器使用 SIGINT,业界惯例 SIGTERM
+8. `getRemoteVersion.go`/`updateprogram.go` 硬编码上游 onlyLTY URL(已改可配置 UPDATE_REPO)
+9. `decompressTarGz`:tar 条目名不校验,存在 zip-slip 路径穿越
+10. `module.ImageUpdateData`:cron 写、HTTP 读共用 map,无锁数据竞争
+
+### P2 · 卫生级
+11. ProgressStore 只写不删(内存泄漏),重启丢进度,无任务取消
+12. `checkImageInUsed` O(容器×镜像) 双重循环
+13. restore 中永假的死代码分支
+14. JWT 84 天有效、secretKey 兼作签名密钥(记录在案,暂无计划改动)
+15. `Timeout: 1e14`、CORS `*`、`InsecureSkipVerify`(已加 REGISTRY_INSECURE 开关;Timeout 已改为 600000ms,SSE 路由带 `WithSSE()` 走 Accept 头豁免,见 `etc/dockerCopilot.yaml` 注释)
+
+## 二、分期计划
+
+| 期 | 内容 | 状态 |
+|---|---|---|
+| P0 | 基线整备:URL 可配置、CI(build/vet/test)、本规划入仓 | ✅ v2.2.0 |
+| P1 | 修复批:缺陷 1/4/5/6/7/9/10/12/13 | ✅ v2.2.0 |
+| P2 | 更新链重构:旧镜像清理、失败回滚、digest 并发检测、compose 标签感知 | ✅ v2.2.0 |
+| P3 | 任务系统:进度落盘、TTL 清理、任务列表/取消 API、SSE 推送 | ✅ v2.2.0 |
+| P4 | FnOS 适配:去 privileged 要求、HEALTHCHECK、备份目录运行时可配(Settings API) | ✅ v2.2.0 |
+| P5 | 增强:webhook 通知、审计日志、定时自动更新白名单 | ✅ v2.2.0 |
+| P6 | 稳定性:SSE 心跳、Docker 调用超时、进度文件并发落盘、Compose 导出保真度 | ✅ 待发版 |
+| P7 | Agent 化:移除网页前端、保留 HTTP API、内置 MCP(Streamable HTTP)暴露全部能力 | ✅ 待发版 |
+
+### P6 补充说明
+
+- SSE 增加 15 秒心跳和断线重连提示,避免反向代理在任务长时间无进度时断开连接。
+- 单次 Docker list/inspect/start/stop/rename/remove 调用增加 60 秒~2 分钟超时;镜像拉取仍由任务上下文控制,避免大镜像被误杀。
+- 进度文件落盘与内存快照在同一把锁内完成,修复并发保存时旧快照覆盖新状态的问题。
+- Compose 导出补齐 healthcheck、devices、cap_add/cap_drop、security_opt、ulimits、extra_hosts、dns、tmpfs、shm_size、user、group_add、logging、stop_signal、stop_grace_period、sysctls、资源限制等字段;命名卷改用 MountPoint.Name,避免导出宿主机卷路径。
+- 恢复 `force=true` 改为“停止并临时改名旧容器 → 创建/启动新容器 → 成功后删除旧容器”;任一步失败都会清理半成品并把旧容器改回原名、恢复运行/暂停状态。
+- 备份/更新/恢复统一清洗网络端点:只保留别名、链接、驱动参数和静态 IPAM,清掉运行期 NetworkID/EndpointID/IP/MAC/DNSNames,避免重建时继承失效端点或地址冲突。
+
+### P7 补充说明
+
+- 前端(`go:embed front/*`、`/manager` 静态路由、CI 的前端构建 job)整体移除,`GET /` 改为返回 JSON 状态,同时满足容器 HEALTHCHECK 的 200 要求。
+- 保留全部 `/api/*` 路由:脚本、CI 与旧客户端不受影响,MCP 与 API 共用同一套 `internal/utiles` / `internal/logic` 实现。
+- 新增 `internal/mcp/`:基于 `mark3labs/mcp-go` 的 Streamable HTTP 服务,挂载 `/mcp`,注册 26 个 `dc_*` 工具,覆盖容器、镜像、备份恢复、进度、设置与自更新。
+- `/mcp` 默认鉴权:`MCP_TOKEN` 优先,未设置时回退 `secretKey`;支持 `Authorization: Bearer`、`X-MCP-Token`、`?token=` 三种携带方式,比较使用常量时间算法。`MCP_TOKEN=off` 可显式关闭(仅隔离内网)。
+- 破坏性 MCP 操作复用现有审计与通知;长任务复用 `RegisterTask`/`CancelTask`/`FinishTask` 与进度持久化。
+
+## 三、配置项一览(环境变量)
+
+| 变量 | 默认 | 说明 |
+|---|---|---|
+| `secretKey` | (必填) | 登录密钥,同时用于 JWT 签名,不少于 8 位且非纯数字 |
+| `DOCKER_HOST` | unix:///var/run/docker.sock | Docker 守护进程地址 |
+| `BACKUP_DIR` | /data/backups | 容器备份目录(优先级:env > Settings API 配置 > 默认) |
+| `DelOldContainer` | true | 更新成功后是否删除旧容器("false" 保留) |
+| `CLEANUP_OLD_IMAGE` | true | 更新成功后是否清理不再被引用的旧镜像 |
+| `UPDATE_REPO` | DongShull/dockerCopilot | 版本检查与自更新仓库 |
+| `UPDATE_BRANCH` | latest | 版本检查分支 |
+| `githubProxy` | 空 | GitHub 代理前缀 |
+| `REGISTRY_INSECURE` | false | 设为 `true` 时 registry 检测跳过 TLS 校验(默认严格校验) |
+| `NOTIFY_WEBHOOK_URL` | 空 | 任务完成通知 webhook(POST JSON) |
+| `AUTOUPDATE_CRON` | 空 | 自动更新周期(cron 表达式,空=关闭) |
+| `AUTOUPDATE_WHITELIST` | 空 | 自动更新容器白名单(逗号分隔,`*`=全部有更新的容器) |
+| `MCP_TOKEN` | 空 | `/mcp` 访问令牌;空则回退 `secretKey`,`off` 显式关闭鉴权 |
+
+## 四、验收标准(v2.2.0)
+
+- `go build ./...` / `go vet ./...` / `go test ./...` 全绿
+- 更新一个容器后 `docker images` 不新增悬空镜像
+- 模拟更新失败(拉取超时)后旧容器自动回到原名并恢复运行
+- 重启进程后任务历史仍可查询;进行中任务可取消
+- 50 个镜像的更新检测从串行分钟级进入 10 秒级
+- 非 privileged 容器(仅挂载 docker.sock + 数据卷)可跑通全部功能
+- `go test ./internal/mcp/` 覆盖工具注册、鉴权与 Streamable HTTP 握手
+- MCP 客户端可完成「列容器 → 更新 → 查进度」全流程,`/api/*` 行为不变
diff --git a/etc/dockerCopilot.yaml b/etc/dockerCopilot.yaml
index fd64a275..7027fd23 100644
--- a/etc/dockerCopilot.yaml
+++ b/etc/dockerCopilot.yaml
@@ -1,9 +1,12 @@
Name: dockerCopilot
Host: 0.0.0.0
Port: 12712
-Timeout: 100000000000000
+# 全局请求超时 10 分钟(此前是 1e14ms≈3170 年,等于没有超时)。
+# SSE 事件流带 Accept: text/event-stream 请求头,go-zero 的 timeout 中间件对它原生豁免;
+# /mcp 的同步工具调用(拉镜像、批量检查)需要分钟级余量,因此全局值放宽到 10 分钟。
+Timeout: 600000
Log:
Encoding: plain
Auth:
AccessSecret: ${secretKey}
- AccessExpire: 7257600
\ No newline at end of file
+ AccessExpire: 7257600
diff --git a/fn_version b/fn_version
index f475f52f..33c7c2ed 100644
--- a/fn_version
+++ b/fn_version
@@ -1 +1 @@
-v2.1.3-FNOS
+v2.2.0-FNOS
diff --git a/go.mod b/go.mod
index 4bfabbf8..da6af1f6 100644
--- a/go.mod
+++ b/go.mod
@@ -2,8 +2,6 @@ module github.com/onlyLTY/dockerCopilot
go 1.23.0
-toolchain go1.24.1
-
require (
github.com/compose-spec/compose-go v1.20.2
github.com/distribution/reference v0.5.0
@@ -11,6 +9,7 @@ require (
github.com/docker/go-connections v0.5.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.6.0
+ github.com/mark3labs/mcp-go v0.44.0
github.com/robfig/cron/v3 v3.0.1
github.com/zeromicro/go-zero v1.8.2
github.com/zeromicro/x v0.0.0-20240408115609-8224c482b07e
@@ -20,7 +19,9 @@ require (
require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
+ github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
+ github.com/buger/jsonparser v1.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
@@ -32,6 +33,8 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
+ github.com/invopop/jsonschema v0.13.0 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
@@ -51,6 +54,9 @@ require (
github.com/prometheus/procfs v0.16.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
+ github.com/spf13/cast v1.10.0 // indirect
+ github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
+ github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
@@ -66,13 +72,13 @@ require (
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
- golang.org/x/mod v0.17.0 // indirect
+ golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.10.0 // indirect
- golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
+ golang.org/x/tools v0.26.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a // indirect
google.golang.org/grpc v1.71.1 // indirect
diff --git a/go.sum b/go.sum
index 5150eca4..be573732 100644
--- a/go.sum
+++ b/go.sum
@@ -2,8 +2,12 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
+github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -29,6 +33,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -50,6 +56,9 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5uk
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
+github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
+github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -60,6 +69,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mark3labs/mcp-go v0.44.0 h1:OlYfcVviAnwNN40QZUrrzU0QZjq3En7rCU5X09a/B7I=
+github.com/mark3labs/mcp-go v0.44.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -106,12 +119,18 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
+github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
+github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zeromicro/go-zero v1.8.2 h1:AbJckBoojbr1lqCN1dkvURTIHOau7yvKReEd7ZmjuCk=
@@ -157,8 +176,8 @@ golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xpp
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -188,8 +207,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
+golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/internal/handler/icons/obtainhandler.go b/internal/handler/icons/obtainhandler.go
index dc191053..41b73ea9 100644
--- a/internal/handler/icons/obtainhandler.go
+++ b/internal/handler/icons/obtainhandler.go
@@ -15,12 +15,12 @@ import (
func ObtainHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
jsPath := "/data/config/imageLogos.js"
- logx.Infof("Reading icons from: %s", jsPath)
+ logx.Infof("读取图标配置: %s", jsPath)
contentBytes, err := os.ReadFile(jsPath)
if err != nil {
if os.IsNotExist(err) {
- logx.Info("Config file does not exist, returning empty.")
+ logx.Info("图标配置文件不存在,返回空")
httpx.OkJsonCtx(r.Context(), w, types.Resp{
Code: 200,
Msg: "Success",
@@ -28,8 +28,8 @@ func ObtainHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
})
return
}
- logx.Errorf("Error reading config: %v", err)
- httpx.ErrorCtx(r.Context(), w, fmt.Errorf("failed to read config: %v", err))
+ logx.Errorf("读取图标配置失败: %v", err)
+ httpx.ErrorCtx(r.Context(), w, fmt.Errorf("读取图标配置失败: %v", err))
return
}
@@ -51,7 +51,7 @@ func ObtainHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
}
}
- logx.Infof("Total icons found: %d", len(icons))
+ logx.Infof("共解析出 %d 个图标", len(icons))
response := struct {
Code int `json:"code"`
diff --git a/internal/handler/icons/uploadhandler.go b/internal/handler/icons/uploadhandler.go
index 253b7139..541d2c07 100644
--- a/internal/handler/icons/uploadhandler.go
+++ b/internal/handler/icons/uploadhandler.go
@@ -9,6 +9,7 @@ import (
"path/filepath"
"regexp"
"strings"
+ "sync"
"github.com/google/uuid"
"github.com/onlyLTY/dockerCopilot/internal/svc"
@@ -28,7 +29,8 @@ var allowedImageTypes = map[string]string{
func UploadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
- // 1. 解析 Multipart 表单
+ // 1. 解析 Multipart 表单;限制请求体总大小,避免超大文件填盘
+ r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
err := r.ParseMultipartForm(10 << 20) // 10MB 限制
if err != nil {
writeUploadError(w, http.StatusBadRequest, "failed to parse form")
@@ -69,12 +71,17 @@ func UploadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
writeUploadError(w, http.StatusInternalServerError, "failed to create file on server")
return
}
- defer dst.Close()
-
if _, err := io.Copy(dst, file); err != nil {
+ _ = dst.Close()
+ _ = os.Remove(dstPath)
writeUploadError(w, http.StatusInternalServerError, "failed to copy file content")
return
}
+ if err := dst.Close(); err != nil {
+ _ = os.Remove(dstPath)
+ writeUploadError(w, http.StatusInternalServerError, "failed to save file")
+ return
+ }
// 5. 更新 imageLogos.js
jsPath := imageLogosPath
@@ -134,7 +141,13 @@ func writeUploadError(w http.ResponseWriter, statusCode int, msg string) {
})
}
+var imageLogosMu sync.Mutex
+
func updateImageLogosJS(filePath, imageName, filename string) error {
+ // 读-改-写必须串行,否则并发上传会互相覆盖。
+ imageLogosMu.Lock()
+ defer imageLogosMu.Unlock()
+
// 读取文件
contentBytes, err := os.ReadFile(filePath)
if err != nil {
@@ -168,5 +181,24 @@ func updateImageLogosJS(filePath, imageName, filename string) error {
content = content[:lastBraceIdx] + newLine + "\n" + content[lastBraceIdx:]
}
- return os.WriteFile(filePath, []byte(content), 0644)
+ tmp, err := os.CreateTemp(filepath.Dir(filePath), ".imageLogos-*.js.tmp")
+ if err != nil {
+ return err
+ }
+ tmpName := tmp.Name()
+ defer func() {
+ _ = os.Remove(tmpName)
+ }()
+ if err = tmp.Chmod(0644); err != nil {
+ _ = tmp.Close()
+ return err
+ }
+ if _, err = tmp.WriteString(content); err != nil {
+ _ = tmp.Close()
+ return err
+ }
+ if err = tmp.Close(); err != nil {
+ return err
+ }
+ return os.Rename(tmpName, filePath)
}
diff --git a/internal/handler/image/cleanimageshandler.go b/internal/handler/image/cleanimageshandler.go
new file mode 100644
index 00000000..8c7ba8fc
--- /dev/null
+++ b/internal/handler/image/cleanimageshandler.go
@@ -0,0 +1,28 @@
+package image
+
+import (
+ "net/http"
+
+ "github.com/onlyLTY/dockerCopilot/internal/logic/image"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/types"
+ "github.com/zeromicro/go-zero/rest/httpx"
+)
+
+func CleanImagesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ var req types.RemoveImagesReq
+ if err := httpx.Parse(r, &req); err != nil {
+ httpx.ErrorCtx(r.Context(), w, err)
+ return
+ }
+
+ l := image.NewCleanImagesLogic(r.Context(), svcCtx)
+ resp, err := l.CleanImages(&req)
+ if err != nil {
+ httpx.WriteJson(w, resp.Code, resp)
+ } else {
+ httpx.WriteJson(w, resp.Code, resp)
+ }
+ }
+}
diff --git a/internal/handler/progress/cancelprogresshandler.go b/internal/handler/progress/cancelprogresshandler.go
new file mode 100644
index 00000000..89f060ba
--- /dev/null
+++ b/internal/handler/progress/cancelprogresshandler.go
@@ -0,0 +1,28 @@
+package progress
+
+import (
+ "net/http"
+
+ "github.com/onlyLTY/dockerCopilot/internal/logic/progress"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/types"
+ "github.com/zeromicro/go-zero/rest/httpx"
+)
+
+func CancelProgressHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ var req types.GetProgressReq
+ if err := httpx.Parse(r, &req); err != nil {
+ httpx.ErrorCtx(r.Context(), w, err)
+ return
+ }
+
+ l := progress.NewCancelProgressLogic(r.Context(), svcCtx)
+ resp, err := l.CancelProgress(&req)
+ if err != nil {
+ httpx.WriteJson(w, resp.Code, resp)
+ } else {
+ httpx.OkJsonCtx(r.Context(), w, resp)
+ }
+ }
+}
diff --git a/internal/handler/progress/listprogresshandler.go b/internal/handler/progress/listprogresshandler.go
new file mode 100644
index 00000000..a8c28bd6
--- /dev/null
+++ b/internal/handler/progress/listprogresshandler.go
@@ -0,0 +1,21 @@
+package progress
+
+import (
+ "net/http"
+
+ "github.com/onlyLTY/dockerCopilot/internal/logic/progress"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/zeromicro/go-zero/rest/httpx"
+)
+
+func ListProgressHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ l := progress.NewListProgressLogic(r.Context(), svcCtx)
+ resp, err := l.ListProgress()
+ if err != nil {
+ httpx.WriteJson(w, resp.Code, resp)
+ } else {
+ httpx.OkJsonCtx(r.Context(), w, resp)
+ }
+ }
+}
diff --git a/internal/handler/progress/streameventshandler.go b/internal/handler/progress/streameventshandler.go
new file mode 100644
index 00000000..41bad359
--- /dev/null
+++ b/internal/handler/progress/streameventshandler.go
@@ -0,0 +1,122 @@
+package progress
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/types"
+ "github.com/zeromicro/go-zero/rest/httpx"
+)
+
+const (
+ progressPollInterval = 500 * time.Millisecond
+ progressHeartbeatInterval = 15 * time.Second
+ // progressStreamMaxDuration 单条 SSE 连接的最长时长。到时主动正常关闭,
+ // 客户端 EventSource 会按 retry 间隔自动重连;避免异常客户端把连接无限占住。
+ progressStreamMaxDuration = 30 * time.Minute
+)
+
+// StreamEventsHandler 以 Server-Sent Events 推送任务进度,替代前端高频轮询。
+// 任务完成或客户端断开时流结束。无进度变化时定期发送注释行,避免代理
+// 因长时间没有字节而断开连接。
+func StreamEventsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ var req types.GetProgressReq
+ if err := httpx.Parse(r, &req); err != nil {
+ httpx.ErrorCtx(r.Context(), w, err)
+ return
+ }
+ if req.TaskId == "" {
+ httpx.ErrorCtx(r.Context(), w, fmt.Errorf("taskID 不能为空"))
+ return
+ }
+
+ _, ok := w.(http.Flusher)
+ if !ok {
+ httpx.ErrorCtx(r.Context(), w, fmt.Errorf("streaming unsupported"))
+ return
+ }
+
+ header := w.Header()
+ header.Set("Content-Type", "text/event-stream")
+ header.Set("Cache-Control", "no-cache")
+ header.Set("Connection", "keep-alive")
+ header.Set("X-Accel-Buffering", "no")
+ w.WriteHeader(http.StatusOK)
+
+ flush := func() bool {
+ if err := http.NewResponseController(w).Flush(); err != nil {
+ return false
+ }
+ return true
+ }
+ writeRaw := func(value string) bool {
+ if _, err := fmt.Fprint(w, value); err != nil {
+ return false
+ }
+ return flush()
+ }
+
+ // EventSource 断线后 3 秒重连;首个进度事件立即发送,不用等第一个 tick。
+ if !writeRaw("retry: 3000\n\n") {
+ return
+ }
+
+ lastPayload := ""
+ sendProgress := func() (done bool, ok bool) {
+ progress, exists := svcCtx.GetProgress(req.TaskId)
+ if !exists {
+ writeRaw("event: error\ndata: {\"msg\":\"taskID 未找到\"}\n\n")
+ return true, false
+ }
+ payload, err := json.Marshal(progress)
+ if err != nil {
+ return false, true
+ }
+ if string(payload) != lastPayload {
+ if !writeRaw("data: " + string(payload) + "\n\n") {
+ return true, false
+ }
+ lastPayload = string(payload)
+ }
+ if progress.IsDone {
+ return true, false
+ }
+ return false, true
+ }
+
+ if done, ok := sendProgress(); done || !ok {
+ return
+ }
+
+ pollTicker := time.NewTicker(progressPollInterval)
+ defer pollTicker.Stop()
+ heartbeatTicker := time.NewTicker(progressHeartbeatInterval)
+ defer heartbeatTicker.Stop()
+
+ streamDeadline := time.After(progressStreamMaxDuration)
+ for {
+ select {
+ case <-r.Context().Done():
+ return
+ case <-streamDeadline:
+ // 到达单连接最长时长:发注释行告知后正常结束,
+ // 客户端 EventSource 按 retry 间隔自动重连继续。
+ _ = writeRaw(": stream deadline reached, reconnecting\n\n")
+ return
+ case <-heartbeatTicker.C:
+ if !writeRaw(": keep-alive\n\n") {
+ return
+ }
+ case <-pollTicker.C:
+ done, ok := sendProgress()
+ if done || !ok {
+ return
+ }
+ }
+ }
+ }
+}
diff --git a/internal/handler/progress/streameventshandler_test.go b/internal/handler/progress/streameventshandler_test.go
new file mode 100644
index 00000000..ac242197
--- /dev/null
+++ b/internal/handler/progress/streameventshandler_test.go
@@ -0,0 +1,40 @@
+package progress
+
+import (
+ "net/http/httptest"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/zeromicro/go-zero/rest/pathvar"
+)
+
+func TestStreamEventsHandlerSendsTerminalProgress(t *testing.T) {
+ t.Setenv("PROGRESS_STORE", filepath.Join(t.TempDir(), "progress.json"))
+ svcCtx := &svc.ServiceContext{ProgressStore: make(svc.ProgressStoreType)}
+ svcCtx.UpdateProgress("task-1", svc.TaskProgress{
+ TaskID: "task-1",
+ Percentage: 100,
+ Message: "done",
+ Name: "demo",
+ IsDone: true,
+ })
+
+ req := httptest.NewRequest("GET", "/api/progress/task-1/events", nil)
+ req = pathvar.WithVars(req, map[string]string{"taskid": "task-1"})
+ rec := httptest.NewRecorder()
+
+ StreamEventsHandler(svcCtx)(rec, req)
+
+ if rec.Code != 200 {
+ t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
+ }
+ body := rec.Body.String()
+ if !strings.Contains(body, "retry: 3000") {
+ t.Fatalf("missing SSE retry hint: %q", body)
+ }
+ if !strings.Contains(body, `"taskID":"task-1"`) || !strings.Contains(body, `"isDone":true`) {
+ t.Fatalf("missing terminal progress payload: %q", body)
+ }
+}
diff --git a/internal/handler/routes.go b/internal/handler/routes.go
index 5a84662d..b78241b2 100644
--- a/internal/handler/routes.go
+++ b/internal/handler/routes.go
@@ -1,6 +1,6 @@
-// Code generated by goctl. DO NOT EDIT.
-// goctl 1.8.5
-
+// 基于 goctl 生成,包含手工扩展:GET /(webindexHandler)、进度/设置/版本路由、
+// SSE 事件流(WithSSE)、批量镜像清理(DELETE /images)。若用 goctl 重新生成,
+// 请对照 git 历史补回手工部分。
package handler
import (
@@ -11,6 +11,7 @@ import (
icons "github.com/onlyLTY/dockerCopilot/internal/handler/icons"
image "github.com/onlyLTY/dockerCopilot/internal/handler/image"
progress "github.com/onlyLTY/dockerCopilot/internal/handler/progress"
+ settings "github.com/onlyLTY/dockerCopilot/internal/handler/settings"
version "github.com/onlyLTY/dockerCopilot/internal/handler/version"
"github.com/onlyLTY/dockerCopilot/internal/svc"
@@ -130,6 +131,12 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/images",
Handler: image.ImagesListHandler(serverCtx),
},
+ {
+ // 批量清理:?filter=untagged 删除无 TAG 镜像,?filter=unused 删除未使用镜像
+ Method: http.MethodDelete,
+ Path: "/images",
+ Handler: image.CleanImagesHandler(serverCtx),
+ },
},
rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
rest.WithPrefix("/api"),
@@ -137,11 +144,53 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
[]rest.Route{
+ {
+ Method: http.MethodGet,
+ Path: "/progress",
+ Handler: progress.ListProgressHandler(serverCtx),
+ },
{
Method: http.MethodGet,
Path: "/progress/:taskid",
Handler: progress.GetProgressHandler(serverCtx),
},
+ {
+ Method: http.MethodPost,
+ Path: "/progress/:taskid/cancel",
+ Handler: progress.CancelProgressHandler(serverCtx),
+ },
+ },
+ rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
+ rest.WithPrefix("/api"),
+ )
+
+ // SSE 事件流单独一组:WithSSE 声明为长连接路由,go-zero 对
+ // Accept: text/event-stream 的请求不做超时包裹。
+ server.AddRoutes(
+ []rest.Route{
+ {
+ Method: http.MethodGet,
+ Path: "/progress/:taskid/events",
+ Handler: progress.StreamEventsHandler(serverCtx),
+ },
+ },
+ rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
+ rest.WithPrefix("/api"),
+ rest.WithSSE(),
+ )
+
+ server.AddRoutes(
+ []rest.Route{
+ {
+ Method: http.MethodGet,
+ Path: "/settings",
+ Handler: settings.GetSettingsHandler(serverCtx),
+ },
+ {
+ Method: http.MethodPut,
+ Path: "/settings",
+ Handler: settings.UpdateSettingsHandler(serverCtx),
+ },
},
rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
rest.WithPrefix("/api"),
diff --git a/internal/handler/settings/settingshandler.go b/internal/handler/settings/settingshandler.go
new file mode 100644
index 00000000..a5468fe7
--- /dev/null
+++ b/internal/handler/settings/settingshandler.go
@@ -0,0 +1,40 @@
+package settings
+
+import (
+ "net/http"
+
+ "github.com/onlyLTY/dockerCopilot/internal/logic/settings"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/types"
+ "github.com/zeromicro/go-zero/rest/httpx"
+)
+
+func GetSettingsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ l := settings.NewSettingsLogic(r.Context(), svcCtx)
+ resp, err := l.GetSettings()
+ if err != nil {
+ httpx.WriteJson(w, resp.Code, resp)
+ } else {
+ httpx.OkJsonCtx(r.Context(), w, resp)
+ }
+ }
+}
+
+func UpdateSettingsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ var req types.UpdateSettingsReq
+ if err := httpx.Parse(r, &req); err != nil {
+ httpx.ErrorCtx(r.Context(), w, err)
+ return
+ }
+
+ l := settings.NewSettingsLogic(r.Context(), svcCtx)
+ resp, err := l.UpdateSettings(&req)
+ if err != nil {
+ httpx.WriteJson(w, resp.Code, resp)
+ } else {
+ httpx.OkJsonCtx(r.Context(), w, resp)
+ }
+ }
+}
diff --git a/internal/handler/webindexhandler.go b/internal/handler/webindexhandler.go
index a02b94d8..21cf9c31 100644
--- a/internal/handler/webindexhandler.go
+++ b/internal/handler/webindexhandler.go
@@ -1,14 +1,47 @@
package handler
import (
+ "encoding/json"
"net/http"
+ "github.com/onlyLTY/dockerCopilot/internal/config"
"github.com/onlyLTY/dockerCopilot/internal/svc"
)
+// webindexHandler 在去前端后作为健康检查与能力说明入口。
+// 容器 HEALTHCHECK 会请求 /,必须返回 200。
+// 响应体经 json.Marshal 生成,避免配置值(含引号/反斜杠)破坏 JSON 结构;
+// 不再暴露备份目录等本机路径,只保留版本与入口信息。
func webindexHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Location", "/manager")
- w.WriteHeader(301)
+ resp := svcInfoResponse{
+ Code: http.StatusOK,
+ Msg: "dockerCopilot API-only mode",
+ Data: svcInfoData{
+ Version: config.Version,
+ MCP: "/mcp",
+ API: "/api",
+ },
+ }
+ body, err := json.Marshal(resp)
+ if err != nil {
+ // Marshal 常量结构不会失败,防御性兜底保证健康检查始终 200。
+ body = []byte(`{"code":200,"msg":"dockerCopilot API-only mode"}`)
+ }
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(body)
}
}
+
+type svcInfoResponse struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data svcInfoData `json:"data"`
+}
+
+type svcInfoData struct {
+ Version string `json:"version"`
+ MCP string `json:"mcp"`
+ API string `json:"api"`
+}
diff --git a/internal/logic/auth/loginlogic.go b/internal/logic/auth/loginlogic.go
index 32ae54b9..6ad36cd2 100644
--- a/internal/logic/auth/loginlogic.go
+++ b/internal/logic/auth/loginlogic.go
@@ -2,12 +2,15 @@ package auth
import (
"context"
+ "crypto/subtle"
"errors"
+ "strings"
+ "time"
+
"github.com/golang-jwt/jwt"
"github.com/onlyLTY/dockerCopilot/internal/svc"
"github.com/onlyLTY/dockerCopilot/internal/types"
"github.com/zeromicro/go-zero/core/logx"
- "time"
)
type LoginLogic struct {
@@ -30,7 +33,15 @@ func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.Resp, err error) {
resp = &types.Resp{}
- if l.svcCtx.Config.Auth.AccessSecret != req.SecretKey {
+ expected := strings.TrimSpace(l.svcCtx.Config.Auth.AccessSecret)
+ if expected == "" {
+ resp.Code = 500
+ resp.Msg = "服务端未配置 secretKey"
+ resp.Data = JwtResponse{Jwt: ""}
+ return resp, errors.New("服务端未配置 secretKey")
+ }
+ // 恒定时间比较,避免通过响应时间逐步猜出密钥。
+ if subtle.ConstantTimeCompare([]byte(expected), []byte(strings.TrimSpace(req.SecretKey))) != 1 {
resp.Code = 401
resp.Msg = "无效的secretKey"
resp.Data = JwtResponse{Jwt: ""}
diff --git a/internal/logic/auth/loginlogic_test.go b/internal/logic/auth/loginlogic_test.go
new file mode 100644
index 00000000..ebe92ea7
--- /dev/null
+++ b/internal/logic/auth/loginlogic_test.go
@@ -0,0 +1,48 @@
+package auth
+
+import (
+ "context"
+ "testing"
+
+ "github.com/onlyLTY/dockerCopilot/internal/config"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/types"
+)
+
+func TestLoginRejectsEmptyConfiguredSecret(t *testing.T) {
+ logic := NewLoginLogic(context.Background(), &svc.ServiceContext{Config: config.Config{}})
+ resp, err := logic.Login(&types.LoginReq{SecretKey: ""})
+ if err == nil {
+ t.Fatal("expected error when server secret is empty")
+ }
+ if resp.Code != 500 {
+ t.Fatalf("code = %d, want 500", resp.Code)
+ }
+}
+
+func TestLoginRejectsWrongSecret(t *testing.T) {
+ cfg := config.Config{}
+ cfg.Auth.AccessSecret = "correct-secret"
+ logic := NewLoginLogic(context.Background(), &svc.ServiceContext{Config: cfg})
+ resp, err := logic.Login(&types.LoginReq{SecretKey: "wrong-secret"})
+ if err == nil {
+ t.Fatal("expected error for wrong secret")
+ }
+ if resp.Code != 401 {
+ t.Fatalf("code = %d, want 401", resp.Code)
+ }
+}
+
+func TestLoginAcceptsCorrectSecret(t *testing.T) {
+ cfg := config.Config{}
+ cfg.Auth.AccessSecret = "correct-secret"
+ cfg.Auth.AccessExpire = 3600
+ logic := NewLoginLogic(context.Background(), &svc.ServiceContext{Config: cfg})
+ resp, err := logic.Login(&types.LoginReq{SecretKey: "correct-secret"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if resp.Code != 200 {
+ t.Fatalf("code = %d, want 200", resp.Code)
+ }
+}
diff --git a/internal/logic/container/backuplogic.go b/internal/logic/container/backuplogic.go
index 4e56b661..099465b5 100644
--- a/internal/logic/container/backuplogic.go
+++ b/internal/logic/container/backuplogic.go
@@ -2,6 +2,7 @@ package container
import (
"context"
+ "fmt"
"github.com/onlyLTY/dockerCopilot/internal/utiles"
"github.com/onlyLTY/dockerCopilot/internal/svc"
@@ -26,15 +27,19 @@ func NewBackupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BackupLogi
func (l *BackupLogic) Backup() (resp *types.Resp, err error) {
resp = &types.Resp{}
- err = utiles.BackupContainer(l.svcCtx)
+ failures, err := utiles.BackupContainer(l.svcCtx)
if err != nil {
resp.Code = 500
resp.Msg = err.Error()
- resp.Data = map[string]interface{}{}
+ resp.Data = map[string]interface{}{"failures": failures}
return resp, err
}
- resp.Msg = "success"
resp.Code = 200
- resp.Data = map[string]interface{}{}
+ if len(failures) > 0 {
+ resp.Msg = fmt.Sprintf("备份完成,%d 个容器失败", len(failures))
+ } else {
+ resp.Msg = "success"
+ }
+ resp.Data = map[string]interface{}{"failures": failures}
return resp, nil
}
diff --git a/internal/logic/container/containerslistlogic.go b/internal/logic/container/containerslistlogic.go
index a8c44db2..e93af8ff 100644
--- a/internal/logic/container/containerslistlogic.go
+++ b/internal/logic/container/containerslistlogic.go
@@ -2,9 +2,11 @@ package container
import (
"context"
- "github.com/onlyLTY/dockerCopilot/internal/utiles"
+ "strings"
"time"
+ "github.com/onlyLTY/dockerCopilot/internal/utiles"
+
"github.com/onlyLTY/dockerCopilot/internal/svc"
"github.com/onlyLTY/dockerCopilot/internal/types"
@@ -26,6 +28,7 @@ type Info struct {
CreateTime string `json:"createTime"`
RunningTime string `json:"runningTime"`
HaveUpdate bool `json:"haveUpdate"`
+ IsCompose bool `json:"isCompose"`
}
func NewContainersListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ContainersListLogic {
@@ -54,28 +57,34 @@ func (l *ContainersListLogic) ContainersList() (resp *types.Resp, err error) {
containerInfo.Id = v.ID
containerInfo.Status = v.State
if len(v.Names) > 0 {
- ContainerName := v.Names[0][1:]
- containerInfo.Name = ContainerName
+ containerInfo.Name = strings.TrimPrefix(v.Names[0], "/")
} else {
- containerInfo.Name = "get container name error"
- l.Error("get container name error" + v.ID)
+ containerInfo.Name = "获取容器名失败"
+ l.Error("获取容器名失败: " + v.ID)
}
if v.Image != "" {
containerInfo.UsingImage = v.Image
} else {
containerInfo.UsingImage = v.ImageID
- l.Error("image dont have name" + v.ID)
+ l.Error("容器镜像无名称: " + v.ID)
}
containerInspect, err := utiles.GetContainerInspect(l.svcCtx, v.ID)
- if err != nil {
+ if err != nil || containerInspect.Config == nil {
containerInfo.CreateImage = ""
- l.Error("get image name error" + v.ID)
+ if err != nil {
+ l.Error("获取容器创建镜像失败: " + v.ID + ": " + err.Error())
+ } else {
+ l.Error("容器配置为空: " + v.ID)
+ }
+ } else {
+ containerInfo.CreateImage = containerInspect.Config.Image
}
- containerInfo.CreateImage = containerInspect.Config.Image
t := time.Unix(v.Created, 0)
containerInfo.CreateTime = t.Format("2006-01-02 15:04:05")
containerInfo.RunningTime = v.Status
containerInfo.HaveUpdate = v.Update
+ // 由 docker compose 创建的容器带 project 标签,前端可提示优先走 compose 更新
+ containerInfo.IsCompose = v.Labels["com.docker.compose.project"] != ""
containerInfoList = append(containerInfoList, containerInfo)
}
resp.Data = containerInfoList
diff --git a/internal/logic/container/renamelogic.go b/internal/logic/container/renamelogic.go
index d95da18b..5e5c1316 100644
--- a/internal/logic/container/renamelogic.go
+++ b/internal/logic/container/renamelogic.go
@@ -2,6 +2,8 @@ package container
import (
"context"
+ "strings"
+
"github.com/onlyLTY/dockerCopilot/internal/utiles"
"github.com/onlyLTY/dockerCopilot/internal/svc"
@@ -26,13 +28,21 @@ func NewRenameLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenameLogi
func (l *RenameLogic) Rename(req *types.ContainerRenameReq) (resp *types.Resp, err error) {
resp = &types.Resp{}
- err = utiles.RenameContainer(l.svcCtx, req.Id, req.NewName)
+ newName := strings.TrimSpace(req.NewName)
+ if newName == "" {
+ resp.Code = 400
+ resp.Msg = "新容器名不能为空"
+ resp.Data = map[string]interface{}{}
+ return resp, nil
+ }
+ err = utiles.RenameContainer(l.svcCtx, req.Id, newName)
if err != nil {
resp.Code = 400
resp.Msg = err.Error()
resp.Data = map[string]interface{}{}
return resp, err
}
+ utiles.AuditLog("container.rename", req.Id, "newName="+newName)
resp.Code = 200
resp.Msg = "success"
resp.Data = map[string]interface{}{}
diff --git a/internal/logic/container/restartlogic.go b/internal/logic/container/restartlogic.go
index e265cefd..da6cca70 100644
--- a/internal/logic/container/restartlogic.go
+++ b/internal/logic/container/restartlogic.go
@@ -33,6 +33,7 @@ func (l *RestartLogic) Restart(req *types.IdReq) (resp *types.Resp, err error) {
resp.Data = map[string]interface{}{}
return resp, err
}
+ utiles.AuditLog("container.restart", req.Id, "")
resp.Code = 200
resp.Msg = "success"
resp.Data = map[string]interface{}{}
diff --git a/internal/logic/container/restorelogic.go b/internal/logic/container/restorelogic.go
index 8334681f..4e82f547 100644
--- a/internal/logic/container/restorelogic.go
+++ b/internal/logic/container/restorelogic.go
@@ -46,17 +46,41 @@ func (l *RestoreLogic) Restore(req *types.ContainerRestoreReq) (resp *types.Resp
resp.Data = map[string]interface{}{}
return resp, err
}
+ taskCtx := l.svcCtx.RegisterTask(taskID)
go func() {
// Catch any panic and log the error
defer func() {
if r := recover(); r != nil {
- l.Errorf("Recovered from panic in restoreContainer: %v", r)
+ l.Errorf("恢复容器任务 panic 已恢复: %v", r)
+ msg := fmt.Sprintf("恢复任务异常中断: %v", r)
+ l.svcCtx.UpdateProgress(taskID, svc.TaskProgress{
+ TaskID: taskID,
+ Percentage: 100,
+ Name: "恢复容器",
+ Message: "恢复失败",
+ DetailMsg: msg,
+ IsDone: true,
+ })
+ utiles.NotifyTaskDone("restore", fileName, taskID, false, msg)
+ utiles.AuditLog("container.restore", fileName, msg)
}
+ l.svcCtx.FinishTask(taskID)
}()
- err := utiles.RestoreContainer(l.svcCtx, fileName, taskID)
+ err := utiles.RestoreContainer(taskCtx, l.svcCtx, fileName, taskID, req.Start, req.Force)
if err != nil {
- l.Errorf("Error in restoreContainer: %v", err)
+ l.Errorf("恢复容器出错: %v", err)
}
+ final, _ := l.svcCtx.GetProgress(taskID)
+ message := final.Message
+ if message == "" {
+ message = "恢复任务已结束"
+ }
+ detail := final.DetailMsg
+ if detail == "" {
+ detail = message
+ }
+ utiles.NotifyTaskDone("restore", fileName, taskID, err == nil, message)
+ utiles.AuditLog("container.restore", fileName, detail)
}()
resp.Code = 200
resp.Msg = "success"
diff --git a/internal/logic/container/startlogic.go b/internal/logic/container/startlogic.go
index cbf2adf2..8cb22cd7 100644
--- a/internal/logic/container/startlogic.go
+++ b/internal/logic/container/startlogic.go
@@ -32,6 +32,7 @@ func (l *StartLogic) Start(req *types.IdReq) (resp *types.Resp, err error) {
resp.Data = map[string]interface{}{}
return resp, err
}
+ utiles.AuditLog("container.start", req.Id, "")
resp.Code = 200
resp.Msg = "success"
resp.Data = map[string]interface{}{}
diff --git a/internal/logic/container/stoplogic.go b/internal/logic/container/stoplogic.go
index dcc10387..1a1f283c 100644
--- a/internal/logic/container/stoplogic.go
+++ b/internal/logic/container/stoplogic.go
@@ -33,6 +33,7 @@ func (l *StopLogic) Stop(req *types.IdReq) (resp *types.Resp, err error) {
resp.Data = map[string]interface{}{}
return resp, err
}
+ utiles.AuditLog("container.stop", req.Id, "")
resp.Code = 200
resp.Msg = "success"
resp.Data = map[string]interface{}{}
diff --git a/internal/logic/container/updatelogic.go b/internal/logic/container/updatelogic.go
index 4feb0bd5..69157f37 100644
--- a/internal/logic/container/updatelogic.go
+++ b/internal/logic/container/updatelogic.go
@@ -2,12 +2,14 @@ package container
import (
"context"
+ "fmt"
+ "os"
+
"github.com/google/uuid"
"github.com/onlyLTY/dockerCopilot/internal/svc"
"github.com/onlyLTY/dockerCopilot/internal/types"
"github.com/onlyLTY/dockerCopilot/internal/utiles"
"github.com/zeromicro/go-zero/core/logx"
- "os"
)
type UpdateLogic struct {
@@ -27,19 +29,39 @@ func NewUpdateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateLogi
func (l *UpdateLogic) Update(req *types.ContainerUpdateReq) (resp *types.Resp, err error) {
resp = &types.Resp{}
taskID := uuid.New().String()
+ taskCtx := l.svcCtx.RegisterTask(taskID)
go func() {
// Catch any panic and log the error
defer func() {
if r := recover(); r != nil {
- l.Errorf("Recovered from panic in UpdateContainer: %v", r)
+ l.Errorf("更新容器任务 panic 已恢复: %v", r)
+ msg := fmt.Sprintf("更新任务异常中断: %v", r)
+ l.svcCtx.UpdateProgress(taskID, svc.TaskProgress{
+ TaskID: taskID,
+ Percentage: 100,
+ Name: req.ContainerName,
+ Message: "更新失败",
+ DetailMsg: msg,
+ IsDone: true,
+ })
+ utiles.NotifyTaskDone("update", req.ContainerName, taskID, false, msg)
+ utiles.AuditLog("container.update", req.ContainerName, msg)
}
+ l.svcCtx.FinishTask(taskID)
}()
imageNameAndTag := req.ImageNameAndTag
delOldContainer := os.Getenv("DelOldContainer") != "false"
- err := utiles.UpdateContainer(l.svcCtx, req.Id, req.ContainerName, imageNameAndTag, delOldContainer, taskID)
+ err := utiles.UpdateContainer(l.svcCtx, taskCtx, req.Id, req.ContainerName, imageNameAndTag, delOldContainer, taskID)
if err != nil {
- l.Errorf("Error in UpdateContainer: %v", err)
+ l.Errorf("更新容器出错: %v", err)
+ }
+ final, _ := l.svcCtx.GetProgress(taskID)
+ detail := final.DetailMsg
+ if detail == "" {
+ detail = final.Message
}
+ utiles.NotifyTaskDone("update", req.ContainerName, taskID, err == nil, final.Message)
+ utiles.AuditLog("container.update", req.ContainerName, detail)
}()
resp.Code = 200
resp.Msg = "success"
diff --git a/internal/logic/image/cleanimageslogic.go b/internal/logic/image/cleanimageslogic.go
new file mode 100644
index 00000000..cc5ab931
--- /dev/null
+++ b/internal/logic/image/cleanimageslogic.go
@@ -0,0 +1,60 @@
+package image
+
+import (
+ "context"
+ "strconv"
+
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/types"
+ "github.com/onlyLTY/dockerCopilot/internal/utiles"
+
+ "github.com/zeromicro/go-zero/core/logx"
+)
+
+type CleanImagesLogic struct {
+ logx.Logger
+ ctx context.Context
+ svcCtx *svc.ServiceContext
+}
+
+func NewCleanImagesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CleanImagesLogic {
+ return &CleanImagesLogic{
+ Logger: logx.WithContext(ctx),
+ ctx: ctx,
+ svcCtx: svcCtx,
+ }
+}
+
+// CleanImages 批量清理镜像:filter=untagged 删除无 TAG 镜像,
+// filter=unused 删除未被任何容器引用的镜像。单个失败不影响其它。
+func (l *CleanImagesLogic) CleanImages(req *types.RemoveImagesReq) (resp *types.Resp, err error) {
+ resp = &types.Resp{}
+ if req.Filter != utiles.CleanupFilterUntagged && req.Filter != utiles.CleanupFilterUnused {
+ resp.Code = 400
+ resp.Msg = "filter 参数错误,只支持 untagged(无 TAG 镜像)或 unused(未使用镜像)"
+ resp.Data = map[string]interface{}{}
+ return resp, nil
+ }
+ removed, failed, err := utiles.RemoveImagesFiltered(l.svcCtx, req.Filter)
+ if err != nil {
+ resp.Code = 500
+ resp.Msg = err.Error()
+ resp.Data = map[string]interface{}{}
+ return resp, err
+ }
+ // 已删除/失败清单都进审计,删除操作不可恢复,必须留痕。
+ utiles.AuditLog("image.cleanup", req.Filter,
+ "removed="+strconv.Itoa(len(removed))+", failed="+strconv.Itoa(len(failed)))
+ resp.Code = 200
+ if len(failed) > 0 {
+ resp.Msg = "清理完成,部分镜像删除失败"
+ } else {
+ resp.Msg = "success"
+ }
+ resp.Data = map[string]interface{}{
+ "filter": req.Filter,
+ "removed": removed,
+ "failed": failed,
+ }
+ return resp, nil
+}
diff --git a/internal/logic/image/removelogic.go b/internal/logic/image/removelogic.go
index 21fe84d4..6af9d13d 100644
--- a/internal/logic/image/removelogic.go
+++ b/internal/logic/image/removelogic.go
@@ -31,13 +31,14 @@ func (l *RemoveLogic) Remove(req *types.RemoveImageReq) (resp *types.Resp, err e
if strings.HasPrefix(imageId, "sha256:") {
imageId = strings.TrimPrefix(imageId, "sha256:")
}
- err = utiles.RemoveImage(l.svcCtx, req.Id, req.Force)
+ err = utiles.RemoveImage(l.svcCtx, imageId, req.Force)
if err != nil {
resp.Code = 409
resp.Msg = err.Error()
resp.Data = map[string]interface{}{}
return resp, nil
}
+ utiles.AuditLog("image.remove", req.Id, "")
resp.Code = 200
resp.Msg = "success"
resp.Data = map[string]interface{}{}
diff --git a/internal/logic/progress/cancelprogresslogic.go b/internal/logic/progress/cancelprogresslogic.go
new file mode 100644
index 00000000..fcbc8ffa
--- /dev/null
+++ b/internal/logic/progress/cancelprogresslogic.go
@@ -0,0 +1,39 @@
+package progress
+
+import (
+ "context"
+
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/types"
+
+ "github.com/zeromicro/go-zero/core/logx"
+)
+
+type CancelProgressLogic struct {
+ logx.Logger
+ ctx context.Context
+ svcCtx *svc.ServiceContext
+}
+
+func NewCancelProgressLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelProgressLogic {
+ return &CancelProgressLogic{
+ Logger: logx.WithContext(ctx),
+ ctx: ctx,
+ svcCtx: svcCtx,
+ }
+}
+
+func (l *CancelProgressLogic) CancelProgress(req *types.GetProgressReq) (resp *types.Resp, err error) {
+ resp = &types.Resp{}
+ ok, msg := l.svcCtx.CancelActiveTask(req.TaskId)
+ if !ok {
+ resp.Code = 400
+ resp.Msg = msg
+ resp.Data = map[string]interface{}{}
+ return resp, nil
+ }
+ resp.Code = 200
+ resp.Msg = "success"
+ resp.Data = map[string]interface{}{"taskID": req.TaskId}
+ return resp, nil
+}
diff --git a/internal/logic/progress/listprogresslogic.go b/internal/logic/progress/listprogresslogic.go
new file mode 100644
index 00000000..98f8a8db
--- /dev/null
+++ b/internal/logic/progress/listprogresslogic.go
@@ -0,0 +1,32 @@
+package progress
+
+import (
+ "context"
+
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/types"
+
+ "github.com/zeromicro/go-zero/core/logx"
+)
+
+type ListProgressLogic struct {
+ logx.Logger
+ ctx context.Context
+ svcCtx *svc.ServiceContext
+}
+
+func NewListProgressLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListProgressLogic {
+ return &ListProgressLogic{
+ Logger: logx.WithContext(ctx),
+ ctx: ctx,
+ svcCtx: svcCtx,
+ }
+}
+
+func (l *ListProgressLogic) ListProgress() (resp *types.Resp, err error) {
+ resp = &types.Resp{}
+ resp.Code = 200
+ resp.Msg = "success"
+ resp.Data = l.svcCtx.ListProgress()
+ return resp, nil
+}
diff --git a/internal/logic/settings/settingslogic.go b/internal/logic/settings/settingslogic.go
new file mode 100644
index 00000000..4eaeb84d
--- /dev/null
+++ b/internal/logic/settings/settingslogic.go
@@ -0,0 +1,90 @@
+package settings
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/types"
+ "github.com/onlyLTY/dockerCopilot/internal/utiles"
+
+ "github.com/zeromicro/go-zero/core/logx"
+)
+
+type SettingsLogic struct {
+ logx.Logger
+ ctx context.Context
+ svcCtx *svc.ServiceContext
+}
+
+func NewSettingsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SettingsLogic {
+ return &SettingsLogic{
+ Logger: logx.WithContext(ctx),
+ ctx: ctx,
+ svcCtx: svcCtx,
+ }
+}
+
+func (l *SettingsLogic) GetSettings() (resp *types.Resp, err error) {
+ resp = &types.Resp{}
+ s := utiles.LoadSettings()
+ resp.Code = 200
+ resp.Msg = "success"
+ resp.Data = map[string]interface{}{
+ "backupDir": s.BackupDir,
+ "effectiveBackupDir": utiles.EffectiveBackupDir(),
+ }
+ return resp, nil
+}
+
+func (l *SettingsLogic) UpdateSettings(req *types.UpdateSettingsReq) (resp *types.Resp, err error) {
+ resp = &types.Resp{}
+ dir := strings.TrimSpace(req.BackupDir)
+ if dir == "" {
+ resp.Code = 400
+ resp.Msg = "备份目录不能为空"
+ resp.Data = map[string]interface{}{}
+ return resp, nil
+ }
+ if !filepath.IsAbs(dir) {
+ resp.Code = 400
+ resp.Msg = "备份目录必须是绝对路径"
+ resp.Data = map[string]interface{}{}
+ return resp, nil
+ }
+ if mkErr := os.MkdirAll(dir, 0755); mkErr != nil {
+ resp.Code = 400
+ resp.Msg = "无法创建备份目录: " + mkErr.Error()
+ resp.Data = map[string]interface{}{}
+ return resp, nil
+ }
+ testFile, testErr := os.CreateTemp(dir, ".write-test-*")
+ if testErr != nil {
+ resp.Code = 400
+ resp.Msg = "备份目录不可写: " + testErr.Error()
+ resp.Data = map[string]interface{}{}
+ return resp, nil
+ }
+ testName := testFile.Name()
+ _ = testFile.Close()
+ _ = os.Remove(testName)
+
+ s := utiles.LoadSettings()
+ s.BackupDir = dir
+ if saveErr := utiles.SaveSettings(s); saveErr != nil {
+ resp.Code = 500
+ resp.Msg = "保存设置失败: " + saveErr.Error()
+ resp.Data = map[string]interface{}{}
+ return resp, nil
+ }
+ utiles.AuditLog("settings.update", "settings", "backupDir="+dir)
+ resp.Code = 200
+ resp.Msg = "success"
+ resp.Data = map[string]interface{}{
+ "backupDir": s.BackupDir,
+ "effectiveBackupDir": utiles.EffectiveBackupDir(),
+ }
+ return resp, nil
+}
diff --git a/internal/logic/version/updateprogramlogic.go b/internal/logic/version/updateprogramlogic.go
index 07c1c34a..610436c9 100644
--- a/internal/logic/version/updateprogramlogic.go
+++ b/internal/logic/version/updateprogramlogic.go
@@ -35,6 +35,10 @@ func (l *UpdateProgramLogic) UpdateProgram() (resp *types.Resp, err error) {
}
resp.Code = 200
resp.Msg = "success"
+ // 与 MCP 通道保持一致:自更新成功也要留审计与通知(进程 10 秒后退出,
+ // 必须在退出前完成落盘)。
+ utiles.AuditLog("program.update", "self", "program update triggered via HTTP API")
+ utiles.NotifyTaskDone("program", "self", "", true, "程序自更新完成,服务即将重启换入新版本")
go func() {
time.Sleep(10 * time.Second)
os.Exit(1)
diff --git a/internal/logic/version/versionlogic.go b/internal/logic/version/versionlogic.go
index 4cf7c9d8..f9579891 100644
--- a/internal/logic/version/versionlogic.go
+++ b/internal/logic/version/versionlogic.go
@@ -2,6 +2,7 @@ package version
import (
"context"
+ "errors"
"github.com/onlyLTY/dockerCopilot/internal/config"
@@ -37,8 +38,20 @@ func (l *VersionLogic) Version(req *types.VersionReq) (resp *types.Resp, err err
return resp, nil
} else if req.Type == "remote" {
remoteVersion, err := utiles.GetRemoteVersion()
+ if errors.Is(err, utiles.ErrFnosManagedUpdate) {
+ // FNOS 版本不走通用更新链:明确告知用户,而不是伪装成“无更新”。
+ resp.Code = 200
+ resp.Msg = err.Error()
+ resp.Data = map[string]string{
+ "remoteVersion": config.Version,
+ "fnos": "true",
+ }
+ return resp, nil
+ }
if err != nil {
- resp.Code = 50001
+ // 注意:resp.Code 会被 handler 当作 HTTP 状态码写入,业务码不能超过 999,
+ // 否则 net/http 直接丢弃响应体(曾导致错误时返回空 500)。
+ resp.Code = 500
resp.Msg = "获取版本错误" + err.Error()
resp.Data = map[string]string{
"remoteVersion": config.Version,
diff --git a/internal/mcp/mcp_test.go b/internal/mcp/mcp_test.go
new file mode 100644
index 00000000..42f8f313
--- /dev/null
+++ b/internal/mcp/mcp_test.go
@@ -0,0 +1,210 @@
+package mcp
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/mark3labs/mcp-go/server"
+ "github.com/onlyLTY/dockerCopilot/internal/config"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+)
+
+func testContext() *svc.ServiceContext {
+ cfg := config.Config{}
+ cfg.Auth.AccessSecret = "test-secret-1234"
+ return &svc.ServiceContext{Config: cfg}
+}
+
+var expectedTools = []string{
+ "dc_ping", "dc_info",
+ "dc_list_containers", "dc_inspect_container", "dc_start_container", "dc_stop_container",
+ "dc_restart_container", "dc_rename_container", "dc_update_container", "dc_autoupdate_run",
+ "dc_list_images", "dc_check_image_update", "dc_remove_image", "dc_clean_images",
+ "dc_backup_containers", "dc_backup_compose", "dc_list_backups", "dc_restore_container", "dc_delete_backup",
+ "dc_list_progress", "dc_get_progress", "dc_cancel_progress",
+ "dc_get_settings", "dc_update_settings",
+ "dc_get_version", "dc_check_program_update", "dc_update_program",
+}
+
+func TestAllToolsRegistered(t *testing.T) {
+ s := NewServer(testContext())
+ got := s.ListTools()
+ for _, name := range expectedTools {
+ if _, ok := got[name]; !ok {
+ t.Errorf("tool %q not registered", name)
+ }
+ }
+ if len(got) != len(expectedTools) {
+ t.Errorf("registered %d tools, want %d", len(got), len(expectedTools))
+ }
+}
+
+func TestAuthMiddleware(t *testing.T) {
+ ctx := testContext()
+ next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+ h := authMiddleware(ctx, next)
+
+ cases := []struct {
+ name string
+ modify func(*http.Request)
+ want int
+ }{
+ {"no token", func(r *http.Request) {}, http.StatusUnauthorized},
+ {"wrong token", func(r *http.Request) { r.Header.Set("X-MCP-Token", "nope") }, http.StatusUnauthorized},
+ {"bearer", func(r *http.Request) { r.Header.Set("Authorization", "Bearer test-secret-1234") }, http.StatusOK},
+ {"x-mcp-token", func(r *http.Request) { r.Header.Set("X-MCP-Token", "test-secret-1234") }, http.StatusOK},
+ {"query token", func(r *http.Request) {
+ q := r.URL.Query()
+ q.Set("token", "test-secret-1234")
+ r.URL.RawQuery = q.Encode()
+ }, http.StatusOK},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ req := httptest.NewRequest(http.MethodPost, "/mcp", nil)
+ tc.modify(req)
+ rec := httptest.NewRecorder()
+ h(rec, req)
+ if rec.Code != tc.want {
+ t.Fatalf("status = %d, want %d", rec.Code, tc.want)
+ }
+ })
+ }
+}
+
+// TestInitializeAndListTools 通过真实 Streamable HTTP 传输验证握手与工具暴露。
+func TestInitializeAndListTools(t *testing.T) {
+ ctx := testContext()
+ mcpServer := NewServer(ctx)
+ h := authMiddleware(ctx, server.NewStreamableHTTPServer(mcpServer))
+ ts := httptest.NewServer(h)
+ defer ts.Close()
+
+ token := "test-secret-1234"
+
+ // 1. initialize
+ initBody := `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}`
+ resp, header := postJSONRPC(t, ts.URL, token, "", initBody)
+ if resp["id"] == nil {
+ t.Fatalf("initialize 响应缺少 id: %v", resp)
+ }
+ sessionID := header.Get("Mcp-Session-Id")
+ if sessionID == "" {
+ t.Fatalf("initialize 未返回 Mcp-Session-Id 头: %v", header)
+ }
+
+ // 2. notifications/initialized
+ postJSONRPC(t, ts.URL, token, sessionID, `{"jsonrpc":"2.0","method":"notifications/initialized"}`)
+
+ // 3. tools/list
+ listResp, _ := postJSONRPC(t, ts.URL, token, sessionID, `{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}`)
+ result, ok := listResp["result"].(map[string]any)
+ if !ok {
+ t.Fatalf("tools/list 响应缺少 result: %v", listResp)
+ }
+ tools, ok := result["tools"].([]any)
+ if !ok {
+ t.Fatalf("tools/list result 缺少 tools: %v", result)
+ }
+ names := make(map[string]bool, len(tools))
+ for _, item := range tools {
+ m, _ := item.(map[string]any)
+ if n, _ := m["name"].(string); n != "" {
+ names[n] = true
+ }
+ }
+ for _, want := range expectedTools {
+ if !names[want] {
+ t.Errorf("tools/list 未包含 %q", want)
+ }
+ }
+}
+
+// TestMCPRequiresAuth 确认未带 token 时传输层直接 401。
+func TestMCPRequiresAuth(t *testing.T) {
+ ctx := testContext()
+ h := authMiddleware(ctx, server.NewStreamableHTTPServer(NewServer(ctx)))
+ ts := httptest.NewServer(h)
+ defer ts.Close()
+
+ req, _ := http.NewRequest(http.MethodPost, ts.URL, strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Accept", "application/json, text/event-stream")
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatalf("请求失败: %v", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusUnauthorized {
+ t.Fatalf("status = %d, want 401", resp.StatusCode)
+ }
+}
+
+// postJSONRPC 发送一条 JSON-RPC 消息并按需解析 JSON 或 SSE 响应。
+func postJSONRPC(t *testing.T, baseURL, token, sessionID, body string) (map[string]any, http.Header) {
+ t.Helper()
+ req, err := http.NewRequest(http.MethodPost, baseURL, bytes.NewBufferString(body))
+ if err != nil {
+ t.Fatalf("构造请求失败: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Accept", "application/json, text/event-stream")
+ req.Header.Set("Authorization", "Bearer "+token)
+ if sessionID != "" {
+ req.Header.Set("Mcp-Session-Id", sessionID)
+ }
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatalf("请求失败: %v", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
+ raw, _ := io.ReadAll(resp.Body)
+ t.Fatalf("status = %d, body = %s", resp.StatusCode, raw)
+ }
+ raw, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatalf("读取响应失败: %v", err)
+ }
+ if len(bytes.TrimSpace(raw)) == 0 {
+ return map[string]any{}, resp.Header
+ }
+ return parsePayload(t, raw), resp.Header
+}
+
+func parsePayload(t *testing.T, raw []byte) map[string]any {
+ t.Helper()
+ trimmed := bytes.TrimSpace(raw)
+ if trimmed[0] == '{' {
+ var m map[string]any
+ if err := json.Unmarshal(trimmed, &m); err != nil {
+ t.Fatalf("解析 JSON 失败: %v (%s)", err, trimmed)
+ }
+ return m
+ }
+ // SSE: data: {...}
+ scanner := bufio.NewScanner(bytes.NewReader(trimmed))
+ scanner.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+ if !strings.HasPrefix(line, "data:") {
+ continue
+ }
+ payload := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
+ var m map[string]any
+ if err := json.Unmarshal([]byte(payload), &m); err != nil {
+ continue
+ }
+ return m
+ }
+ t.Fatalf("无法从响应中解析 JSON-RPC 消息: %s", raw)
+ return nil
+}
diff --git a/internal/mcp/server.go b/internal/mcp/server.go
new file mode 100644
index 00000000..2f45fe36
--- /dev/null
+++ b/internal/mcp/server.go
@@ -0,0 +1,110 @@
+package mcp
+
+import (
+ "crypto/subtle"
+ "net/http"
+ "os"
+ "strings"
+
+ "github.com/mark3labs/mcp-go/server"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/zeromicro/go-zero/core/logx"
+ "github.com/zeromicro/go-zero/rest"
+)
+
+// Version 是 MCP server 对外声明的版本,与程序版本保持一致。
+const Version = "2.2.0"
+
+// Register 把 dockerCopilot 的全部能力注册为 MCP 工具,并挂载到 /mcp(Streamable HTTP)。
+// 与 /api/* 并存:HTTP API 继续给脚本和旧客户端用,MCP 给 Agent 用。
+func Register(s *rest.Server, ctx *svc.ServiceContext) {
+ mcpServer := NewServer(ctx)
+ httpSrv := server.NewStreamableHTTPServer(mcpServer)
+ handler := authMiddleware(ctx, httpSrv)
+
+ s.AddRoutes([]rest.Route{
+ {Method: http.MethodGet, Path: "/mcp", Handler: handler},
+ {Method: http.MethodPost, Path: "/mcp", Handler: handler},
+ {Method: http.MethodDelete, Path: "/mcp", Handler: handler},
+ })
+
+ logx.Infof("MCP 已挂载: /mcp(Streamable HTTP),共 %d 个工具", len(mcpServer.ListTools()))
+ if mcpAuthDisabled() {
+ logx.Error("MCP 鉴权已通过 MCP_TOKEN=off 显式关闭,请确保 /mcp 不暴露在公网")
+ }
+}
+
+// NewServer 构建并注册全部 MCP 工具,供 Register 与测试复用。
+func NewServer(ctx *svc.ServiceContext) *server.MCPServer {
+ mcpServer := server.NewMCPServer(
+ "dockerCopilot",
+ Version,
+ server.WithToolCapabilities(true),
+ server.WithRecovery(),
+ )
+
+ registerCommonTools(mcpServer, ctx)
+ registerContainerTools(mcpServer, ctx)
+ registerImageTools(mcpServer, ctx)
+ registerBackupTools(mcpServer, ctx)
+ registerProgressTools(mcpServer, ctx)
+ registerSettingsTools(mcpServer, ctx)
+ registerVersionTools(mcpServer, ctx)
+
+ return mcpServer
+}
+
+// mcpAuthDisabled 允许用 MCP_TOKEN=off 显式关闭鉴权(仅建议在完全隔离的内网使用)。
+func mcpAuthDisabled() bool {
+ v := strings.TrimSpace(strings.ToLower(os.Getenv("MCP_TOKEN")))
+ return v == "off" || v == "none" || v == "disabled"
+}
+
+// expectedToken 优先使用 MCP_TOKEN,未设置时退回 secretKey(与 /api 的 JWT 密钥同源)。
+func expectedToken(ctx *svc.ServiceContext) string {
+ if t := strings.TrimSpace(os.Getenv("MCP_TOKEN")); t != "" && !mcpAuthDisabled() {
+ return t
+ }
+ return ctx.Config.Auth.AccessSecret
+}
+
+// authMiddleware 保护 /mcp:支持 Bearer、X-MCP-Token 和 ?token= 三种携带方式。
+// MCP 客户端(Claude Code / Cursor / Cline 等)通常只支持自定义 Header 或 URL 参数,
+// 因此三种都接受。
+func authMiddleware(ctx *svc.ServiceContext, next http.Handler) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if mcpAuthDisabled() {
+ next.ServeHTTP(w, r)
+ return
+ }
+ want := expectedToken(ctx)
+ if want == "" {
+ http.Error(w, `{"code":50000,"msg":"server secretKey 未配置,无法校验 MCP 请求"}`, http.StatusInternalServerError)
+ return
+ }
+ if !tokenMatch(r, want) {
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusUnauthorized)
+ _, _ = w.Write([]byte(`{"code":401,"msg":"unauthorized: 需要 Authorization: Bearer 或 X-MCP-Token 或 ?token="}`))
+ return
+ }
+ next.ServeHTTP(w, r)
+ }
+}
+
+func tokenMatch(r *http.Request, want string) bool {
+ candidates := []string{
+ strings.TrimSpace(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")),
+ strings.TrimSpace(r.Header.Get("X-MCP-Token")),
+ strings.TrimSpace(r.URL.Query().Get("token")),
+ }
+ for _, got := range candidates {
+ if got == "" {
+ continue
+ }
+ if subtle.ConstantTimeCompare([]byte(got), []byte(want)) == 1 {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/mcp/tools_backup.go b/internal/mcp/tools_backup.go
new file mode 100644
index 00000000..172cef49
--- /dev/null
+++ b/internal/mcp/tools_backup.go
@@ -0,0 +1,120 @@
+package mcp
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+
+ "github.com/google/uuid"
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/utiles"
+)
+
+func registerBackupTools(s *server.MCPServer, ctx *svc.ServiceContext) {
+ s.AddTool(mcp.NewTool("dc_backup_containers",
+ mcp.WithDescription("备份全部容器配置为 JSON 文件,写入生效的备份目录(BACKUP_DIR > 设置 > /data/backups);单个容器失败不中止整体备份,失败清单在返回的 failures 中"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ failures, backupErr := utiles.BackupContainer(ctx)
+ if backupErr != nil {
+ return mcp.NewToolResultError(backupErr.Error()), nil
+ }
+ utiles.AuditLog("container.backup", "all", "backup completed")
+ result := map[string]any{
+ "result": "backup completed",
+ "dir": utiles.EffectiveBackupDir(),
+ "failures": failures,
+ }
+ if len(failures) > 0 {
+ result["result"] = "backup completed with failures"
+ }
+ return jsonResult(result), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_backup_compose",
+ mcp.WithDescription("把容器配置导出为 docker-compose YAML 备份"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ if err := utiles.Backup2Compose(ctx); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ utiles.AuditLog("container.backup2compose", "all", "compose backup completed")
+ return textResult("compose backup completed, dir=" + utiles.EffectiveBackupDir()), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_list_backups",
+ mcp.WithDescription("列出备份目录下的 .json / .yaml 备份文件"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ list, err := utiles.BackupList(ctx)
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ return jsonResult(map[string]any{
+ "dir": utiles.EffectiveBackupDir(),
+ "files": list,
+ }), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_restore_container",
+ mcp.WithDescription("从 JSON 备份恢复容器;force=true 时同名容器会被替换(改名旧容器→创建新容器→成功后删除旧容器,失败自动回滚)"),
+ mcp.WithString("filename", mcp.Required(), mcp.Description("备份文件名(仅 .json)")),
+ mcp.WithBoolean("start", mcp.Description("是否启动;不传则按备份记录的状态恢复")),
+ mcp.WithBoolean("force", mcp.Description("是否替换同名容器,默认 false(同名则跳过)")),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ args := req.GetArguments()
+ filename, err := mustString(args, "filename")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ fullPath, err := utiles.ResolveBackupPath(filename, ".json")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ var startOverride *bool
+ if _, ok := args["start"]; ok {
+ v := optionalBool(args, "start", false)
+ startOverride = &v
+ }
+ force := optionalBool(args, "force", false)
+
+ taskID := uuid.New().String()
+ taskCtx := ctx.RegisterTask(taskID)
+ defer ctx.FinishTask(taskID)
+
+ restoreErr := utiles.RestoreContainer(taskCtx, ctx, filepath.Base(fullPath), taskID, startOverride, force)
+ final, _ := ctx.GetProgress(taskID)
+ message := final.Message
+ if message == "" {
+ message = "恢复任务已结束"
+ }
+ detail := final.DetailMsg
+ if detail == "" {
+ detail = message
+ }
+ utiles.NotifyTaskDone("restore", filename, taskID, restoreErr == nil, message)
+ utiles.AuditLog("container.restore", filename, detail)
+ if restoreErr != nil {
+ return mcp.NewToolResultError(restoreErr.Error()), nil
+ }
+ return jsonResult(final), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_delete_backup",
+ mcp.WithDescription("删除一个备份文件(只删备份文件,不动容器)"),
+ mcp.WithString("filename", mcp.Required(), mcp.Description("备份文件名")),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ filename, err := mustString(req.GetArguments(), "filename")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ fullPath, err := utiles.ResolveBackupPath(filename, ".json", ".yaml")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ if err = os.Remove(fullPath); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ utiles.AuditLog("container.backup.delete", filename, "deleted")
+ return textResult("deleted backup: " + filename), nil
+ })
+}
diff --git a/internal/mcp/tools_common.go b/internal/mcp/tools_common.go
new file mode 100644
index 00000000..9bc1995d
--- /dev/null
+++ b/internal/mcp/tools_common.go
@@ -0,0 +1,73 @@
+package mcp
+
+import (
+ "context"
+ "os"
+ "runtime"
+ "time"
+
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+ "github.com/onlyLTY/dockerCopilot/internal/config"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/utiles"
+)
+
+func registerCommonTools(s *server.MCPServer, ctx *svc.ServiceContext) {
+ s.AddTool(mcp.NewTool("dc_ping",
+ mcp.WithDescription("健康检查:确认 dockerCopilot 服务存活"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ return textResult("pong from dockerCopilot"), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_info",
+ mcp.WithDescription("获取服务运行信息:版本、构建时间、监听地址、Docker daemon 连通性、备份目录、MCP 鉴权状态"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ info := map[string]any{
+ "version": config.Version,
+ "buildDate": config.BuildDate,
+ "host": ctx.Config.Host,
+ "port": ctx.Config.Port,
+ "dockerHost": os.Getenv("DOCKER_HOST"),
+ "backupDir": utiles.EffectiveBackupDir(),
+ "goVersion": runtime.Version(),
+ "autoUpdateCron": os.Getenv("AUTOUPDATE_CRON"),
+ "mcpAuthEnabled": !mcpAuthDisabled(),
+ "mcpTokenSource": mcpTokenSource(),
+ "toolCount": len(s.ListTools()),
+ }
+
+ // 真实探测一次 daemon,避免"客户端对象创建成功"被误判成可用。
+ if ctx.DockerClient == nil {
+ info["dockerReady"] = false
+ info["dockerError"] = "docker client 未初始化"
+ } else {
+ pingCtx, cancel := context.WithTimeout(ctx2, 5*time.Second)
+ pingResp, err := ctx.DockerClient.Ping(pingCtx)
+ cancel()
+ if err != nil {
+ info["dockerReady"] = false
+ info["dockerError"] = err.Error()
+ } else {
+ info["dockerReady"] = true
+ info["dockerAPIVersion"] = pingResp.APIVersion
+ info["dockerOSType"] = pingResp.OSType
+ }
+ }
+ if ctx.DockerInitErr != nil {
+ info["dockerInitErr"] = ctx.DockerInitErr.Error()
+ }
+ return jsonResult(info), nil
+ })
+}
+
+// mcpTokenSource 说明 /mcp 当前用的是哪个凭据,便于排障。
+func mcpTokenSource() string {
+ if mcpAuthDisabled() {
+ return "disabled"
+ }
+ if v := os.Getenv("MCP_TOKEN"); v != "" {
+ return "MCP_TOKEN"
+ }
+ return "secretKey"
+}
diff --git a/internal/mcp/tools_container.go b/internal/mcp/tools_container.go
new file mode 100644
index 00000000..48bb48fc
--- /dev/null
+++ b/internal/mcp/tools_container.go
@@ -0,0 +1,158 @@
+package mcp
+
+import (
+ "context"
+ "os"
+
+ "github.com/google/uuid"
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/utiles"
+)
+
+func registerContainerTools(s *server.MCPServer, ctx *svc.ServiceContext) {
+ s.AddTool(mcp.NewTool("dc_list_containers",
+ mcp.WithDescription("列出所有容器(含已停止),附带是否有可用更新、是否由 docker compose 管理"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ list, err := utiles.GetContainerList(ctx)
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ list = utiles.CheckImageUpdate(ctx, list)
+ return jsonResult(list), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_inspect_container",
+ mcp.WithDescription("查看单个容器的完整 inspect 明细"),
+ mcp.WithString("containerId", mcp.Required(), mcp.Description("容器 ID 或名称")),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ id, err := mustString(req.GetArguments(), "containerId")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ info, err := utiles.GetContainerInspect(ctx, id)
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ return jsonResult(info), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_start_container",
+ mcp.WithDescription("启动容器"),
+ mcp.WithString("containerId", mcp.Required(), mcp.Description("容器 ID 或名称")),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ id, err := mustString(req.GetArguments(), "containerId")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ if err = utiles.StartContainer(ctx, id); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ utiles.AuditLog("container.start", id, "started")
+ return textResult("started: " + id), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_stop_container",
+ mcp.WithDescription("停止容器(发送 SIGTERM,10 秒宽限期)"),
+ mcp.WithString("containerId", mcp.Required(), mcp.Description("容器 ID 或名称")),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ id, err := mustString(req.GetArguments(), "containerId")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ if err = utiles.StopContainer(ctx, id); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ utiles.AuditLog("container.stop", id, "stopped")
+ return textResult("stopped: " + id), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_restart_container",
+ mcp.WithDescription("重启容器"),
+ mcp.WithString("containerId", mcp.Required(), mcp.Description("容器 ID 或名称")),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ id, err := mustString(req.GetArguments(), "containerId")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ if err = utiles.RestartContainer(ctx, id); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ utiles.AuditLog("container.restart", id, "restarted")
+ return textResult("restarted: " + id), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_rename_container",
+ mcp.WithDescription("重命名容器"),
+ mcp.WithString("containerId", mcp.Required(), mcp.Description("容器 ID 或名称")),
+ mcp.WithString("newName", mcp.Required(), mcp.Description("新容器名")),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ args := req.GetArguments()
+ id, err := mustString(args, "containerId")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ newName, err := mustString(args, "newName")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ if err = utiles.RenameContainer(ctx, id, newName); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ utiles.AuditLog("container.rename", id, "renamed to "+newName)
+ return textResult("renamed " + id + " -> " + newName), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_update_container",
+ mcp.WithDescription("一键更新容器:拉取新镜像 → 停止并改名旧容器 → 克隆配置创建新容器 → 启动并等待健康检查 → 删除旧容器 → 清理旧镜像;任一步失败自动回滚旧容器"),
+ mcp.WithString("containerId", mcp.Required(), mcp.Description("容器 ID")),
+ mcp.WithString("containerName", mcp.Required(), mcp.Description("容器名(用于进度展示与旧容器命名)")),
+ mcp.WithString("image", mcp.Description("镜像名:tag;不传或留空则使用容器当前镜像拉取最新版本")),
+ mcp.WithBoolean("delOldContainer", mcp.Description("更新成功后是否删除旧容器,默认读取 DelOldContainer 环境变量(默认 true)")),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ args := req.GetArguments()
+ id, err := mustString(args, "containerId")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ name, err := mustString(args, "containerName")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ // image 可选:留空时 UpdateContainer 会回退到容器当前镜像。
+ image := optionalString(args, "image")
+ delOld := optionalBool(args, "delOldContainer", os.Getenv("DelOldContainer") != "false")
+
+ taskID := uuid.New().String()
+ taskCtx := ctx.RegisterTask(taskID)
+ defer ctx.FinishTask(taskID)
+
+ updateErr := utiles.UpdateContainer(ctx, taskCtx, id, name, image, delOld, taskID)
+ final, _ := ctx.GetProgress(taskID)
+ message := final.Message
+ if message == "" {
+ message = "更新任务已结束"
+ }
+ detail := final.DetailMsg
+ if detail == "" {
+ detail = message
+ }
+ utiles.NotifyTaskDone("update", name, taskID, updateErr == nil, message)
+ utiles.AuditLog("container.update", name, detail)
+ if updateErr != nil {
+ return mcp.NewToolResultError(updateErr.Error()), nil
+ }
+ return jsonResult(final), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_autoupdate_run",
+ mcp.WithDescription("手动触发一轮自动更新:按 AUTOUPDATE_WHITELIST 过滤,仅更新运行中、非 compose 管理、非 dockerCopilot 自身的容器;全局串行,上一轮未结束时本轮跳过"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ if !utiles.AutoUpdateConfigured() {
+ return mcp.NewToolResultError("自动更新未配置:AUTOUPDATE_CRON 或 AUTOUPDATE_WHITELIST 为空"), nil
+ }
+ utiles.RunAutoUpdate(ctx)
+ return textResult("autoupdate finished"), nil
+ })
+}
diff --git a/internal/mcp/tools_image.go b/internal/mcp/tools_image.go
new file mode 100644
index 00000000..3dad31e4
--- /dev/null
+++ b/internal/mcp/tools_image.go
@@ -0,0 +1,71 @@
+package mcp
+
+import (
+ "context"
+
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/utiles"
+)
+
+func registerImageTools(s *server.MCPServer, ctx *svc.ServiceContext) {
+ s.AddTool(mcp.NewTool("dc_list_images",
+ mcp.WithDescription("列出本地镜像(含大小、tag、是否被容器占用)"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ list, err := utiles.GetImagesList(ctx)
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ return jsonResult(list), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_check_image_update",
+ mcp.WithDescription("立即对所有镜像做一次远端 digest 检查,返回每个镜像 ID 是否有更新(并刷新内部缓存)"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ list, err := utiles.GetImagesList(ctx)
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ ctx.HubImageInfo.CheckUpdate(list)
+ return jsonResult(ctx.HubImageInfo.Snapshot()), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_remove_image",
+ mcp.WithDescription("删除镜像;force=true 时强制删除(可能影响仍在引用它的容器)"),
+ mcp.WithString("imageId", mcp.Required(), mcp.Description("镜像 ID")),
+ mcp.WithBoolean("force", mcp.Description("是否强制删除,默认 false")),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ args := req.GetArguments()
+ id, err := mustString(args, "imageId")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ force := optionalBool(args, "force", false)
+ if err = utiles.RemoveImage(ctx, id, force); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ utiles.AuditLog("image.remove", id, "removed")
+ return textResult("removed image: " + id), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_clean_images",
+ mcp.WithDescription("批量清理镜像:filter=untagged 删除无 TAG 镜像,filter=unused 删除未被任何容器引用的镜像;逐个尝试,单个失败不影响其它"),
+ mcp.WithString("filter", mcp.Required(), mcp.Description("清理过滤器:untagged 或 unused")),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ filter, err := mustString(req.GetArguments(), "filter")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ removed, failed, cleanErr := utiles.RemoveImagesFiltered(ctx, filter)
+ if cleanErr != nil {
+ return mcp.NewToolResultError(cleanErr.Error()), nil
+ }
+ utiles.AuditLog("image.cleanup", filter, "removed and failed lists returned to caller")
+ return jsonResult(map[string]any{
+ "filter": filter,
+ "removed": removed,
+ "failed": failed,
+ }), nil
+ })
+}
diff --git a/internal/mcp/tools_progress.go b/internal/mcp/tools_progress.go
new file mode 100644
index 00000000..ebf033d7
--- /dev/null
+++ b/internal/mcp/tools_progress.go
@@ -0,0 +1,46 @@
+package mcp
+
+import (
+ "context"
+
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+)
+
+func registerProgressTools(s *server.MCPServer, ctx *svc.ServiceContext) {
+ s.AddTool(mcp.NewTool("dc_list_progress",
+ mcp.WithDescription("列出全部任务进度(当前进程内存 + 重启后从磁盘恢复的历史任务)"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ return jsonResult(ctx.ListProgress()), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_get_progress",
+ mcp.WithDescription("查询单个任务的进度详情"),
+ mcp.WithString("taskId", mcp.Required(), mcp.Description("任务 ID")),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ taskID, err := mustString(req.GetArguments(), "taskId")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ p, ok := ctx.GetProgress(taskID)
+ if !ok {
+ return mcp.NewToolResultError("task not found: " + taskID), nil
+ }
+ return jsonResult(p), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_cancel_progress",
+ mcp.WithDescription("取消一个进行中的任务(任务会在下一个步骤边界退出)"),
+ mcp.WithString("taskId", mcp.Required(), mcp.Description("任务 ID")),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ taskID, err := mustString(req.GetArguments(), "taskId")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ if !ctx.CancelTask(taskID) {
+ return mcp.NewToolResultError("task not cancellable: " + taskID), nil
+ }
+ return textResult("cancel requested: " + taskID), nil
+ })
+}
diff --git a/internal/mcp/tools_settings.go b/internal/mcp/tools_settings.go
new file mode 100644
index 00000000..35efae3a
--- /dev/null
+++ b/internal/mcp/tools_settings.go
@@ -0,0 +1,60 @@
+package mcp
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/utiles"
+)
+
+func registerSettingsTools(s *server.MCPServer, ctx *svc.ServiceContext) {
+ s.AddTool(mcp.NewTool("dc_get_settings",
+ mcp.WithDescription("读取运行时设置(当前配置的备份目录与实际生效的备份目录)"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ s := utiles.LoadSettings()
+ return jsonResult(map[string]any{
+ "backupDir": s.BackupDir,
+ "effectiveBackupDir": utiles.EffectiveBackupDir(),
+ }), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_update_settings",
+ mcp.WithDescription("更新运行时设置(目前支持 backupDir,必须是绝对路径且可写)"),
+ mcp.WithString("backupDir", mcp.Required(), mcp.Description("备份目录绝对路径")),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ dir, err := mustString(req.GetArguments(), "backupDir")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ dir = strings.TrimSpace(dir)
+ if !filepath.IsAbs(dir) {
+ return mcp.NewToolResultError("备份目录必须是绝对路径"), nil
+ }
+ if err = os.MkdirAll(dir, 0755); err != nil {
+ return mcp.NewToolResultError("无法创建备份目录: " + err.Error()), nil
+ }
+ testFile, testErr := os.CreateTemp(dir, ".write-test-*")
+ if testErr != nil {
+ return mcp.NewToolResultError("备份目录不可写: " + testErr.Error()), nil
+ }
+ testName := testFile.Name()
+ _ = testFile.Close()
+ _ = os.Remove(testName)
+
+ s := utiles.LoadSettings()
+ s.BackupDir = dir
+ if err = utiles.SaveSettings(s); err != nil {
+ return mcp.NewToolResultError("保存设置失败: " + err.Error()), nil
+ }
+ utiles.AuditLog("settings.update", "settings", "backupDir="+dir)
+ return jsonResult(map[string]any{
+ "backupDir": s.BackupDir,
+ "effectiveBackupDir": utiles.EffectiveBackupDir(),
+ }), nil
+ })
+}
diff --git a/internal/mcp/tools_version.go b/internal/mcp/tools_version.go
new file mode 100644
index 00000000..0c18a7c9
--- /dev/null
+++ b/internal/mcp/tools_version.go
@@ -0,0 +1,56 @@
+package mcp
+
+import (
+ "context"
+ "errors"
+
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+ "github.com/onlyLTY/dockerCopilot/internal/config"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/onlyLTY/dockerCopilot/internal/utiles"
+)
+
+func registerVersionTools(s *server.MCPServer, ctx *svc.ServiceContext) {
+ s.AddTool(mcp.NewTool("dc_get_version",
+ mcp.WithDescription("获取当前本地版本号与构建时间"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ return jsonResult(map[string]string{
+ "version": config.Version,
+ "buildDate": config.BuildDate,
+ }), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_check_program_update",
+ mcp.WithDescription("检查程序是否有新版本(比对远端仓库版本号);FNOS 版本由专用发布包管理,会返回 fnos=true"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ remote, err := utiles.GetRemoteVersion()
+ if errors.Is(err, utiles.ErrFnosManagedUpdate) {
+ // FNOS 版本不走通用更新链:明确告知调用方,而不是伪装成“无更新”。
+ return jsonResult(map[string]any{
+ "localVersion": config.Version,
+ "fnos": true,
+ "hasUpdate": false,
+ "hint": err.Error(),
+ }), nil
+ }
+ if err != nil {
+ return mcp.NewToolResultError("获取远程版本失败: " + err.Error()), nil
+ }
+ return jsonResult(map[string]any{
+ "localVersion": config.Version,
+ "remoteVersion": remote,
+ "hasUpdate": remote != config.Version,
+ }), nil
+ })
+
+ s.AddTool(mcp.NewTool("dc_update_program",
+ mcp.WithDescription("更新 dockerCopilot 自身:下载新版本、校验、解压并重启进程(会中断当前服务)"),
+ ), func(ctx2 context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ if err := utiles.UpdateProgram(ctx); err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ utiles.AuditLog("program.update", "self", "program update triggered")
+ return textResult("program update triggered, service will restart"), nil
+ })
+}
diff --git a/internal/mcp/utils.go b/internal/mcp/utils.go
new file mode 100644
index 00000000..77805aa9
--- /dev/null
+++ b/internal/mcp/utils.go
@@ -0,0 +1,59 @@
+package mcp
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+func textResult(s string) *mcp.CallToolResult {
+ return mcp.NewToolResultText(s)
+}
+
+func jsonResult(v any) *mcp.CallToolResult {
+ b, err := json.MarshalIndent(v, "", " ")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("marshal error: %v", err))
+ }
+ return mcp.NewToolResultText(string(b))
+}
+
+func mustString(args map[string]any, key string) (string, error) {
+ v, ok := args[key]
+ if !ok {
+ return "", fmt.Errorf("missing required param: %s", key)
+ }
+ s, ok := v.(string)
+ if !ok {
+ return "", fmt.Errorf("param %s must be string", key)
+ }
+ if s == "" {
+ return "", fmt.Errorf("param %s cannot be empty", key)
+ }
+ return s, nil
+}
+
+func optionalBool(args map[string]any, key string, def bool) bool {
+ v, ok := args[key]
+ if !ok {
+ return def
+ }
+ b, ok := v.(bool)
+ if !ok {
+ return def
+ }
+ return b
+}
+
+func optionalString(args map[string]any, key string) string {
+ v, ok := args[key]
+ if !ok {
+ return ""
+ }
+ s, ok := v.(string)
+ if !ok {
+ return ""
+ }
+ return s
+}
diff --git a/internal/module/auth.go b/internal/module/auth.go
index 18eecb4d..becf9694 100644
--- a/internal/module/auth.go
+++ b/internal/module/auth.go
@@ -1,31 +1,70 @@
package module
import (
+ "crypto/tls"
"encoding/json"
"errors"
"fmt"
- ref "github.com/distribution/reference"
- "github.com/onlyLTY/dockerCopilot/internal/types"
- "github.com/zeromicro/go-zero/core/logx"
"io"
"net/http"
"net/url"
+ "os"
"strings"
+ "sync"
"time"
+
+ ref "github.com/distribution/reference"
+ "github.com/onlyLTY/dockerCopilot/internal/types"
+ "github.com/zeromicro/go-zero/core/logx"
)
const ChallengeHeader = "WWW-Authenticate"
const (
DefaultRegistryDomain = "docker.io"
- DefaultRegistryHost = "index.docker.io"
+ DefaultRegistryHost = "registry-1.docker.io"
)
+var registryHTTPClient = newRegistryHTTPClient()
+
+func newRegistryHTTPClient() *http.Client {
+ transport, ok := http.DefaultTransport.(*http.Transport)
+ if !ok {
+ transport = &http.Transport{}
+ } else {
+ transport = transport.Clone()
+ }
+ if os.Getenv("REGISTRY_INSECURE") == "true" {
+ transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // #nosec G402 -- 由用户显式开启
+ }
+ return &http.Client{Transport: transport, Timeout: 15 * time.Second}
+}
+
+// host 探测结果缓存,避免每个镜像都重新跑一遍 10 个加速器。
+type hostCheckResult struct {
+ ok bool
+ at time.Time
+}
+
+var hostCheckCache sync.Map
+
+func checkHostCached(host string) bool {
+ if v, ok := hostCheckCache.Load(host); ok {
+ r := v.(hostCheckResult)
+ if time.Since(r.at) < 10*time.Minute {
+ return r.ok
+ }
+ }
+ ok := checkHost(host)
+ hostCheckCache.Store(host, hostCheckResult{ok: ok, at: time.Now()})
+ return ok
+}
+
var DefaultAcceleratorHostList = []string{"docker.1ms.run", "docker.m.daocloud.io",
"docker.1panel.top", "docker.1panel.live", "proxy.1panel.live", "dockerproxy.1panel.live", "docker.1panel.dev",
"docker.anye.in", "hub.rat.dev", "docker.amingg.com"}
func GetToken(image types.Image, registryAuth string) (string, error) {
- logx.Infof("image name %s", image.ImageName)
+ logx.Infof("镜像名称: %s", image.ImageName)
normalizedRef, err := ref.ParseNormalizedNamed(image.ImageName)
if err != nil {
return "", err
@@ -38,9 +77,8 @@ func GetToken(image types.Image, registryAuth string) (string, error) {
return "", err
}
- client := &http.Client{}
var res *http.Response
- if res, err = client.Do(req); err != nil {
+ if res, err = registryHTTPClient.Do(req); err != nil {
return "", err
}
defer func(Body io.ReadCloser) {
@@ -51,19 +89,20 @@ func GetToken(image types.Image, registryAuth string) (string, error) {
}(res.Body)
v := res.Header.Get(ChallengeHeader)
- challenge := strings.ToLower(v)
- if strings.HasPrefix(challenge, "basic") {
+ challenge := strings.TrimSpace(v)
+ lowerChallenge := strings.ToLower(challenge)
+ if strings.HasPrefix(lowerChallenge, "basic") {
if registryAuth == "" {
- return "", fmt.Errorf("no credentials available")
+ return "", fmt.Errorf("无可用凭证")
}
return fmt.Sprintf("Basic %s", registryAuth), nil
}
- if strings.HasPrefix(challenge, "bearer") {
+ if strings.HasPrefix(lowerChallenge, "bearer") {
return GetBearerHeader(challenge, normalizedRef, registryAuth)
}
- return "", errors.New("unsupported challenge type from registry")
+ return "", errors.New("registry 返回了不支持的认证质询类型")
}
func GetChallengeRequest(URL url.URL) (*http.Request, error) {
@@ -72,12 +111,11 @@ func GetChallengeRequest(URL url.URL) (*http.Request, error) {
return nil, err
}
req.Header.Set("Accept", "*/*")
- req.Header.Set("User-Agent", "Watchtower (Docker)")
+ req.Header.Set("User-Agent", "dockerCopilot")
return req, nil
}
func GetBearerHeader(challenge string, imageRef ref.Named, registryAuth string) (string, error) {
- client := http.Client{}
authURL, err := GetAuthURL(challenge, imageRef)
if err != nil {
@@ -93,44 +131,64 @@ func GetBearerHeader(challenge string, imageRef ref.Named, registryAuth string)
logx.Info("私有镜像,无法获取是否有更新")
r.Header.Add("Authorization", fmt.Sprintf("Basic %s", registryAuth))
} else {
- logx.Info("No credentials found.")
+ logx.Info("未找到凭证。")
}
var authResponse *http.Response
- if authResponse, err = client.Do(r); err != nil {
+ if authResponse, err = registryHTTPClient.Do(r); err != nil {
return "", err
}
+ defer func(Body io.ReadCloser) {
+ if closeErr := Body.Close(); closeErr != nil {
+ logx.Errorf("GetBearerHeader关闭Body失败: %v", closeErr)
+ }
+ }(authResponse.Body)
- body, _ := io.ReadAll(authResponse.Body)
+ body, err := io.ReadAll(io.LimitReader(authResponse.Body, 1<<20))
+ if err != nil {
+ return "", err
+ }
+ if authResponse.StatusCode < 200 || authResponse.StatusCode >= 300 {
+ return "", fmt.Errorf("获取 registry token 失败: %s", authResponse.Status)
+ }
tokenResponse := &types.TokenResponse{}
err = json.Unmarshal(body, tokenResponse)
if err != nil {
return "", err
}
+ if tokenResponse.Token == "" {
+ return "", fmt.Errorf("registry 返回的 token 为空")
+ }
return fmt.Sprintf("Bearer %s", tokenResponse.Token), nil
}
func GetAuthURL(challenge string, imageRef ref.Named) (*url.URL, error) {
- loweredChallenge := strings.ToLower(challenge)
- raw := strings.TrimPrefix(loweredChallenge, "bearer")
+ raw := strings.TrimSpace(challenge)
+ if idx := strings.IndexByte(raw, ' '); idx >= 0 {
+ raw = raw[idx+1:]
+ }
pairs := strings.Split(raw, ",")
values := make(map[string]string, len(pairs))
for _, pair := range pairs {
- trimmed := strings.Trim(pair, " ")
+ trimmed := strings.TrimSpace(pair)
if key, val, ok := strings.Cut(trimmed, "="); ok {
- values[key] = strings.Trim(val, `"`)
+ // challenge 参数名大小写不敏感,但 realm URL 的路径必须保留原始大小写。
+ values[strings.ToLower(key)] = strings.Trim(val, `"`)
}
}
if values["realm"] == "" || values["service"] == "" {
- return nil, fmt.Errorf("challenge header did not include all values needed to construct an auth url")
+ return nil, fmt.Errorf("challenge 头缺少构造认证 URL 所需的字段")
}
- authURL, _ := url.Parse(values["realm"])
+ authURL, err := url.Parse(values["realm"])
+ if err != nil || authURL.Host == "" || (authURL.Scheme != "http" && authURL.Scheme != "https") {
+ return nil, fmt.Errorf("registry 返回了非法的 realm: %q", values["realm"])
+ }
q := authURL.Query()
q.Add("service", values["service"])
@@ -144,7 +202,10 @@ func GetAuthURL(challenge string, imageRef ref.Named) (*url.URL, error) {
}
func GetChallengeURL(imageRef ref.Named) url.URL {
- host, _ := GetRegistryAddress(imageRef.Name())
+ host, err := GetRegistryAddress(imageRef.Name())
+ if err != nil || host == "" {
+ host = DefaultRegistryHost
+ }
URL := url.URL{
Scheme: "https",
@@ -163,11 +224,11 @@ func GetRegistryAddress(imageRef string) (string, error) {
address := ref.Domain(normalizedRef)
if address == DefaultRegistryDomain {
- if checkHost(DefaultRegistryHost) {
+ if checkHostCached(DefaultRegistryHost) {
address = DefaultRegistryHost
} else {
for _, host := range DefaultAcceleratorHostList {
- if checkHost(host) {
+ if checkHostCached(host) {
address = host
break
}
@@ -182,20 +243,21 @@ func GetRegistryAddress(imageRef string) (string, error) {
func checkHost(host string) bool {
URL := "https://" + host + "/v2/"
- // 创建带有超时设置的 http.Client
+ // 复用 registry 客户端 transport,确保 REGISTRY_INSECURE 对探测同样生效。
client := http.Client{
- Timeout: 5 * time.Second,
+ Transport: registryHTTPClient.Transport,
+ Timeout: 5 * time.Second,
}
// 发送 HEAD 请求
resp, err := client.Get(URL)
if err != nil {
- logx.Errorf("Failed to connect to %s: %s", URL, err)
+ logx.Errorf("连接 %s 失败: %s", URL, err)
return false
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
- logx.Errorf("关闭body失败" + err.Error())
+ logx.Errorf("关闭body失败: %v", err)
}
}(resp.Body)
@@ -205,6 +267,6 @@ func checkHost(host string) bool {
return true
}
- logx.Errorf("Failed to connect to %s: %s", URL, resp.Status)
+ logx.Errorf("连接 %s 失败: %s", URL, resp.Status)
return false
}
diff --git a/internal/module/auth_test.go b/internal/module/auth_test.go
new file mode 100644
index 00000000..b479c856
--- /dev/null
+++ b/internal/module/auth_test.go
@@ -0,0 +1,37 @@
+package module
+
+import (
+ "testing"
+
+ ref "github.com/distribution/reference"
+)
+
+func TestGetAuthURLPreservesRealmCaseAndScope(t *testing.T) {
+ imageRef, err := ref.ParseNormalizedNamed("docker.io/library/nginx")
+ if err != nil {
+ t.Fatal(err)
+ }
+ authURL, err := GetAuthURL(`Bearer realm="https://Auth.Example.com/Token",service="registry.docker.io"`, imageRef)
+ if err != nil {
+ t.Fatalf("GetAuthURL() error = %v", err)
+ }
+ if authURL.Host != "Auth.Example.com" || authURL.Path != "/Token" {
+ t.Fatalf("realm = %s, want https://Auth.Example.com/Token", authURL.String())
+ }
+ if got := authURL.Query().Get("service"); got != "registry.docker.io" {
+ t.Fatalf("service = %q", got)
+ }
+ if got := authURL.Query().Get("scope"); got != "repository:library/nginx:pull" {
+ t.Fatalf("scope = %q", got)
+ }
+}
+
+func TestGetAuthURLRejectsMalformedRealm(t *testing.T) {
+ imageRef, err := ref.ParseNormalizedNamed("nginx")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err = GetAuthURL(`Bearer realm="not-a-url",service="registry.docker.io"`, imageRef); err == nil {
+ t.Fatal("GetAuthURL() should reject a realm without scheme/host")
+ }
+}
diff --git a/internal/module/checkupdate.go b/internal/module/checkupdate.go
index d2200c35..4674145e 100644
--- a/internal/module/checkupdate.go
+++ b/internal/module/checkupdate.go
@@ -11,7 +11,9 @@ import (
"net"
"net/http"
url2 "net/url"
+ "os"
"strings"
+ "sync"
"time"
)
@@ -20,61 +22,117 @@ type ImageCheckList struct {
NeedUpdate bool
}
type ImageUpdateData struct {
+ mu sync.RWMutex
Data map[string]ImageCheckList
}
+// Get 返回指定镜像的更新检查结果(并发安全)。
+func (i *ImageUpdateData) Get(imageID string) (ImageCheckList, bool) {
+ i.mu.RLock()
+ defer i.mu.RUnlock()
+ v, ok := i.Data[imageID]
+ return v, ok
+}
+
+// Snapshot 返回当前全部检查结果的副本(并发安全),供 MCP 等外部调用方读取。
+func (i *ImageUpdateData) Snapshot() map[string]ImageCheckList {
+ i.mu.RLock()
+ defer i.mu.RUnlock()
+ out := make(map[string]ImageCheckList, len(i.Data))
+ for k, v := range i.Data {
+ out[k] = v
+ }
+ return out
+}
+
const ContentDigestHeader = "Docker-Content-Digest"
+const checkUpdateConcurrency = 4
+
func NewImageCheck() *ImageUpdateData {
return &ImageUpdateData{
Data: map[string]ImageCheckList{},
}
}
func (i *ImageUpdateData) CheckUpdate(imageList []types.Image) {
+ results := make(map[string]ImageCheckList)
+ var resultsMu sync.Mutex
+ var wg sync.WaitGroup
+ sem := make(chan struct{}, checkUpdateConcurrency)
for _, image := range imageList {
- if strings.Contains(image.ImageName, "0nlylty/dockercopilot") {
+ if strings.Contains(strings.ToLower(image.ImageName), "dockercopilot") {
continue
}
- i.checkSingleImage(image)
+ wg.Add(1)
+ go func(img types.Image) {
+ defer wg.Done()
+ sem <- struct{}{}
+ defer func() { <-sem }()
+ if result, ok := i.checkSingleImage(img); ok {
+ resultsMu.Lock()
+ // 同一 image ID 可能有多个 tag。只要任意一个 tag 有更新就保留
+ // NeedUpdate=true,避免最后一个完成的 goroutine 把结果覆盖成 false。
+ if old, exists := results[img.ID]; exists {
+ result.NeedUpdate = result.NeedUpdate || old.NeedUpdate
+ }
+ results[img.ID] = result
+ resultsMu.Unlock()
+ }
+ }(image)
}
+ wg.Wait()
+ i.mu.Lock()
+ i.Data = results
+ i.mu.Unlock()
}
-func (i *ImageUpdateData) checkSingleImage(image types.Image) {
+func (i *ImageUpdateData) checkSingleImage(image types.Image) (ImageCheckList, bool) {
+ if image.ImageName == "" || image.ImageName == "None" || image.ImageTag == "" || image.ImageTag == "None" {
+ // 只有 digest、没有 tag 的镜像无法构造 manifests/ 请求。
+ return ImageCheckList{}, false
+ }
token, err := GetToken(image, "")
if err != nil {
- logx.Error("获取token失败或者无需获取token,继续尝试检查" + err.Error())
+ logx.Errorf("获取 token 失败(%s),将继续直接尝试检查", err)
}
digestURL, err := BuildManifestURL(image)
if err != nil {
logx.Error("获取digestURL失败" + err.Error())
- return
+ return ImageCheckList{}, false
}
remoteDigest, err := GetDigest(digestURL, token)
if err != nil {
logx.Error("获取digest失败" + err.Error())
- return
+ return ImageCheckList{}, false
}
if len(image.RepoDigests) == 0 {
logx.Error("未在本地获取到repoDigest" + image.ImageName + ":" + image.ImageTag)
- return
+ return ImageCheckList{}, false
}
needUpdate := false
for _, localRepoDigests := range image.RepoDigests {
- localDigest := strings.Split(localRepoDigests, "@")[1]
+ parts := strings.SplitN(localRepoDigests, "@", 2)
+ if len(parts) != 2 {
+ continue
+ }
+ localRepo, localDigest := parts[0], parts[1]
+ // 只比较同一仓库的 digest,避免同一镜像 ID 的其它 tag/仓库造成误报。
+ if localRepo != image.ImageName && !strings.HasSuffix(localRepo, "/"+image.ImageName) {
+ continue
+ }
+ if remoteDigest == "" || localDigest == "" {
+ logx.Error("Digest为空" + image.ImageName + ":" + image.ImageTag)
+ continue
+ }
if remoteDigest != localDigest {
- if remoteDigest == "" || localDigest == "" {
- logx.Error("Digest为空" + image.ImageName + ":" + image.ImageTag)
- continue
- }
- logx.Info(image.ImageName + ":" + image.ImageTag + " need update")
- logx.Infof("localDigest: %s, remoteDigest: %s", localDigest, remoteDigest)
+ logx.Info(image.ImageName + ":" + image.ImageTag + " 有可用更新")
+ logx.Infof("本地digest: %s, 远程digest: %s", localDigest, remoteDigest)
needUpdate = true
} else {
- logx.Info(image.ImageName + ":" + image.ImageTag + " not need update")
- needUpdate = false
+ logx.Info(image.ImageName + ":" + image.ImageTag + " 无需更新")
}
}
- i.Data[image.ID] = ImageCheckList{NeedUpdate: needUpdate}
+ return ImageCheckList{NeedUpdate: needUpdate}, true
}
func BuildManifestURL(image types.Image) (string, error) {
@@ -114,11 +172,15 @@ func GetDigest(url string, token string) (string, error) {
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
- TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: os.Getenv("REGISTRY_INSECURE") == "true"},
}
- client := &http.Client{Transport: tr}
+ client := &http.Client{Transport: tr, Timeout: 20 * time.Second}
- req, _ := http.NewRequest("HEAD", url, nil)
+ req, err := http.NewRequest("HEAD", url, nil)
+ if err != nil {
+ return "", err
+ }
+ req.Header.Set("User-Agent", "dockerCopilot")
if token != "" {
req.Header.Add("Authorization", token)
@@ -144,7 +206,7 @@ func GetDigest(url string, token string) (string, error) {
if wwwAuthHeader == "" {
wwwAuthHeader = "not present"
}
- return "", fmt.Errorf("registry responded to head request with %q, auth: %q", res.Status, wwwAuthHeader)
+ return "", fmt.Errorf("registry 对 HEAD 请求返回 %q,认证头: %q", res.Status, wwwAuthHeader)
}
return res.Header.Get(ContentDigestHeader), nil
}
diff --git a/internal/svc/servicecontext.go b/internal/svc/servicecontext.go
index 533ebe4d..e509a8e2 100644
--- a/internal/svc/servicecontext.go
+++ b/internal/svc/servicecontext.go
@@ -1,12 +1,20 @@
package svc
import (
+ "context"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "sync"
+ "time"
+
"github.com/docker/docker/client"
"github.com/onlyLTY/dockerCopilot/internal/config"
"github.com/onlyLTY/dockerCopilot/internal/module"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/rest"
- "sync"
)
type ServiceContext struct {
@@ -20,16 +28,22 @@ type ServiceContext struct {
IndexCheckMiddleware rest.Middleware
ProgressStore ProgressStoreType
DockerClient *client.Client
+ DockerInitErr error
mu sync.Mutex
+ taskCancels map[string]context.CancelFunc
+ saveMu sync.Mutex
+ lastProgressSave time.Time
}
type TaskProgress struct {
- TaskID string
- Percentage int
- Message string
- Name string
- DetailMsg string
- IsDone bool
+ TaskID string `json:"taskID"`
+ Percentage int `json:"percentage"`
+ Message string `json:"message"`
+ Name string `json:"name"`
+ DetailMsg string `json:"detailMsg"`
+ IsDone bool `json:"isDone"`
+ StartedAt int64 `json:"startedAt"`
+ FinishedAt int64 `json:"finishedAt"`
}
type ProgressStoreType map[string]TaskProgress
@@ -37,20 +51,151 @@ type ProgressStoreType map[string]TaskProgress
func NewServiceContext(c config.Config) *ServiceContext {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
- logx.Errorf("Unable to create docker client: %s", err)
+ logx.Errorf("创建 Docker 客户端失败: %s", err)
}
- return &ServiceContext{
+ ctx := &ServiceContext{
Config: c,
HubImageInfo: module.NewImageCheck(),
ProgressStore: make(ProgressStoreType),
+ taskCancels: make(map[string]context.CancelFunc),
DockerClient: cli,
+ DockerInitErr: err,
}
+ ctx.loadProgress()
+ go ctx.taskJanitor()
+ return ctx
}
-func (ctx *ServiceContext) UpdateProgress(taskID string, progress TaskProgress) {
+// progressStorePath 进度持久化文件路径,可用 PROGRESS_STORE 覆盖。
+func progressStorePath() string {
+ if p := os.Getenv("PROGRESS_STORE"); p != "" {
+ return p
+ }
+ return "/data/config/progress.json"
+}
+
+// loadProgress 从磁盘恢复历史任务进度(重启不丢历史)。
+func (ctx *ServiceContext) loadProgress() {
+ data, err := os.ReadFile(progressStorePath())
+ if err != nil {
+ return
+ }
+ var stored ProgressStoreType
+ if err = json.Unmarshal(data, &stored); err != nil {
+ logx.Errorf("进度文件解析失败: %v", err)
+ return
+ }
+ now := time.Now().Unix()
+ interrupted := false
+ ctx.mu.Lock()
+ for k, v := range stored {
+ if !v.IsDone {
+ // 进程刚重启,磁盘里还处于“进行中”的任务不可能再继续执行。
+ v.IsDone = true
+ v.FinishedAt = now
+ v.Percentage = 100
+ v.Message = "进程重启,任务已中断"
+ v.DetailMsg = "任务执行期间服务重启,请重新发起"
+ interrupted = true
+ }
+ ctx.ProgressStore[k] = v
+ }
+ ctx.mu.Unlock()
+ if interrupted {
+ ctx.saveProgress()
+ }
+}
+
+// saveProgress 原子写入进度文件(临时文件 + rename)。
+// 序列化和落盘都持有 ctx.mu:否则并发保存时,旧快照可能后落盘,
+// 把更新的任务状态覆盖掉。进度写入频率已被 UpdateProgress 限制,
+// 这里短暂持锁不会成为任务更新的瓶颈。
+func (ctx *ServiceContext) saveProgress() {
ctx.mu.Lock()
defer ctx.mu.Unlock()
+
+ data, err := json.Marshal(ctx.ProgressStore)
+ if err != nil {
+ logx.Errorf("进度序列化失败: %v", err)
+ return
+ }
+ ctx.saveMu.Lock()
+ defer ctx.saveMu.Unlock()
+ path := progressStorePath()
+ if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ logx.Errorf("创建进度目录失败: %v", err)
+ return
+ }
+ tmp := path + ".tmp"
+ if err = os.WriteFile(tmp, data, 0644); err != nil {
+ logx.Errorf("写进度临时文件失败: %v", err)
+ return
+ }
+ if err = os.Rename(tmp, path); err != nil {
+ logx.Errorf("进度文件落地失败: %v", err)
+ }
+}
+
+// taskJanitor 定期清理超过 TTL 的已完成任务,避免内存与进度文件无限增长。
+func (ctx *ServiceContext) taskJanitor() {
+ ttl := 24 * time.Hour
+ if v := os.Getenv("PROGRESS_TTL_HOURS"); v != "" {
+ if n, err := strconv.Atoi(v); err == nil && n > 0 {
+ ttl = time.Duration(n) * time.Hour
+ }
+ }
+ ticker := time.NewTicker(10 * time.Minute)
+ defer ticker.Stop()
+ for range ticker.C {
+ cutoff := time.Now().Add(-ttl).Unix()
+ removed := false
+ ctx.mu.Lock()
+ for k, v := range ctx.ProgressStore {
+ if v.IsDone && v.FinishedAt > 0 && v.FinishedAt < cutoff {
+ delete(ctx.ProgressStore, k)
+ if cancel, ok := ctx.taskCancels[k]; ok {
+ cancel()
+ delete(ctx.taskCancels, k)
+ }
+ removed = true
+ }
+ }
+ ctx.mu.Unlock()
+ if removed {
+ ctx.saveProgress()
+ }
+ }
+}
+
+func (ctx *ServiceContext) UpdateProgress(taskID string, progress TaskProgress) {
+ ctx.mu.Lock()
+ now := time.Now().Unix()
+ if progress.StartedAt == 0 {
+ if old, ok := ctx.ProgressStore[taskID]; ok {
+ progress.StartedAt = old.StartedAt
+ }
+ if progress.StartedAt == 0 {
+ progress.StartedAt = now
+ }
+ }
+ if progress.IsDone {
+ if old, ok := ctx.ProgressStore[taskID]; ok && old.IsDone && old.FinishedAt > 0 {
+ progress.FinishedAt = old.FinishedAt
+ }
+ if progress.FinishedAt == 0 {
+ progress.FinishedAt = now
+ }
+ }
ctx.ProgressStore[taskID] = progress
+ // 高频拉取进度只留在内存;最多每秒落盘一次,完成时立即落盘。
+ shouldSave := progress.IsDone || time.Since(ctx.lastProgressSave) >= time.Second
+ if shouldSave {
+ ctx.lastProgressSave = time.Now()
+ }
+ ctx.mu.Unlock()
+ if shouldSave {
+ ctx.saveProgress()
+ }
}
func (ctx *ServiceContext) GetProgress(taskID string) (TaskProgress, bool) {
@@ -59,3 +204,79 @@ func (ctx *ServiceContext) GetProgress(taskID string) (TaskProgress, bool) {
progress, ok := ctx.ProgressStore[taskID]
return progress, ok
}
+
+// ListProgress 返回全部任务进度,按开始时间倒序。
+func (ctx *ServiceContext) ListProgress() []TaskProgress {
+ ctx.mu.Lock()
+ defer ctx.mu.Unlock()
+ list := make([]TaskProgress, 0, len(ctx.ProgressStore))
+ for _, v := range ctx.ProgressStore {
+ list = append(list, v)
+ }
+ sort.Slice(list, func(i, j int) bool {
+ if list[i].StartedAt != list[j].StartedAt {
+ return list[i].StartedAt > list[j].StartedAt
+ }
+ return list[i].TaskID > list[j].TaskID
+ })
+ return list
+}
+
+// RegisterTask 为任务创建可取消的上下文。
+func (ctx *ServiceContext) RegisterTask(taskID string) context.Context {
+ taskCtx, cancel := context.WithCancel(context.Background())
+ ctx.mu.Lock()
+ if ctx.taskCancels == nil {
+ ctx.taskCancels = make(map[string]context.CancelFunc)
+ }
+ ctx.taskCancels[taskID] = cancel
+ ctx.mu.Unlock()
+ return taskCtx
+}
+
+// CancelTask 取消一个进行中的任务,返回是否找到该任务。
+func (ctx *ServiceContext) CancelTask(taskID string) bool {
+ ctx.mu.Lock()
+ cancel, ok := ctx.taskCancels[taskID]
+ ctx.mu.Unlock()
+ if ok {
+ cancel()
+ }
+ return ok
+}
+
+// CancelActiveTask 原子地检查并取消任务,避免“刚好完成”时被取消逻辑覆盖状态。
+func (ctx *ServiceContext) CancelActiveTask(taskID string) (ok bool, msg string) {
+ ctx.mu.Lock()
+ progress, exists := ctx.ProgressStore[taskID]
+ if !exists {
+ ctx.mu.Unlock()
+ return false, "taskID 未找到"
+ }
+ if progress.IsDone {
+ ctx.mu.Unlock()
+ return false, "任务已结束,无需取消"
+ }
+ cancel, exists := ctx.taskCancels[taskID]
+ if !exists {
+ ctx.mu.Unlock()
+ return false, "任务不可取消(可能已结束)"
+ }
+ cancel()
+ progress.Message = "正在取消..."
+ progress.DetailMsg = "已发出取消请求,任务将在当前步骤结束后停止"
+ ctx.ProgressStore[taskID] = progress
+ ctx.mu.Unlock()
+ ctx.saveProgress()
+ return true, ""
+}
+
+// FinishTask 任务收尾,释放取消句柄。
+func (ctx *ServiceContext) FinishTask(taskID string) {
+ ctx.mu.Lock()
+ if cancel, ok := ctx.taskCancels[taskID]; ok {
+ cancel()
+ delete(ctx.taskCancels, taskID)
+ }
+ ctx.mu.Unlock()
+}
diff --git a/internal/svc/servicecontext_test.go b/internal/svc/servicecontext_test.go
new file mode 100644
index 00000000..52ae4826
--- /dev/null
+++ b/internal/svc/servicecontext_test.go
@@ -0,0 +1,46 @@
+package svc
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+ "testing"
+)
+
+func TestProgressConcurrentUpdatesPersistValidJSON(t *testing.T) {
+ storePath := filepath.Join(t.TempDir(), "progress.json")
+ t.Setenv("PROGRESS_STORE", storePath)
+
+ ctx := &ServiceContext{ProgressStore: make(ProgressStoreType)}
+ var wg sync.WaitGroup
+ for i := 0; i < 20; i++ {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ ctx.UpdateProgress(fmt.Sprintf("task-%d", i), TaskProgress{
+ TaskID: fmt.Sprintf("task-%d", i),
+ Percentage: 100,
+ Message: "done",
+ IsDone: true,
+ })
+ }(i)
+ }
+ wg.Wait()
+
+ data, err := os.ReadFile(storePath)
+ if err != nil {
+ t.Fatalf("read progress store: %v", err)
+ }
+ var stored ProgressStoreType
+ if err = json.Unmarshal(data, &stored); err != nil {
+ t.Fatalf("progress store is not valid JSON: %v\n%s", err, data)
+ }
+ if len(stored) != 20 {
+ t.Fatalf("stored tasks = %d, want 20", len(stored))
+ }
+ if progress, ok := ctx.GetProgress("task-7"); !ok || !progress.IsDone {
+ t.Fatalf("task-7 not persisted in memory: %+v, %v", progress, ok)
+ }
+}
diff --git a/internal/types/types.go b/internal/types/types.go
index a39ed544..ef443b39 100644
--- a/internal/types/types.go
+++ b/internal/types/types.go
@@ -1,8 +1,12 @@
-// Code generated by goctl. DO NOT EDIT.
-// goctl 1.8.5
+// 基于 goctl 生成的类型定义,包含手工扩展(Nil、RemoveImagesReq、
+// ContainerRestoreReq 等)。若用 goctl 重新生成,请对照 git 历史补回手工部分。
package types
+// Nil 用作 BaseResponse 泛型的占位类型,表示“无数据”。
+// 不要再用 go/types.Nil 之类的第三方类型充当占位符。
+type Nil struct{}
+
type ContainerRenameReq struct {
IdReq
NewName string `form:"newName"`
@@ -10,6 +14,10 @@ type ContainerRenameReq struct {
type ContainerRestoreReq struct {
Filename string `json:"filename"`
+ // Start 为空时按备份记录的状态恢复;true/false 强制启动或不启动。
+ Start *bool `json:"start,optional"`
+ // Force 为 true 时,同名容器会被删除后重建;默认跳过同名容器。
+ Force bool `json:"force,optional"`
}
type ContainerUpdateReq struct {
@@ -58,6 +66,12 @@ type RemoveImageReq struct {
Force bool `form:"force,default=false"`
}
+// RemoveImagesReq 批量镜像清理请求:
+// filter=untagged 删除无 TAG 镜像,filter=unused 删除未被任何容器引用的镜像。
+type RemoveImagesReq struct {
+ Filter string `form:"filter"`
+}
+
type RenameContainerReq struct {
OldName string `json:"oldName"`
NewName string `json:"newName"`
@@ -93,3 +107,7 @@ type VersionReq struct {
type GetNewImageReq struct {
ImageNameAndTag string `json:"image_name_and_tag"`
}
+
+type UpdateSettingsReq struct {
+ BackupDir string `json:"backupDir"`
+}
diff --git a/internal/utiles/audit.go b/internal/utiles/audit.go
new file mode 100644
index 00000000..70123f69
--- /dev/null
+++ b/internal/utiles/audit.go
@@ -0,0 +1,58 @@
+package utiles
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+)
+
+type auditEntry struct {
+ Time string `json:"time"`
+ Action string `json:"action"`
+ Target string `json:"target"`
+ Detail string `json:"detail"`
+}
+
+var auditMu sync.Mutex
+
+func auditLogPath() string {
+ if p := os.Getenv("AUDIT_LOG"); p != "" {
+ return p
+ }
+ return "/data/logs/audit.jsonl"
+}
+
+// AuditLog 追加一条审计记录(JSON Lines),失败静默不影响业务。
+func AuditLog(action, target, detail string) {
+ entry := auditEntry{
+ Time: time.Now().Format("2006-01-02 15:04:05"),
+ Action: action,
+ Target: target,
+ Detail: detail,
+ }
+ data, err := json.Marshal(entry)
+ if err != nil {
+ return
+ }
+ auditMu.Lock()
+ defer auditMu.Unlock()
+ path := auditLogPath()
+ if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ return
+ }
+ // 简单轮转:超过 10MB 就保留一份 .1,避免长期运行无限增长。
+ if info, statErr := os.Stat(path); statErr == nil && info.Size() > 10<<20 {
+ _ = os.Remove(path + ".1")
+ _ = os.Rename(path, path+".1")
+ }
+ f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
+ if err != nil {
+ return
+ }
+ defer func(f *os.File) {
+ _ = f.Close()
+ }(f)
+ _, _ = f.Write(append(data, '\n'))
+}
diff --git a/internal/utiles/autoupdate.go b/internal/utiles/autoupdate.go
new file mode 100644
index 00000000..89796c19
--- /dev/null
+++ b/internal/utiles/autoupdate.go
@@ -0,0 +1,126 @@
+package utiles
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "sync"
+
+ "github.com/google/uuid"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/zeromicro/go-zero/core/logx"
+)
+
+// 自动更新是全局串行任务:一轮没跑完时下一轮直接跳过,避免重复检查和并发更新。
+var autoUpdateMu sync.Mutex
+
+// RunAutoUpdate 依据 AUTOUPDATE_WHITELIST(逗号分隔容器名,* 表示全部)
+// 对有可用更新的容器执行自动更新,逐个串行避免 IO 争抢。
+func RunAutoUpdate(serviceContext *svc.ServiceContext) {
+ if !autoUpdateMu.TryLock() {
+ logx.Infof("自动更新:上一轮仍在执行,本轮跳过")
+ return
+ }
+ defer autoUpdateMu.Unlock()
+
+ whitelistRaw := strings.TrimSpace(os.Getenv("AUTOUPDATE_WHITELIST"))
+ if whitelistRaw == "" {
+ return
+ }
+ allowAll := whitelistRaw == "*"
+ allowed := make(map[string]bool)
+ for _, name := range strings.Split(whitelistRaw, ",") {
+ if name = strings.TrimSpace(name); name != "" {
+ allowed[name] = true
+ }
+ }
+
+ images, err := GetImagesList(serviceContext)
+ if err != nil {
+ logx.Errorf("自动更新:获取镜像列表失败: %v", err)
+ return
+ }
+ serviceContext.HubImageInfo.CheckUpdate(images)
+
+ containers, err := GetContainerList(serviceContext)
+ if err != nil {
+ logx.Errorf("自动更新:获取容器列表失败: %v", err)
+ return
+ }
+
+ // 与手动更新保持一致:默认删除旧容器,显式 DelOldContainer=false 时保留。
+ delOldContainer := os.Getenv("DelOldContainer") != "false"
+
+ for _, c := range containers {
+ if len(c.Names) == 0 || c.Image == "" {
+ continue
+ }
+ if c.State != "running" {
+ // 自动更新不应该把原本停止的容器启动起来。
+ continue
+ }
+ if c.Labels["com.docker.compose.project"] != "" {
+ // Compose 容器交给 compose 管理,避免破坏 compose 状态。
+ continue
+ }
+ name := strings.TrimPrefix(c.Names[0], "/")
+ if strings.Contains(strings.ToLower(c.Image), "dockercopilot") {
+ // 不自动更新自己,避免更新过程中进程被杀。
+ continue
+ }
+ if !allowAll && !allowed[name] {
+ continue
+ }
+ info, ok := serviceContext.HubImageInfo.Get(c.ImageID)
+ if !ok || !info.NeedUpdate {
+ continue
+ }
+
+ func() {
+ taskID := uuid.New().String()
+ taskCtx := serviceContext.RegisterTask(taskID)
+ notified := false
+ defer func() {
+ if r := recover(); r != nil {
+ msg := fmt.Sprintf("自动更新异常中断: %v", r)
+ serviceContext.UpdateProgress(taskID, svc.TaskProgress{
+ TaskID: taskID,
+ Percentage: 100,
+ Name: name,
+ Message: "自动更新失败",
+ DetailMsg: msg,
+ IsDone: true,
+ })
+ if !notified {
+ NotifyTaskDone("autoupdate", name, taskID, false, msg)
+ AuditLog("container.autoupdate", name, msg)
+ }
+ logx.Errorf("自动更新容器 %s panic: %v", name, r)
+ }
+ serviceContext.FinishTask(taskID)
+ }()
+
+ logx.Infof("自动更新容器: %s (%s)", name, c.Image)
+ updateErr := UpdateContainer(serviceContext, taskCtx, c.ID, name, c.Image, delOldContainer, taskID)
+
+ final, _ := serviceContext.GetProgress(taskID)
+ message := final.Message
+ if message == "" {
+ message = "自动更新已结束"
+ }
+ detail := final.DetailMsg
+ if detail == "" {
+ detail = message
+ }
+ NotifyTaskDone("autoupdate", name, taskID, updateErr == nil, message)
+ AuditLog("container.autoupdate", name, detail)
+ notified = true
+ }()
+ }
+}
+
+// AutoUpdateConfigured 自动更新是否已配置(供启动日志使用)。
+func AutoUpdateConfigured() bool {
+ return strings.TrimSpace(os.Getenv("AUTOUPDATE_CRON")) != "" &&
+ strings.TrimSpace(os.Getenv("AUTOUPDATE_WHITELIST")) != ""
+}
diff --git a/internal/utiles/backup2compose.go b/internal/utiles/backup2compose.go
index 37f08692..6996c4d5 100644
--- a/internal/utiles/backup2compose.go
+++ b/internal/utiles/backup2compose.go
@@ -22,10 +22,12 @@ func Backup2Compose(ctx *svc.ServiceContext) (err error) {
}
containerJSONs = append(containerJSONs, inspectedContainer)
}
- err = backupCompose.DockerConfig2ComposeYaml(containerJSONs)
+ err = backupCompose.DockerConfig2ComposeYaml(containerJSONs, BackupDir())
if err != nil {
logx.Error("备份失败" + err.Error())
return err
}
+ // 与 JSON 备份共用保留策略:超出 BACKUP_KEEP(默认 7)的旧 compose 备份自动清理。
+ pruneOldBackups(BackupDir(), "backup-*.yaml", BackupKeepCount())
return nil
}
diff --git a/internal/utiles/backup_compose/config2yaml.go b/internal/utiles/backup_compose/config2yaml.go
index 9058bbcf..c9e99d61 100644
--- a/internal/utiles/backup_compose/config2yaml.go
+++ b/internal/utiles/backup_compose/config2yaml.go
@@ -1,108 +1,524 @@
package backupCompose
import (
- composeType "github.com/compose-spec/compose-go/types"
- dockerTypes "github.com/docker/docker/api/types"
- composeNat "github.com/docker/go-connections/nat"
- "github.com/zeromicro/go-zero/core/logx"
+ "fmt"
"os"
"path/filepath"
- "sigs.k8s.io/yaml"
"strconv"
"strings"
"time"
+
+ composeType "github.com/compose-spec/compose-go/types"
+ dockerTypes "github.com/docker/docker/api/types"
+ "github.com/docker/docker/api/types/container"
+ composeNat "github.com/docker/go-connections/nat"
+ "github.com/zeromicro/go-zero/core/logx"
+ "sigs.k8s.io/yaml"
)
// DockerConfig2ComposeYaml 将docker config转换为docker-compose.yaml
-func DockerConfig2ComposeYaml(containerJSONs []dockerTypes.ContainerJSON) (err error) {
+func DockerConfig2ComposeYaml(containerJSONs []dockerTypes.ContainerJSON, backupDir string) (err error) {
var c composeYaml
c.Services = make(map[string]composeType.ServiceConfig, len(containerJSONs))
- for _, containerJSON := range containerJSONs {
+ c.Volumes = make(composeType.Volumes)
+ c.Networks = make(composeType.Networks)
+ for i, containerJSON := range containerJSONs {
var s composeType.ServiceConfig
formatBaseServiceConfig(containerJSON, &s)
+ if s.Name == "" {
+ s.Name = fmt.Sprintf("container-%d", i+1)
+ s.ContainerName = s.Name
+ }
formatEnvServiceConfig(containerJSON, &s)
formatNetworkServiceConfig(containerJSON, &s)
formatVolumeServiceConfig(containerJSON, &s)
c.Services[s.Name] = s
+
+ // 收集命名卷。bind mount 不需要顶层声明。
+ for _, mount := range containerJSON.Mounts {
+ if mount.Type == "volume" {
+ source := volumeSource(mount)
+ if source == "" {
+ continue
+ }
+ volumeConfig := composeType.VolumeConfig{Name: source}
+ if mount.Driver != "" {
+ volumeConfig.Driver = mount.Driver
+ }
+ c.Volumes[source] = volumeConfig
+ }
+ }
+ // 收集自定义网络。default/bridge/host/none 不需要在 compose 里声明。
+ if containerJSON.NetworkSettings != nil {
+ for networkName := range containerJSON.NetworkSettings.Networks {
+ if networkName == "" || networkName == "bridge" || networkName == "host" || networkName == "none" {
+ continue
+ }
+ c.Networks[networkName] = composeType.NetworkConfig{Name: networkName}
+ }
+ }
+ }
+ if len(c.Volumes) == 0 {
+ c.Volumes = nil
}
+ if len(c.Networks) == 0 {
+ c.Networks = nil
+ }
+
// write to file
- backupDir := os.Getenv("BACKUP_DIR") // 从环境变量中获取备份目录
if backupDir == "" {
- backupDir = "/data/backups" // 如果环境变量未设置,使用默认值
+ backupDir = "/data/backups"
}
_, err = os.Stat(backupDir)
if os.IsNotExist(err) {
err = os.MkdirAll(backupDir, 0755)
if err != nil {
- logx.Error("Error creating backup directory:", err)
+ logx.Errorf("创建备份目录失败: %v", err)
return err
}
}
yamlData, yamlMarshalErr := yaml.Marshal(c)
if yamlMarshalErr != nil {
- logx.Errorf("Error marshalling data err is: %v", yamlMarshalErr)
+ logx.Errorf("序列化 compose 数据失败: %v", yamlMarshalErr)
+ return yamlMarshalErr
}
- currentDate := time.Now().Format("2006-01-02")
- fileName := "backup-" + currentDate + ".yaml"
+ // 文件名带时间戳:同一天多次导出互不覆盖;旧备份由调用方按 BACKUP_KEEP 清理。
+ fileName := "backup-" + time.Now().Format("2006-01-02-150405") + ".yaml"
fullPath := filepath.Join(backupDir, fileName)
- err = os.WriteFile(fullPath, yamlData, 0644)
+ tmp, err := os.CreateTemp(backupDir, ".backup-*.yaml.tmp")
if err != nil {
- logx.Error("Error writing to file:", err)
+ logx.Errorf("创建 compose 临时文件失败: %v", err)
+ return err
+ }
+ tmpName := tmp.Name()
+ defer func() {
+ _ = os.Remove(tmpName)
+ }()
+ if err = tmp.Chmod(0600); err != nil {
+ _ = tmp.Close()
+ logx.Errorf("设置 compose 临时文件权限失败: %v", err)
+ return err
+ }
+ if _, err = tmp.Write(yamlData); err != nil {
+ _ = tmp.Close()
+ logx.Errorf("写入 compose 临时文件失败: %v", err)
+ return err
+ }
+ if err = tmp.Close(); err != nil {
+ logx.Errorf("关闭 compose 临时文件失败: %v", err)
+ return err
+ }
+ if err = os.Rename(tmpName, fullPath); err != nil {
+ logx.Errorf("替换 compose 备份文件失败: %v", err)
return err
}
return
}
func formatBaseServiceConfig(containerJSON dockerTypes.ContainerJSON, s *composeType.ServiceConfig) {
- s.Image = containerJSON.Config.Image
+ if containerJSON.Config != nil {
+ cfg := containerJSON.Config
+ s.Image = cfg.Image
+ s.Tty = cfg.Tty
+ s.StdinOpen = cfg.OpenStdin
+ s.User = cfg.User
+ s.DomainName = cfg.Domainname
+ if cfg.MacAddress != "" {
+ s.MacAddress = cfg.MacAddress
+ }
+ // 容器默认 hostname 是容器 ID;导出成固定值会在重建时变成旧 ID,
+ // 只有用户显式设置了 hostname 才写入 compose。
+ if cfg.Hostname != "" && !sameShortID(cfg.Hostname, containerJSON.ID) {
+ s.Hostname = cfg.Hostname
+ }
+ if len(cfg.Entrypoint) > 0 {
+ s.Entrypoint = composeType.ShellCommand(cfg.Entrypoint)
+ }
+ s.WorkingDir = cfg.WorkingDir
+ if len(cfg.Cmd) > 0 {
+ s.Command = composeType.ShellCommand(cfg.Cmd)
+ }
+ s.Labels = cleanComposeLabels(cfg.Labels)
+ s.StopSignal = cfg.StopSignal
+ if cfg.StopTimeout != nil {
+ s.StopGracePeriod = durationPtr(time.Duration(*cfg.StopTimeout) * time.Second)
+ }
+ s.HealthCheck = formatHealthCheck(cfg.Healthcheck)
+ s.Expose = formatExposedPorts(cfg.ExposedPorts)
+ }
name, cutNameResult := strings.CutPrefix(containerJSON.Name, "/")
if !cutNameResult {
- logx.Infof("cutting name is: %s", containerJSON.Name)
+ logx.Infof("容器名缺少 / 前缀: %s", containerJSON.Name)
}
s.ContainerName = name
s.Name = name
- s.Tty = containerJSON.Config.Tty
- if len(containerJSON.Config.Entrypoint) > 0 {
- s.Entrypoint = composeType.ShellCommand(containerJSON.Config.Entrypoint)
+ if containerJSON.HostConfig != nil {
+ formatHostConfig(containerJSON.HostConfig, containerJSON.Config, s)
}
- s.WorkingDir = containerJSON.Config.WorkingDir
- s.Restart = string(containerJSON.HostConfig.RestartPolicy.Name)
- s.Privileged = containerJSON.HostConfig.Privileged
- if len(containerJSON.Config.Cmd) > 0 {
- s.Command = composeType.ShellCommand(containerJSON.Config.Cmd)
+ if containerJSON.Platform != "" {
+ s.Platform = containerJSON.Platform
}
return
}
+func formatHostConfig(h *container.HostConfig, cfg *container.Config, s *composeType.ServiceConfig) {
+ s.Restart = formatRestartPolicy(h.RestartPolicy)
+ s.Privileged = h.Privileged
+ s.ReadOnly = h.ReadonlyRootfs
+ s.Init = h.Init
+ s.CapAdd = []string(h.CapAdd)
+ s.CapDrop = []string(h.CapDrop)
+ s.CgroupParent = h.CgroupParent
+ s.Cgroup = string(h.Cgroup)
+ s.DNS = composeType.StringList(h.DNS)
+ s.DNSOpts = h.DNSOptions
+ s.DNSSearch = composeType.StringList(h.DNSSearch)
+ s.ExtraHosts = parseExtraHosts(h.ExtraHosts)
+ s.GroupAdd = h.GroupAdd
+ s.Ipc = string(h.IpcMode)
+ s.Links = h.Links
+ s.Pid = string(h.PidMode)
+ s.SecurityOpt = h.SecurityOpt
+ s.ShmSize = composeType.UnitBytes(h.ShmSize)
+ s.Sysctls = composeType.Mapping(h.Sysctls)
+ s.Tmpfs = formatTmpfs(h.Tmpfs)
+ s.Ulimits = formatUlimits(h.Ulimits)
+ s.UserNSMode = string(h.UsernsMode)
+ s.Uts = string(h.UTSMode)
+ s.VolumeDriver = h.VolumeDriver
+ s.VolumesFrom = h.VolumesFrom
+ s.Runtime = h.Runtime
+ s.Isolation = string(h.Isolation)
+ s.Devices = formatDevices(h.Devices)
+ s.DeviceCgroupRules = h.DeviceCgroupRules
+ s.Annotations = composeType.Mapping(h.Annotations)
+ s.OomScoreAdj = int64(h.OomScoreAdj)
+ s.OomKillDisable = h.OomKillDisable != nil && *h.OomKillDisable
+ if h.PidsLimit != nil {
+ s.PidsLimit = *h.PidsLimit
+ }
+ if h.MemorySwappiness != nil {
+ s.MemSwappiness = composeType.UnitBytes(*h.MemorySwappiness)
+ }
+ s.MemLimit = composeType.UnitBytes(h.Memory)
+ s.MemReservation = composeType.UnitBytes(h.MemoryReservation)
+ s.MemSwapLimit = composeType.UnitBytes(h.MemorySwap)
+ s.CPUShares = h.CPUShares
+ s.CPUQuota = h.CPUQuota
+ s.CPUPeriod = h.CPUPeriod
+ s.CPURTPeriod = h.CPURealtimePeriod
+ s.CPURTRuntime = h.CPURealtimeRuntime
+ s.CPUSet = h.CpusetCpus
+ s.CPUS = float32(h.NanoCPUs) / 1e9
+ s.CPUCount = h.CPUCount
+ s.CPUPercent = float32(h.CPUPercent)
+ if h.BlkioWeight > 0 {
+ s.BlkioConfig = &composeType.BlkioConfig{Weight: h.BlkioWeight}
+ }
+ if h.LogConfig.Type != "" || len(h.LogConfig.Config) > 0 {
+ logging := &composeType.LoggingConfig{
+ Options: composeType.Options(h.LogConfig.Config),
+ }
+ if h.LogConfig.Type != "" {
+ logging.Driver = h.LogConfig.Type
+ }
+ s.Logging = logging
+ }
+ if cfg != nil {
+ if cfg.NetworkDisabled {
+ s.NetworkMode = "none"
+ }
+ }
+}
+
func formatEnvServiceConfig(containerJSON dockerTypes.ContainerJSON, s *composeType.ServiceConfig) {
+ if containerJSON.Config == nil {
+ return
+ }
s.Environment = composeType.NewMappingWithEquals(containerJSON.Config.Env)
- return
}
func formatNetworkServiceConfig(containerJSON dockerTypes.ContainerJSON, s *composeType.ServiceConfig) {
- s.NetworkMode = string(containerJSON.HostConfig.NetworkMode)
- for containerPort, v := range containerJSON.HostConfig.PortBindings {
- var p composeType.ServicePortConfig
+ if containerJSON.HostConfig == nil {
+ return
+ }
+ mode := string(containerJSON.HostConfig.NetworkMode)
+ // compose 的 network_mode 只接受 host/none/container:...;自定义网络要用
+ // services..networks 挂载,不能把网络名塞进 network_mode。
+ if mode == "host" || mode == "none" || strings.HasPrefix(mode, "container:") {
+ s.NetworkMode = mode
+ }
+ if containerJSON.NetworkSettings != nil {
+ for networkName, endpoint := range containerJSON.NetworkSettings.Networks {
+ if networkName == "" || networkName == "bridge" || networkName == "host" || networkName == "none" {
+ continue
+ }
+ networkConfig := &composeType.ServiceNetworkConfig{}
+ if endpoint != nil {
+ networkConfig.Aliases = filterAliases(endpoint.Aliases, containerJSON.ID, containerJSON.Name)
+ // 只导出用户静态指定的地址;运行中的 IPAddress 是 Docker 动态分配的,
+ // 写成 ipv4_address 会在重建时造成地址冲突。
+ if endpoint.IPAMConfig != nil {
+ networkConfig.Ipv4Address = endpoint.IPAMConfig.IPv4Address
+ networkConfig.Ipv6Address = endpoint.IPAMConfig.IPv6Address
+ networkConfig.LinkLocalIPs = endpoint.IPAMConfig.LinkLocalIPs
+ }
+ }
+ if s.Networks == nil {
+ s.Networks = make(map[string]*composeType.ServiceNetworkConfig)
+ }
+ s.Networks[networkName] = networkConfig
+ }
+ }
+ for containerPort, bindings := range containerJSON.HostConfig.PortBindings {
+ if len(bindings) == 0 {
+ // 某些容器的 PortBindings 是空切片,直接 v[0] 会 panic。
+ logx.Errorf("端口 %s 没有绑定地址,跳过", containerPort)
+ continue
+ }
proto, port := composeNat.SplitProtoPort(string(containerPort))
portNum, convertErr := strconv.Atoi(port)
if convertErr != nil {
- logx.Errorf("Error converting port err is: %v", convertErr)
+ logx.Errorf("端口转换失败: %v", convertErr)
continue
}
- p.Target = uint32(portNum)
- p.Published = v[0].HostPort
- p.Protocol = proto
- s.Ports = append(s.Ports, p)
+ for _, binding := range bindings {
+ s.Ports = append(s.Ports, composeType.ServicePortConfig{
+ HostIP: binding.HostIP,
+ Published: binding.HostPort,
+ Target: uint32(portNum),
+ Protocol: proto,
+ })
+ }
}
}
func formatVolumeServiceConfig(containerJSON dockerTypes.ContainerJSON, s *composeType.ServiceConfig) {
for _, containerVolume := range containerJSON.Mounts {
- var v composeType.ServiceVolumeConfig
- v.Type = string(containerVolume.Type)
- v.Source = containerVolume.Source
- v.Target = containerVolume.Destination
- v.ReadOnly = containerVolume.RW
+ if string(containerVolume.Type) == "tmpfs" {
+ // HostConfig.Tmpfs 已经会导出成 tmpfs:,这里再写 volumes 会造成重复挂载点。
+ if containerJSON.HostConfig != nil {
+ if _, exists := containerJSON.HostConfig.Tmpfs[containerVolume.Destination]; exists {
+ continue
+ }
+ }
+ // tmpfs 没有 Source,长语法只需要 type + target。
+ s.Volumes = append(s.Volumes, composeType.ServiceVolumeConfig{
+ Type: "tmpfs",
+ Target: containerVolume.Destination,
+ ReadOnly: !containerVolume.RW,
+ })
+ continue
+ }
+ source := volumeSource(containerVolume)
+ if source == "" {
+ continue
+ }
+ v := composeType.ServiceVolumeConfig{
+ Type: string(containerVolume.Type),
+ Source: source,
+ Target: containerVolume.Destination,
+ ReadOnly: !containerVolume.RW,
+ }
+ for _, part := range strings.Split(containerVolume.Mode, ",") {
+ switch strings.TrimSpace(part) {
+ case "z", "Z":
+ if v.Bind == nil {
+ v.Bind = &composeType.ServiceVolumeBind{}
+ }
+ v.Bind.SELinux = part
+ case "nocopy":
+ if v.Type == "volume" {
+ v.Volume = &composeType.ServiceVolumeVolume{NoCopy: true}
+ }
+ }
+ }
+ if containerVolume.Propagation != "" {
+ if v.Bind == nil {
+ v.Bind = &composeType.ServiceVolumeBind{}
+ }
+ v.Bind.Propagation = string(containerVolume.Propagation)
+ }
s.Volumes = append(s.Volumes, v)
}
}
+
+func filterAliases(aliases []string, containerID, containerName string) []string {
+ if len(aliases) == 0 {
+ return nil
+ }
+ name := strings.TrimPrefix(containerName, "/")
+ shortID := containerID
+ if len(shortID) > 12 {
+ shortID = shortID[:12]
+ }
+ result := make([]string, 0, len(aliases))
+ for _, alias := range aliases {
+ if alias == "" || alias == name || alias == containerID || alias == shortID {
+ continue
+ }
+ result = append(result, alias)
+ }
+ if len(result) == 0 {
+ return nil
+ }
+ return result
+}
+
+func volumeSource(mount dockerTypes.MountPoint) string {
+ if mount.Type == "volume" {
+ // 匿名卷/异常卷没有 Name 时不要退化成 Source 宿主机路径,否则会导出非法卷名。
+ return mount.Name
+ }
+ return mount.Source
+}
+
+func formatExposedPorts(ports composeNat.PortSet) composeType.StringOrNumberList {
+ if len(ports) == 0 {
+ return nil
+ }
+ result := make([]string, 0, len(ports))
+ for port := range ports {
+ result = append(result, string(port))
+ }
+ return composeType.StringOrNumberList(result)
+}
+
+func formatRestartPolicy(policy container.RestartPolicy) string {
+ name := string(policy.Name)
+ if name == "on-failure" && policy.MaximumRetryCount > 0 {
+ return fmt.Sprintf("on-failure:%d", policy.MaximumRetryCount)
+ }
+ return name
+}
+
+func formatHealthCheck(hc *container.HealthConfig) *composeType.HealthCheckConfig {
+ if hc == nil {
+ return nil
+ }
+ if len(hc.Test) == 1 && strings.EqualFold(hc.Test[0], "NONE") {
+ return &composeType.HealthCheckConfig{Disable: true}
+ }
+ // 没有 test 的 healthcheck 无法还原;继续写会生成 compose 不接受的空 test。
+ if len(hc.Test) == 0 {
+ return nil
+ }
+ out := &composeType.HealthCheckConfig{
+ Test: composeType.HealthCheckTest(hc.Test),
+ }
+ if hc.Interval > 0 {
+ out.Interval = durationPtr(hc.Interval)
+ }
+ if hc.Timeout > 0 {
+ out.Timeout = durationPtr(hc.Timeout)
+ }
+ if hc.StartPeriod > 0 {
+ out.StartPeriod = durationPtr(hc.StartPeriod)
+ }
+ if hc.StartInterval > 0 {
+ out.StartInterval = durationPtr(hc.StartInterval)
+ }
+ if hc.Retries > 0 {
+ retries := uint64(hc.Retries)
+ out.Retries = &retries
+ }
+ return out
+}
+
+func durationPtr(d time.Duration) *composeType.Duration {
+ value := composeType.Duration(d)
+ return &value
+}
+
+func parseExtraHosts(hosts []string) composeType.HostsList {
+ if len(hosts) == 0 {
+ return nil
+ }
+ result := make(composeType.HostsList, len(hosts))
+ for _, entry := range hosts {
+ host, ip, ok := strings.Cut(entry, ":")
+ if !ok || host == "" || ip == "" {
+ continue
+ }
+ result[host] = ip
+ }
+ return result
+}
+
+func formatTmpfs(tmpfs map[string]string) composeType.StringList {
+ if len(tmpfs) == 0 {
+ return nil
+ }
+ result := make([]string, 0, len(tmpfs))
+ for path, opts := range tmpfs {
+ if opts == "" {
+ result = append(result, path)
+ } else {
+ result = append(result, path+":"+opts)
+ }
+ }
+ return composeType.StringList(result)
+}
+
+func formatUlimits(ulimits []*container.Ulimit) map[string]*composeType.UlimitsConfig {
+ if len(ulimits) == 0 {
+ return nil
+ }
+ result := make(map[string]*composeType.UlimitsConfig, len(ulimits))
+ for _, ulimit := range ulimits {
+ if ulimit == nil || ulimit.Name == "" {
+ continue
+ }
+ item := &composeType.UlimitsConfig{}
+ if ulimit.Soft == ulimit.Hard {
+ item.Single = int(ulimit.Soft)
+ } else {
+ item.Soft = int(ulimit.Soft)
+ item.Hard = int(ulimit.Hard)
+ }
+ result[ulimit.Name] = item
+ }
+ return result
+}
+
+func formatDevices(devices []container.DeviceMapping) []string {
+ if len(devices) == 0 {
+ return nil
+ }
+ result := make([]string, 0, len(devices))
+ for _, device := range devices {
+ entry := device.PathOnHost + ":" + device.PathInContainer
+ if device.CgroupPermissions != "" {
+ entry += ":" + device.CgroupPermissions
+ }
+ result = append(result, entry)
+ }
+ return result
+}
+
+func cleanComposeLabels(labels map[string]string) composeType.Labels {
+ if len(labels) == 0 {
+ return nil
+ }
+ result := make(composeType.Labels, len(labels))
+ for key, value := range labels {
+ if strings.HasPrefix(key, "com.docker.compose.") {
+ continue
+ }
+ result[key] = value
+ }
+ if len(result) == 0 {
+ return nil
+ }
+ return result
+}
+
+func sameShortID(hostname, id string) bool {
+ if hostname == "" || id == "" {
+ return false
+ }
+ if len(id) > 12 {
+ id = id[:12]
+ }
+ return hostname == id
+}
diff --git a/internal/utiles/backup_compose/config2yaml_test.go b/internal/utiles/backup_compose/config2yaml_test.go
new file mode 100644
index 00000000..cff5205e
--- /dev/null
+++ b/internal/utiles/backup_compose/config2yaml_test.go
@@ -0,0 +1,149 @@
+package backupCompose
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ composeType "github.com/compose-spec/compose-go/types"
+ "github.com/docker/docker/api/types"
+ "github.com/docker/docker/api/types/container"
+ "github.com/docker/docker/api/types/network"
+ "github.com/docker/go-connections/nat"
+)
+
+func TestDockerConfig2ComposeYamlEmptyPortBindingsAndTopLevel(t *testing.T) {
+ dir := t.TempDir()
+ id := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+ stopTimeout := 15
+ containerJSON := types.ContainerJSON{
+ ContainerJSONBase: &types.ContainerJSONBase{
+ ID: id,
+ Name: "/demo",
+ Image: "sha256:old",
+ Platform: "linux",
+ HostConfig: &container.HostConfig{
+ NetworkMode: "demo-net",
+ PortBindings: nat.PortMap{
+ nat.Port("80/tcp"): {},
+ },
+ CapAdd: []string{"NET_ADMIN"},
+ CapDrop: []string{"MKNOD"},
+ DNS: []string{"1.1.1.1"},
+ Resources: container.Resources{
+ Devices: []container.DeviceMapping{
+ {PathOnHost: "/dev/fuse", PathInContainer: "/dev/fuse", CgroupPermissions: "rwm"},
+ },
+ },
+ RestartPolicy: container.RestartPolicy{Name: "on-failure", MaximumRetryCount: 3},
+ LogConfig: container.LogConfig{Type: "json-file", Config: map[string]string{"max-size": "10m"}},
+ },
+ },
+ Config: &container.Config{
+ Hostname: id[:12], // 默认 hostname 等于短容器 ID,不应写入 compose
+ Image: "nginx:latest",
+ StopTimeout: &stopTimeout,
+ Labels: map[string]string{
+ "com.docker.compose.project": "old-project",
+ "app": "demo",
+ },
+ Healthcheck: &container.HealthConfig{
+ Test: []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"},
+ Interval: 30 * time.Second,
+ Timeout: 5 * time.Second,
+ Retries: 3,
+ },
+ },
+ Mounts: []types.MountPoint{
+ {
+ Type: "volume",
+ Name: "demo-data",
+ Source: "/var/lib/docker/volumes/demo-data/_data",
+ Destination: "/data",
+ RW: true,
+ },
+ {
+ Type: "tmpfs",
+ Destination: "/tmp/cache",
+ RW: true,
+ },
+ },
+ NetworkSettings: &types.NetworkSettings{
+ Networks: map[string]*network.EndpointSettings{
+ "demo-net": {
+ IPAddress: "172.18.0.5",
+ Aliases: []string{"demo", "0123456789ab", "real-alias"},
+ },
+ },
+ },
+ }
+
+ if err := DockerConfig2ComposeYaml([]types.ContainerJSON{containerJSON}, dir); err != nil {
+ t.Fatalf("DockerConfig2ComposeYaml() error = %v", err)
+ }
+ matches, err := filepath.Glob(filepath.Join(dir, "backup-*.yaml"))
+ if err != nil || len(matches) != 1 {
+ t.Fatalf("backup files = %v, err = %v", matches, err)
+ }
+ data, err := os.ReadFile(matches[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+ text := string(data)
+ for _, want := range []string{
+ "volumes:",
+ "demo-data:",
+ "networks:",
+ "demo-net:",
+ "healthcheck:",
+ "cap_add:",
+ "devices:",
+ "restart: on-failure:3",
+ "stop_grace_period: 15s",
+ "app: demo",
+ "type: tmpfs",
+ "target: /tmp/cache",
+ "real-alias",
+ } {
+ if !strings.Contains(text, want) {
+ t.Fatalf("yaml missing %q:\n%s", want, text)
+ }
+ }
+ for _, notWant := range []string{
+ "/var/lib/docker/volumes",
+ "com.docker.compose.project",
+ "hostname: abc123",
+ "172.18.0.5",
+ "0123456789ab",
+ } {
+ if strings.Contains(text, notWant) {
+ t.Fatalf("yaml should not contain %q:\n%s", notWant, text)
+ }
+ }
+}
+
+func TestFormatVolumeServiceConfigSkipsTmpfsAlreadyInHostConfig(t *testing.T) {
+ containerJSON := types.ContainerJSON{
+ ContainerJSONBase: &types.ContainerJSONBase{
+ HostConfig: &container.HostConfig{
+ Tmpfs: map[string]string{"/tmp/cache": "size=64m"},
+ },
+ },
+ Mounts: []types.MountPoint{
+ {Type: "tmpfs", Destination: "/tmp/cache", RW: true},
+ {Type: "tmpfs", Destination: "/tmp/other", RW: true},
+ {Type: "volume", Source: "/var/lib/docker/volumes/anonymous/_data", Destination: "/anon", RW: true},
+ },
+ }
+ var service composeType.ServiceConfig
+ formatVolumeServiceConfig(containerJSON, &service)
+
+ if len(service.Volumes) != 1 {
+ t.Fatalf("volumes = %#v, want only the tmpfs missing from HostConfig.Tmpfs", service.Volumes)
+ }
+ if service.Volumes[0].Target != "/tmp/other" {
+ t.Fatalf("volume target = %q, want /tmp/other", service.Volumes[0].Target)
+ }
+}
diff --git a/internal/utiles/backup_compose/types.go b/internal/utiles/backup_compose/types.go
index 4688a31d..2a03f018 100644
--- a/internal/utiles/backup_compose/types.go
+++ b/internal/utiles/backup_compose/types.go
@@ -6,4 +6,8 @@ import (
type composeYaml struct {
Services map[string]composeType.ServiceConfig `yaml:"services" json:"services"`
+ // 顶层声明命名卷和自定义网络。没有这两个声明时,导出的 compose 在
+ // `docker compose up` 时无法还原卷/网络,只能得到匿名卷。
+ Volumes composeType.Volumes `yaml:"volumes,omitempty" json:"volumes,omitempty"`
+ Networks composeType.Networks `yaml:"networks,omitempty" json:"networks,omitempty"`
}
diff --git a/internal/utiles/backup_types.go b/internal/utiles/backup_types.go
new file mode 100644
index 00000000..bc674981
--- /dev/null
+++ b/internal/utiles/backup_types.go
@@ -0,0 +1,15 @@
+package utiles
+
+import (
+ dockerBackend "github.com/docker/docker/api/types/backend"
+)
+
+// ContainerBackupEntry 是备份文件中的一条容器记录。
+// 前四个字段来自旧的 ContainerCreateConfig 备份格式,保持向后兼容;
+// StateKnown 之后是新增字段,旧备份没有这些字段时按“状态未知”处理。
+type ContainerBackupEntry struct {
+ dockerBackend.ContainerCreateConfig
+ StateKnown bool `json:"stateKnown,omitempty"`
+ WasRunning bool `json:"wasRunning,omitempty"`
+ WasPaused bool `json:"wasPaused,omitempty"`
+}
diff --git a/internal/utiles/backup_types_test.go b/internal/utiles/backup_types_test.go
new file mode 100644
index 00000000..0a5d823e
--- /dev/null
+++ b/internal/utiles/backup_types_test.go
@@ -0,0 +1,38 @@
+package utiles
+
+import (
+ "encoding/json"
+ "strings"
+ "testing"
+
+ dockerBackend "github.com/docker/docker/api/types/backend"
+)
+
+func TestContainerBackupEntryKeepsStateAndOldFormat(t *testing.T) {
+ entry := ContainerBackupEntry{
+ ContainerCreateConfig: dockerBackend.ContainerCreateConfig{Name: "demo"},
+ StateKnown: true,
+ WasRunning: true,
+ WasPaused: true,
+ }
+ data, err := json.Marshal(entry)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, want := range []string{`"Name":"demo"`, `"stateKnown":true`, `"wasRunning":true`, `"wasPaused":true`} {
+ if !strings.Contains(string(data), want) {
+ t.Fatalf("marshaled entry missing %s: %s", want, data)
+ }
+ }
+
+ var old []ContainerBackupEntry
+ if err := json.Unmarshal([]byte(`[{"Name":"old","Config":{"Image":"nginx"}}]`), &old); err != nil {
+ t.Fatal(err)
+ }
+ if len(old) != 1 || old[0].Name != "old" || old[0].Config == nil || old[0].Config.Image != "nginx" {
+ t.Fatalf("old format unmarshal failed: %+v", old)
+ }
+ if old[0].StateKnown {
+ t.Fatal("old format should be treated as state unknown")
+ }
+}
diff --git a/internal/utiles/backupcontainer.go b/internal/utiles/backupcontainer.go
index 45ba6e92..276b6f48 100644
--- a/internal/utiles/backupcontainer.go
+++ b/internal/utiles/backupcontainer.go
@@ -3,52 +3,91 @@ package utiles
import (
"context"
"encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
dockerBackend "github.com/docker/docker/api/types/backend"
"github.com/docker/docker/api/types/network"
"github.com/onlyLTY/dockerCopilot/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
- "os"
- "path/filepath"
- "time"
)
-func BackupContainer(ctx *svc.ServiceContext) error {
+// BackupContainer 把全部容器配置备份为一个 JSON 文件。
+// 单个容器 inspect 失败不中止整体备份,失败清单通过返回值带回;
+// 全部容器都失败时才返回错误。
+func BackupContainer(ctx *svc.ServiceContext) (failures []string, err error) {
+ if ctx.DockerClient == nil {
+ return nil, fmt.Errorf("Docker 客户端未初始化,请检查 DOCKER_HOST 配置")
+ }
containerList, err := GetContainerList(ctx)
if err != nil {
- return err
+ return nil, err
}
- var backupList []dockerBackend.ContainerCreateConfig
- for i, v := range containerList {
+ backupList := make([]ContainerBackupEntry, 0, len(containerList))
+ failures = make([]string, 0)
+ for i := range containerList {
containerID := containerList[i].ID
- ctx.DockerClient.NegotiateAPIVersion(context.TODO())
- inspectedContainer, err := ctx.DockerClient.ContainerInspect(context.TODO(), containerID)
+ inspectCtx, cancel := dockerTimeout(context.Background(), dockerInspectTimeout)
+ inspectedContainer, err := ctx.DockerClient.ContainerInspect(inspectCtx, containerID)
+ cancel()
if err != nil {
- logx.Error("获取容器信息失败" + err.Error())
- return err
+ // 一个坏容器不应该挡住其它容器的日常备份。
+ logx.Errorf("容器 %s 获取信息失败,跳过备份: %v", containerID, err)
+ failures = append(failures, containerID+": "+err.Error())
+ continue
+ }
+ if inspectedContainer.Config == nil {
+ logx.Errorf("容器 %s 配置为空,跳过备份", containerID)
+ failures = append(failures, containerID+": 配置为空,无法备份")
+ continue
}
- var containerName string
- if len(v.Names) > 0 {
- containerName = v.Names[0][1:]
- } else {
- containerName = "get container name error"
- logx.Error("get container name error" + v.ID)
+ containerName := strings.TrimPrefix(inspectedContainer.Name, "/")
+ if containerName == "" && len(containerList[i].Names) > 0 {
+ containerName = strings.TrimPrefix(containerList[i].Names[0], "/")
+ }
+ if containerName == "" {
+ containerName = containerID
+ if len(containerName) > 12 {
+ containerName = containerName[:12]
+ }
+ logx.Errorf("容器 %s 名称为空,使用 ID 作为备份名称", containerID)
}
inspectedContainer.Config.Hostname = ""
inspectedContainer.Image = inspectedContainer.Config.Image
- config := inspectedContainer.Config
- hostConfig := inspectedContainer.HostConfig
+ var endpoints map[string]*network.EndpointSettings
+ if inspectedContainer.NetworkSettings != nil {
+ endpoints = sanitizeEndpointSettings(inspectedContainer.NetworkSettings.Networks)
+ }
networkingConfig := &network.NetworkingConfig{
- EndpointsConfig: inspectedContainer.NetworkSettings.Networks,
+ EndpointsConfig: endpoints,
+ }
+ entry := ContainerBackupEntry{
+ ContainerCreateConfig: dockerBackend.ContainerCreateConfig{
+ Config: inspectedContainer.Config,
+ HostConfig: inspectedContainer.HostConfig,
+ NetworkingConfig: networkingConfig,
+ Name: containerName,
+ },
}
- createConfig := dockerBackend.ContainerCreateConfig{Config: config, HostConfig: hostConfig, NetworkingConfig: networkingConfig, Name: containerName}
- backupList = append(backupList, createConfig)
+ if inspectedContainer.State != nil {
+ entry.StateKnown = true
+ entry.WasRunning = inspectedContainer.State.Running
+ entry.WasPaused = inspectedContainer.State.Paused
+ }
+ backupList = append(backupList, entry)
+ }
+ if len(backupList) == 0 && len(failures) > 0 {
+ return failures, fmt.Errorf("全部 %d 个容器备份失败,未生成备份文件", len(failures))
}
jsonData, err := json.MarshalIndent(backupList, "", " ")
if err != nil {
- logx.Error("Error marshalling data:", err)
- return err
+ logx.Error("数据序列化失败:", err)
+ return failures, err
}
- backupDir := os.Getenv("BACKUP_DIR") // 从环境变量中获取备份目录
+ backupDir := BackupDir() // env BACKUP_DIR > Settings > 默认 /data/backups
if backupDir == "" {
backupDir = "/data/backups" // 如果环境变量未设置,使用默认值
}
@@ -56,17 +95,41 @@ func BackupContainer(ctx *svc.ServiceContext) error {
if os.IsNotExist(err) {
err = os.MkdirAll(backupDir, 0755)
if err != nil {
- logx.Error("Error creating backup directory:", err)
- return err
+ logx.Errorf("创建备份目录失败: %v", err)
+ return failures, err
}
}
- currentDate := time.Now().Format("2006-01-02")
- fileName := "backup-" + currentDate + ".json"
+ // 文件名带时间戳:同一天多次备份互不覆盖,可回退到任一次;
+ // 超出保留份数(BACKUP_KEEP,默认 7)的旧备份自动清理。
+ fileName := "backup-" + time.Now().Format("2006-01-02-150405") + ".json"
fullPath := filepath.Join(backupDir, fileName)
- err = os.WriteFile(fullPath, jsonData, 0644)
+ tmp, err := os.CreateTemp(backupDir, ".backup-*.json.tmp")
if err != nil {
- logx.Error("Error writing to file:", err)
- return err
+ logx.Errorf("创建备份临时文件失败: %v", err)
+ return failures, err
+ }
+ tmpName := tmp.Name()
+ defer func() {
+ _ = os.Remove(tmpName)
+ }()
+ if err = tmp.Chmod(0600); err != nil {
+ _ = tmp.Close()
+ logx.Errorf("设置备份临时文件权限失败: %v", err)
+ return failures, err
+ }
+ if _, err = tmp.Write(jsonData); err != nil {
+ _ = tmp.Close()
+ logx.Errorf("写入备份临时文件失败: %v", err)
+ return failures, err
+ }
+ if err = tmp.Close(); err != nil {
+ logx.Errorf("关闭备份临时文件失败: %v", err)
+ return failures, err
+ }
+ if err = os.Rename(tmpName, fullPath); err != nil {
+ logx.Errorf("替换备份文件失败: %v", err)
+ return failures, err
}
- return nil
+ pruneOldBackups(backupDir, "backup-*.json", BackupKeepCount())
+ return failures, nil
}
diff --git a/internal/utiles/backuppath.go b/internal/utiles/backuppath.go
index 6c2151db..5c1f7ad7 100644
--- a/internal/utiles/backuppath.go
+++ b/internal/utiles/backuppath.go
@@ -11,11 +11,7 @@ import (
var backupFilenamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
func BackupDir() string {
- backupDir := os.Getenv("BACKUP_DIR")
- if backupDir == "" {
- backupDir = "/data/backups"
- }
- return backupDir
+ return EffectiveBackupDir()
}
func ResolveBackupPath(filename string, allowedExts ...string) (string, error) {
@@ -23,6 +19,9 @@ func ResolveBackupPath(filename string, allowedExts ...string) (string, error) {
if name == "" {
return "", fmt.Errorf("非法文件名")
}
+ if name == "." || name == ".." {
+ return "", fmt.Errorf("非法文件名")
+ }
if name != filepath.Base(name) {
return "", fmt.Errorf("非法文件名")
}
@@ -44,10 +43,13 @@ func ResolveBackupPath(filename string, allowedExts ...string) (string, error) {
}
}
- base := filepath.Clean(BackupDir())
- fullPath := filepath.Clean(filepath.Join(base, name))
- basePrefix := base + string(os.PathSeparator)
- if fullPath != base && !strings.HasPrefix(fullPath, basePrefix) {
+ base, err := filepath.Abs(filepath.Clean(BackupDir()))
+ if err != nil {
+ return "", fmt.Errorf("备份目录不合法")
+ }
+ fullPath := filepath.Join(base, name)
+ rel, err := filepath.Rel(base, fullPath)
+ if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return "", fmt.Errorf("非法文件名")
}
diff --git a/internal/utiles/backuppath_test.go b/internal/utiles/backuppath_test.go
new file mode 100644
index 00000000..17110b46
--- /dev/null
+++ b/internal/utiles/backuppath_test.go
@@ -0,0 +1,31 @@
+package utiles
+
+import (
+ "path/filepath"
+ "testing"
+)
+
+func TestResolveBackupPathRelativeBackupDir(t *testing.T) {
+ t.Setenv("BACKUP_DIR", ".")
+ t.Setenv("SETTINGS_FILE", filepath.Join(t.TempDir(), "settings.json"))
+
+ got, err := ResolveBackupPath("backup-2026-01-01.json", ".json")
+ if err != nil {
+ t.Fatalf("ResolveBackupPath() error = %v", err)
+ }
+ if !filepath.IsAbs(got) {
+ t.Fatalf("resolved path = %q, want absolute path", got)
+ }
+ if filepath.Base(got) != "backup-2026-01-01.json" {
+ t.Fatalf("base = %q", filepath.Base(got))
+ }
+}
+
+func TestResolveBackupPathRejectsTraversal(t *testing.T) {
+ t.Setenv("BACKUP_DIR", t.TempDir())
+ t.Setenv("SETTINGS_FILE", filepath.Join(t.TempDir(), "settings.json"))
+
+ if _, err := ResolveBackupPath("../escape.json", ".json"); err == nil {
+ t.Fatal("expected traversal error")
+ }
+}
diff --git a/internal/utiles/backupretention.go b/internal/utiles/backupretention.go
new file mode 100644
index 00000000..7d1767b5
--- /dev/null
+++ b/internal/utiles/backupretention.go
@@ -0,0 +1,76 @@
+package utiles
+
+import (
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/zeromicro/go-zero/core/logx"
+)
+
+// backupKeepDefault 每种备份(JSON / compose)默认保留份数。
+const backupKeepDefault = 7
+
+// BackupKeepCount 备份保留份数:env BACKUP_KEEP,默认 7,最小 1。
+// 每次备份生成带时间戳的新文件,超出的旧备份由 pruneOldBackups 清理。
+func BackupKeepCount() int {
+ if v := os.Getenv("BACKUP_KEEP"); v != "" {
+ if n, err := strconv.Atoi(v); err == nil && n > 0 {
+ return n
+ }
+ logx.Errorf("BACKUP_KEEP 非法: %q,使用默认值 %d", v, backupKeepDefault)
+ }
+ return backupKeepDefault
+}
+
+// pruneOldBackups 按修改时间保留最近 keep 个匹配 pattern 的备份文件。
+// 隐藏文件(含备份临时文件)不参与清理。返回被删除的文件名;
+// 删除失败只记日志,不影响本次备份结果。
+func pruneOldBackups(dir, pattern string, keep int) []string {
+ if keep <= 0 {
+ keep = 1
+ }
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ logx.Errorf("清理旧备份失败,读取目录出错: %v", err)
+ return nil
+ }
+ type backupFile struct {
+ name string
+ modTime int64
+ }
+ var files []backupFile
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+ name := entry.Name()
+ if strings.HasPrefix(name, ".") {
+ continue
+ }
+ matched, matchErr := filepath.Match(pattern, name)
+ if matchErr != nil || !matched {
+ continue
+ }
+ info, statErr := entry.Info()
+ if statErr != nil {
+ continue
+ }
+ files = append(files, backupFile{name: name, modTime: info.ModTime().UnixNano()})
+ }
+ if len(files) <= keep {
+ return nil
+ }
+ sort.Slice(files, func(i, j int) bool { return files[i].modTime > files[j].modTime })
+ var removed []string
+ for _, f := range files[keep:] {
+ if rmErr := os.Remove(filepath.Join(dir, f.name)); rmErr != nil {
+ logx.Errorf("删除旧备份 %s 失败: %v", f.name, rmErr)
+ continue
+ }
+ removed = append(removed, f.name)
+ }
+ return removed
+}
diff --git a/internal/utiles/backupretention_test.go b/internal/utiles/backupretention_test.go
new file mode 100644
index 00000000..2bff45e0
--- /dev/null
+++ b/internal/utiles/backupretention_test.go
@@ -0,0 +1,83 @@
+package utiles
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func TestBackupKeepCountDefault(t *testing.T) {
+ t.Setenv("BACKUP_KEEP", "")
+ if got := BackupKeepCount(); got != 7 {
+ t.Fatalf("default keep = %d, want 7", got)
+ }
+}
+
+func TestBackupKeepCountFromEnv(t *testing.T) {
+ t.Setenv("BACKUP_KEEP", "3")
+ if got := BackupKeepCount(); got != 3 {
+ t.Fatalf("keep = %d, want 3", got)
+ }
+}
+
+func TestBackupKeepCountInvalidFallsBack(t *testing.T) {
+ t.Setenv("BACKUP_KEEP", "abc")
+ if got := BackupKeepCount(); got != 7 {
+ t.Fatalf("invalid keep should fall back to 7, got %d", got)
+ }
+ t.Setenv("BACKUP_KEEP", "-2")
+ if got := BackupKeepCount(); got != 7 {
+ t.Fatalf("negative keep should fall back to 7, got %d", got)
+ }
+}
+
+func TestPruneOldBackupsKeepsNewest(t *testing.T) {
+ dir := t.TempDir()
+ names := []string{"backup-1.json", "backup-2.json", "backup-3.json", "backup-4.json", "backup-5.json"}
+ for i, name := range names {
+ path := filepath.Join(dir, name)
+ if err := os.WriteFile(path, []byte("{}"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ stamp := time.Now().Add(-time.Duration(len(names)-i) * time.Hour)
+ if err := os.Chtimes(path, stamp, stamp); err != nil {
+ t.Fatal(err)
+ }
+ }
+ // 隐藏文件(临时文件)不参与清理。
+ if err := os.WriteFile(filepath.Join(dir, ".backup-x.json.tmp"), []byte("tmp"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+
+ removed := pruneOldBackups(dir, "backup-*.json", 3)
+ if len(removed) != 2 {
+ t.Fatalf("expected 2 removed, got %v", removed)
+ }
+ // 最旧的两个(backup-1/2)应被删除。
+ for _, gone := range []string{"backup-1.json", "backup-2.json"} {
+ if _, err := os.Stat(filepath.Join(dir, gone)); !os.IsNotExist(err) {
+ t.Fatalf("%s should be removed", gone)
+ }
+ }
+ for _, kept := range []string{"backup-3.json", "backup-4.json", "backup-5.json"} {
+ if _, err := os.Stat(filepath.Join(dir, kept)); err != nil {
+ t.Fatalf("%s should be kept: %v", kept, err)
+ }
+ }
+ if _, err := os.Stat(filepath.Join(dir, ".backup-x.json.tmp")); err != nil {
+ t.Fatal("hidden temp file should not be touched")
+ }
+}
+
+func TestPruneOldBackupsNoopWhenUnderKeep(t *testing.T) {
+ dir := t.TempDir()
+ for _, name := range []string{"backup-a.json", "backup-b.json"} {
+ if err := os.WriteFile(filepath.Join(dir, name), []byte("{}"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if removed := pruneOldBackups(dir, "backup-*.json", 7); removed != nil {
+ t.Fatalf("expected no removal, got %v", removed)
+ }
+}
diff --git a/internal/utiles/backupslist.go b/internal/utiles/backupslist.go
index 8e3502a1..6729dd89 100644
--- a/internal/utiles/backupslist.go
+++ b/internal/utiles/backupslist.go
@@ -8,10 +8,7 @@ import (
func BackupList(ctx *svc.ServiceContext) ([]string, error) {
var backupList []string
- dir := os.Getenv("BACKUP_DIR") // 从环境变量中获取备份目录
- if dir == "" {
- dir = "/data/backups" // 如果环境变量未设置,使用默认值
- }
+ dir := BackupDir() // env BACKUP_DIR > Settings > 默认 /data/backups
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
diff --git a/internal/utiles/containerInspect.go b/internal/utiles/containerInspect.go
index 48d4ce29..c24d9b9f 100644
--- a/internal/utiles/containerInspect.go
+++ b/internal/utiles/containerInspect.go
@@ -7,7 +7,9 @@ import (
)
func GetContainerInspect(ctx *svc.ServiceContext, id string) (types.ContainerJSON, error) {
- inspectedContainer, err := ctx.DockerClient.ContainerInspect(context.TODO(), id)
+ requestCtx, cancel := dockerTimeout(context.Background(), dockerInspectTimeout)
+ defer cancel()
+ inspectedContainer, err := ctx.DockerClient.ContainerInspect(requestCtx, id)
if err != nil {
return types.ContainerJSON{}, err
}
diff --git a/internal/utiles/dockercontext.go b/internal/utiles/dockercontext.go
new file mode 100644
index 00000000..3ee0178b
--- /dev/null
+++ b/internal/utiles/dockercontext.go
@@ -0,0 +1,22 @@
+package utiles
+
+import (
+ "context"
+ "time"
+)
+
+const (
+ dockerListTimeout = 60 * time.Second
+ dockerInspectTimeout = 60 * time.Second
+ dockerMutationTimeout = 2 * time.Minute
+ dockerNegotiateTimeout = 30 * time.Second
+)
+
+// dockerTimeout 给单次 Docker API 调用加超时。
+// 更新/恢复的 ImagePull 使用任务上下文,不在这里限制,避免大镜像被误杀。
+func dockerTimeout(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
+ if parent == nil {
+ parent = context.Background()
+ }
+ return context.WithTimeout(parent, timeout)
+}
diff --git a/internal/utiles/getRemoteVersion.go b/internal/utiles/getRemoteVersion.go
index e7413ef6..b20267bf 100644
--- a/internal/utiles/getRemoteVersion.go
+++ b/internal/utiles/getRemoteVersion.go
@@ -1,31 +1,39 @@
package utiles
import (
+ "errors"
+ "fmt"
"io"
"net/http"
"os"
"strings"
+ "time"
"github.com/onlyLTY/dockerCopilot/internal/config"
"github.com/zeromicro/go-zero/core/logx"
)
+// ErrFnosManagedUpdate 表示 FNOS 版本的版本检查与自更新由专用发布包管理,
+// 不走通用在线更新链。调用方应显式向用户说明,而不是伪装成“无更新”。
+var ErrFnosManagedUpdate = errors.New("FnOS 版本由专用发布包管理,不支持在线版本检查与自更新")
+
func GetRemoteVersion() (remoteVersion string, err error) {
+ localVersion := config.Version
+ if strings.Contains(localVersion, "FNOS") {
+ // FNOS 版本使用独立发布包,不需要也不应该访问通用版 version 文件。
+ logx.Infof("飞牛版本,不支持在线版本检查")
+ return "", ErrFnosManagedUpdate
+ }
+
githubProxy := os.Getenv("githubProxy")
if githubProxy != "" {
githubProxy = strings.TrimRight(githubProxy, "/") + "/"
}
- versionURL := githubProxy + "https://raw.githubusercontent.com/onlyLTY/dockerCopilot/UGREEN/version"
+ versionURL := githubProxy + "https://raw.githubusercontent.com/" + UpdateRepo() + "/" + UpdateBranch() + "/version"
remoteVersion, err = fetchVersionFromURL(versionURL)
if err != nil {
return "0.0.0", err
}
-
- localVersion := config.Version
- if strings.Contains(localVersion, "FNOS") {
- logx.Infof("飞牛版本,无需在线更新")
- return localVersion, nil
- }
if localVersion == remoteVersion {
logx.Info("版本一致:", localVersion)
return remoteVersion, nil
@@ -38,6 +46,7 @@ func GetRemoteVersion() (remoteVersion string, err error) {
func fetchVersionFromURL(url string) (string, error) {
client := &http.Client{
+ Timeout: 30 * time.Second,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
@@ -54,7 +63,11 @@ func fetchVersionFromURL(url string) (string, error) {
}
}(resp.Body)
- versionData, err := io.ReadAll(resp.Body)
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("获取版本信息失败:%s", resp.Status)
+ }
+
+ versionData, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return "", err
}
diff --git a/internal/utiles/getcontainerlist.go b/internal/utiles/getcontainerlist.go
index cfc7344e..3d68046f 100644
--- a/internal/utiles/getcontainerlist.go
+++ b/internal/utiles/getcontainerlist.go
@@ -2,6 +2,8 @@ package utiles
import (
"context"
+ "fmt"
+
"github.com/docker/docker/api/types/container"
"github.com/onlyLTY/dockerCopilot/internal/svc"
MyType "github.com/onlyLTY/dockerCopilot/internal/types"
@@ -9,12 +11,17 @@ import (
)
func GetContainerList(ctx *svc.ServiceContext) ([]MyType.Container, error) {
+ if ctx.DockerClient == nil {
+ return nil, fmt.Errorf("Docker 客户端未初始化,请检查 DOCKER_HOST 配置")
+ }
// 获取所有容器(包括停止的容器)
- dockerContainerList, err := ctx.DockerClient.ContainerList(context.Background(), container.ListOptions{
+ requestCtx, cancel := dockerTimeout(context.Background(), dockerListTimeout)
+ defer cancel()
+ dockerContainerList, err := ctx.DockerClient.ContainerList(requestCtx, container.ListOptions{
All: true, // 设置为true来获取所有容器
})
if err != nil {
- logx.Errorf("get container list error: %v", err)
+ logx.Errorf("获取容器列表失败: %v", err)
return nil, err
}
var containerList []MyType.Container
@@ -29,10 +36,8 @@ func GetContainerList(ctx *svc.ServiceContext) ([]MyType.Container, error) {
func CheckImageUpdate(ctx *svc.ServiceContext, containerListData []MyType.Container) []MyType.Container {
for i, v := range containerListData {
- if _, ok := ctx.HubImageInfo.Data[v.ImageID]; ok {
- if ctx.HubImageInfo.Data[v.ImageID].NeedUpdate {
- containerListData[i].Update = true
- }
+ if info, ok := ctx.HubImageInfo.Get(v.ImageID); ok && info.NeedUpdate {
+ containerListData[i].Update = true
}
}
return containerListData
diff --git a/internal/utiles/image.go b/internal/utiles/image.go
index b0b37a69..628ca261 100644
--- a/internal/utiles/image.go
+++ b/internal/utiles/image.go
@@ -6,15 +6,21 @@ import (
"github.com/docker/docker/api/types/image"
"github.com/onlyLTY/dockerCopilot/internal/svc"
MyType "github.com/onlyLTY/dockerCopilot/internal/types"
- "log"
+ "github.com/zeromicro/go-zero/core/logx"
"strings"
)
func GetImagesList(ctx *svc.ServiceContext) ([]MyType.Image, error) {
var imagesList []MyType.Image
- dockerImages, err := ctx.DockerClient.ImageList(context.Background(), image.ListOptions{})
+ if ctx.DockerClient == nil {
+ return nil, fmt.Errorf("Docker 客户端未初始化,请检查 DOCKER_HOST 配置")
+ }
+ requestCtx, cancel := dockerTimeout(context.Background(), dockerListTimeout)
+ defer cancel()
+ dockerImages, err := ctx.DockerClient.ImageList(requestCtx, image.ListOptions{})
if err != nil {
- log.Fatalf("Unable to fetch docker images: %s", err)
+ logx.Errorf("获取镜像列表失败: %s", err)
+ return nil, err
}
for _, img := range dockerImages {
@@ -38,8 +44,15 @@ func GetImagesList(ctx *svc.ServiceContext) ([]MyType.Image, error) {
func splitImageNameAndTag(imagesList []MyType.Image) []MyType.Image {
for i, imageInfo := range imagesList {
if len(imageInfo.RepoTags) != 0 {
- imagesList[i].ImageName = strings.Split(imageInfo.RepoTags[0], ":")[0]
- imagesList[i].ImageTag = strings.Split(imageInfo.RepoTags[0], ":")[1]
+ repoTag := imageInfo.RepoTags[0]
+ // 用最后一个冒号切分,兼容带端口的私有仓库(如 registry.example.com:5000/app:v1)
+ if idx := strings.LastIndex(repoTag, ":"); idx > 0 && !strings.Contains(repoTag[idx+1:], "/") {
+ imagesList[i].ImageName = repoTag[:idx]
+ imagesList[i].ImageTag = repoTag[idx+1:]
+ } else {
+ imagesList[i].ImageName = repoTag
+ imagesList[i].ImageTag = "None"
+ }
} else if len(imageInfo.RepoDigests) != 0 {
imagesList[i].ImageName = strings.Split(imageInfo.RepoDigests[0], "@")[0]
imagesList[i].ImageTag = "None"
@@ -55,13 +68,13 @@ func checkImageInUsed(svc *svc.ServiceContext, imageList []MyType.Image) ([]MyTy
if err != nil {
return imageList, err
}
- // 这里可以用mapreduce 我懒等pr
+ used := make(map[string]struct{}, len(list))
for _, v := range list {
- for i, imageInfo := range imageList {
- if v.ImageID == imageInfo.ID {
- imageList[i].InUsed = true
- break
- }
+ used[v.ImageID] = struct{}{}
+ }
+ for i := range imageList {
+ if _, ok := used[imageList[i].ID]; ok {
+ imageList[i].InUsed = true
}
}
return imageList, nil
@@ -78,3 +91,57 @@ func calculateImageSize(imagesList []MyType.Image) []MyType.Image {
}
return imagesList
}
+
+// 批量镜像清理过滤器:删除无 TAG 镜像 / 删除未被容器引用的镜像。
+const (
+ CleanupFilterUntagged = "untagged"
+ CleanupFilterUnused = "unused"
+)
+
+// isUntaggedImage 判断镜像是否没有可用的 tag。
+// splitImageNameAndTag 会把无 tag 镜像的 ImageTag 归一为 "None",
+// 少数异常镜像的 RepoTags 里是字面 ":",这里一并覆盖。
+func isUntaggedImage(img MyType.Image) bool {
+ return img.ImageTag == "None" || img.ImageTag == "" ||
+ img.ImageName == "None" || img.ImageName == ""
+}
+
+// PickImagesForCleanup 按过滤器挑选待清理镜像(纯函数,便于测试)。
+// untagged:无 TAG 镜像;unused:未被任何容器(含已停止容器)引用的镜像。
+func PickImagesForCleanup(images []MyType.Image, filter string) []MyType.Image {
+ picked := make([]MyType.Image, 0, len(images))
+ for _, img := range images {
+ switch filter {
+ case CleanupFilterUntagged:
+ if isUntaggedImage(img) {
+ picked = append(picked, img)
+ }
+ case CleanupFilterUnused:
+ if !img.InUsed {
+ picked = append(picked, img)
+ }
+ }
+ }
+ return picked
+}
+
+// RemoveImagesFiltered 批量删除镜像,逐个尝试,单个失败不影响其它。
+// 返回已删除与失败的镜像清单(失败时带原因)。filter 只接受上面两个常量。
+func RemoveImagesFiltered(ctx *svc.ServiceContext, filter string) (removed []string, failed []string, err error) {
+ if filter != CleanupFilterUntagged && filter != CleanupFilterUnused {
+ return nil, nil, fmt.Errorf("filter 只支持 %s 或 %s", CleanupFilterUntagged, CleanupFilterUnused)
+ }
+ images, err := GetImagesList(ctx)
+ if err != nil {
+ return nil, nil, err
+ }
+ for _, img := range PickImagesForCleanup(images, filter) {
+ if removeErr := RemoveImage(ctx, img.ID, false); removeErr != nil {
+ logx.Errorf("批量清理镜像失败 %s: %v", img.ID, removeErr)
+ failed = append(failed, img.ID+" ("+img.ImageName+":"+img.ImageTag+"): "+removeErr.Error())
+ continue
+ }
+ removed = append(removed, img.ID+" ("+img.ImageName+":"+img.ImageTag+")")
+ }
+ return removed, failed, nil
+}
diff --git a/internal/utiles/imagecleanup_test.go b/internal/utiles/imagecleanup_test.go
new file mode 100644
index 00000000..12754a76
--- /dev/null
+++ b/internal/utiles/imagecleanup_test.go
@@ -0,0 +1,51 @@
+package utiles
+
+import (
+ "testing"
+
+ MyType "github.com/onlyLTY/dockerCopilot/internal/types"
+)
+
+func TestPickImagesForCleanupUntagged(t *testing.T) {
+ images := []MyType.Image{
+ {ImageName: "nginx", ImageTag: "latest", InUsed: false},
+ {ImageName: "None", ImageTag: "None", InUsed: false},
+ {ImageName: "", ImageTag: "", InUsed: true},
+ {ImageName: "redis", ImageTag: "None", InUsed: false},
+ }
+ picked := PickImagesForCleanup(images, CleanupFilterUntagged)
+ if len(picked) != 3 {
+ t.Fatalf("expected 3 untagged images, got %d: %+v", len(picked), picked)
+ }
+ for _, img := range picked {
+ if img.ImageName == "nginx" {
+ t.Fatalf("tagged image should not be picked: %+v", img)
+ }
+ }
+}
+
+func TestPickImagesForCleanupUnused(t *testing.T) {
+ images := []MyType.Image{
+ {ImageName: "nginx", ImageTag: "latest", InUsed: false},
+ {ImageName: "redis", ImageTag: "7", InUsed: true},
+ {ImageName: "None", ImageTag: "None", InUsed: false},
+ }
+ picked := PickImagesForCleanup(images, CleanupFilterUnused)
+ if len(picked) != 2 {
+ t.Fatalf("expected 2 unused images, got %d", len(picked))
+ }
+ for _, img := range picked {
+ if img.InUsed {
+ t.Fatalf("used image should not be picked: %+v", img)
+ }
+ }
+}
+
+func TestPickImagesForCleanupUnknownFilter(t *testing.T) {
+ images := []MyType.Image{
+ {ImageName: "nginx", ImageTag: "latest", InUsed: false},
+ }
+ if picked := PickImagesForCleanup(images, "bogus"); len(picked) != 0 {
+ t.Fatalf("unknown filter should pick nothing, got %d", len(picked))
+ }
+}
diff --git a/internal/utiles/network.go b/internal/utiles/network.go
new file mode 100644
index 00000000..b346246c
--- /dev/null
+++ b/internal/utiles/network.go
@@ -0,0 +1,32 @@
+package utiles
+
+import "github.com/docker/docker/api/types/network"
+
+// sanitizeEndpointSettings 只保留创建容器需要的网络配置,清掉运行期产生的
+// NetworkID/EndpointID/IP/MAC 等字段。否则把旧容器的 inspect 结果直接传给
+// ContainerCreate,可能继承失效的端点信息或造成 MAC/IP 冲突。
+func sanitizeEndpointSettings(networks map[string]*network.EndpointSettings) map[string]*network.EndpointSettings {
+ if len(networks) == 0 {
+ return nil
+ }
+ result := make(map[string]*network.EndpointSettings, len(networks))
+ for name, endpoint := range networks {
+ if endpoint == nil {
+ result[name] = nil
+ continue
+ }
+ cleaned := endpoint.Copy()
+ cleaned.NetworkID = ""
+ cleaned.EndpointID = ""
+ cleaned.Gateway = ""
+ cleaned.IPAddress = ""
+ cleaned.IPPrefixLen = 0
+ cleaned.IPv6Gateway = ""
+ cleaned.GlobalIPv6Address = ""
+ cleaned.GlobalIPv6PrefixLen = 0
+ cleaned.MacAddress = ""
+ cleaned.DNSNames = nil
+ result[name] = cleaned
+ }
+ return result
+}
diff --git a/internal/utiles/network_test.go b/internal/utiles/network_test.go
new file mode 100644
index 00000000..d6861414
--- /dev/null
+++ b/internal/utiles/network_test.go
@@ -0,0 +1,82 @@
+package utiles
+
+import (
+ "testing"
+
+ "github.com/docker/docker/api/types/network"
+)
+
+func TestSanitizeEndpointSettings(t *testing.T) {
+ original := map[string]*network.EndpointSettings{
+ "demo": {
+ Aliases: []string{"demo-alias"},
+ MacAddress: "02:42:ac:11:00:02",
+ NetworkID: "net-id",
+ EndpointID: "endpoint-id",
+ Gateway: "172.18.0.1",
+ IPAddress: "172.18.0.5",
+ IPPrefixLen: 16,
+ IPv6Gateway: "fe80::1",
+ GlobalIPv6Address: "2001:db8::5",
+ GlobalIPv6PrefixLen: 64,
+ DNSNames: []string{"demo"},
+ IPAMConfig: &network.EndpointIPAMConfig{
+ IPv4Address: "172.18.0.9",
+ IPv6Address: "2001:db8::9",
+ },
+ },
+ "nil": nil,
+ }
+
+ got := sanitizeEndpointSettings(original)
+ if got == nil {
+ t.Fatal("sanitizeEndpointSettings() = nil")
+ }
+ demo := got["demo"]
+ if demo == nil {
+ t.Fatal("demo endpoint missing")
+ }
+ if demo.IPAddress != "" || demo.NetworkID != "" || demo.EndpointID != "" ||
+ demo.Gateway != "" || demo.IPPrefixLen != 0 || demo.IPv6Gateway != "" ||
+ demo.GlobalIPv6Address != "" || demo.GlobalIPv6PrefixLen != 0 ||
+ demo.MacAddress != "" || demo.DNSNames != nil {
+ t.Fatalf("operational fields were not cleared: %#v", demo)
+ }
+ if demo.IPAMConfig == nil || demo.IPAMConfig.IPv4Address != "172.18.0.9" || demo.IPAMConfig.IPv6Address != "2001:db8::9" {
+ t.Fatalf("IPAMConfig was not preserved: %#v", demo.IPAMConfig)
+ }
+ if len(demo.Aliases) != 1 || demo.Aliases[0] != "demo-alias" {
+ t.Fatalf("Aliases were not preserved: %#v", demo.Aliases)
+ }
+ if got["nil"] != nil {
+ t.Fatalf("nil endpoint should stay nil")
+ }
+ if original["demo"].IPAddress != "172.18.0.5" {
+ t.Fatal("sanitizeEndpointSettings mutated the original map")
+ }
+}
+
+func TestRestoreNetworkingConfig(t *testing.T) {
+ if got := restoreNetworkingConfig(nil); got != nil {
+ t.Fatalf("restoreNetworkingConfig(nil) = %#v, want nil", got)
+ }
+ empty := &network.NetworkingConfig{}
+ if got := restoreNetworkingConfig(empty); got != empty {
+ t.Fatalf("restoreNetworkingConfig(empty) should return original pointer")
+ }
+ config := &network.NetworkingConfig{
+ EndpointsConfig: map[string]*network.EndpointSettings{
+ "demo": {IPAddress: "172.18.0.5", Aliases: []string{"demo"}},
+ },
+ }
+ got := restoreNetworkingConfig(config)
+ if got.EndpointsConfig["demo"].IPAddress != "" {
+ t.Fatalf("IPAddress was not cleared: %#v", got.EndpointsConfig["demo"])
+ }
+ if len(got.EndpointsConfig["demo"].Aliases) != 1 {
+ t.Fatalf("Aliases were not preserved: %#v", got.EndpointsConfig["demo"])
+ }
+ if config.EndpointsConfig["demo"].IPAddress != "172.18.0.5" {
+ t.Fatal("restoreNetworkingConfig mutated the input")
+ }
+}
diff --git a/internal/utiles/notify.go b/internal/utiles/notify.go
new file mode 100644
index 00000000..93277fe7
--- /dev/null
+++ b/internal/utiles/notify.go
@@ -0,0 +1,36 @@
+package utiles
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "os"
+ "time"
+)
+
+// NotifyTaskDone 在任务收尾时向 NOTIFY_WEBHOOK_URL POST 一条 JSON 通知。
+// 未配置 webhook 时静默跳过;发送失败不影响业务。
+func NotifyTaskDone(kind, name, taskID string, ok bool, message string) {
+ url := os.Getenv("NOTIFY_WEBHOOK_URL")
+ if url == "" {
+ return
+ }
+ payload := map[string]interface{}{
+ "kind": kind,
+ "name": name,
+ "taskID": taskID,
+ "ok": ok,
+ "message": message,
+ "time": time.Now().Format("2006-01-02 15:04:05"),
+ }
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return
+ }
+ client := &http.Client{Timeout: 5 * time.Second}
+ resp, err := client.Post(url, "application/json", bytes.NewReader(body))
+ if err != nil {
+ return
+ }
+ _ = resp.Body.Close()
+}
diff --git a/internal/utiles/removeimage.go b/internal/utiles/removeimage.go
index 60c6af4a..9d597115 100644
--- a/internal/utiles/removeimage.go
+++ b/internal/utiles/removeimage.go
@@ -7,7 +7,9 @@ import (
)
func RemoveImage(ctx *svc.ServiceContext, imageID string, force bool) error {
- _, err := ctx.DockerClient.ImageRemove(context.Background(), imageID, image.RemoveOptions{Force: force})
+ requestCtx, cancel := dockerTimeout(context.Background(), dockerMutationTimeout)
+ defer cancel()
+ _, err := ctx.DockerClient.ImageRemove(requestCtx, imageID, image.RemoveOptions{Force: force})
if err != nil {
return err
}
diff --git a/internal/utiles/renamecontainer.go b/internal/utiles/renamecontainer.go
index 849ab4a7..3183820b 100644
--- a/internal/utiles/renamecontainer.go
+++ b/internal/utiles/renamecontainer.go
@@ -6,7 +6,9 @@ import (
)
func RenameContainer(ctx *svc.ServiceContext, id string, newName string) error {
- err := ctx.DockerClient.ContainerRename(context.TODO(), id, newName)
+ requestCtx, cancel := dockerTimeout(context.Background(), dockerMutationTimeout)
+ defer cancel()
+ err := ctx.DockerClient.ContainerRename(requestCtx, id, newName)
if err != nil {
return err
}
diff --git a/internal/utiles/restartcontainer.go b/internal/utiles/restartcontainer.go
index 639401f2..e874dee3 100644
--- a/internal/utiles/restartcontainer.go
+++ b/internal/utiles/restartcontainer.go
@@ -8,12 +8,14 @@ import (
func RestartContainer(ctx *svc.ServiceContext, id string) error {
timeout := 10
- signal := "SIGINT"
+ signal := "SIGTERM"
stopOptions := container.StopOptions{
Signal: signal,
Timeout: &timeout,
}
- err := ctx.DockerClient.ContainerRestart(context.Background(), id, stopOptions)
+ requestCtx, cancel := dockerTimeout(context.Background(), dockerMutationTimeout)
+ defer cancel()
+ err := ctx.DockerClient.ContainerRestart(requestCtx, id, stopOptions)
if err != nil {
return err
}
diff --git a/internal/utiles/restorecontainer.go b/internal/utiles/restorecontainer.go
index f955239a..31b85f5f 100644
--- a/internal/utiles/restorecontainer.go
+++ b/internal/utiles/restorecontainer.go
@@ -3,94 +3,361 @@ package utiles
import (
"context"
"encoding/json"
- dockerBackend "github.com/docker/docker/api/types/backend"
- "github.com/docker/docker/api/types/image"
- "github.com/onlyLTY/dockerCopilot/internal/svc"
- "github.com/zeromicro/go-zero/core/logx"
+ "fmt"
"os"
"strconv"
"strings"
+ "sync"
+ "time"
+
+ dockerTypes "github.com/docker/docker/api/types"
+ "github.com/docker/docker/api/types/container"
+ "github.com/docker/docker/api/types/image"
+ "github.com/docker/docker/api/types/network"
+ dockerclient "github.com/docker/docker/client"
+ "github.com/onlyLTY/dockerCopilot/internal/svc"
+ "github.com/zeromicro/go-zero/core/logx"
)
-func RestoreContainer(ctx *svc.ServiceContext, filename string, taskID string) error {
- var backupList []string
- oldProgress := svc.TaskProgress{
+// 恢复会按容器名做冲突判断,两个恢复任务并发时可能都认为“不存在”
+// 然后同时创建同名容器;这里串行化整个恢复流程。
+var restoreMu sync.Mutex
+
+func restoreShouldStart(entry ContainerBackupEntry, startOverride *bool) bool {
+ if startOverride != nil {
+ return *startOverride
+ }
+ if entry.StateKnown {
+ return entry.WasRunning
+ }
+ // 旧备份没有状态字段:保持之前“恢复后启动”的行为。
+ return true
+}
+
+// restoreOldContainerName 为被替换的旧容器生成临时名。
+// 恢复流程先把旧容器改名而不是直接删除,新容器创建/启动失败时才能完整回滚。
+func restoreOldContainerName(name string, at time.Time) string {
+ return name + "-dc-old-" + at.Format("20060102-150405.000000000")
+}
+
+// rollbackRestoredContainer 在 force 恢复失败时清理新容器并把旧容器恢复原名/原状态。
+// oldTempName 为空表示旧容器尚未改名;newID 为空表示还没有创建新容器。
+func rollbackRestoredContainer(serviceContext *svc.ServiceContext, oldID, oldName, oldTempName, newID string, wasRunning, wasPaused bool) string {
+ var problems []string
+ bg, cancel := dockerTimeout(context.Background(), dockerMutationTimeout)
+ defer cancel()
+
+ // 新容器可能还占着原名,必须先移除才能把旧容器改回去。
+ if newID != "" {
+ if err := serviceContext.DockerClient.ContainerRemove(bg, newID, container.RemoveOptions{Force: true}); err != nil {
+ problems = append(problems, "新容器清理失败: "+err.Error())
+ }
+ }
+ if oldTempName != "" {
+ if err := serviceContext.DockerClient.ContainerRename(bg, oldID, oldName); err != nil {
+ problems = append(problems, "旧容器改回原名失败: "+err.Error())
+ }
+ }
+ if wasRunning {
+ if wasPaused {
+ _ = serviceContext.DockerClient.ContainerUnpause(bg, oldID)
+ }
+ current, inspectErr := serviceContext.DockerClient.ContainerInspect(bg, oldID)
+ if inspectErr != nil || current.State == nil || !current.State.Running {
+ if startErr := serviceContext.DockerClient.ContainerStart(bg, oldID, container.StartOptions{}); startErr != nil {
+ problems = append(problems, "旧容器重新启动失败: "+startErr.Error())
+ }
+ }
+ if wasPaused {
+ if pauseErr := serviceContext.DockerClient.ContainerPause(bg, oldID); pauseErr != nil {
+ problems = append(problems, "旧容器恢复暂停状态失败: "+pauseErr.Error())
+ }
+ }
+ }
+ if len(problems) == 0 {
+ return ""
+ }
+ return ";回滚异常: " + strings.Join(problems, ";")
+}
+
+// inspectRestoreTarget 返回同名容器;不存在时返回 nil。
+func restoreNetworkingConfig(config *network.NetworkingConfig) *network.NetworkingConfig {
+ if config == nil || len(config.EndpointsConfig) == 0 {
+ return config
+ }
+ return &network.NetworkingConfig{
+ EndpointsConfig: sanitizeEndpointSettings(config.EndpointsConfig),
+ }
+}
+
+func inspectRestoreTarget(ctx context.Context, serviceContext *svc.ServiceContext, name string) (*dockerTypes.ContainerJSON, error) {
+ existing, err := serviceContext.DockerClient.ContainerInspect(ctx, name)
+ if err != nil {
+ if dockerclient.IsErrNotFound(err) {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &existing, nil
+}
+
+// RestoreContainer 从备份 JSON 恢复容器。
+// 默认按容器在备份时的运行状态恢复;startOverride 可强制启动/不启动。
+// 同名容器已存在时默认跳过(镜像不一致则报错),force=true 时删除后重建。
+func RestoreContainer(taskCtx context.Context, serviceContext *svc.ServiceContext, filename string, taskID string, startOverride *bool, force bool) error {
+ progress := svc.TaskProgress{
TaskID: taskID,
Percentage: 0,
- Message: "",
- Name: "",
+ Message: "准备恢复容器",
+ Name: "恢复容器",
DetailMsg: "",
IsDone: false,
}
- oldProgress.Name = "恢复容器"
+ if serviceContext.DockerClient == nil {
+ progress.Message = "恢复失败"
+ progress.DetailMsg = "Docker 客户端未初始化,请检查 DOCKER_HOST 配置"
+ progress.IsDone = true
+ serviceContext.UpdateProgress(taskID, progress)
+ return fmt.Errorf("Docker 客户端未初始化,请检查 DOCKER_HOST 配置")
+ }
+ restoreMu.Lock()
+ defer restoreMu.Unlock()
fullPath, err := ResolveBackupPath(filename, ".json")
if err != nil {
- logx.Errorf("Failed to resolve backup path: %v", err)
- oldProgress.Message = "非法文件名"
- oldProgress.DetailMsg = err.Error()
- oldProgress.IsDone = true
- ctx.UpdateProgress(taskID, oldProgress)
+ logx.Errorf("解析备份文件路径失败: %v", err)
+ progress.Message = "非法文件名"
+ progress.DetailMsg = err.Error()
+ progress.IsDone = true
+ serviceContext.UpdateProgress(taskID, progress)
return err
}
content, err := os.ReadFile(fullPath)
if err != nil {
- logx.Errorf("Failed to read file: %v", err)
- oldProgress.Percentage = 0
- oldProgress.Message = "读取文件失败或者未找到文件。请确认文件名仅由大小写字母、数字和短横线组成"
- oldProgress.DetailMsg = err.Error()
- oldProgress.IsDone = true
- ctx.UpdateProgress(taskID, oldProgress)
+ logx.Errorf("读取备份文件失败: %v", err)
+ progress.Percentage = 0
+ progress.Message = "读取文件失败或者未找到文件。请确认文件名仅由大小写字母、数字和短横线组成"
+ progress.DetailMsg = err.Error()
+ progress.IsDone = true
+ serviceContext.UpdateProgress(taskID, progress)
return err
}
- var configList []dockerBackend.ContainerCreateConfig
- err = json.Unmarshal(content, &configList)
- if err != nil {
- logx.Errorf("Failed to parse json: %v", err)
- oldProgress.Percentage = 0
- oldProgress.Message = "解析文件失败"
- oldProgress.DetailMsg = err.Error()
- oldProgress.IsDone = true
- ctx.UpdateProgress(taskID, oldProgress)
+ // 新格式在 ContainerCreateConfig 之外带 stateKnown/wasRunning/wasPaused;
+ // 旧格式(只有 Name/Config/HostConfig/...)同样能反序列化,状态按未知处理。
+ var configList []ContainerBackupEntry
+ if err = json.Unmarshal(content, &configList); err != nil {
+ logx.Errorf("解析备份 JSON 失败: %v", err)
+ progress.Percentage = 0
+ progress.Message = "解析文件失败"
+ progress.DetailMsg = err.Error()
+ progress.IsDone = true
+ serviceContext.UpdateProgress(taskID, progress)
return err
}
- for i, containerInfo := range configList {
- info := "正在恢复第" + strconv.Itoa(i+1) + "个容器"
- oldProgress.Percentage = int(float64(i) / float64(len(configList)) * 100)
- oldProgress.Message = info
- oldProgress.DetailMsg = info
- ctx.UpdateProgress(taskID, oldProgress)
- ctx.DockerClient.NegotiateAPIVersion(context.TODO())
- if err != nil {
- backupList = append(backupList, "出现错误"+err.Error())
- logx.Errorf("Failed to inspect container: %v", err)
+ if len(configList) == 0 {
+ progress.Percentage = 100
+ progress.Message = "备份文件中没有容器"
+ progress.DetailMsg = "备份文件中没有容器"
+ progress.IsDone = true
+ serviceContext.UpdateProgress(taskID, progress)
+ return fmt.Errorf("备份文件中没有容器")
+ }
+
+ negotiateCtx, negotiateCancel := dockerTimeout(taskCtx, dockerNegotiateTimeout)
+ serviceContext.DockerClient.NegotiateAPIVersion(negotiateCtx)
+ negotiateCancel()
+ var failures []string
+ var completed []string
+ for i, entry := range configList {
+ if err = taskCtx.Err(); err != nil {
+ progress.Percentage = 100
+ progress.Message = "任务已取消"
+ progress.DetailMsg = "已取消恢复,已完成部分不会回滚"
+ progress.IsDone = true
+ serviceContext.UpdateProgress(taskID, progress)
return err
}
- reader, err := ctx.DockerClient.ImagePull(context.TODO(), containerInfo.Config.Image, image.PullOptions{})
- if err != nil {
- backupList = append(backupList, containerInfo.Config.Image+"拉取镜像出现错误"+err.Error())
- logx.Errorf("Failed to pull image: %v", err)
+ if entry.Config == nil || strings.TrimSpace(entry.Config.Image) == "" {
+ failures = append(failures, fmt.Sprintf("第 %d 个容器缺少镜像配置", i+1))
continue
}
- err = decodePullResp(reader, ctx, taskID)
+
+ info := "正在恢复第" + strconv.Itoa(i+1) + "个容器"
+ progress.Percentage = int(float64(i) / float64(len(configList)) * 90)
+ progress.Message = info
+ progress.DetailMsg = info
+ serviceContext.UpdateProgress(taskID, progress)
+
+ startDesired := restoreShouldStart(entry, startOverride)
+ keepPaused := startOverride == nil && entry.StateKnown && entry.WasPaused
+ hasName := strings.TrimSpace(entry.Name) != ""
+ var replaceExisting *dockerTypes.ContainerJSON
+
+ // 幂等恢复:同名容器已存在时,默认不重复创建。
+ if hasName {
+ existing, inspectErr := inspectRestoreTarget(taskCtx, serviceContext, entry.Name)
+ if inspectErr != nil {
+ failures = append(failures, entry.Name+" 检查同名容器失败: "+inspectErr.Error())
+ continue
+ }
+ if existing != nil {
+ if force {
+ // 拉镜像成功后再停止/改名旧容器;创建或启动失败时还能完整回滚。
+ replaceExisting = existing
+ } else {
+ if existing.Config != nil && existing.Config.Image != entry.Config.Image {
+ failures = append(failures, entry.Name+" 已存在且镜像不一致(现有 "+existing.Config.Image+",备份 "+entry.Config.Image+"),如需覆盖请使用 force=true")
+ continue
+ }
+ if startDesired {
+ if existing.State != nil && existing.State.Paused {
+ if !keepPaused {
+ if unpauseErr := serviceContext.DockerClient.ContainerUnpause(taskCtx, existing.ID); unpauseErr != nil {
+ failures = append(failures, entry.Name+" 已存在但取消暂停失败: "+unpauseErr.Error())
+ continue
+ }
+ }
+ } else if existing.State == nil || !existing.State.Running {
+ if startErr := serviceContext.DockerClient.ContainerStart(taskCtx, existing.ID, container.StartOptions{}); startErr != nil {
+ failures = append(failures, entry.Name+" 已存在但启动失败: "+startErr.Error())
+ continue
+ }
+ }
+ if keepPaused && (existing.State == nil || !existing.State.Paused) {
+ if pauseErr := serviceContext.DockerClient.ContainerPause(taskCtx, existing.ID); pauseErr != nil {
+ failures = append(failures, entry.Name+" 已存在但暂停失败: "+pauseErr.Error())
+ continue
+ }
+ }
+ }
+ completed = append(completed, entry.Name+" 已存在,跳过创建")
+ progress.Percentage = int(float64(i+1) / float64(len(configList)) * 90)
+ progress.DetailMsg = entry.Name + " 已存在,跳过创建"
+ serviceContext.UpdateProgress(taskID, progress)
+ continue
+ }
+ }
+ }
+
+ err = func() error {
+ reader, pullErr := serviceContext.DockerClient.ImagePull(taskCtx, entry.Config.Image, image.PullOptions{})
+ if pullErr != nil {
+ return fmt.Errorf("拉取镜像失败: %w", pullErr)
+ }
+ defer func() {
+ if closeErr := reader.Close(); closeErr != nil {
+ logx.Errorf("关闭镜像拉取流失败: %v", closeErr)
+ }
+ }()
+ if pullErr = decodePullResp(reader, serviceContext, taskCtx, taskID); pullErr != nil {
+ return fmt.Errorf("拉取镜像失败: %w", pullErr)
+ }
+ return nil
+ }()
if err != nil {
- backupList = append(backupList, containerInfo.Config.Image+"拉取镜像出现错误"+err.Error())
- logx.Errorf("Failed to pull image: %v", err)
+ logx.Errorf("恢复容器 %s 拉取镜像失败: %v", entry.Name, err)
+ failures = append(failures, entry.Name+" 拉取镜像失败: "+err.Error())
continue
}
- _, err = ctx.DockerClient.ContainerCreate(context.TODO(), containerInfo.Config, containerInfo.HostConfig, containerInfo.NetworkingConfig, nil, containerInfo.Name)
- if err != nil {
- logx.Errorf("Failed to create container: %v", err)
- info = "正在恢复第" + strconv.Itoa(i+1) + "个容器"
- backupList = append(backupList, containerInfo.Name+"恢复失败"+err.Error())
+
+ oldID := ""
+ oldWasRunning := false
+ oldWasPaused := false
+ oldTempName := ""
+ if replaceExisting != nil {
+ if replaceExisting.State == nil {
+ failures = append(failures, entry.Name+" 旧容器状态未知,无法安全执行 force 覆盖")
+ continue
+ }
+ oldID = replaceExisting.ID
+ oldWasRunning = replaceExisting.State.Running
+ oldWasPaused = replaceExisting.State.Paused
+ stopTimeout := 10
+ if oldWasRunning {
+ if oldWasPaused {
+ if unpauseErr := serviceContext.DockerClient.ContainerUnpause(taskCtx, replaceExisting.ID); unpauseErr != nil {
+ failures = append(failures, entry.Name+" 旧容器取消暂停失败: "+unpauseErr.Error())
+ continue
+ }
+ }
+ if stopErr := serviceContext.DockerClient.ContainerStop(taskCtx, replaceExisting.ID, container.StopOptions{Signal: "SIGTERM", Timeout: &stopTimeout}); stopErr != nil {
+ rollbackMsg := rollbackRestoredContainer(serviceContext, replaceExisting.ID, entry.Name, "", "", oldWasRunning, oldWasPaused)
+ failures = append(failures, entry.Name+" 旧容器停止失败: "+stopErr.Error()+rollbackMsg)
+ continue
+ }
+ }
+ oldTempName = restoreOldContainerName(entry.Name, time.Now())
+ if renameErr := serviceContext.DockerClient.ContainerRename(taskCtx, replaceExisting.ID, oldTempName); renameErr != nil {
+ rollbackMsg := rollbackRestoredContainer(serviceContext, replaceExisting.ID, entry.Name, "", "", oldWasRunning, oldWasPaused)
+ failures = append(failures, entry.Name+" 旧容器改名失败: "+renameErr.Error()+rollbackMsg)
+ continue
+ }
+ }
+
+ created, createErr := serviceContext.DockerClient.ContainerCreate(
+ taskCtx,
+ entry.Config,
+ entry.HostConfig,
+ restoreNetworkingConfig(entry.NetworkingConfig),
+ nil,
+ entry.Name,
+ )
+ if createErr != nil {
+ rollbackMsg := rollbackRestoredContainer(serviceContext, oldID, entry.Name, oldTempName, "", oldWasRunning, oldWasPaused)
+ logx.Errorf("恢复容器 %s 创建失败: %v", entry.Name, createErr)
+ failures = append(failures, entry.Name+" 创建失败: "+createErr.Error()+rollbackMsg)
continue
- } else {
- backupList = append(backupList, containerInfo.Name+"恢复成功")
}
+ if startDesired {
+ if startErr := serviceContext.DockerClient.ContainerStart(taskCtx, created.ID, container.StartOptions{}); startErr != nil {
+ rollbackMsg := rollbackRestoredContainer(serviceContext, oldID, entry.Name, oldTempName, created.ID, oldWasRunning, oldWasPaused)
+ failures = append(failures, entry.Name+" 启动失败: "+startErr.Error()+rollbackMsg)
+ continue
+ }
+ if keepPaused {
+ if pauseErr := serviceContext.DockerClient.ContainerPause(taskCtx, created.ID); pauseErr != nil {
+ rollbackMsg := rollbackRestoredContainer(serviceContext, oldID, entry.Name, oldTempName, created.ID, oldWasRunning, oldWasPaused)
+ failures = append(failures, entry.Name+" 启动成功但暂停失败: "+pauseErr.Error()+rollbackMsg)
+ continue
+ }
+ }
+ }
+
+ // 新容器已经创建并达到目标状态,才真正删除被替换的旧容器。
+ result := entry.Name + " 恢复成功"
+ if replaceExisting != nil && oldTempName != "" {
+ cleanupCtx, cleanupCancel := dockerTimeout(context.Background(), dockerMutationTimeout)
+ if removeErr := serviceContext.DockerClient.ContainerRemove(cleanupCtx, replaceExisting.ID, container.RemoveOptions{Force: true}); removeErr != nil {
+ result = entry.Name + " 恢复成功,但旧容器清理失败,请手动删除 " + oldTempName + ": " + removeErr.Error()
+ }
+ cleanupCancel()
+ }
+ completed = append(completed, result)
+ progress.Percentage = int(float64(i+1) / float64(len(configList)) * 90)
+ progress.DetailMsg = result
+ serviceContext.UpdateProgress(taskID, progress)
+ }
+
+ if err = taskCtx.Err(); err != nil {
+ progress.Percentage = 100
+ progress.Message = "任务已取消"
+ progress.DetailMsg = "已取消恢复,已完成部分不会回滚"
+ progress.IsDone = true
+ serviceContext.UpdateProgress(taskID, progress)
+ return err
+ }
+
+ progress.Percentage = 100
+ details := append([]string{}, completed...)
+ details = append(details, failures...)
+ progress.DetailMsg = strings.Join(details, ";\n")
+ if len(failures) > 0 {
+ progress.Message = fmt.Sprintf("恢复完成,%d 个容器失败", len(failures))
+ progress.IsDone = true
+ serviceContext.UpdateProgress(taskID, progress)
+ return fmt.Errorf("%d 个容器恢复失败: %s", len(failures), strings.Join(failures, ";"))
}
- oldProgress.Percentage = 100
- oldProgress.DetailMsg = strings.Join(backupList, ",\n")
- oldProgress.Message = "恢复完成"
- oldProgress.IsDone = true
- ctx.UpdateProgress(taskID, oldProgress)
+ progress.Message = "恢复完成"
+ progress.IsDone = true
+ serviceContext.UpdateProgress(taskID, progress)
return nil
}
diff --git a/internal/utiles/restorecontainer_test.go b/internal/utiles/restorecontainer_test.go
new file mode 100644
index 00000000..2755c54b
--- /dev/null
+++ b/internal/utiles/restorecontainer_test.go
@@ -0,0 +1,48 @@
+package utiles
+
+import (
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestRestoreShouldStart(t *testing.T) {
+ trueValue := true
+ falseValue := false
+ tests := []struct {
+ name string
+ entry ContainerBackupEntry
+ override *bool
+ want bool
+ }{
+ {name: "old backup defaults to start", entry: ContainerBackupEntry{}, want: true},
+ {name: "known stopped stays stopped", entry: ContainerBackupEntry{StateKnown: true, WasRunning: false}, want: false},
+ {name: "known running starts", entry: ContainerBackupEntry{StateKnown: true, WasRunning: true}, want: true},
+ {name: "override start", entry: ContainerBackupEntry{StateKnown: true, WasRunning: false}, override: &trueValue, want: true},
+ {name: "override stop", entry: ContainerBackupEntry{StateKnown: true, WasRunning: true}, override: &falseValue, want: false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := restoreShouldStart(tt.entry, tt.override); got != tt.want {
+ t.Fatalf("restoreShouldStart() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestRestoreOldContainerName(t *testing.T) {
+ at := time.Date(2026, 10, 30, 12, 34, 56, 123456789, time.UTC)
+ got := restoreOldContainerName("demo", at)
+ if !strings.HasPrefix(got, "demo-dc-old-") {
+ t.Fatalf("restoreOldContainerName() = %q, want demo-dc-old- prefix", got)
+ }
+ if got == restoreOldContainerName("demo", at.Add(time.Nanosecond)) {
+ t.Fatalf("restoreOldContainerName() should be unique per timestamp")
+ }
+}
+
+func TestRollbackRestoredContainerNoop(t *testing.T) {
+ if msg := rollbackRestoredContainer(nil, "", "", "", "", false, false); msg != "" {
+ t.Fatalf("rollbackRestoredContainer() = %q, want empty", msg)
+ }
+}
diff --git a/internal/utiles/settings.go b/internal/utiles/settings.go
new file mode 100644
index 00000000..d9c757c1
--- /dev/null
+++ b/internal/utiles/settings.go
@@ -0,0 +1,68 @@
+package utiles
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "sync"
+)
+
+// Settings 运行时可修改的设置(持久化到 /data/config/settings.json)。
+type Settings struct {
+ BackupDir string `json:"backupDir"`
+}
+
+var settingsMu sync.Mutex
+
+func settingsPath() string {
+ if p := os.Getenv("SETTINGS_FILE"); p != "" {
+ return p
+ }
+ return "/data/config/settings.json"
+}
+
+// LoadSettings 读取设置文件;文件不存在或损坏时返回空设置。
+func LoadSettings() Settings {
+ settingsMu.Lock()
+ defer settingsMu.Unlock()
+ var s Settings
+ data, err := os.ReadFile(settingsPath())
+ if err != nil {
+ return s
+ }
+ if err = json.Unmarshal(data, &s); err != nil {
+ return Settings{}
+ }
+ return s
+}
+
+// SaveSettings 写入设置文件。
+func SaveSettings(s Settings) error {
+ settingsMu.Lock()
+ defer settingsMu.Unlock()
+ path := settingsPath()
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ return err
+ }
+ data, err := json.MarshalIndent(s, "", " ")
+ if err != nil {
+ return err
+ }
+ tmp := path + ".tmp"
+ if err = os.WriteFile(tmp, data, 0644); err != nil {
+ return err
+ }
+ return os.Rename(tmp, path)
+}
+
+// EffectiveBackupDir 返回实际生效的备份目录:
+// 环境变量 BACKUP_DIR > Settings 中的 backupDir > 默认 /data/backups。
+func EffectiveBackupDir() string {
+ if dir := os.Getenv("BACKUP_DIR"); dir != "" {
+ return dir
+ }
+ if s := LoadSettings(); s.BackupDir != "" {
+ return s.BackupDir
+ }
+ return "/data/backups"
+}
diff --git a/internal/utiles/startcontainer.go b/internal/utiles/startcontainer.go
index fce4433e..d06b48a7 100644
--- a/internal/utiles/startcontainer.go
+++ b/internal/utiles/startcontainer.go
@@ -7,8 +7,10 @@ import (
)
func StartContainer(ctx *svc.ServiceContext, id string) error {
+ requestCtx, cancel := dockerTimeout(context.Background(), dockerMutationTimeout)
+ defer cancel()
startOptions := container.StartOptions{}
- err := ctx.DockerClient.ContainerStart(context.Background(), id, startOptions)
+ err := ctx.DockerClient.ContainerStart(requestCtx, id, startOptions)
if err != nil {
return err
}
diff --git a/internal/utiles/stopcontainer.go b/internal/utiles/stopcontainer.go
index 86ca600b..dd9c885d 100644
--- a/internal/utiles/stopcontainer.go
+++ b/internal/utiles/stopcontainer.go
@@ -8,12 +8,14 @@ import (
func StopContainer(ctx *svc.ServiceContext, id string) error {
timeout := 10
- signal := "SIGINT"
+ signal := "SIGTERM"
stopOptions := container.StopOptions{
Signal: signal,
Timeout: &timeout,
}
- err := ctx.DockerClient.ContainerStop(context.Background(), id, stopOptions)
+ requestCtx, cancel := dockerTimeout(context.Background(), dockerMutationTimeout)
+ defer cancel()
+ err := ctx.DockerClient.ContainerStop(requestCtx, id, stopOptions)
if err != nil {
return err
}
diff --git a/internal/utiles/updatecontainer.go b/internal/utiles/updatecontainer.go
index 6ddba209..a9f07037 100644
--- a/internal/utiles/updatecontainer.go
+++ b/internal/utiles/updatecontainer.go
@@ -4,181 +4,357 @@ import (
"context"
"encoding/json"
"fmt"
+ "io"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
dockerMsgType "github.com/docker/docker/pkg/jsonmessage"
"github.com/onlyLTY/dockerCopilot/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
- "io"
- "time"
)
-func UpdateContainer(serviceContext *svc.ServiceContext, id string, name string, imageNameAndTag string, delOldContainer bool, taskID string) error {
- ctx := context.Background()
- serviceContext.UpdateProgress(taskID, svc.TaskProgress{
- TaskID: taskID,
- Percentage: 0,
- Name: name,
- Message: "正在连接Docker",
- DetailMsg: "正在连接Docker",
- IsDone: false,
- })
- var oldTaskProgress, result = serviceContext.GetProgress(taskID)
- if !result {
- oldTaskProgress = svc.TaskProgress{
- Percentage: 0,
- Name: "",
- Message: "",
- DetailMsg: "",
- IsDone: false,
+// 同一个容器只允许一个更新流程,避免手动更新与自动更新并发改名/创建。
+var containerUpdateLocks sync.Map
+
+func lockContainerUpdate(id string) func() {
+ value, _ := containerUpdateLocks.LoadOrStore(id, &sync.Mutex{})
+ mutex := value.(*sync.Mutex)
+ mutex.Lock()
+ return mutex.Unlock
+}
+
+// UpdateContainer 执行一次容器更新:拉取新镜像 → 停止并改名旧容器 → 克隆配置创建新容器
+// → 启动新容器 → 删除旧容器 → 清理旧镜像。任一步失败都会尝试把旧容器恢复到原名并重新启动。
+// ctx 支持取消:任务被取消时在步骤边界退出。
+func UpdateContainer(serviceContext *svc.ServiceContext, ctx context.Context, id string, name string, imageNameAndTag string, delOldContainer bool, taskID string) error {
+ unlock := lockContainerUpdate(id)
+ defer unlock()
+
+ progress := func(percentage int, message, detailMsg string, isDone bool) {
+ serviceContext.UpdateProgress(taskID, svc.TaskProgress{
+ TaskID: taskID,
+ Percentage: percentage,
+ Name: name,
+ Message: message,
+ DetailMsg: detailMsg,
+ IsDone: isDone,
+ })
+ }
+ cancelled := func() bool {
+ if ctx.Err() != nil {
+ progress(100, "任务已取消", "任务已取消", true)
+ return true
}
+ return false
}
+
+ progress(0, "正在连接Docker", "正在连接Docker", false)
+ if cancelled() {
+ return ctx.Err()
+ }
+
timeout := 10
- signal := "SIGINT"
-
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- oldTaskProgress.Message = "正在拉取新镜像"
- oldTaskProgress.Percentage = 10
- oldTaskProgress.DetailMsg = "正在拉取新镜像"
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- serviceContext.DockerClient.NegotiateAPIVersion(ctx)
- reader, err := serviceContext.DockerClient.ImagePull(ctx, imageNameAndTag, image.PullOptions{})
+ signal := "SIGTERM"
+
+ negotiateCtx, negotiateCancel := dockerTimeout(ctx, dockerNegotiateTimeout)
+ serviceContext.DockerClient.NegotiateAPIVersion(negotiateCtx)
+ negotiateCancel()
+
+ // 先记录旧容器信息:旧镜像 ID 用于更新后的清理,旧容器名用于失败回滚
+ inspectedContainer, err := serviceContext.DockerClient.ContainerInspect(ctx, id)
if err != nil {
- oldTaskProgress.Message = "拉取镜像失败"
- oldTaskProgress.DetailMsg = err.Error()
- oldTaskProgress.IsDone = true
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- logx.Errorf("Failed to pull image: %s", err)
+ progress(0, "获取容器信息失败", err.Error(), true)
+ logx.Errorf("获取容器信息失败: %v", err)
+ return err
+ }
+ if inspectedContainer.Config == nil {
+ err = fmt.Errorf("旧容器配置为空,无法更新")
+ progress(0, "获取容器配置失败", err.Error(), true)
+ return err
+ }
+ if inspectedContainer.HostConfig == nil {
+ err = fmt.Errorf("旧容器 HostConfig 为空,无法克隆配置")
+ progress(0, "获取容器配置失败", err.Error(), true)
return err
}
- err = decodePullResp(reader, serviceContext, taskID)
+ if strings.TrimSpace(imageNameAndTag) == "" {
+ imageNameAndTag = inspectedContainer.Config.Image
+ }
+ if strings.TrimSpace(imageNameAndTag) == "" {
+ err = fmt.Errorf("镜像名称为空")
+ progress(0, "更新失败", err.Error(), true)
+ return err
+ }
+ oldImageID := inspectedContainer.Image
+ oldContainerName := strings.TrimPrefix(inspectedContainer.Name, "/")
+ if oldContainerName == "" {
+ oldContainerName = name
+ }
+ wasRunning := inspectedContainer.State != nil && inspectedContainer.State.Running
+ wasPaused := inspectedContainer.State != nil && inspectedContainer.State.Paused
+ backupName := oldContainerName + "-" + time.Now().Format("2006-01-02-15-04-05.000000000")
+ renamed := false
+ newCreated := false
+ var warnings []string
+ if inspectedContainer.Config.Labels["com.docker.compose.project"] != "" {
+ warnings = append(warnings, "该容器由 Compose 管理,本次按容器方式重建;建议优先使用 docker compose pull/up 更新")
+ }
+
+ // rollback 只恢复本流程真正改过名的旧容器;原本停止的容器不会被误启动。
+ rollback := func(reason string) {
+ bg, bgCancel := dockerTimeout(context.Background(), dockerMutationTimeout)
+ defer bgCancel()
+ // 新容器还占着原名时先移除,否则旧容器改不回去。
+ if newCreated {
+ if removeErr := serviceContext.DockerClient.ContainerRemove(bg, oldContainerName, container.RemoveOptions{Force: true}); removeErr != nil {
+ logx.Errorf("回滚移除新容器失败: %v", removeErr)
+ }
+ newCreated = false
+ }
+ if renamed {
+ if renameErr := serviceContext.DockerClient.ContainerRename(bg, id, oldContainerName); renameErr != nil {
+ logx.Errorf("回滚重命名失败: %v", renameErr)
+ progress(100, "更新失败且回滚失败,请手动处理旧容器 "+backupName, reason, true)
+ return
+ }
+ renamed = false
+ }
+ if wasRunning {
+ // 原本是 paused 的容器,先取消暂停再确保运行,最后恢复暂停状态。
+ if wasPaused {
+ _ = serviceContext.DockerClient.ContainerUnpause(bg, id)
+ }
+ current, inspectErr := serviceContext.DockerClient.ContainerInspect(bg, id)
+ if inspectErr != nil || current.State == nil || !current.State.Running {
+ if startErr := serviceContext.DockerClient.ContainerStart(bg, id, container.StartOptions{}); startErr != nil {
+ logx.Errorf("回滚启动失败: %v", startErr)
+ progress(100, "更新失败,旧容器已改回原名但启动失败", reason+";启动错误: "+startErr.Error(), true)
+ return
+ }
+ }
+ if wasPaused {
+ if pauseErr := serviceContext.DockerClient.ContainerPause(bg, id); pauseErr != nil {
+ logx.Errorf("回滚恢复暂停状态失败: %v", pauseErr)
+ progress(100, "更新失败,旧容器已恢复但暂停状态恢复失败", reason+";暂停错误: "+pauseErr.Error(), true)
+ return
+ }
+ }
+ }
+ progress(100, "更新失败,已回滚旧容器", reason, true)
+ }
+
+ // 1. 拉取新镜像(流式进度 10% → 25%)
+ progress(10, "正在拉取新镜像", "正在拉取新镜像", false)
+ reader, err := serviceContext.DockerClient.ImagePull(ctx, imageNameAndTag, image.PullOptions{})
if err != nil {
- oldTaskProgress.Message = "拉取镜像失败"
- oldTaskProgress.DetailMsg = err.Error()
- oldTaskProgress.IsDone = true
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- logx.Errorf("Failed to pull image: %s", err)
+ if ctx.Err() != nil {
+ progress(100, "任务已取消", "任务已在拉取镜像阶段取消", true)
+ return ctx.Err()
+ }
+ progress(100, "拉取镜像失败", err.Error(), true)
+ logx.Errorf("拉取镜像失败: %s", err)
return err
}
- oldTaskProgress, result = serviceContext.GetProgress(taskID)
- if !result {
- oldTaskProgress = svc.TaskProgress{
- Percentage: 0,
- Name: "",
- Message: "",
- DetailMsg: "",
- IsDone: false,
+ defer func() {
+ if closeErr := reader.Close(); closeErr != nil {
+ logx.Errorf("关闭镜像拉取流失败: %v", closeErr)
}
+ }()
+ if err = decodePullResp(reader, serviceContext, ctx, taskID); err != nil {
+ if ctx.Err() != nil {
+ // 取消发生在拉取阶段时,错误本质是 context.Canceled,
+ // 如实报“任务已取消”而不是误导性的“拉取镜像失败”。
+ progress(100, "任务已取消", "任务已在拉取镜像阶段取消", true)
+ return ctx.Err()
+ }
+ progress(100, "拉取镜像失败", err.Error(), true)
+ logx.Errorf("拉取镜像失败: %s", err)
+ return err
+ }
+ if cancelled() {
+ return ctx.Err()
}
- oldTaskProgress.Message = "拉取镜像成功"
- oldTaskProgress.DetailMsg = "拉取镜像成功"
- oldTaskProgress.Percentage = 30
- oldTaskProgress.Message = "正在停止容器"
- oldTaskProgress.DetailMsg = "正在停止容器"
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
+ // 2. 停止旧容器(原本停止的容器跳过,避免更新后把它误启动)
+ progress(30, "正在停止容器", "正在停止容器", false)
stopOptions := container.StopOptions{
Signal: signal,
Timeout: &timeout,
}
- err = serviceContext.DockerClient.ContainerStop(context.Background(), id, stopOptions)
- if err != nil {
- oldTaskProgress.Message = "停止容器失败"
- oldTaskProgress.DetailMsg = err.Error()
- oldTaskProgress.IsDone = true
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- return err
+ if wasRunning {
+ // Docker 不允许直接 stop paused 容器,先取消暂停;回滚/新容器启动后会恢复。
+ if wasPaused {
+ if err = serviceContext.DockerClient.ContainerUnpause(ctx, id); err != nil {
+ rollback("取消旧容器暂停失败: " + err.Error())
+ return err
+ }
+ }
+ if err = serviceContext.DockerClient.ContainerStop(ctx, id, stopOptions); err != nil {
+ rollback("停止容器失败: " + err.Error())
+ return err
+ }
}
- oldTaskProgress.Message = "容器停止成功"
- oldTaskProgress.DetailMsg = "容器停止成功"
- oldTaskProgress.Percentage = 40
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- oldTaskProgress.Message = "正在重命名旧容器"
- oldTaskProgress.DetailMsg = "正在重命名旧容器"
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- currentDate := time.Now().Format("2006-01-02-15-04-05")
- err = serviceContext.DockerClient.ContainerRename(context.Background(), id, name+"-"+currentDate)
- if err != nil {
- oldTaskProgress.Message = "重命名旧容器失败"
- oldTaskProgress.DetailMsg = err.Error()
- oldTaskProgress.IsDone = true
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- return err
- }
- oldTaskProgress.Message = "重命名旧容器成功"
- oldTaskProgress.DetailMsg = "重命名旧容器成功"
- oldTaskProgress.Percentage = 60
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- oldTaskProgress.Message = "正在创建新容器"
- oldTaskProgress.DetailMsg = "正在创建新容器"
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- inspectedContainer, err := serviceContext.DockerClient.ContainerInspect(ctx, id)
- if err != nil {
- oldTaskProgress.Message = "获取容器信息失败"
- oldTaskProgress.DetailMsg = err.Error()
- oldTaskProgress.IsDone = true
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- logx.Error("获取容器信息失败" + err.Error())
+ // 3. 重命名旧容器(腾出原名)
+ progress(40, "正在重命名旧容器", "正在重命名旧容器", false)
+ if err = serviceContext.DockerClient.ContainerRename(ctx, id, backupName); err != nil {
+ // 还没改名成功,旧容器仍是原名,只恢复它原来的运行状态
+ rollback("重命名旧容器失败: " + err.Error())
return err
}
+ renamed = true
+
+ // 4. 克隆旧容器配置创建新容器(只替换镜像)
+ progress(60, "正在创建新容器", "正在创建新容器", false)
inspectedContainer.Config.Hostname = ""
inspectedContainer.Config.Image = imageNameAndTag
inspectedContainer.Image = imageNameAndTag
config := inspectedContainer.Config
hostConfig := inspectedContainer.HostConfig
+ var endpoints map[string]*network.EndpointSettings
+ if inspectedContainer.NetworkSettings != nil {
+ endpoints = sanitizeEndpointSettings(inspectedContainer.NetworkSettings.Networks)
+ }
networkingConfig := &network.NetworkingConfig{
- EndpointsConfig: inspectedContainer.NetworkSettings.Networks,
+ EndpointsConfig: endpoints,
}
- containerName := name
- _, err = serviceContext.DockerClient.ContainerCreate(ctx, config, hostConfig, networkingConfig, nil, containerName)
- if err != nil {
- oldTaskProgress.Message = "创建新容器失败"
- oldTaskProgress.DetailMsg = err.Error()
- oldTaskProgress.IsDone = true
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
+ if _, err = serviceContext.DockerClient.ContainerCreate(ctx, config, hostConfig, networkingConfig, nil, oldContainerName); err != nil {
+ rollback("创建新容器失败: " + err.Error())
return err
}
- oldTaskProgress.Message = "创建新容器成功"
- oldTaskProgress.DetailMsg = "创建新容器成功"
- oldTaskProgress.Percentage = 80
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- oldTaskProgress.Message = "正在启动新容器以及删除旧容器(如果不保留旧容器)"
- oldTaskProgress.DetailMsg = "正在启动新容器以及删除旧容器(如果不保留旧容器)"
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- err = serviceContext.DockerClient.ContainerStart(context.Background(), containerName, container.StartOptions{
- CheckpointID: "",
- CheckpointDir: "",
- })
- if err != nil {
- oldTaskProgress.Message = "启动新容器失败"
- oldTaskProgress.DetailMsg = err.Error()
- oldTaskProgress.IsDone = true
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- return err
+ newCreated = true
+
+ // 5. 启动新容器并等待健康检查;原 paused 容器启动后再暂停。
+ if wasRunning {
+ progress(80, "正在启动新容器", "正在启动新容器", false)
+ if err = serviceContext.DockerClient.ContainerStart(ctx, oldContainerName, container.StartOptions{}); err != nil {
+ // 新容器已创建但起不来:rollback 会移除它并恢复旧容器。
+ rollback("启动新容器失败: " + err.Error())
+ return err
+ }
+ if !wasPaused {
+ progress(85, "正在等待容器健康", "正在等待新容器通过健康检查", false)
+ if err = waitForContainerHealthy(ctx, serviceContext, oldContainerName, inspectedContainer.Config.Healthcheck); err != nil {
+ rollback("新容器健康检查失败: " + err.Error())
+ return err
+ }
+ }
+ if wasPaused {
+ if err = serviceContext.DockerClient.ContainerPause(ctx, oldContainerName); err != nil {
+ rollback("恢复新容器暂停状态失败: " + err.Error())
+ return err
+ }
+ }
}
+
+ // 6. 删除旧容器(默认保留开关可关闭)
if delOldContainer {
- err = serviceContext.DockerClient.ContainerRemove(context.Background(), id, container.RemoveOptions{})
- if err != nil {
- oldTaskProgress.Message = "删除旧容器失败"
- oldTaskProgress.DetailMsg = err.Error()
- oldTaskProgress.IsDone = true
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
- return err
+ if err = serviceContext.DockerClient.ContainerRemove(ctx, id, container.RemoveOptions{}); err != nil {
+ // 旧容器删除失败不算更新失败,但需要告知
+ logx.Errorf("删除旧容器失败: %v", err)
+ warning := "旧容器删除失败,请手动清理 " + backupName + ": " + err.Error()
+ warnings = append(warnings, warning)
+ progress(90, "更新成功(旧容器删除失败)", warning, false)
+ }
+ }
+
+ // 7. 清理旧镜像:仅当旧容器已删除、且旧镜像不再被任何容器引用时删除
+ if delOldContainer && os.Getenv("CLEANUP_OLD_IMAGE") != "false" {
+ progress(95, "正在清理旧镜像", "正在清理旧镜像", false)
+ if cleanupMsg := cleanupOldImage(serviceContext, oldImageID); cleanupMsg != "" {
+ warnings = append(warnings, cleanupMsg)
+ progress(95, "更新成功(旧镜像未清理)", cleanupMsg, false)
}
}
- oldTaskProgress.Message = "更新成功"
- oldTaskProgress.DetailMsg = "更新成功"
- oldTaskProgress.Percentage = 100
- oldTaskProgress.IsDone = true
- serviceContext.UpdateProgress(taskID, oldTaskProgress)
+
+ detail := "更新成功"
+ if len(warnings) > 0 {
+ detail = strings.Join(warnings, ";\n")
+ }
+ progress(100, "更新成功", detail, true)
return nil
}
-func decodePullResp(reader io.Reader, ctx *svc.ServiceContext, taskID string) (err error) {
+// waitForContainerHealthy 在容器启动后确认它没有立刻退出。
+// 有 HEALTHCHECK 时等待 healthy;没有时只观察 2 秒,避免把“启动成功但马上崩”当成成功。
+func waitForContainerHealthy(ctx context.Context, serviceContext *svc.ServiceContext, name string, healthcheck *container.HealthConfig) error {
+ checkCtx, cancel := context.WithTimeout(ctx, 90*time.Second)
+ defer cancel()
+
+ hasHealthcheck := healthcheck != nil && len(healthcheck.Test) > 0 &&
+ !(len(healthcheck.Test) == 1 && strings.EqualFold(healthcheck.Test[0], "NONE"))
+ stableDeadline := time.Now().Add(2 * time.Second)
+ ticker := time.NewTicker(time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-checkCtx.Done():
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+ return fmt.Errorf("等待容器健康超时")
+ case <-ticker.C:
+ inspected, err := serviceContext.DockerClient.ContainerInspect(checkCtx, name)
+ if err != nil {
+ return fmt.Errorf("检查新容器状态失败: %w", err)
+ }
+ if inspected.State == nil {
+ return fmt.Errorf("新容器状态为空")
+ }
+ if !inspected.State.Running {
+ return fmt.Errorf("新容器已退出,退出码 %d", inspected.State.ExitCode)
+ }
+ if hasHealthcheck && inspected.State.Health != nil {
+ switch strings.ToLower(string(inspected.State.Health.Status)) {
+ case "healthy":
+ return nil
+ case "unhealthy":
+ return fmt.Errorf("新容器健康检查结果为 unhealthy")
+ }
+ } else if !hasHealthcheck && time.Now().After(stableDeadline) {
+ return nil
+ }
+ }
+ }
+}
+
+// cleanupOldImage 在没有任何容器引用 oldImageID 时删除该镜像。
+// 返回补充信息(清理失败原因),失败不影响更新结果。
+func cleanupOldImage(serviceContext *svc.ServiceContext, oldImageID string) string {
+ if oldImageID == "" {
+ return ""
+ }
+ listCtx, listCancel := dockerTimeout(context.Background(), dockerListTimeout)
+ containers, err := serviceContext.DockerClient.ContainerList(listCtx, container.ListOptions{All: true})
+ listCancel()
+ if err != nil {
+ logx.Errorf("清理旧镜像前获取容器列表失败: %v", err)
+ return "旧镜像清理检查失败(未确认是否仍被引用): " + err.Error()
+ }
+ for _, c := range containers {
+ if c.ImageID == oldImageID {
+ // 仍被引用,不删
+ return ""
+ }
+ }
+ removeCtx, removeCancel := dockerTimeout(context.Background(), dockerMutationTimeout)
+ defer removeCancel()
+ if _, err := serviceContext.DockerClient.ImageRemove(removeCtx, oldImageID, image.RemoveOptions{}); err != nil {
+ logx.Errorf("清理旧镜像失败(不影响更新结果): %v", err)
+ return "旧镜像清理失败: " + err.Error()
+ }
+ return ""
+}
+
+func decodePullResp(reader io.Reader, svcCtx *svc.ServiceContext, ctx context.Context, taskID string) (err error) {
decoder := json.NewDecoder(reader)
- var oldTaskProgress, result = ctx.GetProgress(taskID)
+ var oldTaskProgress, result = svcCtx.GetProgress(taskID)
if !result {
oldTaskProgress = svc.TaskProgress{
Percentage: 0,
@@ -189,6 +365,9 @@ func decodePullResp(reader io.Reader, ctx *svc.ServiceContext, taskID string) (e
}
}
for {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
var msg dockerMsgType.JSONMessage
if err = decoder.Decode(&msg); err != nil {
if err == io.EOF {
@@ -197,31 +376,27 @@ func decodePullResp(reader io.Reader, ctx *svc.ServiceContext, taskID string) (e
oldTaskProgress.Message = "拉取镜像失败"
oldTaskProgress.DetailMsg = err.Error()
oldTaskProgress.Percentage = 25
- oldTaskProgress.IsDone = true
- ctx.UpdateProgress(taskID, oldTaskProgress)
- logx.Errorf("Failed to decode pull image response: %s", err)
+ svcCtx.UpdateProgress(taskID, oldTaskProgress)
+ logx.Errorf("解析镜像拉取响应失败: %s", err)
return fmt.Errorf("拉取镜像失败: %w", err)
}
- // Print the progress or error information from the response
if msg.Error != nil {
oldTaskProgress.Message = "拉取镜像失败"
oldTaskProgress.DetailMsg = msg.Error.Error()
oldTaskProgress.Percentage = 25
- oldTaskProgress.IsDone = true
- ctx.UpdateProgress(taskID, oldTaskProgress)
- logx.Errorf("Error: %s", msg.Error)
+ svcCtx.UpdateProgress(taskID, oldTaskProgress)
+ logx.Errorf("registry 返回错误: %s", msg.Error)
return fmt.Errorf("拉取镜像失败: %w", msg.Error)
+ }
+ var formattedMsg string
+ if msg.Progress != nil {
+ formattedMsg = fmt.Sprintf("进度%s: %s", msg.Status, msg.Progress.String())
} else {
- var formattedMsg string
- if msg.Progress != nil {
- formattedMsg = fmt.Sprintf("进度%s: %s", msg.Status, msg.Progress.String())
- } else {
- formattedMsg = fmt.Sprintf("进度%s", msg.Status)
- }
- oldTaskProgress.DetailMsg = formattedMsg
- oldTaskProgress.Percentage = 25
- ctx.UpdateProgress(taskID, oldTaskProgress)
- logx.Infof("拉取镜像进度\t %s: %s\n", msg.Status, msg.Progress)
+ formattedMsg = fmt.Sprintf("进度%s", msg.Status)
}
+ oldTaskProgress.DetailMsg = formattedMsg
+ oldTaskProgress.Percentage = 25
+ svcCtx.UpdateProgress(taskID, oldTaskProgress)
+ logx.Infof("拉取镜像进度\t %s: %s\n", msg.Status, msg.Progress)
}
}
diff --git a/internal/utiles/updateprogram.go b/internal/utiles/updateprogram.go
index 81f633d6..07c6b0bb 100644
--- a/internal/utiles/updateprogram.go
+++ b/internal/utiles/updateprogram.go
@@ -3,28 +3,50 @@ package utiles
import (
"archive/tar"
"compress/gzip"
+ "crypto/sha256"
+ "errors"
"fmt"
+ "github.com/onlyLTY/dockerCopilot/internal/config"
"github.com/onlyLTY/dockerCopilot/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
"io"
"net/http"
"os"
+ "path/filepath"
"runtime"
"strings"
+ "sync"
+ "time"
)
+// updateProgramMu 串行化自更新:并发两次调用会交错写同一个
+// dockerCopilot.tar.gz / dockerCopilot-new,可能装上损坏或错版的包。
+var updateProgramMu sync.Mutex
+
+// ErrUpdateProgramRunning 已有一轮自更新在执行时返回。
+var ErrUpdateProgramRunning = errors.New("已有一轮自更新在执行,请稍后再试")
+
func UpdateProgram(ctx *svc.ServiceContext) error {
+ if !updateProgramMu.TryLock() {
+ return ErrUpdateProgramRunning
+ }
+ defer updateProgramMu.Unlock()
+
+ if strings.Contains(config.Version, "FNOS") {
+ return fmt.Errorf("飞牛版本请使用 FNOS 专用发布包更新,禁止覆盖为通用版")
+ }
githubProxy := os.Getenv("githubProxy")
if githubProxy != "" {
githubProxy = strings.TrimRight(githubProxy, "/") + "/"
}
- versionURL := githubProxy + "https://raw.githubusercontent.com/onlyLTY/dockerCopilot/UGREEN/version"
- releaseBaseURL := githubProxy + "https://github.com/onlyLTY/dockerCopilot/releases/download"
- logx.Infof("versionURL: %s", versionURL)
- resp, err := http.Get(versionURL)
+ versionURL := githubProxy + "https://raw.githubusercontent.com/" + UpdateRepo() + "/" + UpdateBranch() + "/version"
+ releaseBaseURL := githubProxy + "https://github.com/" + UpdateRepo() + "/releases/download"
+ logx.Infof("版本检查地址: %s", versionURL)
+ client := &http.Client{Timeout: 60 * time.Second}
+ resp, err := client.Get(versionURL)
if err != nil {
logx.Info("没有获取到最新版本信息:", err)
- return nil
+ return err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
@@ -33,11 +55,14 @@ func UpdateProgram(ctx *svc.ServiceContext) error {
}
}(resp.Body)
- versionData, err := io.ReadAll(resp.Body)
- logx.Infof("versionData: %s", versionData)
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("获取版本信息失败: HTTP %s", resp.Status)
+ }
+ versionData, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
+ logx.Infof("远端版本文件内容: %s", versionData)
if err != nil {
logx.Info("没有获取到最新版本信息:", err)
- return nil
+ return err
}
version := strings.TrimSpace(string(versionData))
@@ -46,24 +71,70 @@ func UpdateProgram(ctx *svc.ServiceContext) error {
downloadURL := fmt.Sprintf("%s/%s/dockerCopilot-%s.tar.gz", releaseBaseURL, version, runtime.GOARCH)
logx.Info("下载链接:", downloadURL)
dest := "dockerCopilot.tar.gz"
+ checksumDest := dest + ".sha256"
+ defer func() {
+ _ = os.Remove(dest)
+ _ = os.Remove(checksumDest)
+ }()
if err := downloadFile(downloadURL, dest); err != nil {
logx.Error("下载失败:", err)
return err
}
+ if os.Getenv("UPDATE_SKIP_CHECKSUM") != "true" {
+ if err := downloadFile(downloadURL+".sha256", checksumDest); err != nil {
+ return fmt.Errorf("下载校验文件失败(如为旧版本可临时设置 UPDATE_SKIP_CHECKSUM=true): %w", err)
+ }
+ checksumData, readErr := os.ReadFile(checksumDest)
+ if readErr != nil {
+ return readErr
+ }
+ fields := strings.Fields(string(checksumData))
+ if len(fields) == 0 {
+ return fmt.Errorf("校验文件为空")
+ }
+ if err := verifySHA256(dest, fields[0]); err != nil {
+ return fmt.Errorf("安装包 SHA256 校验失败: %w", err)
+ }
+ logx.Info("SHA256 校验通过")
+ }
logx.Info("下载成功")
- if err := decompressTarGz(dest, "."); err != nil {
+ // 先解压到临时目录,成功后再原子替换;避免解压失败留下半个 dockerCopilot-new
+ // 被 start.sh 在下次启动时误当成新版本。
+ tmpDir, err := os.MkdirTemp(".", ".dc-update-")
+ if err != nil {
+ return err
+ }
+ defer func() {
+ _ = os.RemoveAll(tmpDir)
+ }()
+ if err := decompressTarGz(dest, tmpDir); err != nil {
logx.Info("解压缩失败:", err)
return err
}
- logx.Info("解压缩成功")
- return nil
+ candidates := []string{
+ filepath.Join(tmpDir, "dockerCopilot-new"),
+ filepath.Join(tmpDir, "dist", "linux", runtime.GOARCH, "dockerCopilot-new"),
+ }
+ for _, candidate := range candidates {
+ if info, statErr := os.Stat(candidate); statErr == nil && info.Mode().IsRegular() {
+ if err := os.Rename(candidate, "dockerCopilot-new"); err != nil {
+ return err
+ }
+ logx.Info("解压缩成功")
+ return nil
+ }
+ }
+ return fmt.Errorf("解压完成但未找到 dockerCopilot-new")
}
+const maxUpdateAssetSize = 512 << 20 // 512MB,足够覆盖当前发布包,同时防止异常响应写满磁盘
+
func downloadFile(url string, dest string) error {
- resp, err := http.Get(url)
+ client := &http.Client{Timeout: 10 * time.Minute}
+ resp, err := client.Get(url)
if err != nil {
return err
}
@@ -74,6 +145,10 @@ func downloadFile(url string, dest string) error {
}
}(resp.Body)
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("下载 %s 失败: HTTP %s", url, resp.Status)
+ }
+
out, err := os.Create(dest)
if err != nil {
return err
@@ -85,8 +160,31 @@ func downloadFile(url string, dest string) error {
}
}(out)
- _, err = io.Copy(out, resp.Body)
- return err
+ written, err := io.Copy(out, io.LimitReader(resp.Body, maxUpdateAssetSize+1))
+ if err != nil {
+ return err
+ }
+ if written > maxUpdateAssetSize {
+ return fmt.Errorf("下载文件超过大小限制 %d 字节", maxUpdateAssetSize)
+ }
+ return nil
+}
+
+func verifySHA256(path, expected string) error {
+ file, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ h := sha256.New()
+ if _, err := io.Copy(h, file); err != nil {
+ return err
+ }
+ actual := fmt.Sprintf("%x", h.Sum(nil))
+ if !strings.EqualFold(actual, strings.TrimSpace(expected)) {
+ return fmt.Errorf("期望 %s,实际 %s", expected, actual)
+ }
+ return nil
}
func decompressTarGz(gzFilePath string, dest string) error {
@@ -123,7 +221,20 @@ func decompressTarGz(gzFilePath string, dest string) error {
return err
}
- target := dest + "/" + header.Name
+ target := filepath.Join(dest, filepath.FromSlash(header.Name))
+ // 防止 zip-slip:用绝对路径判断,避免 dest="." 时把正常文件全部误判。
+ absDest, err := filepath.Abs(dest)
+ if err != nil {
+ return err
+ }
+ absTarget, err := filepath.Abs(target)
+ if err != nil {
+ return err
+ }
+ rel, err := filepath.Rel(absDest, absTarget)
+ if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
+ return fmt.Errorf("非法的文件路径: %s", header.Name)
+ }
switch header.Typeflag {
case tar.TypeDir:
@@ -131,6 +242,9 @@ func decompressTarGz(gzFilePath string, dest string) error {
return err
}
case tar.TypeReg:
+ if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
+ return err
+ }
outFile, err := os.Create(target)
if err != nil {
return err
diff --git a/internal/utiles/updateprogram_test.go b/internal/utiles/updateprogram_test.go
new file mode 100644
index 00000000..fd5411b9
--- /dev/null
+++ b/internal/utiles/updateprogram_test.go
@@ -0,0 +1,105 @@
+package utiles
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "crypto/sha256"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func writeTarGz(t *testing.T, path string, entries map[string]string) {
+ t.Helper()
+ file, err := os.Create(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer file.Close()
+
+ gzw := gzip.NewWriter(file)
+ defer gzw.Close()
+ tw := tar.NewWriter(gzw)
+ defer tw.Close()
+
+ for name, content := range entries {
+ header := &tar.Header{
+ Name: name,
+ Mode: 0644,
+ Size: int64(len(content)),
+ }
+ if err := tw.WriteHeader(header); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := tw.Write([]byte(content)); err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+func TestDecompressTarGzAllowsNormalRootFile(t *testing.T) {
+ archive := filepath.Join(t.TempDir(), "dockerCopilot.tar.gz")
+ writeTarGz(t, archive, map[string]string{"dockerCopilot-new": "binary"})
+
+ dest := t.TempDir()
+ if err := decompressTarGz(archive, dest); err != nil {
+ t.Fatalf("decompressTarGz() error = %v", err)
+ }
+ data, err := os.ReadFile(filepath.Join(dest, "dockerCopilot-new"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(data) != "binary" {
+ t.Fatalf("content = %q, want binary", data)
+ }
+}
+
+func TestDecompressTarGzRejectsTraversal(t *testing.T) {
+ archive := filepath.Join(t.TempDir(), "dockerCopilot.tar.gz")
+ writeTarGz(t, archive, map[string]string{"../escape": "bad"})
+
+ dest := t.TempDir()
+ if err := decompressTarGz(archive, dest); err == nil {
+ t.Fatal("decompressTarGz() expected traversal error, got nil")
+ }
+}
+
+func TestVerifySHA256(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "payload")
+ if err := os.WriteFile(path, []byte("payload"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ sum := sha256.Sum256([]byte("payload"))
+ if err := verifySHA256(path, fmt.Sprintf("%x", sum)); err != nil {
+ t.Fatalf("verifySHA256() error = %v", err)
+ }
+ if err := verifySHA256(path, "deadbeef"); err == nil {
+ t.Fatal("verifySHA256() expected mismatch error")
+ }
+}
+
+func TestDownloadFileRejectsNon200(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "not found", http.StatusNotFound)
+ }))
+ defer server.Close()
+
+ dest := filepath.Join(t.TempDir(), "out")
+ if err := downloadFile(server.URL, dest); err == nil {
+ t.Fatal("downloadFile() expected HTTP 404 error")
+ }
+}
+
+func TestFetchVersionFromURLRejectsNon200(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "not found", http.StatusNotFound)
+ }))
+ defer server.Close()
+
+ if _, err := fetchVersionFromURL(server.URL); err == nil {
+ t.Fatal("fetchVersionFromURL() expected HTTP 404 error")
+ }
+}
diff --git a/internal/utiles/updaterepo.go b/internal/utiles/updaterepo.go
new file mode 100644
index 00000000..bb2f62e0
--- /dev/null
+++ b/internal/utiles/updaterepo.go
@@ -0,0 +1,25 @@
+package utiles
+
+import (
+ "os"
+ "strings"
+)
+
+// UpdateRepo 返回自更新与版本检查所使用的仓库(owner/repo)。
+// 默认指向本项目的自维护 fork,可通过环境变量 UPDATE_REPO 覆盖。
+func UpdateRepo() string {
+ repo := strings.TrimSpace(os.Getenv("UPDATE_REPO"))
+ if repo == "" {
+ repo = "DongShull/dockerCopilot"
+ }
+ return repo
+}
+
+// UpdateBranch 返回版本检查读取的分支,默认 latest,可用 UPDATE_BRANCH 覆盖。
+func UpdateBranch() string {
+ branch := strings.TrimSpace(os.Getenv("UPDATE_BRANCH"))
+ if branch == "" {
+ branch = "latest"
+ }
+ return branch
+}
diff --git a/skills/dockercopilot-maintainer/SKILL.md b/skills/dockercopilot-maintainer/SKILL.md
new file mode 100644
index 00000000..c7982740
--- /dev/null
+++ b/skills/dockercopilot-maintainer/SKILL.md
@@ -0,0 +1,342 @@
+---
+name: dockercopilot-maintainer
+description: |
+ 维护、二开、构建、发布和排障 dockerCopilot(DC)项目时使用。触发场景包括:
+ 修改 Go 后端、容器更新/回滚、备份与 force 恢复、Compose 导出、Docker 网络清洗、
+ 进度任务与 SSE、审计日志、自更新与 SHA256、MCP(/mcp)工具增删与鉴权、HTTP API、
+ FNOS/UGREEN 构建、GitHub Actions、Docker 镜像打包、日志中文化,
+ 以及“DC 这个项目怎么改/怎么发版/为什么失败”。
+ 也适用于检查 skills/ 等开发资料是否被误打进镜像。
+metadata:
+ harness:
+ emoji: "🧭"
+ octop:
+ label:
+ zh: "dockerCopilot 维护"
+ en: "dockerCopilot Maintainer"
+ summary:
+ zh: "DC 项目的开发、构建、发布与排障全流程技能。"
+ en: "Full workflow skill for developing, building, releasing and troubleshooting DC."
+---
+
+# dockerCopilot 维护技能
+
+dockerCopilot(简称 DC)是一个 Go 后端 + MCP/HTTP API 的 Docker 容器管理工具,
+**已移除网页前端**:所有能力通过 `/api/*`(HTTP)与 `/mcp`(Streamable HTTP)暴露。
+本技能覆盖:代码地图、开发检查、备份恢复、更新回滚、Compose 导出、MCP 工具适配、
+FNOS/UGREEN 构建、CI/Release、镜像边界和日志排障。
+
+## 0. 先确认边界
+
+工作目录应为 DC 仓库根目录,特征文件:
+
+```text
+go.mod
+dockercopilot.go
+docker/Dockerfile
+internal/
+docs/MCP.md
+version
+fn_version
+ugreen_version
+```
+
+如果不在仓库根目录,先 `cd` 到包含 `go.mod` 的目录。不要假设路径是
+`/vol1/1000/Github/dockerCopilot`;以实际仓库为准。
+
+## 1. 快速预检
+
+```bash
+skills/dockercopilot-maintainer/scripts/preflight.sh
+```
+
+它会检查:
+
+- git 分支/工作区;
+- Go、Docker、Node、npm;
+- `version` / `fn_version` / `ugreen_version`;
+- `internal/mcp` 工具是否可编译、`/mcp` 鉴权配置;
+- Docker 守护进程是否可达;
+- `.dockerignore` 是否排除 `skills`;
+- Dockerfile 是否出现 `COPY .`。
+
+## 2. 代码地图
+
+| 路径 | 职责 |
+|---|---|
+| `dockercopilot.go` | 启动、配置、日志、路由注册、定时任务 |
+| `internal/handler/` | HTTP handler,goctl 生成 |
+| `internal/logic/` | 业务逻辑,参数校验后调用 `internal/utiles` |
+| `internal/svc/` | ServiceContext:Docker 客户端、进度、设置、Hub 镜像信息 |
+| `internal/utiles/` | Docker 操作、备份/恢复、更新、自更新、审计、网络清洗 |
+| `internal/module/` | 镜像仓库鉴权、digest 检查 |
+| `internal/types/` | 请求/响应类型 |
+| `internal/mcp/` | MCP 工具层:`server.go` 注册与鉴权,`tools_*.go` 按域暴露能力 |
+| `docker/Dockerfile` | 运行镜像,白名单 COPY |
+| `.github/workflows/` | CI、FNOS、UGREEN、DockerHub |
+| `skills/` | 开发资料,**禁止进入镜像** |
+
+详细说明见 `references/architecture.md`。
+
+## 3. 开发检查(每次改完必跑)
+
+```bash
+skills/dockercopilot-maintainer/scripts/dev-check.sh
+# 需要 race:
+skills/dockercopilot-maintainer/scripts/dev-check.sh --race
+```
+
+等价手动命令:
+
+```bash
+gofmt -l $(git ls-files '*.go')
+go build ./...
+go vet ./...
+go test ./... -count=1
+go test -race ./... -count=1
+git diff --check
+```
+
+要求:
+
+- 新增/修改 Go 文件必须 `gofmt`;
+- 不允许 `go vet` 警告;
+- 修 bug 先补回归测试;
+- Docker 相关逻辑优先用接口/helper 做单元测试,不依赖真实 daemon;
+- 提交前 `git diff --check`。
+
+## 4. 高风险区域
+
+### 4.1 更新容器(`internal/utiles/updatecontainer.go`)
+
+必须保持:
+
+1. inspect 旧容器,记录镜像、名称、运行/暂停状态;
+2. 拉新镜像;
+3. 停止旧容器(paused 先 unpause);
+4. 旧容器改名;
+5. 创建/启动新容器;
+6. 健康检查;
+7. 成功后才删除旧容器;
+8. 任一步失败回滚:移除新容器、旧容器改回原名、恢复运行/暂停;
+9. 旧镜像只在无容器引用时清理;
+10. 旧容器删除失败只告警,不算更新失败。
+
+不要改成“先删旧容器再建新容器”。
+
+### 4.2 恢复(`internal/utiles/restorecontainer.go`)
+
+- `restoreMu` 串行化恢复;
+- 默认幂等:同名容器存在则跳过,镜像不一致报错;
+- `force=true`:停止 → 临时改名 → 创建/启动 → 成功才删旧;
+- 失败必须 `rollbackRestoredContainer`;
+- 旧备份没有 `StateKnown/WasRunning/WasPaused` 时,默认恢复后启动;
+- 恢复网络配置必须经过 `restoreNetworkingConfig` / `sanitizeEndpointSettings`。
+
+### 4.3 网络清洗(`internal/utiles/network.go`)
+
+只保留:
+
+- `Aliases`
+- `Links`
+- `DriverOpts`
+- `IPAMConfig`
+
+清空:
+
+- `NetworkID`
+- `EndpointID`
+- `Gateway`
+- `IPAddress`
+- `IPPrefixLen`
+- `IPv6Gateway`
+- `GlobalIPv6Address`
+- `MacAddress`
+- `DNSNames`
+
+备份、更新、恢复三条链必须一致。
+
+### 4.4 备份与 Compose 导出
+
+- JSON 备份要保留 `Config`、`HostConfig`、`NetworkingConfig`、状态;
+- Compose 导出要保留 healthcheck、devices、cap_add/cap_drop、security_opt、
+ ulimits、extra_hosts、dns、tmpfs、shm_size、user、group_add、logging、
+ stop_signal、stop_grace_period、sysctls、资源限制;
+- 命名卷用 `MountPoint.Name`,不要导宿主路径;
+- 网络别名过滤容器 ID/名称。
+
+### 4.5 自更新
+
+- 下载限制 512MB;
+- 非 200 必须报错;
+- 默认校验 SHA256;
+- 解压先到临时目录,成功后再替换;
+- `FNOS` 版本禁止走通用版自更新;
+- `UPDATE_SKIP_CHECKSUM=true` 只作为旧版本过渡。
+
+### 4.6 日志
+
+- 使用中文;
+- `SetupLog` 使用 `Encoding: "plain"`,输出“时间 + 级别 + 内容”;
+- 不要恢复 `dockerCopilot:` 前缀;
+- HTTP 404/500 等错误要以中文描述输出;
+- `docker compose logs` 的前缀用 `--no-log-prefix` 去掉,不要改代码硬扛。
+
+## 5. MCP 层(`internal/mcp/`)
+
+前端已移除,Agent 通过 `/mcp` 调用;`/api/*` 保留给脚本与旧客户端。
+
+结构:
+
+| 文件 | 职责 |
+|---|---|
+| `server.go` | 构建 MCPServer、注册 26 个工具、挂载 `/mcp`、token 鉴权中间件 |
+| `utils.go` | `textResult` / `jsonResult` / 参数读取等公共辅助 |
+| `tools_common.go` | `dc_ping` / `dc_info`(含 Docker daemon 真实 Ping) |
+| `tools_container.go` | 列表、inspect、启停重启、重命名、更新、自动更新 |
+| `tools_image.go` | 镜像列表、更新检查、删除 |
+| `tools_backup.go` | JSON/Compose 备份、列表、恢复、删除 |
+| `tools_progress.go` | 进度列表/详情/取消 |
+| `tools_settings.go` | 备份目录读写 |
+| `tools_version.go` | 版本、检查更新、自更新 |
+
+新增工具必须:
+
+1. 复用 `internal/utiles` / `internal/logic`,不要在 `internal/mcp` 里重写 Docker 逻辑;
+2. 名称以 `dc_` 开头、描述用中文、必填参数用 `mcp.Required()`;
+3. 破坏性操作补 `utiles.AuditLog(...)`;
+4. 长任务用 `ctx.RegisterTask/CancelTask/FinishTask` 并返回最终进度;
+5. 在 `internal/mcp/mcp_test.go` 的 `expectedTools` 里登记,保证工具清单可回归。
+
+鉴权:`MCP_TOKEN` > `secretKey`;`MCP_TOKEN=off` 关闭(仅隔离内网)。支持
+`Authorization: Bearer`、`X-MCP-Token`、`?token=` 三种方式。
+
+详见 `docs/MCP.md` 与 `references/mcp.md`。
+
+## 6. 本地构建
+
+```bash
+# amd64 + version
+skills/dockercopilot-maintainer/scripts/build-linux.sh
+
+# arm64 + fn_version
+skills/dockercopilot-maintainer/scripts/build-linux.sh arm64 fn_version
+```
+
+产物:`dist/linux//dockerCopilot-new`。
+
+手动等价:
+
+```bash
+VERSION=$(cat version)
+CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a --trimpath \
+ -ldflags="-w -s -X 'github.com/onlyLTY/dockerCopilot/internal/config.Version=$VERSION' \
+ -X 'github.com/onlyLTY/dockerCopilot/internal/config.BuildDate=$(date)'" \
+ -o dist/linux/amd64/dockerCopilot-new .
+```
+
+## 7. 发布
+
+```bash
+skills/dockercopilot-maintainer/scripts/release-check.sh
+```
+
+流程:
+
+1. 更新 `version` / `fn_version` / `ugreen_version`(按平台);
+2. 跑 `dev-check.sh --race`;
+3. 跑 `package-audit.sh`;
+4. 提交并推送对应分支;
+5. GitHub Actions 构建后端 + Docker 镜像(前端步骤已移除);
+6. 创建/上传 Release 资产(tar.gz + sha256)。
+
+镜像标签:
+
+| 工作流 | 标签 |
+|---|---|
+| `docker-build-push-latest.yml` | `dockercopilot:latest-dev` |
+| `docker-build-push-latest-stable.yml` | `dockercopilot:latest` |
+| `docker-build-push-UGREEN.yml` | `dockercopilot:UGREEN-DEV` |
+| `docker-build-push-UGREEN-stable.yml` | `dockercopilot:UGREEN` |
+
+详细说明见 `references/release.md`。
+
+## 8. 镜像边界(本技能的关键约束)
+
+`skills/` 只用于开发,**不得进入镜像/发布包**。
+
+三层保护:
+
+1. `docker/Dockerfile` 只白名单 COPY:
+ - `dc-back/dist/$TARGETPLATFORM/dockerCopilot`
+ - `dc-back/etc/.`
+ - `start.sh`
+2. `.dockerignore` 排除 `skills/`;
+3. `scripts/package-audit.sh` 检查:
+ - Dockerfile 没有 `COPY .` / `ADD .`;
+ - 没有引用 `skills`;
+ - `.dockerignore` 含 `skills`;
+ - 本地发布包不含 `skills/`。
+
+每次改 Dockerfile 或打包流程后必须跑:
+
+```bash
+skills/dockercopilot-maintainer/scripts/package-audit.sh
+```
+
+## 9. FNOS / UGREEN
+
+- `fn_version` 触发 `FNOS.yml`;
+- `ugreen_version` 触发 `UGREEN.yml`;
+- FNOS 版本 `config.Version` 含 `FNOS`,禁止通用版自更新覆盖;
+- 启动时 Docker 不可达只记录错误,不 `os.Exit`;
+- 不需要 `privileged: true`,只挂 `docker.sock`。
+
+详见 `references/fnos.md`。
+
+## 10. 排障
+
+先看:
+
+```bash
+docker logs -f
+# 或
+docker compose logs --no-log-prefix -f
+```
+
+再按 `references/troubleshooting.md` 查:
+
+- 启动即退出;
+- 页面只有占位;
+- 更新/恢复失败与回滚残留;
+- 端口/IP/MAC 冲突;
+- registry digest 检测;
+- 自更新 404 / SHA256;
+- MCP 401 / 工具不可见 / 客户端握手失败;
+- docker.sock 权限。
+
+## 11. 交付清单
+
+- [ ] `gofmt` 通过
+- [ ] `go build ./...` 通过
+- [ ] `go vet ./...` 通过
+- [ ] `go test ./... -count=1` 通过
+- [ ] 必要时 `go test -race ./...` 通过
+- [ ] 新增回归测试
+- [ ] `git diff --check` 通过
+- [ ] 日志为中文,无 `dockerCopilot:` 前缀
+- [ ] `package-audit.sh` 通过
+- [ ] `/mcp` 工具清单与 `docs/MCP.md` 一致
+- [ ] 版本文件已更新
+- [ ] 没有提交 `.env`、token
+- [ ] 文档/ROADMAP 与行为一致
+
+## 12. 禁止事项
+
+- 不要在日志/错误里输出密钥、token、cookie;
+- 不要 `COPY .` 进 Dockerfile;
+- 不要把 `skills/` 打进镜像或 Release;
+- 不要删除 `force` 恢复的回滚步骤;
+- 不要在恢复/更新失败时静默吞错;
+- 不要用 `docker system prune -a` 清理用户环境;
+- 不要在没有确认时执行删除容器/镜像的破坏性命令。
diff --git a/skills/dockercopilot-maintainer/references/api.md b/skills/dockercopilot-maintainer/references/api.md
new file mode 100644
index 00000000..16eee694
--- /dev/null
+++ b/skills/dockercopilot-maintainer/references/api.md
@@ -0,0 +1,145 @@
+# API 速查
+
+> MCP(`/mcp`)与 HTTP API 并存。Agent 优先用 MCP:见 `references/mcp.md` 与 `docs/MCP.md`。
+
+所有受保护接口需要 JWT:
+
+```http
+Authorization: Bearer
+```
+
+登录(form):
+
+```http
+POST /api/auth
+Content-Type: application/x-www-form-urlencoded
+
+secretKey=...
+```
+
+## 容器
+
+| 方法 | 路径 | 说明 |
+|---|---|---|
+| GET | `/api/containers` | 容器列表 |
+| POST | `/api/container/:id/start` | 启动 |
+| POST | `/api/container/:id/stop` | 停止 |
+| POST | `/api/container/:id/restart` | 重启 |
+| POST | `/api/container/:id/rename` | 重命名 |
+| POST | `/api/container/:id/update` | 更新 |
+
+更新(form):
+
+```http
+POST /api/container/:id/update
+Content-Type: application/x-www-form-urlencoded
+
+imageNameAndTag=redis:7&containerName=redis
+```
+
+- `imageNameAndTag`:目标镜像,可带 tag;
+- `containerName`:容器名,用于日志/回滚命名;
+- 是否删除旧容器由环境变量 `DelOldContainer` 控制,不在请求体里。
+
+## 备份 / 恢复
+
+| 方法 | 路径 | 说明 |
+|---|---|---|
+| GET | `/api/container/backup` | 备份全部容器为 JSON |
+| GET | `/api/container/backup2compose` | 导出 Compose |
+| GET | `/api/container/listBackups` | 备份文件列表 |
+| DELETE | `/api/container/backups` | 删除备份 |
+| POST | `/api/container/backups/restore` | 恢复 |
+
+恢复请求:
+
+```json
+{
+ "filename": "backup-2026-10-30.json",
+ "start": true,
+ "force": false
+}
+```
+
+- `start` 省略:按备份中的容器状态恢复;
+- `force=false`:同名容器存在时跳过/补启动,镜像不一致报错;
+- `force=true`:先停止并临时改名旧容器,新建成功后才删除旧容器;失败自动回滚。
+
+## 镜像
+
+| 方法 | 路径 | 说明 |
+|---|---|---|
+| GET | `/api/images` | 镜像列表 |
+| DELETE | `/api/image/:id` | 删除镜像 |
+| DELETE | `/api/images?filter=untagged\|unused` | 批量清理:无 TAG 镜像 / 未使用镜像,返回 removed/failed 明细 |
+
+## 进度
+
+| 方法 | 路径 | 说明 |
+|---|---|---|
+| GET | `/api/progress` | 任务列表 |
+| GET | `/api/progress/:taskid` | 单任务详情 |
+| POST | `/api/progress/:taskid/cancel` | 取消任务 |
+| GET | `/api/progress/:taskid/events` | SSE 事件流 |
+
+SSE 事件示例:
+
+```text
+event: progress
+data: {"taskid":"...","percentage":40,"message":"正在创建新容器","isDone":false}
+
+event: ping
+data: {}
+```
+
+## 设置
+
+| 方法 | 路径 | 说明 |
+|---|---|---|
+| GET | `/api/settings` | 读取设置 |
+| PUT | `/api/settings` | 更新设置 |
+
+设置项主要包含备份目录等。`BACKUP_DIR` 环境变量优先级最高。
+
+## 图标
+
+| 方法 | 路径 | 说明 |
+|---|---|---|
+| GET | `/api/icons` | 读取自定义图标配置 |
+| POST | `/api/icons` | 上传图标 |
+
+## 版本 / 自更新
+
+| 方法 | 路径 | 说明 |
+|---|---|---|
+| GET | `/api/version` | 当前版本 + 远程版本 |
+| PUT | `/api/program` | 触发自更新 |
+
+自更新依赖:
+
+- `UPDATE_REPO`
+- `UPDATE_BRANCH`
+- Release 资产 `dockerCopilot-.tar.gz` 与 `.sha256`
+- 可选 `githubProxy`
+- `UPDATE_SKIP_CHECKSUM=true` 仅用于旧版本过渡
+
+## 错误格式
+
+业务错误通常返回:
+
+```json
+{
+ "code": 50000,
+ "msg": "错误信息"
+}
+```
+
+401 返回:
+
+```json
+{
+ "code": 401,
+ "msg": "未授权",
+ "data": {}
+}
+```
diff --git a/skills/dockercopilot-maintainer/references/architecture.md b/skills/dockercopilot-maintainer/references/architecture.md
new file mode 100644
index 00000000..29292420
--- /dev/null
+++ b/skills/dockercopilot-maintainer/references/architecture.md
@@ -0,0 +1,205 @@
+# 架构与数据流
+
+## 1. 启动链路
+
+`main()`:
+
+1. `SetupLog("./logs")`:`logx.MustSetup` + 控制台 writer;当前使用 `Encoding: "plain"`,日志行只保留时间、级别、内容。
+2. `conf.Load` 读取 `etc/dockerCopilot.yaml`,支持环境变量覆盖。
+3. 校验 `Auth.AccessSecret`(非空、非纯数字、长度 > 8)。
+4. `rest.MustNewServer` + CORS + 401 回调。
+5. `svc.NewServiceContext(c)`:
+ - 创建 Docker client(`DOCKER_HOST`,默认 `unix:///var/run/docker.sock`);
+ - 加载进度文件;
+ - 初始化 Settings。
+6. 注册路由;
+7. 启动 cron:镜像更新检查、自动更新白名单;
+8. `server.Start()`。
+
+Docker 不可达时**不退出进程**,只记录错误,由定时任务稍后重试。
+
+## 2. 包职责
+
+```
+internal/
+├── config/ 版本、构建时间
+├── handler/ HTTP 参数解析、响应
+├── logic/ 业务编排
+├── module/ 认证、registry digest、镜像更新检查
+├── svc/ ServiceContext、进度持久化、全局锁
+├── types/ 请求/响应结构
+└── utiles/ Docker 原子操作、备份/恢复、更新、Compose、审计
+```
+
+### 2.1 `internal/utiles` 重点文件
+
+| 文件 | 作用 |
+|---|---|
+| `getcontainerlist.go` | 容器列表 + 镜像信息 |
+| `containerInspect.go` | 容器 inspect 封装 |
+| `startcontainer.go` / `stopcontainer.go` / `restartcontainer.go` | 生命周期操作 |
+| `renamecontainer.go` | 重命名 |
+| `removeimage.go` / `image.go` | 镜像删除/列表 |
+| `backupcontainer.go` | 导出容器 JSON 备份 |
+| `backuppath.go` | 备份路径解析与文件名校验 |
+| `backupslist.go` | 备份列表 |
+| `backup2compose.go` | 导出 Compose |
+| `backup_compose/config2yaml.go` | Docker inspect → compose-go 结构 |
+| `restorecontainer.go` | 从 JSON 恢复,含 `force` 回滚 |
+| `updatecontainer.go` | 更新容器,含回滚、健康检查、旧镜像清理 |
+| `autoupdate.go` | 白名单自动更新 |
+| `updateprogram.go` | 自更新下载、解压、SHA256 |
+| `getRemoteVersion.go` | 远程 version 文件 |
+| `audit.go` | JSONL 审计日志,10MB 轮转,0600 |
+| `network.go` | 端点配置清洗 |
+
+## 3. 容器更新流程
+
+`UpdateContainer`:
+
+1. `ContainerInspect` 旧容器,记录:
+ - 旧镜像 ID;
+ - 旧容器名;
+ - 是否运行、是否 paused;
+ - `Config` / `HostConfig` / `NetworkSettings`。
+2. 拉取目标镜像,流式解码进度。
+3. 如果原本运行:
+ - paused 先 `ContainerUnpause`;
+ - `ContainerStop`(SIGTERM,10s)。
+4. 旧容器改名 `-`。
+5. `ContainerCreate` 新容器:
+ - `Config.Image` 替换为目标镜像;
+ - `Hostname` 清空;
+ - 网络端点经 `sanitizeEndpointSettings`;
+ - 名称使用旧名。
+6. 如果原本运行:
+ - `ContainerStart`;
+ - 非 paused 时等待健康检查;
+ - 原本 paused 时启动后 `ContainerPause`。
+7. 删除旧容器(`DelOldContainer=false` 可保留)。
+8. 清理旧镜像(旧容器已删除且无引用)。
+
+失败回滚:
+
+- 删除新容器(如果已创建);
+- 旧容器改回原名;
+- 原本运行则启动;
+- 原本 paused 则恢复 paused。
+
+## 4. 容器恢复流程
+
+`RestoreContainer`:
+
+1. 解析文件名(只允许安全字符,防目录穿越)。
+2. 读取 JSON → `[]ContainerBackupEntry`。
+3. 串行化(`restoreMu`),避免并发恢复同名容器。
+4. 逐条:
+ - 校验 `Config.Image`;
+ - 计算目标状态:
+ - `startOverride != nil`:强制;
+ - 否则 `stateKnown`:按 `wasRunning`;
+ - 旧备份无状态:默认启动。
+ - 同名容器存在:
+ - 非 `force`:镜像一致则跳过/补启动,不一致报错;
+ - `force`:进入替换流程。
+5. `force` 替换流程:
+ - 拉镜像;
+ - 停止旧容器(paused 先 unpause);
+ - 改名旧容器;
+ - 创建新容器;
+ - 启动(按目标状态);
+ - 成功后删除旧容器。
+6. 失败:`rollbackRestoredContainer` 清理新容器、恢复旧容器名与状态。
+7. 汇总 `completed` / `failures` 到进度详情。
+
+## 5. 备份格式
+
+`ContainerBackupEntry` 大致结构:
+
+```json
+{
+ "Name": "redis",
+ "Config": { "...": "Docker container.Config" },
+ "HostConfig": { "...": "Docker container.HostConfig" },
+ "NetworkingConfig": {
+ "EndpointsConfig": {
+ "bridge": {
+ "Aliases": ["redis"],
+ "IPAMConfig": null
+ }
+ }
+ },
+ "stateKnown": true,
+ "wasRunning": true,
+ "wasPaused": false
+}
+```
+
+兼容规则:
+
+- 旧 JSON 没有 `StateKnown/WasRunning/WasPaused` 时按“未知”处理;
+- 恢复默认启动;
+- 新增字段必须可选,不能破坏旧文件反序列化。
+
+## 6. 进度与 SSE
+
+- `svc.TaskProgress`:`TaskID/Percentage/Message/Name/DetailMsg/IsDone`。
+- 内存 map + 文件持久化(`PROGRESS_STORE`,默认 `/data/config/progress.json`)。
+- 保存与读取在同一把锁内,临时文件 + rename 原子落盘。
+- 已完成任务按 `PROGRESS_TTL_HOURS` 清理。
+- SSE 端点:`GET /api/progress/:taskid/events`,15s 心跳。
+- 取消:`POST /api/progress/:taskid/cancel`,通过 context cancel 传播。
+
+## 7. 日志与审计
+
+### 7.1 运行日志
+
+`SetupLog`:
+
+```go
+logx.MustSetup(logx.LogConf{
+ Path: "./logs",
+ Level: "info",
+ KeepDays: 7,
+ Compress: true,
+ Mode: "file",
+ Encoding: "plain",
+})
+logx.AddWriter(logx.NewWriter(os.Stdout))
+```
+
+- 文件:`logs/access.log`、`error.log` 等;
+- 控制台同样输出;
+- `plain` 格式:`时间 级别 内容`;
+- 不再输出 `dockerCopilot:` 服务名前缀。
+
+### 7.2 审计日志
+
+`internal/utiles/audit.go`:
+
+- 路径:`AUDIT_LOG`,默认 `/data/logs/audit.jsonl`;
+- 每行一个 JSON:`time/action/target/detail`;
+- 文件权限 `0600`;
+- 超过 10MB 轮转为 `.1`。
+
+## 8. 配置优先级
+
+| 配置 | 优先级 |
+|---|---|
+| 备份目录 | `BACKUP_DIR` env > Settings API > `/data/backups` |
+| 进度文件 | `PROGRESS_STORE` env > 默认 |
+| 审计日志 | `AUDIT_LOG` env > 默认 |
+| 更新仓库 | `UPDATE_REPO` env > 默认 |
+| 更新分支 | `UPDATE_BRANCH` env > 默认 |
+| 代理 | `githubProxy` env |
+| registry TLS | `REGISTRY_INSECURE` env |
+
+## 9. 镜像边界
+
+Dockerfile 只 COPY:
+
+- `dc-back/dist/$TARGETPLATFORM/dockerCopilot`
+- `dc-back/etc/`
+- `start.sh`
+
+`skills/` 仅用于开发辅助,必须被 `.dockerignore` 排除,不进入镜像。
diff --git a/skills/dockercopilot-maintainer/references/fnos.md b/skills/dockercopilot-maintainer/references/fnos.md
new file mode 100644
index 00000000..65acaff6
--- /dev/null
+++ b/skills/dockercopilot-maintainer/references/fnos.md
@@ -0,0 +1,77 @@
+# FNOS / UGREEN 适配
+
+## 1. 版本区分
+
+| 文件 | 平台 | 用途 |
+|---|---|---|
+| `version` | 通用 | GitHub Release、latest 镜像 |
+| `fn_version` | 飞牛 FnOS | FNOS Release、飞牛专用包 |
+| `ugreen_version` | 绿联 UGREEN | UGREEN Release、绿联专用包 |
+
+## 2. FNOS
+
+构建入口:`.github/workflows/FNOS.yml`
+
+- 触发:`fn_version` 变更 / 手动;
+- 版本取自 `fn_version`;
+- 产物:`dockerCopilot-.tar.gz` + `.sha256`;
+- 二进制名:`dockerCopilot-new`;
+- 运行逻辑:
+
+```go
+if strings.Contains(config.Version, "FNOS") {
+ // 飞牛版本使用独立发布包,不访问通用版 version 文件
+}
+```
+
+### 注意
+
+- FNOS 版本不能被通用版自更新覆盖;
+- `GetRemoteVersion` 遇到 `FNOS` 直接返回本地版本;
+- `UpdateProgram` 遇到 `FNOS` 直接报错,要求使用专用发布包;
+- 不要删除这些判断。
+
+## 3. UGREEN
+
+构建入口:`.github/workflows/UGREEN.yml`
+
+- 触发:`ugreen_version` 变更 / 手动;
+- 版本取自 `ugreen_version`;
+- 产物同上。
+
+## 4. 权限与运行
+
+DC 只要求:
+
+- 挂载 `/var/run/docker.sock`;
+- 访问 `/data`;
+- 端口 `12712`。
+
+不需要 `privileged: true`。
+
+如果 NAS 的 Docker socket 属于 `docker` 组,非 root 运行需要:
+
+```yaml
+group_add:
+ -
+```
+
+## 5. 开机顺序
+
+NAS 开机时 `docker.sock` 可能晚于 DC 容器就绪。启动逻辑必须:
+
+- Docker 初始化失败只记录错误;
+- 不 `os.Exit`;
+- 定时任务重试。
+
+不要在启动路径恢复 `log.Fatalf`。
+
+## 6. 常见问题
+
+| 现象 | 检查 |
+|---|---|
+| FNOS 包被通用版更新覆盖 | `config.Version` 是否含 `FNOS`;`GetRemoteVersion` / `UpdateProgram` 判断是否保留 |
+| 容器启动但接口 502 | 检查端口映射与 `secretKey`;`GET /` 应返回 200 JSON |
+| 无法管理 Docker | socket 挂载、权限、`DOCKER_HOST` |
+| 时区不对 | `TZ=Asia/Shanghai` |
+| 图标不显示 | `/data/config/image` 挂载与权限 |
diff --git a/skills/dockercopilot-maintainer/references/mcp.md b/skills/dockercopilot-maintainer/references/mcp.md
new file mode 100644
index 00000000..7235ebab
--- /dev/null
+++ b/skills/dockercopilot-maintainer/references/mcp.md
@@ -0,0 +1,114 @@
+# MCP 层速查(`internal/mcp/`)
+
+前端已移除;Agent 走 `/mcp`(Streamable HTTP),脚本走 `/api/*`,两条通道复用同一套
+`internal/utiles` / `internal/logic` 实现。
+
+## 1. 端点
+
+| 项 | 值 |
+|---|---|
+| 路径 | `/mcp` |
+| 方法 | `POST`(JSON-RPC)、`GET`(SSE)、`DELETE`(结束会话) |
+| 协议版本 | `2024-11-05`(客户端协商) |
+| 服务端标识 | `dockerCopilot` + `mcp.Version` |
+
+## 2. 鉴权
+
+- token 取值:`MCP_TOKEN` → 回退 `secretKey`;
+- `MCP_TOKEN=off|none|disabled` 关闭鉴权(启动时打 error 告警);
+- 携带方式:`Authorization: Bearer `、`X-MCP-Token: `、`?token=`;
+- 校验用 `crypto/subtle.ConstantTimeCompare`,避免时序侧信道。
+
+未授权返回:
+
+```json
+{"code":401,"msg":"unauthorized: 需要 Authorization: Bearer 或 X-MCP-Token 或 ?token="}
+```
+
+## 3. 工具注册约定
+
+```go
+s.AddTool(mcp.NewTool("dc_xxx",
+ mcp.WithDescription("中文描述"),
+ mcp.WithString("containerId", mcp.Required(), mcp.Description("容器 ID 或名称")),
+), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ // 1) 解析参数
+ // 2) 调用 internal/utiles 或 internal/logic
+ // 3) 破坏性操作写审计
+ // 4) 返回 jsonResult/textResult
+})
+```
+
+约束:
+
+1. 工具名统一 `dc_` 前缀,中文描述;
+2. 必填参数必须 `mcp.Required()`;
+3. 工具里不做 HTTP 自调用,直接复用 Go 函数;
+4. 错误用 `mcp.NewToolResultError(err.Error())` 返回,让客户端看到 `isError: true`;
+5. 长任务:
+
+ ```go
+ taskID := uuid.New().String()
+ taskCtx := ctx.RegisterTask(taskID)
+ defer ctx.FinishTask(taskID)
+ err := utiles.UpdateContainer(ctx, taskCtx, ..., taskID)
+ final, _ := ctx.GetProgress(taskID)
+ ```
+
+6. 新增工具必须同步:
+ - `internal/mcp/mcp_test.go` 的 `expectedTools`;
+ - `docs/MCP.md` 工具清单;
+ - `dc_info` 里的 `toolCount`(自动统计,无需手改)。
+
+## 4. 工具清单(27)
+
+| 分组 | 工具 |
+|---|---|
+| 通用 | `dc_ping` `dc_info` |
+| 容器 | `dc_list_containers` `dc_inspect_container` `dc_start_container` `dc_stop_container` `dc_restart_container` `dc_rename_container` `dc_update_container` `dc_autoupdate_run` |
+| 镜像 | `dc_list_images` `dc_check_image_update` `dc_remove_image` `dc_clean_images` |
+| 备份 | `dc_backup_containers` `dc_backup_compose` `dc_list_backups` `dc_restore_container` `dc_delete_backup` |
+| 进度 | `dc_list_progress` `dc_get_progress` `dc_cancel_progress` |
+| 设置 | `dc_get_settings` `dc_update_settings` |
+| 版本 | `dc_get_version` `dc_check_program_update` `dc_update_program` |
+
+## 5. 测试
+
+```bash
+go test ./internal/mcp/ -v
+```
+
+覆盖:
+
+- `TestAllToolsRegistered`:工具数量与名字(防漏注册/改名);
+- `TestAuthMiddleware`:无 token / 错误 token / 三种正确携带方式;
+- `TestInitializeAndListTools`:真实 Streamable HTTP 握手 + `tools/list`;
+- `TestMCPRequiresAuth`:未授权传输层 401。
+
+## 6. 手动联调
+
+```bash
+SID=$(curl -s -D - -o /dev/null -X POST http://127.0.0.1:12712/mcp \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json, text/event-stream' \
+ -H "Authorization: Bearer $TOKEN" \
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \
+ | grep -i '^mcp-session-id' | tr -d '\r' | awk '{print $2}')
+
+curl -s -X POST http://127.0.0.1:12712/mcp \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json, text/event-stream' \
+ -H "Authorization: Bearer $TOKEN" \
+ -H "Mcp-Session-Id: $SID" \
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"dc_info","arguments":{}}}'
+```
+
+## 7. 常见坑
+
+| 现象 | 原因 | 处理 |
+|---|---|---|
+| 401 | 未带 token / token 与 `secretKey` 不一致 | 检查三种携带方式与 `MCP_TOKEN` |
+| `tools/list` 为空 | 传输层未建立会话 | 先 `initialize`,后续请求带 `Mcp-Session-Id` |
+| 工具报 `isError` | 业务错误(如 Docker 不可达) | 看 `content[0].text` 的错误文案 |
+| 客户端连不上 | 只支持 stdio | 本项目只提供 Streamable HTTP,需客户端支持 http url |
+| 工具数量变少 | `registerXxxTools` 未被 `NewServer` 调用 | 检查 `server.go` 的注册顺序 |
diff --git a/skills/dockercopilot-maintainer/references/release.md b/skills/dockercopilot-maintainer/references/release.md
new file mode 100644
index 00000000..e12b486e
--- /dev/null
+++ b/skills/dockercopilot-maintainer/references/release.md
@@ -0,0 +1,117 @@
+# 构建与发布
+
+## 1. 版本文件
+
+| 文件 | 用途 |
+|---|---|
+| `version` | 通用版 / latest 发布版本 |
+| `fn_version` | 飞牛 FnOS 专用版本 |
+| `ugreen_version` | 绿联 UGREEN 专用版本 |
+
+版本号同时通过 ldflags 注入:
+
+```bash
+-X 'github.com/onlyLTY/dockerCopilot/internal/config.Version='
+-X 'github.com/onlyLTY/dockerCopilot/internal/config.BuildDate='
+```
+
+## 2. CI
+
+`.github/workflows/ci.yml`:
+
+- 触发:push / PR 到 `latest`、`dev`;
+- 步骤:`go build ./...` → `go vet ./...` → `go test ./... -count=1`(前端步骤已移除)。
+
+本地等价:
+
+```bash
+skills/dockercopilot-maintainer/scripts/dev-check.sh
+```
+
+## 3. Release 工作流
+
+| 工作流 | 触发 | 版本文件 | 产物 |
+|---|---|---|---|
+| `latest.yml` | `latest` 分支 | `version` | `dockerCopilot-.tar.gz` + `.sha256` |
+| `FNOS.yml` | `fn_version` 变更 / 手动 | `fn_version` | 同上,飞牛专用 |
+| `UGREEN.yml` | `ugreen_version` 变更 / 手动 | `ugreen_version` | 同上,绿联专用 |
+
+产物内是单个 `dockerCopilot-new`,`start.sh` 会在启动时把它移动为 `dockerCopilot`。
+
+## 4. DockerHub 工作流
+
+| 工作流 | 分支 | 镜像 tag |
+|---|---|---|
+| `docker-build-push-latest.yml` | `latest-dev` | `dockercopilot:latest-dev` |
+| `docker-build-push-latest-stable.yml` | `latest`(`version` 变更) | `dockercopilot:latest` |
+| `docker-build-push-UGREEN.yml` | `latest-dev` | `dockercopilot:UGREEN-DEV` |
+| `docker-build-push-UGREEN-stable.yml` | `latest`(`ugreen_version` 变更) | `dockercopilot:UGREEN` |
+
+需要 Secrets:
+
+- `DOCKERHUB_USERNAME`
+- `DOCKERHUB_TOKEN`
+
+fork 也可用;只要配置自己的 Secrets。
+
+## 5. 镜像构建
+
+`.github/workflows/docker-build-push-*.yml` 的流程:
+
+1. 交叉编译 `dist/linux//dockerCopilot`;
+2. 整理 `dc-back-/{dist,etc}`;
+3. Docker buildx 构建并推送;
+4. `docker/Dockerfile` 只 COPY:
+ - `dc-back/dist/$TARGETPLATFORM/dockerCopilot`
+ - `dc-back/etc/`
+ - `start.sh`
+
+## 6. 发布前检查
+
+```bash
+skills/dockercopilot-maintainer/scripts/release-check.sh
+```
+
+检查项:
+
+1. `git status` 是否干净;
+2. `version` 与目标 tag 是否一致;
+3. `gofmt`、`go build`、`go vet`、`go test`;
+4. 是否误提交 `dist/`、`logs/`、临时文件;
+5. `.dockerignore` 是否包含 `skills/`;
+6. Dockerfile 是否只 COPY 白名单;
+7. 是否包含 `.sha256` 生成逻辑。
+
+## 7. 发布后验证
+
+```bash
+# 二进制
+tar -tzf dockerCopilot-amd64.tar.gz
+
+# 校验
+sha256sum -c dockerCopilot-amd64.tar.gz.sha256
+
+# 镜像内不应出现 skills
+docker run --rm --entrypoint sh -c 'ls /app'
+```
+
+镜像 `/app` 应只有:
+
+```text
+dockerCopilot
+etc/
+start.sh
+```
+
+不应出现 `skills/`、`docs/`、`static/`。
+
+## 8. 常见发布问题
+
+| 问题 | 原因 | 处理 |
+|---|---|---|
+| Release 404 | `UPDATE_REPO`/`UPDATE_BRANCH` 与资产名不匹配 | 检查 `version`、仓库、资产命名 |
+| 自更新 SHA256 失败 | 资产未上传 `.sha256` 或内容不匹配 | 重新生成并上传 |
+| DockerHub push 失败 | Secrets 未配置或无权限 | 检查 `DOCKERHUB_USERNAME/TOKEN` |
+| 镜像里出现源码 | Dockerfile 被改成 `COPY . .` | 恢复白名单 COPY |
+| MCP 端点 401 | 客户端未带 token | 用 `MCP_TOKEN`/`secretKey` 配 Authorization |
+| FNOS 版本被通用版覆盖 | 版本检查/更新逻辑未区分 | 保留 `FNOS` 判断 |
diff --git a/skills/dockercopilot-maintainer/references/troubleshooting.md b/skills/dockercopilot-maintainer/references/troubleshooting.md
new file mode 100644
index 00000000..5a9bc005
--- /dev/null
+++ b/skills/dockercopilot-maintainer/references/troubleshooting.md
@@ -0,0 +1,323 @@
+# 排障手册
+
+## 1. 启动
+
+### 启动即退出
+
+日志关键字:
+
+```text
+secretKey 配置不合法
+Docker 客户端初始化失败
+```
+
+检查:
+
+```bash
+grep -n "secretKey" etc/dockerCopilot.yaml
+ls -l /var/run/docker.sock
+docker version
+```
+
+要求:
+
+- `secretKey` 非空;
+- 长度 > 8;
+- 不能是纯数字;
+- 不能包含未展开的 `${...}`。
+
+### 打开首页没有界面
+
+本项目已移除网页前端,`GET /` 只返回一段 JSON 状态(`API-only mode`),这是预期行为。
+管理操作走 `/api/*` 或 `/mcp`,见 `docs/MCP.md`。
+
+## 2. 容器更新
+
+### 更新后容器没了 / 旧容器还在
+
+检查进度详情:
+
+```bash
+curl -H "Authorization: Bearer $TOKEN" \
+ http://127.0.0.1:12712/api/progress/
+```
+
+重点字段:
+
+- `Percentage`
+- `Message`
+- `DetailMsg`
+- `IsDone`
+
+日志关键字:
+
+```text
+停止容器失败
+重命名旧容器失败
+创建新容器失败
+启动新容器失败
+新容器健康检查失败
+回滚
+```
+
+### 更新成功但端口冲突
+
+可能原因:
+
+1. 旧容器仍在运行;
+2. 旧容器 paused,端口仍被占用;
+3. 新容器启动前旧容器未停止。
+
+处理:
+
+```bash
+docker ps -a --filter name=
+docker inspect --format '{{.State.Status}} {{.State.Paused}}'
+```
+
+`force` 恢复/更新会先停止并改名旧容器;若仍冲突,检查是否有其他容器占用同一端口。
+
+### 健康检查超时
+
+`waitForContainerHealthy` 默认最多 90 秒。
+
+如果容器 `health-start-period` 很长,可能误判。临时处理:
+
+- 去掉/缩短 healthcheck;
+- 或调整 `waitForContainerHealthy` 的超时计算(建议按 `StartPeriod + Interval*Retries` 动态计算,并设上限)。
+
+## 3. 备份 / 恢复
+
+### 恢复后容器是停止的
+
+检查备份 JSON 是否含:
+
+```json
+"stateKnown": true,
+"wasRunning": true,
+"wasPaused": false
+```
+
+旧备份没有这些字段时,`restoreShouldStart` 默认返回 `true`。如果仍然停止,检查请求里的 `start` 是否显式传了 `false`。
+
+### 同名容器已存在
+
+默认行为:
+
+- 镜像一致:跳过创建,必要时补启动;
+- 镜像不一致:报错,提示使用 `force=true`。
+
+要覆盖:
+
+```json
+{"filename":"backup.json","force":true}
+```
+
+### `force=true` 失败后旧容器不见了
+
+理论上不会:旧容器先改名,不删除;失败会改回原名。
+
+检查:
+
+```bash
+docker ps -a --filter name=-dc-old-
+```
+
+如果看到 `-dc-old-` 容器,说明回滚改名失败。日志会带:
+
+```text
+回滚异常: 旧容器改回原名失败
+```
+
+手动恢复:
+
+```bash
+docker rename -dc-old-
+docker start
+```
+
+### 备份文件读取失败
+
+`ResolveBackupPath` 只允许安全文件名:
+
+- 大小写字母;
+- 数字;
+- 短横线;
+- `.json` 后缀。
+
+不要传路径、`..`、斜杠。
+
+## 4. 网络
+
+### 重建后 IP / MAC 冲突
+
+确认是否经过:
+
+```go
+sanitizeEndpointSettings(...)
+```
+
+应清空:
+
+- `NetworkID`
+- `EndpointID`
+- `Gateway`
+- `IPAddress`
+- `MacAddress`
+- `DNSNames`
+
+保留:
+
+- `Aliases`
+- `Links`
+- `DriverOpts`
+- `IPAMConfig`
+
+### 自定义网络丢失
+
+Compose 导出只保留非 `bridge/host/none` 的网络;恢复 JSON 时 `NetworkingConfig` 必须包含 `EndpointsConfig`。
+
+检查备份:
+
+```bash
+jq '.NetworkingConfig.EndpointsConfig' backup.json
+```
+
+## 5. 镜像更新检测
+
+### 总是提示有更新
+
+检查:
+
+1. registry 是否需要认证;
+2. `GetDigest` 是否拿到远程 digest;
+3. 本地 `RepoDigests` 是否为空;
+4. 是否配置了 `REGISTRY_INSECURE`;
+5. 代理是否污染 registry 请求。
+
+日志关键字:
+
+```text
+获取token失败
+获取digest失败
+未在本地获取到repoDigest
+Digest为空
+```
+
+### 私有镜像无法检测
+
+`GetBearerHeader` 会使用 `registryAuth`;若没有凭据,只能判断公开接口。
+
+## 6. 自更新
+
+### 404
+
+检查:
+
+- `UPDATE_REPO`
+- `UPDATE_BRANCH`
+- `version` 文件内容
+- Release 资产名:`dockerCopilot-.tar.gz`
+- `githubProxy` 是否拼接正确
+
+日志/错误可能显示:
+
+```text
+获取版本信息失败:未找到
+下载 ... 失败:未找到
+```
+
+### SHA256 失败
+
+- 确认 `.sha256` 与 tar.gz 同批上传;
+- 不要手动改包;
+- 旧版本过渡可临时 `UPDATE_SKIP_CHECKSUM=true`,但不推荐长期使用。
+
+### 解压后找不到 `dockerCopilot-new`
+
+发布包结构应为:
+
+```text
+dockerCopilot-new
+```
+
+或旧结构:
+
+```text
+dist/linux//dockerCopilot-new
+```
+
+## 7. 日志
+
+### 日志中间出现 `dockerCopilot`
+
+两种来源:
+
+1. `docker compose logs` 的服务名前缀:
+
+ ```bash
+ docker compose logs --no-log-prefix -f
+ ```
+
+2. 旧版本 `logx` 配置带了服务名。当前 `SetupLog` 使用:
+
+ ```go
+ Encoding: "plain"
+ ```
+
+ 输出应为:
+
+ ```text
+ 2026-10-30 01:20:11 INFO 备份容器开始
+ ```
+
+### 日志是 JSON
+
+检查 `SetupLog` 是否被改回默认 JSON。应保留:
+
+```go
+Encoding: "plain"
+```
+
+### 日志级别
+
+`SetupLog` 当前固定 `Level: "info"`。需要 debug 时临时改为 `debug`,不要提交。
+
+## 8. Docker / 权限
+
+### `permission denied` 访问 docker.sock
+
+```bash
+ls -l /var/run/docker.sock
+```
+
+容器内运行需要挂载 socket;非 root 运行需要 `group_add` 对应 docker 组 GID。
+
+### Docker 暂不可用
+
+启动时 Docker 不可达不会退出;定时任务会重试。检查:
+
+```bash
+docker version
+echo $DOCKER_HOST
+```
+
+## 9. MCP
+
+### `/mcp` 返回 401
+
+- 确认 token:`MCP_TOKEN` 优先,否则用 `secretKey`;
+- 三种携带方式任选其一:`Authorization: Bearer`、`X-MCP-Token`、`?token=`;
+- 若刻意关闭:`MCP_TOKEN=off`(仅隔离内网)。
+
+### 客户端握手失败 / 工具为空
+
+- 本项目只提供 Streamable HTTP,不支持 stdio;
+- 先 `initialize` 拿 `Mcp-Session-Id`,后续请求必须带上;
+- `Accept` 需包含 `application/json, text/event-stream`;
+- 启动日志应出现 `MCP 已挂载: /mcp ... 共 26 个工具`,没有则检查 `mcp.Register` 是否被调用。
+
+### 工具返回 `isError`
+
+业务错误(如 Docker 不可达)会以 `isError: true` + 文本返回,读 `content[0].text`。
+用 `dc_info` 看 `dockerReady` / `dockerError`。
diff --git a/skills/dockercopilot-maintainer/scripts/build-linux.sh b/skills/dockercopilot-maintainer/scripts/build-linux.sh
new file mode 100644
index 00000000..0831cde1
--- /dev/null
+++ b/skills/dockercopilot-maintainer/scripts/build-linux.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+# 本地交叉编译 Linux 二进制。
+# 用法:
+# scripts/build-linux.sh [amd64|arm64] [version-file]
+# 默认:amd64 + version
+set -euo pipefail
+
+ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
+cd "$ROOT"
+
+ARCH="${1:-amd64}"
+VERSION_FILE="${2:-version}"
+
+case "$ARCH" in
+ amd64|arm64) ;;
+ *) echo "不支持的架构: $ARCH(只支持 amd64 / arm64)" >&2; exit 1 ;;
+esac
+
+if [ ! -f "$VERSION_FILE" ]; then
+ echo "版本文件不存在: $VERSION_FILE" >&2
+ exit 1
+fi
+
+VERSION="$(tr -d '\r\n' < "$VERSION_FILE")"
+BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+OUT_DIR="dist/linux/$ARCH"
+OUT="$OUT_DIR/dockerCopilot-new"
+
+mkdir -p "$OUT_DIR"
+
+echo "编译: $OUT"
+echo "版本: $VERSION"
+echo "日期: $BUILD_DATE"
+
+CGO_ENABLED=0 GOOS=linux GOARCH="$ARCH" \
+ go build -a --trimpath \
+ -ldflags="-w -s -X 'github.com/onlyLTY/dockerCopilot/internal/config.Version=$VERSION' -X 'github.com/onlyLTY/dockerCopilot/internal/config.BuildDate=$BUILD_DATE'" \
+ -o "$OUT" .
+
+ls -lh "$OUT"
diff --git a/skills/dockercopilot-maintainer/scripts/dev-check.sh b/skills/dockercopilot-maintainer/scripts/dev-check.sh
new file mode 100644
index 00000000..b6991da3
--- /dev/null
+++ b/skills/dockercopilot-maintainer/scripts/dev-check.sh
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+# 开发检查:gofmt + build + vet + test,可选 race。
+# 用法:
+# scripts/dev-check.sh # 常规
+# scripts/dev-check.sh --race # 追加 -race
+set -euo pipefail
+
+ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
+cd "$ROOT"
+
+RACE=0
+if [ "${1:-}" = "--race" ] || [ "${RACE:-0}" = "1" ]; then
+ RACE=1
+fi
+
+echo "== gofmt =="
+UNFORMATTED="$(gofmt -l $(git ls-files '*.go' 2>/dev/null || find . -name '*.go' -not -path './.git/*'))"
+if [ -n "$UNFORMATTED" ]; then
+ echo "以下文件未格式化:"
+ echo "$UNFORMATTED"
+ echo "执行: gofmt -w "
+ exit 1
+fi
+echo "通过"
+echo
+
+echo "== go build =="
+go build ./...
+echo "通过"
+echo
+
+echo "== go vet =="
+go vet ./...
+echo "通过"
+echo
+
+echo "== go test =="
+go test ./... -count=1
+echo "通过"
+echo
+
+if [ "$RACE" = "1" ]; then
+ echo "== go test -race =="
+ go test -race ./... -count=1
+ echo "通过"
+ echo
+fi
+
+echo "== git diff --check =="
+git diff --check
+echo "通过"
+echo
+
+echo "全部检查通过"
diff --git a/skills/dockercopilot-maintainer/scripts/package-audit.sh b/skills/dockercopilot-maintainer/scripts/package-audit.sh
new file mode 100644
index 00000000..1181d8b1
--- /dev/null
+++ b/skills/dockercopilot-maintainer/scripts/package-audit.sh
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+# 镜像边界审计:确保 skills/ 不会进入 Docker 镜像。
+set -euo pipefail
+
+ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
+cd "$ROOT"
+
+FAIL=0
+warn() { echo "警告: $*"; }
+fail() { echo "失败: $*"; FAIL=1; }
+
+echo "== 1. Dockerfile COPY/ADD 白名单 =="
+DOCKERFILE="docker/Dockerfile"
+if [ ! -f "$DOCKERFILE" ]; then
+ fail "找不到 $DOCKERFILE"
+else
+ # 禁止 COPY . / ADD .
+ if grep -nE '^[[:space:]]*(COPY|ADD)[[:space:]]+\.([[:space:]]|$)' "$DOCKERFILE"; then
+ fail "Dockerfile 使用了 COPY . / ADD .,会把 skills 等开发资料带进镜像"
+ else
+ echo "未发现 COPY . / ADD ."
+ fi
+
+ # 逐条检查 COPY/ADD:只允许 dc-back 与 start.sh
+ while IFS= read -r line; do
+ case "$line" in
+ *dc-back*|*start.sh*)
+ ;;
+ *)
+ fail "Dockerfile 出现非白名单 COPY/ADD: $line"
+ ;;
+ esac
+ done < <(grep -E '^[[:space:]]*(COPY|ADD)[[:space:]]' "$DOCKERFILE" || true)
+
+ if grep -q 'skills' "$DOCKERFILE"; then
+ fail "Dockerfile 显式引用了 skills"
+ fi
+fi
+echo
+
+echo "== 2. .dockerignore =="
+if [ ! -f .dockerignore ]; then
+ fail "缺少 .dockerignore"
+else
+ if grep -qxE 'skills/?|/skills/?' .dockerignore; then
+ echo ".dockerignore 已排除 skills"
+ else
+ fail ".dockerignore 未排除 skills"
+ fi
+fi
+echo
+
+echo "== 3. 发布包内容(如果已生成) =="
+FOUND=0
+for f in dockerCopilot-*.tar.gz; do
+ [ -e "$f" ] || continue
+ FOUND=1
+ echo "检查 $f"
+ if tar -tzf "$f" | grep -qE '(^|/)skills/'; then
+ fail "$f 包含 skills/"
+ else
+ echo " 未包含 skills/"
+ fi
+done
+if [ "$FOUND" = "0" ]; then
+ echo "没有本地发布包,跳过"
+fi
+echo
+
+echo "== 4. 源码泄漏检查(Dockerfile) =="
+if grep -rqE 'go:embed[[:space:]]+front' --include='*.go' . 2>/dev/null; then
+ fail "仍有 go:embed front/*;前端已移除,不应再嵌入"
+else
+ echo "未发现前端 go:embed"
+fi
+if grep -qE '^[[:space:]]*(COPY|ADD)[[:space:]]+(\./)?(internal|docs|static|assets)' "$DOCKERFILE" 2>/dev/null; then
+ fail "Dockerfile 直接 COPY 源码/文档目录"
+else
+ echo "未直接 COPY 源码/文档目录"
+fi
+echo
+
+if [ "$FAIL" != "0" ]; then
+ echo "审计未通过"
+ exit 1
+fi
+
+echo "审计通过:skills/ 不会进入镜像"
diff --git a/skills/dockercopilot-maintainer/scripts/preflight.sh b/skills/dockercopilot-maintainer/scripts/preflight.sh
new file mode 100644
index 00000000..65f45501
--- /dev/null
+++ b/skills/dockercopilot-maintainer/scripts/preflight.sh
@@ -0,0 +1,107 @@
+#!/usr/bin/env bash
+# dockerCopilot 预检:只读,不修改仓库。
+set -u
+
+# 常见 Go 安装路径兜底(NAS/非交互 shell 常缺 PATH)
+for d in /usr/local/go/bin /tmp/go/bin "$HOME/go/bin"; do
+ [ -x "$d/go" ] && PATH="$d:$PATH"
+done
+export PATH
+
+ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
+cd "$ROOT" || exit 1
+
+echo "== 仓库 =="
+echo "路径: $ROOT"
+echo "分支: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '非 git 仓库')"
+echo "提交: $(git rev-parse --short HEAD 2>/dev/null || echo '无')"
+echo
+
+echo "== 工作区 =="
+if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "有未提交改动:"
+ git status --short
+ else
+ echo "干净"
+ fi
+else
+ echo "不是 git 仓库"
+fi
+echo
+
+echo "== 工具 =="
+for c in go git docker node npm; do
+ if command -v "$c" >/dev/null 2>&1; then
+ case "$c" in
+ go) v="$("$c" version 2>/dev/null | head -n 1)" ;;
+ git) v="$("$c" --version 2>/dev/null | head -n 1)" ;;
+ docker) v="$("$c" --version 2>/dev/null | head -n 1)" ;;
+ node) v="$("$c" --version 2>/dev/null | head -n 1)" ;;
+ npm) v="$("$c" --version 2>/dev/null | head -n 1)" ;;
+ *) v="未知" ;;
+ esac
+ printf '%-7s %s\n' "$c" "$v"
+ else
+ printf '%-7s 未安装\n' "$c"
+ fi
+done
+echo
+
+echo "== 版本文件 =="
+for f in version fn_version ugreen_version; do
+ if [ -f "$f" ]; then
+ printf '%-15s %s\n' "$f" "$(tr -d '\r\n' < "$f")"
+ else
+ printf '%-15s 缺失\n' "$f"
+ fi
+done
+echo
+
+echo "== MCP =="
+if [ -d internal/mcp ]; then
+ TOOL_COUNT=$(grep -rho 'NewTool("dc_[a-z_]*"' internal/mcp/*.go | sort -u | wc -l | tr -d ' ')
+ echo "internal/mcp 工具数: ${TOOL_COUNT}(期望 26)"
+ if [ "$TOOL_COUNT" -eq 0 ]; then
+ echo "警告: 未发现 dc_* 工具注册"
+ fi
+ if grep -q "MCP_TOKEN" internal/mcp/server.go 2>/dev/null; then
+ echo "MCP 鉴权: 已实现(MCP_TOKEN > secretKey)"
+ else
+ echo "警告: 未找到 MCP 鉴权逻辑"
+ fi
+else
+ echo "警告: internal/mcp 不存在,/mcp 未接入"
+fi
+echo
+
+echo "== Docker =="
+if command -v docker >/dev/null 2>&1; then
+ if docker version >/dev/null 2>&1; then
+ echo "Docker 可用"
+ docker version --format 'Server: {{.Server.Version}}' 2>/dev/null || true
+ else
+ echo "Docker 客户端存在,但无法连接守护进程"
+ echo "DOCKER_HOST=${DOCKER_HOST:-未设置}"
+ fi
+else
+ echo "未安装 docker"
+fi
+echo
+
+echo "== 构建边界 =="
+if [ -f .dockerignore ]; then
+ if grep -qx 'skills' .dockerignore 2>/dev/null || grep -qx 'skills/' .dockerignore 2>/dev/null; then
+ echo ".dockerignore 已排除 skills"
+ else
+ echo "警告: .dockerignore 未排除 skills"
+ fi
+else
+ echo "警告: 没有 .dockerignore(Dockerfile 虽为白名单 COPY,仍建议补上)"
+fi
+
+if grep -qE '^[[:space:]]*(COPY|ADD)[[:space:]]+\. ' docker/Dockerfile 2>/dev/null; then
+ echo "警告: Dockerfile 出现 COPY . / ADD .,可能把 skills 打进镜像"
+else
+ echo "Dockerfile 未使用 COPY . / ADD ."
+fi
diff --git a/skills/dockercopilot-maintainer/scripts/release-check.sh b/skills/dockercopilot-maintainer/scripts/release-check.sh
new file mode 100644
index 00000000..f111fa88
--- /dev/null
+++ b/skills/dockercopilot-maintainer/scripts/release-check.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+# 发布前检查:版本、测试、构建、镜像边界。
+# 用法:
+# scripts/release-check.sh [version]
+# 不传 version 时读取 ./version。
+set -euo pipefail
+
+ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
+cd "$ROOT"
+HERE="$(cd "$(dirname "$0")" && pwd)"
+
+TARGET="${1:-$(tr -d '\r\n' < version 2>/dev/null || true)}"
+
+echo "== 目标版本 =="
+if [ -z "$TARGET" ]; then
+ echo "版本为空" >&2
+ exit 1
+fi
+echo "$TARGET"
+echo
+
+echo "== 1. 工作区 =="
+if [ -n "$(git status --porcelain 2>/dev/null || true)" ]; then
+ echo "警告: 工作区有未提交改动"
+ git status --short
+else
+ echo "干净"
+fi
+echo
+
+echo "== 2. 版本文件一致性 =="
+for f in version fn_version ugreen_version; do
+ if [ -f "$f" ]; then
+ v="$(tr -d '\r\n' < "$f")"
+ printf '%-15s %s\n' "$f" "$v"
+ fi
+done
+echo "目标: $TARGET"
+echo
+
+echo "== 3. 开发检查 =="
+"$HERE/dev-check.sh"
+echo
+
+echo "== 4. 镜像边界 =="
+"$HERE/package-audit.sh"
+echo
+
+echo "== 5. MCP =="
+if go test ./internal/mcp/ -count=1 >/dev/null 2>&1; then
+ COUNT=$(grep -rho 'NewTool("dc_[a-z_]*"' internal/mcp/*.go | sort -u | wc -l | tr -d ' ')
+ echo "MCP 测试通过,注册工具数: ${COUNT}"
+else
+ echo "失败: internal/mcp 测试未通过" >&2
+ exit 1
+fi
+if grep -q "MCP_TOKEN" docs/MCP.md 2>/dev/null; then
+ echo "docs/MCP.md 已说明鉴权"
+else
+ echo "警告: docs/MCP.md 缺少 MCP_TOKEN 说明"
+fi
+echo
+
+echo "== 6. 敏感文件 =="
+for p in .env .env.local etc/secret.yaml; do
+ if [ -e "$p" ] && git ls-files --error-unmatch "$p" >/dev/null 2>&1; then
+ echo "失败: $p 已被 git 跟踪"
+ exit 1
+ fi
+done
+echo "未发现被跟踪的 .env/secret 文件"
+echo
+
+echo "发布前检查通过"
diff --git a/start.sh b/start.sh
index 828a37d8..3548c079 100644
--- a/start.sh
+++ b/start.sh
@@ -1,12 +1,72 @@
#!/bin/sh
-cd "${WORKDIR}" || exit
-# 判断当前目录下是否存在名为 dockerCopilot-new 的二进制文件
+cd "${WORKDIR:-/app}" || exit
+
+# 新发布包把二进制放在归档根目录;旧发布包在 dist/linux// 下。
+new_bin=""
if [ -f "./dockerCopilot-new" ]; then
- # 如果存在,则用它覆盖 dockerCopilot
- mv ./dockerCopilot-new ./dockerCopilot
- # 赋予 dockerCopilot 执行权限
+ new_bin="./dockerCopilot-new"
+else
+ for candidate in ./dist/linux/*/dockerCopilot-new; do
+ if [ -f "$candidate" ]; then
+ new_bin="$candidate"
+ break
+ fi
+ done
+fi
+
+if [ -n "$new_bin" ]; then
+ # 换入新版本前保留上一版(dockerCopilot.bak),供启动失败时自动回退。
+ if [ -f ./dockerCopilot ]; then
+ cp ./dockerCopilot ./dockerCopilot.bak
+ fi
+ mv "$new_bin" ./dockerCopilot
chmod +x ./dockerCopilot
+ # 清掉上一轮回退可能留下的失败版本残留。
+ rm -f ./dockerCopilot.failed
fi
-# 运行 dockerCopilot
-./dockerCopilot
+# ---- 自更新闭环:新版连续启动失败时自动回退到上一版 ----
+FAIL_THRESHOLD=3
+FAIL_FILE=./.start-fail-count
+child=""
+
+on_term() {
+ # docker stop 发来的 SIGTERM 转发给子进程,让它优雅退出。
+ if [ -n "$child" ]; then
+ kill -TERM "$child" 2>/dev/null
+ fi
+}
+trap on_term TERM INT
+
+while true; do
+ start_ts=$(date +%s)
+ ./dockerCopilot &
+ child=$!
+ wait "$child"
+ code=$?
+ child=""
+ elapsed=$(( $(date +%s) - start_ts ))
+
+ # 以下情况不算启动失败:
+ # - 正常退出(0)或被信号终止(143=SIGTERM/137=SIGKILL);
+ # - 已稳定运行超过 30 秒(自更新的主动退出发生在服务运行良久之后,
+ # 崩溃循环则会在启动后几秒内退出)。
+ if [ "$elapsed" -ge 30 ] || [ "$code" -eq 0 ] || [ "$code" -eq 143 ] || [ "$code" -eq 137 ]; then
+ rm -f "$FAIL_FILE"
+ break
+ fi
+
+ fails=$(cat "$FAIL_FILE" 2>/dev/null || echo 0)
+ fails=$((fails + 1))
+ echo "$fails" > "$FAIL_FILE"
+ echo "dockerCopilot exited with code $code after ${elapsed}s (fail $fails/$FAIL_THRESHOLD)" >&2
+
+ if [ "$fails" -ge "$FAIL_THRESHOLD" ] && [ -f ./dockerCopilot.bak ]; then
+ echo "rolling back to previous binary" >&2
+ mv ./dockerCopilot ./dockerCopilot.failed
+ mv ./dockerCopilot.bak ./dockerCopilot
+ chmod +x ./dockerCopilot
+ rm -f "$FAIL_FILE"
+ fi
+ sleep 2
+done
diff --git a/static/asset/Circular-logo.png b/static/asset/Circular-logo.png
deleted file mode 100644
index 9d0cdd50..00000000
Binary files a/static/asset/Circular-logo.png and /dev/null differ
diff --git a/static/asset/Circularlogo.png b/static/asset/Circularlogo.png
deleted file mode 100644
index 9d0cdd50..00000000
Binary files a/static/asset/Circularlogo.png and /dev/null differ
diff --git a/static/asset/DockerCopilotlogo.png b/static/asset/DockerCopilotlogo.png
deleted file mode 100644
index eeb39bfd..00000000
Binary files a/static/asset/DockerCopilotlogo.png and /dev/null differ
diff --git a/static/asset/favicon.png b/static/asset/favicon.png
deleted file mode 100644
index 3baa78df..00000000
Binary files a/static/asset/favicon.png and /dev/null differ
diff --git a/static/asset/github.png b/static/asset/github.png
deleted file mode 100644
index b9eb51f7..00000000
Binary files a/static/asset/github.png and /dev/null differ
diff --git a/static/asset/ugreen.png b/static/asset/ugreen.png
deleted file mode 100644
index 1fbebfaf..00000000
Binary files a/static/asset/ugreen.png and /dev/null differ
diff --git a/static/asset/zspace.png b/static/asset/zspace.png
deleted file mode 100644
index 2d598153..00000000
Binary files a/static/asset/zspace.png and /dev/null differ
diff --git a/static/bootstrap/css/bootstrap.min.css b/static/bootstrap/css/bootstrap.min.css
deleted file mode 100644
index f5910acc..00000000
--- a/static/bootstrap/css/bootstrap.min.css
+++ /dev/null
@@ -1,6 +0,0 @@
-@charset "UTF-8";/*!
- * Bootstrap v5.3.2 (https://getbootstrap.com/)
- * Copyright 2011-2023 The Bootstrap Authors
- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
- */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-emphasis-color);--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-emphasis-color);--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:var(--bs-emphasis-color);--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:var(--bs-emphasis-color);--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#a6b5cc;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#b5b6b7;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#a7b9b1;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#a6c3ca;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#ccc2a4;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#c6acae;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#c6c7c8;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#4d5154;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control-plaintext~label::after,.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.form-floating>.form-control:disabled~label::after,.form-floating>:disabled~label::after{background-color:var(--bs-secondary-bg)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width) * -1);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:var(--bs-body-color);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(var(--bs-border-width) * -1)}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(var(--bs-border-width) * -1)}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23052c65'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-focus-border-color:#86b7fe;--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:var(--bs-accordion-btn-focus-border-color);outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button,.accordion-flush .accordion-item .accordion-button.collapsed{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:var(--bs-tertiary-bg);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:var(--bs-secondary-color);--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-secondary-bg);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:var(--bs-border-color);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-tertiary-bg);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#000;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;--bs-btn-close-white-filter:invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y))}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(10,88,202,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(86,94,100,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(20,108,67,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(61,213,243,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(255,205,57,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(176,42,55,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(249,250,251,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}
-/*# sourceMappingURL=bootstrap.min.css.map */
\ No newline at end of file
diff --git a/static/bootstrap/icons/bootstrap-icons.css b/static/bootstrap/icons/bootstrap-icons.css
deleted file mode 100644
index 6d3e532c..00000000
--- a/static/bootstrap/icons/bootstrap-icons.css
+++ /dev/null
@@ -1,8227 +0,0 @@
-/*!
- * Bootstrap Icons v1.11.2 (https://icons.getbootstrap.com/)
- * Copyright 2019-2023 The Bootstrap Authors
- * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
- */
-
-@font-face {
- font-display: block;
- font-family: "bootstrap-icons";
- src: url("./fonts/bootstrap-icons.woff2?7141511ac37f13e1a387fb9fc6646256") format("woff2"),
- url("./fonts/bootstrap-icons.woff?7141511ac37f13e1a387fb9fc6646256") format("woff");
-}
-
-.bi::before,
-[class^="bi-"]::before,
-[class*=" bi-"]::before {
- display: inline-block;
- font-family: bootstrap-icons !important;
- font-style: normal;
- font-weight: normal !important;
- font-variant: normal;
- text-transform: none;
- line-height: 1;
- vertical-align: -.125em;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-.bi-123::before {
- content: "\f67f";
-}
-
-.bi-alarm-fill::before {
- content: "\f101";
-}
-
-.bi-alarm::before {
- content: "\f102";
-}
-
-.bi-align-bottom::before {
- content: "\f103";
-}
-
-.bi-align-center::before {
- content: "\f104";
-}
-
-.bi-align-end::before {
- content: "\f105";
-}
-
-.bi-align-middle::before {
- content: "\f106";
-}
-
-.bi-align-start::before {
- content: "\f107";
-}
-
-.bi-align-top::before {
- content: "\f108";
-}
-
-.bi-alt::before {
- content: "\f109";
-}
-
-.bi-app-indicator::before {
- content: "\f10a";
-}
-
-.bi-app::before {
- content: "\f10b";
-}
-
-.bi-archive-fill::before {
- content: "\f10c";
-}
-
-.bi-archive::before {
- content: "\f10d";
-}
-
-.bi-arrow-90deg-down::before {
- content: "\f10e";
-}
-
-.bi-arrow-90deg-left::before {
- content: "\f10f";
-}
-
-.bi-arrow-90deg-right::before {
- content: "\f110";
-}
-
-.bi-arrow-90deg-up::before {
- content: "\f111";
-}
-
-.bi-arrow-bar-down::before {
- content: "\f112";
-}
-
-.bi-arrow-bar-left::before {
- content: "\f113";
-}
-
-.bi-arrow-bar-right::before {
- content: "\f114";
-}
-
-.bi-arrow-bar-up::before {
- content: "\f115";
-}
-
-.bi-arrow-clockwise::before {
- content: "\f116";
-}
-
-.bi-arrow-counterclockwise::before {
- content: "\f117";
-}
-
-.bi-arrow-down-circle-fill::before {
- content: "\f118";
-}
-
-.bi-arrow-down-circle::before {
- content: "\f119";
-}
-
-.bi-arrow-down-left-circle-fill::before {
- content: "\f11a";
-}
-
-.bi-arrow-down-left-circle::before {
- content: "\f11b";
-}
-
-.bi-arrow-down-left-square-fill::before {
- content: "\f11c";
-}
-
-.bi-arrow-down-left-square::before {
- content: "\f11d";
-}
-
-.bi-arrow-down-left::before {
- content: "\f11e";
-}
-
-.bi-arrow-down-right-circle-fill::before {
- content: "\f11f";
-}
-
-.bi-arrow-down-right-circle::before {
- content: "\f120";
-}
-
-.bi-arrow-down-right-square-fill::before {
- content: "\f121";
-}
-
-.bi-arrow-down-right-square::before {
- content: "\f122";
-}
-
-.bi-arrow-down-right::before {
- content: "\f123";
-}
-
-.bi-arrow-down-short::before {
- content: "\f124";
-}
-
-.bi-arrow-down-square-fill::before {
- content: "\f125";
-}
-
-.bi-arrow-down-square::before {
- content: "\f126";
-}
-
-.bi-arrow-down-up::before {
- content: "\f127";
-}
-
-.bi-arrow-down::before {
- content: "\f128";
-}
-
-.bi-arrow-left-circle-fill::before {
- content: "\f129";
-}
-
-.bi-arrow-left-circle::before {
- content: "\f12a";
-}
-
-.bi-arrow-left-right::before {
- content: "\f12b";
-}
-
-.bi-arrow-left-short::before {
- content: "\f12c";
-}
-
-.bi-arrow-left-square-fill::before {
- content: "\f12d";
-}
-
-.bi-arrow-left-square::before {
- content: "\f12e";
-}
-
-.bi-arrow-left::before {
- content: "\f12f";
-}
-
-.bi-arrow-repeat::before {
- content: "\f130";
-}
-
-.bi-arrow-return-left::before {
- content: "\f131";
-}
-
-.bi-arrow-return-right::before {
- content: "\f132";
-}
-
-.bi-arrow-right-circle-fill::before {
- content: "\f133";
-}
-
-.bi-arrow-right-circle::before {
- content: "\f134";
-}
-
-.bi-arrow-right-short::before {
- content: "\f135";
-}
-
-.bi-arrow-right-square-fill::before {
- content: "\f136";
-}
-
-.bi-arrow-right-square::before {
- content: "\f137";
-}
-
-.bi-arrow-right::before {
- content: "\f138";
-}
-
-.bi-arrow-up-circle-fill::before {
- content: "\f139";
-}
-
-.bi-arrow-up-circle::before {
- content: "\f13a";
-}
-
-.bi-arrow-up-left-circle-fill::before {
- content: "\f13b";
-}
-
-.bi-arrow-up-left-circle::before {
- content: "\f13c";
-}
-
-.bi-arrow-up-left-square-fill::before {
- content: "\f13d";
-}
-
-.bi-arrow-up-left-square::before {
- content: "\f13e";
-}
-
-.bi-arrow-up-left::before {
- content: "\f13f";
-}
-
-.bi-arrow-up-right-circle-fill::before {
- content: "\f140";
-}
-
-.bi-arrow-up-right-circle::before {
- content: "\f141";
-}
-
-.bi-arrow-up-right-square-fill::before {
- content: "\f142";
-}
-
-.bi-arrow-up-right-square::before {
- content: "\f143";
-}
-
-.bi-arrow-up-right::before {
- content: "\f144";
-}
-
-.bi-arrow-up-short::before {
- content: "\f145";
-}
-
-.bi-arrow-up-square-fill::before {
- content: "\f146";
-}
-
-.bi-arrow-up-square::before {
- content: "\f147";
-}
-
-.bi-arrow-up::before {
- content: "\f148";
-}
-
-.bi-arrows-angle-contract::before {
- content: "\f149";
-}
-
-.bi-arrows-angle-expand::before {
- content: "\f14a";
-}
-
-.bi-arrows-collapse::before {
- content: "\f14b";
-}
-
-.bi-arrows-expand::before {
- content: "\f14c";
-}
-
-.bi-arrows-fullscreen::before {
- content: "\f14d";
-}
-
-.bi-arrows-move::before {
- content: "\f14e";
-}
-
-.bi-aspect-ratio-fill::before {
- content: "\f14f";
-}
-
-.bi-aspect-ratio::before {
- content: "\f150";
-}
-
-.bi-asterisk::before {
- content: "\f151";
-}
-
-.bi-at::before {
- content: "\f152";
-}
-
-.bi-award-fill::before {
- content: "\f153";
-}
-
-.bi-award::before {
- content: "\f154";
-}
-
-.bi-back::before {
- content: "\f155";
-}
-
-.bi-backspace-fill::before {
- content: "\f156";
-}
-
-.bi-backspace-reverse-fill::before {
- content: "\f157";
-}
-
-.bi-backspace-reverse::before {
- content: "\f158";
-}
-
-.bi-backspace::before {
- content: "\f159";
-}
-
-.bi-badge-3d-fill::before {
- content: "\f15a";
-}
-
-.bi-badge-3d::before {
- content: "\f15b";
-}
-
-.bi-badge-4k-fill::before {
- content: "\f15c";
-}
-
-.bi-badge-4k::before {
- content: "\f15d";
-}
-
-.bi-badge-8k-fill::before {
- content: "\f15e";
-}
-
-.bi-badge-8k::before {
- content: "\f15f";
-}
-
-.bi-badge-ad-fill::before {
- content: "\f160";
-}
-
-.bi-badge-ad::before {
- content: "\f161";
-}
-
-.bi-badge-ar-fill::before {
- content: "\f162";
-}
-
-.bi-badge-ar::before {
- content: "\f163";
-}
-
-.bi-badge-cc-fill::before {
- content: "\f164";
-}
-
-.bi-badge-cc::before {
- content: "\f165";
-}
-
-.bi-badge-hd-fill::before {
- content: "\f166";
-}
-
-.bi-badge-hd::before {
- content: "\f167";
-}
-
-.bi-badge-tm-fill::before {
- content: "\f168";
-}
-
-.bi-badge-tm::before {
- content: "\f169";
-}
-
-.bi-badge-vo-fill::before {
- content: "\f16a";
-}
-
-.bi-badge-vo::before {
- content: "\f16b";
-}
-
-.bi-badge-vr-fill::before {
- content: "\f16c";
-}
-
-.bi-badge-vr::before {
- content: "\f16d";
-}
-
-.bi-badge-wc-fill::before {
- content: "\f16e";
-}
-
-.bi-badge-wc::before {
- content: "\f16f";
-}
-
-.bi-bag-check-fill::before {
- content: "\f170";
-}
-
-.bi-bag-check::before {
- content: "\f171";
-}
-
-.bi-bag-dash-fill::before {
- content: "\f172";
-}
-
-.bi-bag-dash::before {
- content: "\f173";
-}
-
-.bi-bag-fill::before {
- content: "\f174";
-}
-
-.bi-bag-plus-fill::before {
- content: "\f175";
-}
-
-.bi-bag-plus::before {
- content: "\f176";
-}
-
-.bi-bag-x-fill::before {
- content: "\f177";
-}
-
-.bi-bag-x::before {
- content: "\f178";
-}
-
-.bi-bag::before {
- content: "\f179";
-}
-
-.bi-bar-chart-fill::before {
- content: "\f17a";
-}
-
-.bi-bar-chart-line-fill::before {
- content: "\f17b";
-}
-
-.bi-bar-chart-line::before {
- content: "\f17c";
-}
-
-.bi-bar-chart-steps::before {
- content: "\f17d";
-}
-
-.bi-bar-chart::before {
- content: "\f17e";
-}
-
-.bi-basket-fill::before {
- content: "\f17f";
-}
-
-.bi-basket::before {
- content: "\f180";
-}
-
-.bi-basket2-fill::before {
- content: "\f181";
-}
-
-.bi-basket2::before {
- content: "\f182";
-}
-
-.bi-basket3-fill::before {
- content: "\f183";
-}
-
-.bi-basket3::before {
- content: "\f184";
-}
-
-.bi-battery-charging::before {
- content: "\f185";
-}
-
-.bi-battery-full::before {
- content: "\f186";
-}
-
-.bi-battery-half::before {
- content: "\f187";
-}
-
-.bi-battery::before {
- content: "\f188";
-}
-
-.bi-bell-fill::before {
- content: "\f189";
-}
-
-.bi-bell::before {
- content: "\f18a";
-}
-
-.bi-bezier::before {
- content: "\f18b";
-}
-
-.bi-bezier2::before {
- content: "\f18c";
-}
-
-.bi-bicycle::before {
- content: "\f18d";
-}
-
-.bi-binoculars-fill::before {
- content: "\f18e";
-}
-
-.bi-binoculars::before {
- content: "\f18f";
-}
-
-.bi-blockquote-left::before {
- content: "\f190";
-}
-
-.bi-blockquote-right::before {
- content: "\f191";
-}
-
-.bi-book-fill::before {
- content: "\f192";
-}
-
-.bi-book-half::before {
- content: "\f193";
-}
-
-.bi-book::before {
- content: "\f194";
-}
-
-.bi-bookmark-check-fill::before {
- content: "\f195";
-}
-
-.bi-bookmark-check::before {
- content: "\f196";
-}
-
-.bi-bookmark-dash-fill::before {
- content: "\f197";
-}
-
-.bi-bookmark-dash::before {
- content: "\f198";
-}
-
-.bi-bookmark-fill::before {
- content: "\f199";
-}
-
-.bi-bookmark-heart-fill::before {
- content: "\f19a";
-}
-
-.bi-bookmark-heart::before {
- content: "\f19b";
-}
-
-.bi-bookmark-plus-fill::before {
- content: "\f19c";
-}
-
-.bi-bookmark-plus::before {
- content: "\f19d";
-}
-
-.bi-bookmark-star-fill::before {
- content: "\f19e";
-}
-
-.bi-bookmark-star::before {
- content: "\f19f";
-}
-
-.bi-bookmark-x-fill::before {
- content: "\f1a0";
-}
-
-.bi-bookmark-x::before {
- content: "\f1a1";
-}
-
-.bi-bookmark::before {
- content: "\f1a2";
-}
-
-.bi-bookmarks-fill::before {
- content: "\f1a3";
-}
-
-.bi-bookmarks::before {
- content: "\f1a4";
-}
-
-.bi-bookshelf::before {
- content: "\f1a5";
-}
-
-.bi-bootstrap-fill::before {
- content: "\f1a6";
-}
-
-.bi-bootstrap-reboot::before {
- content: "\f1a7";
-}
-
-.bi-bootstrap::before {
- content: "\f1a8";
-}
-
-.bi-border-all::before {
- content: "\f1a9";
-}
-
-.bi-border-bottom::before {
- content: "\f1aa";
-}
-
-.bi-border-center::before {
- content: "\f1ab";
-}
-
-.bi-border-inner::before {
- content: "\f1ac";
-}
-
-.bi-border-left::before {
- content: "\f1ad";
-}
-
-.bi-border-middle::before {
- content: "\f1ae";
-}
-
-.bi-border-outer::before {
- content: "\f1af";
-}
-
-.bi-border-right::before {
- content: "\f1b0";
-}
-
-.bi-border-style::before {
- content: "\f1b1";
-}
-
-.bi-border-top::before {
- content: "\f1b2";
-}
-
-.bi-border-width::before {
- content: "\f1b3";
-}
-
-.bi-border::before {
- content: "\f1b4";
-}
-
-.bi-bounding-box-circles::before {
- content: "\f1b5";
-}
-
-.bi-bounding-box::before {
- content: "\f1b6";
-}
-
-.bi-box-arrow-down-left::before {
- content: "\f1b7";
-}
-
-.bi-box-arrow-down-right::before {
- content: "\f1b8";
-}
-
-.bi-box-arrow-down::before {
- content: "\f1b9";
-}
-
-.bi-box-arrow-in-down-left::before {
- content: "\f1ba";
-}
-
-.bi-box-arrow-in-down-right::before {
- content: "\f1bb";
-}
-
-.bi-box-arrow-in-down::before {
- content: "\f1bc";
-}
-
-.bi-box-arrow-in-left::before {
- content: "\f1bd";
-}
-
-.bi-box-arrow-in-right::before {
- content: "\f1be";
-}
-
-.bi-box-arrow-in-up-left::before {
- content: "\f1bf";
-}
-
-.bi-box-arrow-in-up-right::before {
- content: "\f1c0";
-}
-
-.bi-box-arrow-in-up::before {
- content: "\f1c1";
-}
-
-.bi-box-arrow-left::before {
- content: "\f1c2";
-}
-
-.bi-box-arrow-right::before {
- content: "\f1c3";
-}
-
-.bi-box-arrow-up-left::before {
- content: "\f1c4";
-}
-
-.bi-box-arrow-up-right::before {
- content: "\f1c5";
-}
-
-.bi-box-arrow-up::before {
- content: "\f1c6";
-}
-
-.bi-box-seam::before {
- content: "\f1c7";
-}
-
-.bi-box::before {
- content: "\f1c8";
-}
-
-.bi-braces::before {
- content: "\f1c9";
-}
-
-.bi-bricks::before {
- content: "\f1ca";
-}
-
-.bi-briefcase-fill::before {
- content: "\f1cb";
-}
-
-.bi-briefcase::before {
- content: "\f1cc";
-}
-
-.bi-brightness-alt-high-fill::before {
- content: "\f1cd";
-}
-
-.bi-brightness-alt-high::before {
- content: "\f1ce";
-}
-
-.bi-brightness-alt-low-fill::before {
- content: "\f1cf";
-}
-
-.bi-brightness-alt-low::before {
- content: "\f1d0";
-}
-
-.bi-brightness-high-fill::before {
- content: "\f1d1";
-}
-
-.bi-brightness-high::before {
- content: "\f1d2";
-}
-
-.bi-brightness-low-fill::before {
- content: "\f1d3";
-}
-
-.bi-brightness-low::before {
- content: "\f1d4";
-}
-
-.bi-broadcast-pin::before {
- content: "\f1d5";
-}
-
-.bi-broadcast::before {
- content: "\f1d6";
-}
-
-.bi-brush-fill::before {
- content: "\f1d7";
-}
-
-.bi-brush::before {
- content: "\f1d8";
-}
-
-.bi-bucket-fill::before {
- content: "\f1d9";
-}
-
-.bi-bucket::before {
- content: "\f1da";
-}
-
-.bi-bug-fill::before {
- content: "\f1db";
-}
-
-.bi-bug::before {
- content: "\f1dc";
-}
-
-.bi-building::before {
- content: "\f1dd";
-}
-
-.bi-bullseye::before {
- content: "\f1de";
-}
-
-.bi-calculator-fill::before {
- content: "\f1df";
-}
-
-.bi-calculator::before {
- content: "\f1e0";
-}
-
-.bi-calendar-check-fill::before {
- content: "\f1e1";
-}
-
-.bi-calendar-check::before {
- content: "\f1e2";
-}
-
-.bi-calendar-date-fill::before {
- content: "\f1e3";
-}
-
-.bi-calendar-date::before {
- content: "\f1e4";
-}
-
-.bi-calendar-day-fill::before {
- content: "\f1e5";
-}
-
-.bi-calendar-day::before {
- content: "\f1e6";
-}
-
-.bi-calendar-event-fill::before {
- content: "\f1e7";
-}
-
-.bi-calendar-event::before {
- content: "\f1e8";
-}
-
-.bi-calendar-fill::before {
- content: "\f1e9";
-}
-
-.bi-calendar-minus-fill::before {
- content: "\f1ea";
-}
-
-.bi-calendar-minus::before {
- content: "\f1eb";
-}
-
-.bi-calendar-month-fill::before {
- content: "\f1ec";
-}
-
-.bi-calendar-month::before {
- content: "\f1ed";
-}
-
-.bi-calendar-plus-fill::before {
- content: "\f1ee";
-}
-
-.bi-calendar-plus::before {
- content: "\f1ef";
-}
-
-.bi-calendar-range-fill::before {
- content: "\f1f0";
-}
-
-.bi-calendar-range::before {
- content: "\f1f1";
-}
-
-.bi-calendar-week-fill::before {
- content: "\f1f2";
-}
-
-.bi-calendar-week::before {
- content: "\f1f3";
-}
-
-.bi-calendar-x-fill::before {
- content: "\f1f4";
-}
-
-.bi-calendar-x::before {
- content: "\f1f5";
-}
-
-.bi-calendar::before {
- content: "\f1f6";
-}
-
-.bi-calendar2-check-fill::before {
- content: "\f1f7";
-}
-
-.bi-calendar2-check::before {
- content: "\f1f8";
-}
-
-.bi-calendar2-date-fill::before {
- content: "\f1f9";
-}
-
-.bi-calendar2-date::before {
- content: "\f1fa";
-}
-
-.bi-calendar2-day-fill::before {
- content: "\f1fb";
-}
-
-.bi-calendar2-day::before {
- content: "\f1fc";
-}
-
-.bi-calendar2-event-fill::before {
- content: "\f1fd";
-}
-
-.bi-calendar2-event::before {
- content: "\f1fe";
-}
-
-.bi-calendar2-fill::before {
- content: "\f1ff";
-}
-
-.bi-calendar2-minus-fill::before {
- content: "\f200";
-}
-
-.bi-calendar2-minus::before {
- content: "\f201";
-}
-
-.bi-calendar2-month-fill::before {
- content: "\f202";
-}
-
-.bi-calendar2-month::before {
- content: "\f203";
-}
-
-.bi-calendar2-plus-fill::before {
- content: "\f204";
-}
-
-.bi-calendar2-plus::before {
- content: "\f205";
-}
-
-.bi-calendar2-range-fill::before {
- content: "\f206";
-}
-
-.bi-calendar2-range::before {
- content: "\f207";
-}
-
-.bi-calendar2-week-fill::before {
- content: "\f208";
-}
-
-.bi-calendar2-week::before {
- content: "\f209";
-}
-
-.bi-calendar2-x-fill::before {
- content: "\f20a";
-}
-
-.bi-calendar2-x::before {
- content: "\f20b";
-}
-
-.bi-calendar2::before {
- content: "\f20c";
-}
-
-.bi-calendar3-event-fill::before {
- content: "\f20d";
-}
-
-.bi-calendar3-event::before {
- content: "\f20e";
-}
-
-.bi-calendar3-fill::before {
- content: "\f20f";
-}
-
-.bi-calendar3-range-fill::before {
- content: "\f210";
-}
-
-.bi-calendar3-range::before {
- content: "\f211";
-}
-
-.bi-calendar3-week-fill::before {
- content: "\f212";
-}
-
-.bi-calendar3-week::before {
- content: "\f213";
-}
-
-.bi-calendar3::before {
- content: "\f214";
-}
-
-.bi-calendar4-event::before {
- content: "\f215";
-}
-
-.bi-calendar4-range::before {
- content: "\f216";
-}
-
-.bi-calendar4-week::before {
- content: "\f217";
-}
-
-.bi-calendar4::before {
- content: "\f218";
-}
-
-.bi-camera-fill::before {
- content: "\f219";
-}
-
-.bi-camera-reels-fill::before {
- content: "\f21a";
-}
-
-.bi-camera-reels::before {
- content: "\f21b";
-}
-
-.bi-camera-video-fill::before {
- content: "\f21c";
-}
-
-.bi-camera-video-off-fill::before {
- content: "\f21d";
-}
-
-.bi-camera-video-off::before {
- content: "\f21e";
-}
-
-.bi-camera-video::before {
- content: "\f21f";
-}
-
-.bi-camera::before {
- content: "\f220";
-}
-
-.bi-camera2::before {
- content: "\f221";
-}
-
-.bi-capslock-fill::before {
- content: "\f222";
-}
-
-.bi-capslock::before {
- content: "\f223";
-}
-
-.bi-card-checklist::before {
- content: "\f224";
-}
-
-.bi-card-heading::before {
- content: "\f225";
-}
-
-.bi-card-image::before {
- content: "\f226";
-}
-
-.bi-card-list::before {
- content: "\f227";
-}
-
-.bi-card-text::before {
- content: "\f228";
-}
-
-.bi-caret-down-fill::before {
- content: "\f229";
-}
-
-.bi-caret-down-square-fill::before {
- content: "\f22a";
-}
-
-.bi-caret-down-square::before {
- content: "\f22b";
-}
-
-.bi-caret-down::before {
- content: "\f22c";
-}
-
-.bi-caret-left-fill::before {
- content: "\f22d";
-}
-
-.bi-caret-left-square-fill::before {
- content: "\f22e";
-}
-
-.bi-caret-left-square::before {
- content: "\f22f";
-}
-
-.bi-caret-left::before {
- content: "\f230";
-}
-
-.bi-caret-right-fill::before {
- content: "\f231";
-}
-
-.bi-caret-right-square-fill::before {
- content: "\f232";
-}
-
-.bi-caret-right-square::before {
- content: "\f233";
-}
-
-.bi-caret-right::before {
- content: "\f234";
-}
-
-.bi-caret-up-fill::before {
- content: "\f235";
-}
-
-.bi-caret-up-square-fill::before {
- content: "\f236";
-}
-
-.bi-caret-up-square::before {
- content: "\f237";
-}
-
-.bi-caret-up::before {
- content: "\f238";
-}
-
-.bi-cart-check-fill::before {
- content: "\f239";
-}
-
-.bi-cart-check::before {
- content: "\f23a";
-}
-
-.bi-cart-dash-fill::before {
- content: "\f23b";
-}
-
-.bi-cart-dash::before {
- content: "\f23c";
-}
-
-.bi-cart-fill::before {
- content: "\f23d";
-}
-
-.bi-cart-plus-fill::before {
- content: "\f23e";
-}
-
-.bi-cart-plus::before {
- content: "\f23f";
-}
-
-.bi-cart-x-fill::before {
- content: "\f240";
-}
-
-.bi-cart-x::before {
- content: "\f241";
-}
-
-.bi-cart::before {
- content: "\f242";
-}
-
-.bi-cart2::before {
- content: "\f243";
-}
-
-.bi-cart3::before {
- content: "\f244";
-}
-
-.bi-cart4::before {
- content: "\f245";
-}
-
-.bi-cash-stack::before {
- content: "\f246";
-}
-
-.bi-cash::before {
- content: "\f247";
-}
-
-.bi-cast::before {
- content: "\f248";
-}
-
-.bi-chat-dots-fill::before {
- content: "\f249";
-}
-
-.bi-chat-dots::before {
- content: "\f24a";
-}
-
-.bi-chat-fill::before {
- content: "\f24b";
-}
-
-.bi-chat-left-dots-fill::before {
- content: "\f24c";
-}
-
-.bi-chat-left-dots::before {
- content: "\f24d";
-}
-
-.bi-chat-left-fill::before {
- content: "\f24e";
-}
-
-.bi-chat-left-quote-fill::before {
- content: "\f24f";
-}
-
-.bi-chat-left-quote::before {
- content: "\f250";
-}
-
-.bi-chat-left-text-fill::before {
- content: "\f251";
-}
-
-.bi-chat-left-text::before {
- content: "\f252";
-}
-
-.bi-chat-left::before {
- content: "\f253";
-}
-
-.bi-chat-quote-fill::before {
- content: "\f254";
-}
-
-.bi-chat-quote::before {
- content: "\f255";
-}
-
-.bi-chat-right-dots-fill::before {
- content: "\f256";
-}
-
-.bi-chat-right-dots::before {
- content: "\f257";
-}
-
-.bi-chat-right-fill::before {
- content: "\f258";
-}
-
-.bi-chat-right-quote-fill::before {
- content: "\f259";
-}
-
-.bi-chat-right-quote::before {
- content: "\f25a";
-}
-
-.bi-chat-right-text-fill::before {
- content: "\f25b";
-}
-
-.bi-chat-right-text::before {
- content: "\f25c";
-}
-
-.bi-chat-right::before {
- content: "\f25d";
-}
-
-.bi-chat-square-dots-fill::before {
- content: "\f25e";
-}
-
-.bi-chat-square-dots::before {
- content: "\f25f";
-}
-
-.bi-chat-square-fill::before {
- content: "\f260";
-}
-
-.bi-chat-square-quote-fill::before {
- content: "\f261";
-}
-
-.bi-chat-square-quote::before {
- content: "\f262";
-}
-
-.bi-chat-square-text-fill::before {
- content: "\f263";
-}
-
-.bi-chat-square-text::before {
- content: "\f264";
-}
-
-.bi-chat-square::before {
- content: "\f265";
-}
-
-.bi-chat-text-fill::before {
- content: "\f266";
-}
-
-.bi-chat-text::before {
- content: "\f267";
-}
-
-.bi-chat::before {
- content: "\f268";
-}
-
-.bi-check-all::before {
- content: "\f269";
-}
-
-.bi-check-circle-fill::before {
- content: "\f26a";
-}
-
-.bi-check-circle::before {
- content: "\f26b";
-}
-
-.bi-check-square-fill::before {
- content: "\f26c";
-}
-
-.bi-check-square::before {
- content: "\f26d";
-}
-
-.bi-check::before {
- content: "\f26e";
-}
-
-.bi-check2-all::before {
- content: "\f26f";
-}
-
-.bi-check2-circle::before {
- content: "\f270";
-}
-
-.bi-check2-square::before {
- content: "\f271";
-}
-
-.bi-check2::before {
- content: "\f272";
-}
-
-.bi-chevron-bar-contract::before {
- content: "\f273";
-}
-
-.bi-chevron-bar-down::before {
- content: "\f274";
-}
-
-.bi-chevron-bar-expand::before {
- content: "\f275";
-}
-
-.bi-chevron-bar-left::before {
- content: "\f276";
-}
-
-.bi-chevron-bar-right::before {
- content: "\f277";
-}
-
-.bi-chevron-bar-up::before {
- content: "\f278";
-}
-
-.bi-chevron-compact-down::before {
- content: "\f279";
-}
-
-.bi-chevron-compact-left::before {
- content: "\f27a";
-}
-
-.bi-chevron-compact-right::before {
- content: "\f27b";
-}
-
-.bi-chevron-compact-up::before {
- content: "\f27c";
-}
-
-.bi-chevron-contract::before {
- content: "\f27d";
-}
-
-.bi-chevron-double-down::before {
- content: "\f27e";
-}
-
-.bi-chevron-double-left::before {
- content: "\f27f";
-}
-
-.bi-chevron-double-right::before {
- content: "\f280";
-}
-
-.bi-chevron-double-up::before {
- content: "\f281";
-}
-
-.bi-chevron-down::before {
- content: "\f282";
-}
-
-.bi-chevron-expand::before {
- content: "\f283";
-}
-
-.bi-chevron-left::before {
- content: "\f284";
-}
-
-.bi-chevron-right::before {
- content: "\f285";
-}
-
-.bi-chevron-up::before {
- content: "\f286";
-}
-
-.bi-circle-fill::before {
- content: "\f287";
-}
-
-.bi-circle-half::before {
- content: "\f288";
-}
-
-.bi-circle-square::before {
- content: "\f289";
-}
-
-.bi-circle::before {
- content: "\f28a";
-}
-
-.bi-clipboard-check::before {
- content: "\f28b";
-}
-
-.bi-clipboard-data::before {
- content: "\f28c";
-}
-
-.bi-clipboard-minus::before {
- content: "\f28d";
-}
-
-.bi-clipboard-plus::before {
- content: "\f28e";
-}
-
-.bi-clipboard-x::before {
- content: "\f28f";
-}
-
-.bi-clipboard::before {
- content: "\f290";
-}
-
-.bi-clock-fill::before {
- content: "\f291";
-}
-
-.bi-clock-history::before {
- content: "\f292";
-}
-
-.bi-clock::before {
- content: "\f293";
-}
-
-.bi-cloud-arrow-down-fill::before {
- content: "\f294";
-}
-
-.bi-cloud-arrow-down::before {
- content: "\f295";
-}
-
-.bi-cloud-arrow-up-fill::before {
- content: "\f296";
-}
-
-.bi-cloud-arrow-up::before {
- content: "\f297";
-}
-
-.bi-cloud-check-fill::before {
- content: "\f298";
-}
-
-.bi-cloud-check::before {
- content: "\f299";
-}
-
-.bi-cloud-download-fill::before {
- content: "\f29a";
-}
-
-.bi-cloud-download::before {
- content: "\f29b";
-}
-
-.bi-cloud-drizzle-fill::before {
- content: "\f29c";
-}
-
-.bi-cloud-drizzle::before {
- content: "\f29d";
-}
-
-.bi-cloud-fill::before {
- content: "\f29e";
-}
-
-.bi-cloud-fog-fill::before {
- content: "\f29f";
-}
-
-.bi-cloud-fog::before {
- content: "\f2a0";
-}
-
-.bi-cloud-fog2-fill::before {
- content: "\f2a1";
-}
-
-.bi-cloud-fog2::before {
- content: "\f2a2";
-}
-
-.bi-cloud-hail-fill::before {
- content: "\f2a3";
-}
-
-.bi-cloud-hail::before {
- content: "\f2a4";
-}
-
-.bi-cloud-haze-fill::before {
- content: "\f2a6";
-}
-
-.bi-cloud-haze::before {
- content: "\f2a7";
-}
-
-.bi-cloud-haze2-fill::before {
- content: "\f2a8";
-}
-
-.bi-cloud-lightning-fill::before {
- content: "\f2a9";
-}
-
-.bi-cloud-lightning-rain-fill::before {
- content: "\f2aa";
-}
-
-.bi-cloud-lightning-rain::before {
- content: "\f2ab";
-}
-
-.bi-cloud-lightning::before {
- content: "\f2ac";
-}
-
-.bi-cloud-minus-fill::before {
- content: "\f2ad";
-}
-
-.bi-cloud-minus::before {
- content: "\f2ae";
-}
-
-.bi-cloud-moon-fill::before {
- content: "\f2af";
-}
-
-.bi-cloud-moon::before {
- content: "\f2b0";
-}
-
-.bi-cloud-plus-fill::before {
- content: "\f2b1";
-}
-
-.bi-cloud-plus::before {
- content: "\f2b2";
-}
-
-.bi-cloud-rain-fill::before {
- content: "\f2b3";
-}
-
-.bi-cloud-rain-heavy-fill::before {
- content: "\f2b4";
-}
-
-.bi-cloud-rain-heavy::before {
- content: "\f2b5";
-}
-
-.bi-cloud-rain::before {
- content: "\f2b6";
-}
-
-.bi-cloud-slash-fill::before {
- content: "\f2b7";
-}
-
-.bi-cloud-slash::before {
- content: "\f2b8";
-}
-
-.bi-cloud-sleet-fill::before {
- content: "\f2b9";
-}
-
-.bi-cloud-sleet::before {
- content: "\f2ba";
-}
-
-.bi-cloud-snow-fill::before {
- content: "\f2bb";
-}
-
-.bi-cloud-snow::before {
- content: "\f2bc";
-}
-
-.bi-cloud-sun-fill::before {
- content: "\f2bd";
-}
-
-.bi-cloud-sun::before {
- content: "\f2be";
-}
-
-.bi-cloud-upload-fill::before {
- content: "\f2bf";
-}
-
-.bi-cloud-upload::before {
- content: "\f2c0";
-}
-
-.bi-cloud::before {
- content: "\f2c1";
-}
-
-.bi-clouds-fill::before {
- content: "\f2c2";
-}
-
-.bi-clouds::before {
- content: "\f2c3";
-}
-
-.bi-cloudy-fill::before {
- content: "\f2c4";
-}
-
-.bi-cloudy::before {
- content: "\f2c5";
-}
-
-.bi-code-slash::before {
- content: "\f2c6";
-}
-
-.bi-code-square::before {
- content: "\f2c7";
-}
-
-.bi-code::before {
- content: "\f2c8";
-}
-
-.bi-collection-fill::before {
- content: "\f2c9";
-}
-
-.bi-collection-play-fill::before {
- content: "\f2ca";
-}
-
-.bi-collection-play::before {
- content: "\f2cb";
-}
-
-.bi-collection::before {
- content: "\f2cc";
-}
-
-.bi-columns-gap::before {
- content: "\f2cd";
-}
-
-.bi-columns::before {
- content: "\f2ce";
-}
-
-.bi-command::before {
- content: "\f2cf";
-}
-
-.bi-compass-fill::before {
- content: "\f2d0";
-}
-
-.bi-compass::before {
- content: "\f2d1";
-}
-
-.bi-cone-striped::before {
- content: "\f2d2";
-}
-
-.bi-cone::before {
- content: "\f2d3";
-}
-
-.bi-controller::before {
- content: "\f2d4";
-}
-
-.bi-cpu-fill::before {
- content: "\f2d5";
-}
-
-.bi-cpu::before {
- content: "\f2d6";
-}
-
-.bi-credit-card-2-back-fill::before {
- content: "\f2d7";
-}
-
-.bi-credit-card-2-back::before {
- content: "\f2d8";
-}
-
-.bi-credit-card-2-front-fill::before {
- content: "\f2d9";
-}
-
-.bi-credit-card-2-front::before {
- content: "\f2da";
-}
-
-.bi-credit-card-fill::before {
- content: "\f2db";
-}
-
-.bi-credit-card::before {
- content: "\f2dc";
-}
-
-.bi-crop::before {
- content: "\f2dd";
-}
-
-.bi-cup-fill::before {
- content: "\f2de";
-}
-
-.bi-cup-straw::before {
- content: "\f2df";
-}
-
-.bi-cup::before {
- content: "\f2e0";
-}
-
-.bi-cursor-fill::before {
- content: "\f2e1";
-}
-
-.bi-cursor-text::before {
- content: "\f2e2";
-}
-
-.bi-cursor::before {
- content: "\f2e3";
-}
-
-.bi-dash-circle-dotted::before {
- content: "\f2e4";
-}
-
-.bi-dash-circle-fill::before {
- content: "\f2e5";
-}
-
-.bi-dash-circle::before {
- content: "\f2e6";
-}
-
-.bi-dash-square-dotted::before {
- content: "\f2e7";
-}
-
-.bi-dash-square-fill::before {
- content: "\f2e8";
-}
-
-.bi-dash-square::before {
- content: "\f2e9";
-}
-
-.bi-dash::before {
- content: "\f2ea";
-}
-
-.bi-diagram-2-fill::before {
- content: "\f2eb";
-}
-
-.bi-diagram-2::before {
- content: "\f2ec";
-}
-
-.bi-diagram-3-fill::before {
- content: "\f2ed";
-}
-
-.bi-diagram-3::before {
- content: "\f2ee";
-}
-
-.bi-diamond-fill::before {
- content: "\f2ef";
-}
-
-.bi-diamond-half::before {
- content: "\f2f0";
-}
-
-.bi-diamond::before {
- content: "\f2f1";
-}
-
-.bi-dice-1-fill::before {
- content: "\f2f2";
-}
-
-.bi-dice-1::before {
- content: "\f2f3";
-}
-
-.bi-dice-2-fill::before {
- content: "\f2f4";
-}
-
-.bi-dice-2::before {
- content: "\f2f5";
-}
-
-.bi-dice-3-fill::before {
- content: "\f2f6";
-}
-
-.bi-dice-3::before {
- content: "\f2f7";
-}
-
-.bi-dice-4-fill::before {
- content: "\f2f8";
-}
-
-.bi-dice-4::before {
- content: "\f2f9";
-}
-
-.bi-dice-5-fill::before {
- content: "\f2fa";
-}
-
-.bi-dice-5::before {
- content: "\f2fb";
-}
-
-.bi-dice-6-fill::before {
- content: "\f2fc";
-}
-
-.bi-dice-6::before {
- content: "\f2fd";
-}
-
-.bi-disc-fill::before {
- content: "\f2fe";
-}
-
-.bi-disc::before {
- content: "\f2ff";
-}
-
-.bi-discord::before {
- content: "\f300";
-}
-
-.bi-display-fill::before {
- content: "\f301";
-}
-
-.bi-display::before {
- content: "\f302";
-}
-
-.bi-distribute-horizontal::before {
- content: "\f303";
-}
-
-.bi-distribute-vertical::before {
- content: "\f304";
-}
-
-.bi-door-closed-fill::before {
- content: "\f305";
-}
-
-.bi-door-closed::before {
- content: "\f306";
-}
-
-.bi-door-open-fill::before {
- content: "\f307";
-}
-
-.bi-door-open::before {
- content: "\f308";
-}
-
-.bi-dot::before {
- content: "\f309";
-}
-
-.bi-download::before {
- content: "\f30a";
-}
-
-.bi-droplet-fill::before {
- content: "\f30b";
-}
-
-.bi-droplet-half::before {
- content: "\f30c";
-}
-
-.bi-droplet::before {
- content: "\f30d";
-}
-
-.bi-earbuds::before {
- content: "\f30e";
-}
-
-.bi-easel-fill::before {
- content: "\f30f";
-}
-
-.bi-easel::before {
- content: "\f310";
-}
-
-.bi-egg-fill::before {
- content: "\f311";
-}
-
-.bi-egg-fried::before {
- content: "\f312";
-}
-
-.bi-egg::before {
- content: "\f313";
-}
-
-.bi-eject-fill::before {
- content: "\f314";
-}
-
-.bi-eject::before {
- content: "\f315";
-}
-
-.bi-emoji-angry-fill::before {
- content: "\f316";
-}
-
-.bi-emoji-angry::before {
- content: "\f317";
-}
-
-.bi-emoji-dizzy-fill::before {
- content: "\f318";
-}
-
-.bi-emoji-dizzy::before {
- content: "\f319";
-}
-
-.bi-emoji-expressionless-fill::before {
- content: "\f31a";
-}
-
-.bi-emoji-expressionless::before {
- content: "\f31b";
-}
-
-.bi-emoji-frown-fill::before {
- content: "\f31c";
-}
-
-.bi-emoji-frown::before {
- content: "\f31d";
-}
-
-.bi-emoji-heart-eyes-fill::before {
- content: "\f31e";
-}
-
-.bi-emoji-heart-eyes::before {
- content: "\f31f";
-}
-
-.bi-emoji-laughing-fill::before {
- content: "\f320";
-}
-
-.bi-emoji-laughing::before {
- content: "\f321";
-}
-
-.bi-emoji-neutral-fill::before {
- content: "\f322";
-}
-
-.bi-emoji-neutral::before {
- content: "\f323";
-}
-
-.bi-emoji-smile-fill::before {
- content: "\f324";
-}
-
-.bi-emoji-smile-upside-down-fill::before {
- content: "\f325";
-}
-
-.bi-emoji-smile-upside-down::before {
- content: "\f326";
-}
-
-.bi-emoji-smile::before {
- content: "\f327";
-}
-
-.bi-emoji-sunglasses-fill::before {
- content: "\f328";
-}
-
-.bi-emoji-sunglasses::before {
- content: "\f329";
-}
-
-.bi-emoji-wink-fill::before {
- content: "\f32a";
-}
-
-.bi-emoji-wink::before {
- content: "\f32b";
-}
-
-.bi-envelope-fill::before {
- content: "\f32c";
-}
-
-.bi-envelope-open-fill::before {
- content: "\f32d";
-}
-
-.bi-envelope-open::before {
- content: "\f32e";
-}
-
-.bi-envelope::before {
- content: "\f32f";
-}
-
-.bi-eraser-fill::before {
- content: "\f330";
-}
-
-.bi-eraser::before {
- content: "\f331";
-}
-
-.bi-exclamation-circle-fill::before {
- content: "\f332";
-}
-
-.bi-exclamation-circle::before {
- content: "\f333";
-}
-
-.bi-exclamation-diamond-fill::before {
- content: "\f334";
-}
-
-.bi-exclamation-diamond::before {
- content: "\f335";
-}
-
-.bi-exclamation-octagon-fill::before {
- content: "\f336";
-}
-
-.bi-exclamation-octagon::before {
- content: "\f337";
-}
-
-.bi-exclamation-square-fill::before {
- content: "\f338";
-}
-
-.bi-exclamation-square::before {
- content: "\f339";
-}
-
-.bi-exclamation-triangle-fill::before {
- content: "\f33a";
-}
-
-.bi-exclamation-triangle::before {
- content: "\f33b";
-}
-
-.bi-exclamation::before {
- content: "\f33c";
-}
-
-.bi-exclude::before {
- content: "\f33d";
-}
-
-.bi-eye-fill::before {
- content: "\f33e";
-}
-
-.bi-eye-slash-fill::before {
- content: "\f33f";
-}
-
-.bi-eye-slash::before {
- content: "\f340";
-}
-
-.bi-eye::before {
- content: "\f341";
-}
-
-.bi-eyedropper::before {
- content: "\f342";
-}
-
-.bi-eyeglasses::before {
- content: "\f343";
-}
-
-.bi-facebook::before {
- content: "\f344";
-}
-
-.bi-file-arrow-down-fill::before {
- content: "\f345";
-}
-
-.bi-file-arrow-down::before {
- content: "\f346";
-}
-
-.bi-file-arrow-up-fill::before {
- content: "\f347";
-}
-
-.bi-file-arrow-up::before {
- content: "\f348";
-}
-
-.bi-file-bar-graph-fill::before {
- content: "\f349";
-}
-
-.bi-file-bar-graph::before {
- content: "\f34a";
-}
-
-.bi-file-binary-fill::before {
- content: "\f34b";
-}
-
-.bi-file-binary::before {
- content: "\f34c";
-}
-
-.bi-file-break-fill::before {
- content: "\f34d";
-}
-
-.bi-file-break::before {
- content: "\f34e";
-}
-
-.bi-file-check-fill::before {
- content: "\f34f";
-}
-
-.bi-file-check::before {
- content: "\f350";
-}
-
-.bi-file-code-fill::before {
- content: "\f351";
-}
-
-.bi-file-code::before {
- content: "\f352";
-}
-
-.bi-file-diff-fill::before {
- content: "\f353";
-}
-
-.bi-file-diff::before {
- content: "\f354";
-}
-
-.bi-file-earmark-arrow-down-fill::before {
- content: "\f355";
-}
-
-.bi-file-earmark-arrow-down::before {
- content: "\f356";
-}
-
-.bi-file-earmark-arrow-up-fill::before {
- content: "\f357";
-}
-
-.bi-file-earmark-arrow-up::before {
- content: "\f358";
-}
-
-.bi-file-earmark-bar-graph-fill::before {
- content: "\f359";
-}
-
-.bi-file-earmark-bar-graph::before {
- content: "\f35a";
-}
-
-.bi-file-earmark-binary-fill::before {
- content: "\f35b";
-}
-
-.bi-file-earmark-binary::before {
- content: "\f35c";
-}
-
-.bi-file-earmark-break-fill::before {
- content: "\f35d";
-}
-
-.bi-file-earmark-break::before {
- content: "\f35e";
-}
-
-.bi-file-earmark-check-fill::before {
- content: "\f35f";
-}
-
-.bi-file-earmark-check::before {
- content: "\f360";
-}
-
-.bi-file-earmark-code-fill::before {
- content: "\f361";
-}
-
-.bi-file-earmark-code::before {
- content: "\f362";
-}
-
-.bi-file-earmark-diff-fill::before {
- content: "\f363";
-}
-
-.bi-file-earmark-diff::before {
- content: "\f364";
-}
-
-.bi-file-earmark-easel-fill::before {
- content: "\f365";
-}
-
-.bi-file-earmark-easel::before {
- content: "\f366";
-}
-
-.bi-file-earmark-excel-fill::before {
- content: "\f367";
-}
-
-.bi-file-earmark-excel::before {
- content: "\f368";
-}
-
-.bi-file-earmark-fill::before {
- content: "\f369";
-}
-
-.bi-file-earmark-font-fill::before {
- content: "\f36a";
-}
-
-.bi-file-earmark-font::before {
- content: "\f36b";
-}
-
-.bi-file-earmark-image-fill::before {
- content: "\f36c";
-}
-
-.bi-file-earmark-image::before {
- content: "\f36d";
-}
-
-.bi-file-earmark-lock-fill::before {
- content: "\f36e";
-}
-
-.bi-file-earmark-lock::before {
- content: "\f36f";
-}
-
-.bi-file-earmark-lock2-fill::before {
- content: "\f370";
-}
-
-.bi-file-earmark-lock2::before {
- content: "\f371";
-}
-
-.bi-file-earmark-medical-fill::before {
- content: "\f372";
-}
-
-.bi-file-earmark-medical::before {
- content: "\f373";
-}
-
-.bi-file-earmark-minus-fill::before {
- content: "\f374";
-}
-
-.bi-file-earmark-minus::before {
- content: "\f375";
-}
-
-.bi-file-earmark-music-fill::before {
- content: "\f376";
-}
-
-.bi-file-earmark-music::before {
- content: "\f377";
-}
-
-.bi-file-earmark-person-fill::before {
- content: "\f378";
-}
-
-.bi-file-earmark-person::before {
- content: "\f379";
-}
-
-.bi-file-earmark-play-fill::before {
- content: "\f37a";
-}
-
-.bi-file-earmark-play::before {
- content: "\f37b";
-}
-
-.bi-file-earmark-plus-fill::before {
- content: "\f37c";
-}
-
-.bi-file-earmark-plus::before {
- content: "\f37d";
-}
-
-.bi-file-earmark-post-fill::before {
- content: "\f37e";
-}
-
-.bi-file-earmark-post::before {
- content: "\f37f";
-}
-
-.bi-file-earmark-ppt-fill::before {
- content: "\f380";
-}
-
-.bi-file-earmark-ppt::before {
- content: "\f381";
-}
-
-.bi-file-earmark-richtext-fill::before {
- content: "\f382";
-}
-
-.bi-file-earmark-richtext::before {
- content: "\f383";
-}
-
-.bi-file-earmark-ruled-fill::before {
- content: "\f384";
-}
-
-.bi-file-earmark-ruled::before {
- content: "\f385";
-}
-
-.bi-file-earmark-slides-fill::before {
- content: "\f386";
-}
-
-.bi-file-earmark-slides::before {
- content: "\f387";
-}
-
-.bi-file-earmark-spreadsheet-fill::before {
- content: "\f388";
-}
-
-.bi-file-earmark-spreadsheet::before {
- content: "\f389";
-}
-
-.bi-file-earmark-text-fill::before {
- content: "\f38a";
-}
-
-.bi-file-earmark-text::before {
- content: "\f38b";
-}
-
-.bi-file-earmark-word-fill::before {
- content: "\f38c";
-}
-
-.bi-file-earmark-word::before {
- content: "\f38d";
-}
-
-.bi-file-earmark-x-fill::before {
- content: "\f38e";
-}
-
-.bi-file-earmark-x::before {
- content: "\f38f";
-}
-
-.bi-file-earmark-zip-fill::before {
- content: "\f390";
-}
-
-.bi-file-earmark-zip::before {
- content: "\f391";
-}
-
-.bi-file-earmark::before {
- content: "\f392";
-}
-
-.bi-file-easel-fill::before {
- content: "\f393";
-}
-
-.bi-file-easel::before {
- content: "\f394";
-}
-
-.bi-file-excel-fill::before {
- content: "\f395";
-}
-
-.bi-file-excel::before {
- content: "\f396";
-}
-
-.bi-file-fill::before {
- content: "\f397";
-}
-
-.bi-file-font-fill::before {
- content: "\f398";
-}
-
-.bi-file-font::before {
- content: "\f399";
-}
-
-.bi-file-image-fill::before {
- content: "\f39a";
-}
-
-.bi-file-image::before {
- content: "\f39b";
-}
-
-.bi-file-lock-fill::before {
- content: "\f39c";
-}
-
-.bi-file-lock::before {
- content: "\f39d";
-}
-
-.bi-file-lock2-fill::before {
- content: "\f39e";
-}
-
-.bi-file-lock2::before {
- content: "\f39f";
-}
-
-.bi-file-medical-fill::before {
- content: "\f3a0";
-}
-
-.bi-file-medical::before {
- content: "\f3a1";
-}
-
-.bi-file-minus-fill::before {
- content: "\f3a2";
-}
-
-.bi-file-minus::before {
- content: "\f3a3";
-}
-
-.bi-file-music-fill::before {
- content: "\f3a4";
-}
-
-.bi-file-music::before {
- content: "\f3a5";
-}
-
-.bi-file-person-fill::before {
- content: "\f3a6";
-}
-
-.bi-file-person::before {
- content: "\f3a7";
-}
-
-.bi-file-play-fill::before {
- content: "\f3a8";
-}
-
-.bi-file-play::before {
- content: "\f3a9";
-}
-
-.bi-file-plus-fill::before {
- content: "\f3aa";
-}
-
-.bi-file-plus::before {
- content: "\f3ab";
-}
-
-.bi-file-post-fill::before {
- content: "\f3ac";
-}
-
-.bi-file-post::before {
- content: "\f3ad";
-}
-
-.bi-file-ppt-fill::before {
- content: "\f3ae";
-}
-
-.bi-file-ppt::before {
- content: "\f3af";
-}
-
-.bi-file-richtext-fill::before {
- content: "\f3b0";
-}
-
-.bi-file-richtext::before {
- content: "\f3b1";
-}
-
-.bi-file-ruled-fill::before {
- content: "\f3b2";
-}
-
-.bi-file-ruled::before {
- content: "\f3b3";
-}
-
-.bi-file-slides-fill::before {
- content: "\f3b4";
-}
-
-.bi-file-slides::before {
- content: "\f3b5";
-}
-
-.bi-file-spreadsheet-fill::before {
- content: "\f3b6";
-}
-
-.bi-file-spreadsheet::before {
- content: "\f3b7";
-}
-
-.bi-file-text-fill::before {
- content: "\f3b8";
-}
-
-.bi-file-text::before {
- content: "\f3b9";
-}
-
-.bi-file-word-fill::before {
- content: "\f3ba";
-}
-
-.bi-file-word::before {
- content: "\f3bb";
-}
-
-.bi-file-x-fill::before {
- content: "\f3bc";
-}
-
-.bi-file-x::before {
- content: "\f3bd";
-}
-
-.bi-file-zip-fill::before {
- content: "\f3be";
-}
-
-.bi-file-zip::before {
- content: "\f3bf";
-}
-
-.bi-file::before {
- content: "\f3c0";
-}
-
-.bi-files-alt::before {
- content: "\f3c1";
-}
-
-.bi-files::before {
- content: "\f3c2";
-}
-
-.bi-film::before {
- content: "\f3c3";
-}
-
-.bi-filter-circle-fill::before {
- content: "\f3c4";
-}
-
-.bi-filter-circle::before {
- content: "\f3c5";
-}
-
-.bi-filter-left::before {
- content: "\f3c6";
-}
-
-.bi-filter-right::before {
- content: "\f3c7";
-}
-
-.bi-filter-square-fill::before {
- content: "\f3c8";
-}
-
-.bi-filter-square::before {
- content: "\f3c9";
-}
-
-.bi-filter::before {
- content: "\f3ca";
-}
-
-.bi-flag-fill::before {
- content: "\f3cb";
-}
-
-.bi-flag::before {
- content: "\f3cc";
-}
-
-.bi-flower1::before {
- content: "\f3cd";
-}
-
-.bi-flower2::before {
- content: "\f3ce";
-}
-
-.bi-flower3::before {
- content: "\f3cf";
-}
-
-.bi-folder-check::before {
- content: "\f3d0";
-}
-
-.bi-folder-fill::before {
- content: "\f3d1";
-}
-
-.bi-folder-minus::before {
- content: "\f3d2";
-}
-
-.bi-folder-plus::before {
- content: "\f3d3";
-}
-
-.bi-folder-symlink-fill::before {
- content: "\f3d4";
-}
-
-.bi-folder-symlink::before {
- content: "\f3d5";
-}
-
-.bi-folder-x::before {
- content: "\f3d6";
-}
-
-.bi-folder::before {
- content: "\f3d7";
-}
-
-.bi-folder2-open::before {
- content: "\f3d8";
-}
-
-.bi-folder2::before {
- content: "\f3d9";
-}
-
-.bi-fonts::before {
- content: "\f3da";
-}
-
-.bi-forward-fill::before {
- content: "\f3db";
-}
-
-.bi-forward::before {
- content: "\f3dc";
-}
-
-.bi-front::before {
- content: "\f3dd";
-}
-
-.bi-fullscreen-exit::before {
- content: "\f3de";
-}
-
-.bi-fullscreen::before {
- content: "\f3df";
-}
-
-.bi-funnel-fill::before {
- content: "\f3e0";
-}
-
-.bi-funnel::before {
- content: "\f3e1";
-}
-
-.bi-gear-fill::before {
- content: "\f3e2";
-}
-
-.bi-gear-wide-connected::before {
- content: "\f3e3";
-}
-
-.bi-gear-wide::before {
- content: "\f3e4";
-}
-
-.bi-gear::before {
- content: "\f3e5";
-}
-
-.bi-gem::before {
- content: "\f3e6";
-}
-
-.bi-geo-alt-fill::before {
- content: "\f3e7";
-}
-
-.bi-geo-alt::before {
- content: "\f3e8";
-}
-
-.bi-geo-fill::before {
- content: "\f3e9";
-}
-
-.bi-geo::before {
- content: "\f3ea";
-}
-
-.bi-gift-fill::before {
- content: "\f3eb";
-}
-
-.bi-gift::before {
- content: "\f3ec";
-}
-
-.bi-github::before {
- content: "\f3ed";
-}
-
-.bi-globe::before {
- content: "\f3ee";
-}
-
-.bi-globe2::before {
- content: "\f3ef";
-}
-
-.bi-google::before {
- content: "\f3f0";
-}
-
-.bi-graph-down::before {
- content: "\f3f1";
-}
-
-.bi-graph-up::before {
- content: "\f3f2";
-}
-
-.bi-grid-1x2-fill::before {
- content: "\f3f3";
-}
-
-.bi-grid-1x2::before {
- content: "\f3f4";
-}
-
-.bi-grid-3x2-gap-fill::before {
- content: "\f3f5";
-}
-
-.bi-grid-3x2-gap::before {
- content: "\f3f6";
-}
-
-.bi-grid-3x2::before {
- content: "\f3f7";
-}
-
-.bi-grid-3x3-gap-fill::before {
- content: "\f3f8";
-}
-
-.bi-grid-3x3-gap::before {
- content: "\f3f9";
-}
-
-.bi-grid-3x3::before {
- content: "\f3fa";
-}
-
-.bi-grid-fill::before {
- content: "\f3fb";
-}
-
-.bi-grid::before {
- content: "\f3fc";
-}
-
-.bi-grip-horizontal::before {
- content: "\f3fd";
-}
-
-.bi-grip-vertical::before {
- content: "\f3fe";
-}
-
-.bi-hammer::before {
- content: "\f3ff";
-}
-
-.bi-hand-index-fill::before {
- content: "\f400";
-}
-
-.bi-hand-index-thumb-fill::before {
- content: "\f401";
-}
-
-.bi-hand-index-thumb::before {
- content: "\f402";
-}
-
-.bi-hand-index::before {
- content: "\f403";
-}
-
-.bi-hand-thumbs-down-fill::before {
- content: "\f404";
-}
-
-.bi-hand-thumbs-down::before {
- content: "\f405";
-}
-
-.bi-hand-thumbs-up-fill::before {
- content: "\f406";
-}
-
-.bi-hand-thumbs-up::before {
- content: "\f407";
-}
-
-.bi-handbag-fill::before {
- content: "\f408";
-}
-
-.bi-handbag::before {
- content: "\f409";
-}
-
-.bi-hash::before {
- content: "\f40a";
-}
-
-.bi-hdd-fill::before {
- content: "\f40b";
-}
-
-.bi-hdd-network-fill::before {
- content: "\f40c";
-}
-
-.bi-hdd-network::before {
- content: "\f40d";
-}
-
-.bi-hdd-rack-fill::before {
- content: "\f40e";
-}
-
-.bi-hdd-rack::before {
- content: "\f40f";
-}
-
-.bi-hdd-stack-fill::before {
- content: "\f410";
-}
-
-.bi-hdd-stack::before {
- content: "\f411";
-}
-
-.bi-hdd::before {
- content: "\f412";
-}
-
-.bi-headphones::before {
- content: "\f413";
-}
-
-.bi-headset::before {
- content: "\f414";
-}
-
-.bi-heart-fill::before {
- content: "\f415";
-}
-
-.bi-heart-half::before {
- content: "\f416";
-}
-
-.bi-heart::before {
- content: "\f417";
-}
-
-.bi-heptagon-fill::before {
- content: "\f418";
-}
-
-.bi-heptagon-half::before {
- content: "\f419";
-}
-
-.bi-heptagon::before {
- content: "\f41a";
-}
-
-.bi-hexagon-fill::before {
- content: "\f41b";
-}
-
-.bi-hexagon-half::before {
- content: "\f41c";
-}
-
-.bi-hexagon::before {
- content: "\f41d";
-}
-
-.bi-hourglass-bottom::before {
- content: "\f41e";
-}
-
-.bi-hourglass-split::before {
- content: "\f41f";
-}
-
-.bi-hourglass-top::before {
- content: "\f420";
-}
-
-.bi-hourglass::before {
- content: "\f421";
-}
-
-.bi-house-door-fill::before {
- content: "\f422";
-}
-
-.bi-house-door::before {
- content: "\f423";
-}
-
-.bi-house-fill::before {
- content: "\f424";
-}
-
-.bi-house::before {
- content: "\f425";
-}
-
-.bi-hr::before {
- content: "\f426";
-}
-
-.bi-hurricane::before {
- content: "\f427";
-}
-
-.bi-image-alt::before {
- content: "\f428";
-}
-
-.bi-image-fill::before {
- content: "\f429";
-}
-
-.bi-image::before {
- content: "\f42a";
-}
-
-.bi-images::before {
- content: "\f42b";
-}
-
-.bi-inbox-fill::before {
- content: "\f42c";
-}
-
-.bi-inbox::before {
- content: "\f42d";
-}
-
-.bi-inboxes-fill::before {
- content: "\f42e";
-}
-
-.bi-inboxes::before {
- content: "\f42f";
-}
-
-.bi-info-circle-fill::before {
- content: "\f430";
-}
-
-.bi-info-circle::before {
- content: "\f431";
-}
-
-.bi-info-square-fill::before {
- content: "\f432";
-}
-
-.bi-info-square::before {
- content: "\f433";
-}
-
-.bi-info::before {
- content: "\f434";
-}
-
-.bi-input-cursor-text::before {
- content: "\f435";
-}
-
-.bi-input-cursor::before {
- content: "\f436";
-}
-
-.bi-instagram::before {
- content: "\f437";
-}
-
-.bi-intersect::before {
- content: "\f438";
-}
-
-.bi-journal-album::before {
- content: "\f439";
-}
-
-.bi-journal-arrow-down::before {
- content: "\f43a";
-}
-
-.bi-journal-arrow-up::before {
- content: "\f43b";
-}
-
-.bi-journal-bookmark-fill::before {
- content: "\f43c";
-}
-
-.bi-journal-bookmark::before {
- content: "\f43d";
-}
-
-.bi-journal-check::before {
- content: "\f43e";
-}
-
-.bi-journal-code::before {
- content: "\f43f";
-}
-
-.bi-journal-medical::before {
- content: "\f440";
-}
-
-.bi-journal-minus::before {
- content: "\f441";
-}
-
-.bi-journal-plus::before {
- content: "\f442";
-}
-
-.bi-journal-richtext::before {
- content: "\f443";
-}
-
-.bi-journal-text::before {
- content: "\f444";
-}
-
-.bi-journal-x::before {
- content: "\f445";
-}
-
-.bi-journal::before {
- content: "\f446";
-}
-
-.bi-journals::before {
- content: "\f447";
-}
-
-.bi-joystick::before {
- content: "\f448";
-}
-
-.bi-justify-left::before {
- content: "\f449";
-}
-
-.bi-justify-right::before {
- content: "\f44a";
-}
-
-.bi-justify::before {
- content: "\f44b";
-}
-
-.bi-kanban-fill::before {
- content: "\f44c";
-}
-
-.bi-kanban::before {
- content: "\f44d";
-}
-
-.bi-key-fill::before {
- content: "\f44e";
-}
-
-.bi-key::before {
- content: "\f44f";
-}
-
-.bi-keyboard-fill::before {
- content: "\f450";
-}
-
-.bi-keyboard::before {
- content: "\f451";
-}
-
-.bi-ladder::before {
- content: "\f452";
-}
-
-.bi-lamp-fill::before {
- content: "\f453";
-}
-
-.bi-lamp::before {
- content: "\f454";
-}
-
-.bi-laptop-fill::before {
- content: "\f455";
-}
-
-.bi-laptop::before {
- content: "\f456";
-}
-
-.bi-layer-backward::before {
- content: "\f457";
-}
-
-.bi-layer-forward::before {
- content: "\f458";
-}
-
-.bi-layers-fill::before {
- content: "\f459";
-}
-
-.bi-layers-half::before {
- content: "\f45a";
-}
-
-.bi-layers::before {
- content: "\f45b";
-}
-
-.bi-layout-sidebar-inset-reverse::before {
- content: "\f45c";
-}
-
-.bi-layout-sidebar-inset::before {
- content: "\f45d";
-}
-
-.bi-layout-sidebar-reverse::before {
- content: "\f45e";
-}
-
-.bi-layout-sidebar::before {
- content: "\f45f";
-}
-
-.bi-layout-split::before {
- content: "\f460";
-}
-
-.bi-layout-text-sidebar-reverse::before {
- content: "\f461";
-}
-
-.bi-layout-text-sidebar::before {
- content: "\f462";
-}
-
-.bi-layout-text-window-reverse::before {
- content: "\f463";
-}
-
-.bi-layout-text-window::before {
- content: "\f464";
-}
-
-.bi-layout-three-columns::before {
- content: "\f465";
-}
-
-.bi-layout-wtf::before {
- content: "\f466";
-}
-
-.bi-life-preserver::before {
- content: "\f467";
-}
-
-.bi-lightbulb-fill::before {
- content: "\f468";
-}
-
-.bi-lightbulb-off-fill::before {
- content: "\f469";
-}
-
-.bi-lightbulb-off::before {
- content: "\f46a";
-}
-
-.bi-lightbulb::before {
- content: "\f46b";
-}
-
-.bi-lightning-charge-fill::before {
- content: "\f46c";
-}
-
-.bi-lightning-charge::before {
- content: "\f46d";
-}
-
-.bi-lightning-fill::before {
- content: "\f46e";
-}
-
-.bi-lightning::before {
- content: "\f46f";
-}
-
-.bi-link-45deg::before {
- content: "\f470";
-}
-
-.bi-link::before {
- content: "\f471";
-}
-
-.bi-linkedin::before {
- content: "\f472";
-}
-
-.bi-list-check::before {
- content: "\f473";
-}
-
-.bi-list-nested::before {
- content: "\f474";
-}
-
-.bi-list-ol::before {
- content: "\f475";
-}
-
-.bi-list-stars::before {
- content: "\f476";
-}
-
-.bi-list-task::before {
- content: "\f477";
-}
-
-.bi-list-ul::before {
- content: "\f478";
-}
-
-.bi-list::before {
- content: "\f479";
-}
-
-.bi-lock-fill::before {
- content: "\f47a";
-}
-
-.bi-lock::before {
- content: "\f47b";
-}
-
-.bi-mailbox::before {
- content: "\f47c";
-}
-
-.bi-mailbox2::before {
- content: "\f47d";
-}
-
-.bi-map-fill::before {
- content: "\f47e";
-}
-
-.bi-map::before {
- content: "\f47f";
-}
-
-.bi-markdown-fill::before {
- content: "\f480";
-}
-
-.bi-markdown::before {
- content: "\f481";
-}
-
-.bi-mask::before {
- content: "\f482";
-}
-
-.bi-megaphone-fill::before {
- content: "\f483";
-}
-
-.bi-megaphone::before {
- content: "\f484";
-}
-
-.bi-menu-app-fill::before {
- content: "\f485";
-}
-
-.bi-menu-app::before {
- content: "\f486";
-}
-
-.bi-menu-button-fill::before {
- content: "\f487";
-}
-
-.bi-menu-button-wide-fill::before {
- content: "\f488";
-}
-
-.bi-menu-button-wide::before {
- content: "\f489";
-}
-
-.bi-menu-button::before {
- content: "\f48a";
-}
-
-.bi-menu-down::before {
- content: "\f48b";
-}
-
-.bi-menu-up::before {
- content: "\f48c";
-}
-
-.bi-mic-fill::before {
- content: "\f48d";
-}
-
-.bi-mic-mute-fill::before {
- content: "\f48e";
-}
-
-.bi-mic-mute::before {
- content: "\f48f";
-}
-
-.bi-mic::before {
- content: "\f490";
-}
-
-.bi-minecart-loaded::before {
- content: "\f491";
-}
-
-.bi-minecart::before {
- content: "\f492";
-}
-
-.bi-moisture::before {
- content: "\f493";
-}
-
-.bi-moon-fill::before {
- content: "\f494";
-}
-
-.bi-moon-stars-fill::before {
- content: "\f495";
-}
-
-.bi-moon-stars::before {
- content: "\f496";
-}
-
-.bi-moon::before {
- content: "\f497";
-}
-
-.bi-mouse-fill::before {
- content: "\f498";
-}
-
-.bi-mouse::before {
- content: "\f499";
-}
-
-.bi-mouse2-fill::before {
- content: "\f49a";
-}
-
-.bi-mouse2::before {
- content: "\f49b";
-}
-
-.bi-mouse3-fill::before {
- content: "\f49c";
-}
-
-.bi-mouse3::before {
- content: "\f49d";
-}
-
-.bi-music-note-beamed::before {
- content: "\f49e";
-}
-
-.bi-music-note-list::before {
- content: "\f49f";
-}
-
-.bi-music-note::before {
- content: "\f4a0";
-}
-
-.bi-music-player-fill::before {
- content: "\f4a1";
-}
-
-.bi-music-player::before {
- content: "\f4a2";
-}
-
-.bi-newspaper::before {
- content: "\f4a3";
-}
-
-.bi-node-minus-fill::before {
- content: "\f4a4";
-}
-
-.bi-node-minus::before {
- content: "\f4a5";
-}
-
-.bi-node-plus-fill::before {
- content: "\f4a6";
-}
-
-.bi-node-plus::before {
- content: "\f4a7";
-}
-
-.bi-nut-fill::before {
- content: "\f4a8";
-}
-
-.bi-nut::before {
- content: "\f4a9";
-}
-
-.bi-octagon-fill::before {
- content: "\f4aa";
-}
-
-.bi-octagon-half::before {
- content: "\f4ab";
-}
-
-.bi-octagon::before {
- content: "\f4ac";
-}
-
-.bi-option::before {
- content: "\f4ad";
-}
-
-.bi-outlet::before {
- content: "\f4ae";
-}
-
-.bi-paint-bucket::before {
- content: "\f4af";
-}
-
-.bi-palette-fill::before {
- content: "\f4b0";
-}
-
-.bi-palette::before {
- content: "\f4b1";
-}
-
-.bi-palette2::before {
- content: "\f4b2";
-}
-
-.bi-paperclip::before {
- content: "\f4b3";
-}
-
-.bi-paragraph::before {
- content: "\f4b4";
-}
-
-.bi-patch-check-fill::before {
- content: "\f4b5";
-}
-
-.bi-patch-check::before {
- content: "\f4b6";
-}
-
-.bi-patch-exclamation-fill::before {
- content: "\f4b7";
-}
-
-.bi-patch-exclamation::before {
- content: "\f4b8";
-}
-
-.bi-patch-minus-fill::before {
- content: "\f4b9";
-}
-
-.bi-patch-minus::before {
- content: "\f4ba";
-}
-
-.bi-patch-plus-fill::before {
- content: "\f4bb";
-}
-
-.bi-patch-plus::before {
- content: "\f4bc";
-}
-
-.bi-patch-question-fill::before {
- content: "\f4bd";
-}
-
-.bi-patch-question::before {
- content: "\f4be";
-}
-
-.bi-pause-btn-fill::before {
- content: "\f4bf";
-}
-
-.bi-pause-btn::before {
- content: "\f4c0";
-}
-
-.bi-pause-circle-fill::before {
- content: "\f4c1";
-}
-
-.bi-pause-circle::before {
- content: "\f4c2";
-}
-
-.bi-pause-fill::before {
- content: "\f4c3";
-}
-
-.bi-pause::before {
- content: "\f4c4";
-}
-
-.bi-peace-fill::before {
- content: "\f4c5";
-}
-
-.bi-peace::before {
- content: "\f4c6";
-}
-
-.bi-pen-fill::before {
- content: "\f4c7";
-}
-
-.bi-pen::before {
- content: "\f4c8";
-}
-
-.bi-pencil-fill::before {
- content: "\f4c9";
-}
-
-.bi-pencil-square::before {
- content: "\f4ca";
-}
-
-.bi-pencil::before {
- content: "\f4cb";
-}
-
-.bi-pentagon-fill::before {
- content: "\f4cc";
-}
-
-.bi-pentagon-half::before {
- content: "\f4cd";
-}
-
-.bi-pentagon::before {
- content: "\f4ce";
-}
-
-.bi-people-fill::before {
- content: "\f4cf";
-}
-
-.bi-people::before {
- content: "\f4d0";
-}
-
-.bi-percent::before {
- content: "\f4d1";
-}
-
-.bi-person-badge-fill::before {
- content: "\f4d2";
-}
-
-.bi-person-badge::before {
- content: "\f4d3";
-}
-
-.bi-person-bounding-box::before {
- content: "\f4d4";
-}
-
-.bi-person-check-fill::before {
- content: "\f4d5";
-}
-
-.bi-person-check::before {
- content: "\f4d6";
-}
-
-.bi-person-circle::before {
- content: "\f4d7";
-}
-
-.bi-person-dash-fill::before {
- content: "\f4d8";
-}
-
-.bi-person-dash::before {
- content: "\f4d9";
-}
-
-.bi-person-fill::before {
- content: "\f4da";
-}
-
-.bi-person-lines-fill::before {
- content: "\f4db";
-}
-
-.bi-person-plus-fill::before {
- content: "\f4dc";
-}
-
-.bi-person-plus::before {
- content: "\f4dd";
-}
-
-.bi-person-square::before {
- content: "\f4de";
-}
-
-.bi-person-x-fill::before {
- content: "\f4df";
-}
-
-.bi-person-x::before {
- content: "\f4e0";
-}
-
-.bi-person::before {
- content: "\f4e1";
-}
-
-.bi-phone-fill::before {
- content: "\f4e2";
-}
-
-.bi-phone-landscape-fill::before {
- content: "\f4e3";
-}
-
-.bi-phone-landscape::before {
- content: "\f4e4";
-}
-
-.bi-phone-vibrate-fill::before {
- content: "\f4e5";
-}
-
-.bi-phone-vibrate::before {
- content: "\f4e6";
-}
-
-.bi-phone::before {
- content: "\f4e7";
-}
-
-.bi-pie-chart-fill::before {
- content: "\f4e8";
-}
-
-.bi-pie-chart::before {
- content: "\f4e9";
-}
-
-.bi-pin-angle-fill::before {
- content: "\f4ea";
-}
-
-.bi-pin-angle::before {
- content: "\f4eb";
-}
-
-.bi-pin-fill::before {
- content: "\f4ec";
-}
-
-.bi-pin::before {
- content: "\f4ed";
-}
-
-.bi-pip-fill::before {
- content: "\f4ee";
-}
-
-.bi-pip::before {
- content: "\f4ef";
-}
-
-.bi-play-btn-fill::before {
- content: "\f4f0";
-}
-
-.bi-play-btn::before {
- content: "\f4f1";
-}
-
-.bi-play-circle-fill::before {
- content: "\f4f2";
-}
-
-.bi-play-circle::before {
- content: "\f4f3";
-}
-
-.bi-play-fill::before {
- content: "\f4f4";
-}
-
-.bi-play::before {
- content: "\f4f5";
-}
-
-.bi-plug-fill::before {
- content: "\f4f6";
-}
-
-.bi-plug::before {
- content: "\f4f7";
-}
-
-.bi-plus-circle-dotted::before {
- content: "\f4f8";
-}
-
-.bi-plus-circle-fill::before {
- content: "\f4f9";
-}
-
-.bi-plus-circle::before {
- content: "\f4fa";
-}
-
-.bi-plus-square-dotted::before {
- content: "\f4fb";
-}
-
-.bi-plus-square-fill::before {
- content: "\f4fc";
-}
-
-.bi-plus-square::before {
- content: "\f4fd";
-}
-
-.bi-plus::before {
- content: "\f4fe";
-}
-
-.bi-power::before {
- content: "\f4ff";
-}
-
-.bi-printer-fill::before {
- content: "\f500";
-}
-
-.bi-printer::before {
- content: "\f501";
-}
-
-.bi-puzzle-fill::before {
- content: "\f502";
-}
-
-.bi-puzzle::before {
- content: "\f503";
-}
-
-.bi-question-circle-fill::before {
- content: "\f504";
-}
-
-.bi-question-circle::before {
- content: "\f505";
-}
-
-.bi-question-diamond-fill::before {
- content: "\f506";
-}
-
-.bi-question-diamond::before {
- content: "\f507";
-}
-
-.bi-question-octagon-fill::before {
- content: "\f508";
-}
-
-.bi-question-octagon::before {
- content: "\f509";
-}
-
-.bi-question-square-fill::before {
- content: "\f50a";
-}
-
-.bi-question-square::before {
- content: "\f50b";
-}
-
-.bi-question::before {
- content: "\f50c";
-}
-
-.bi-rainbow::before {
- content: "\f50d";
-}
-
-.bi-receipt-cutoff::before {
- content: "\f50e";
-}
-
-.bi-receipt::before {
- content: "\f50f";
-}
-
-.bi-reception-0::before {
- content: "\f510";
-}
-
-.bi-reception-1::before {
- content: "\f511";
-}
-
-.bi-reception-2::before {
- content: "\f512";
-}
-
-.bi-reception-3::before {
- content: "\f513";
-}
-
-.bi-reception-4::before {
- content: "\f514";
-}
-
-.bi-record-btn-fill::before {
- content: "\f515";
-}
-
-.bi-record-btn::before {
- content: "\f516";
-}
-
-.bi-record-circle-fill::before {
- content: "\f517";
-}
-
-.bi-record-circle::before {
- content: "\f518";
-}
-
-.bi-record-fill::before {
- content: "\f519";
-}
-
-.bi-record::before {
- content: "\f51a";
-}
-
-.bi-record2-fill::before {
- content: "\f51b";
-}
-
-.bi-record2::before {
- content: "\f51c";
-}
-
-.bi-reply-all-fill::before {
- content: "\f51d";
-}
-
-.bi-reply-all::before {
- content: "\f51e";
-}
-
-.bi-reply-fill::before {
- content: "\f51f";
-}
-
-.bi-reply::before {
- content: "\f520";
-}
-
-.bi-rss-fill::before {
- content: "\f521";
-}
-
-.bi-rss::before {
- content: "\f522";
-}
-
-.bi-rulers::before {
- content: "\f523";
-}
-
-.bi-save-fill::before {
- content: "\f524";
-}
-
-.bi-save::before {
- content: "\f525";
-}
-
-.bi-save2-fill::before {
- content: "\f526";
-}
-
-.bi-save2::before {
- content: "\f527";
-}
-
-.bi-scissors::before {
- content: "\f528";
-}
-
-.bi-screwdriver::before {
- content: "\f529";
-}
-
-.bi-search::before {
- content: "\f52a";
-}
-
-.bi-segmented-nav::before {
- content: "\f52b";
-}
-
-.bi-server::before {
- content: "\f52c";
-}
-
-.bi-share-fill::before {
- content: "\f52d";
-}
-
-.bi-share::before {
- content: "\f52e";
-}
-
-.bi-shield-check::before {
- content: "\f52f";
-}
-
-.bi-shield-exclamation::before {
- content: "\f530";
-}
-
-.bi-shield-fill-check::before {
- content: "\f531";
-}
-
-.bi-shield-fill-exclamation::before {
- content: "\f532";
-}
-
-.bi-shield-fill-minus::before {
- content: "\f533";
-}
-
-.bi-shield-fill-plus::before {
- content: "\f534";
-}
-
-.bi-shield-fill-x::before {
- content: "\f535";
-}
-
-.bi-shield-fill::before {
- content: "\f536";
-}
-
-.bi-shield-lock-fill::before {
- content: "\f537";
-}
-
-.bi-shield-lock::before {
- content: "\f538";
-}
-
-.bi-shield-minus::before {
- content: "\f539";
-}
-
-.bi-shield-plus::before {
- content: "\f53a";
-}
-
-.bi-shield-shaded::before {
- content: "\f53b";
-}
-
-.bi-shield-slash-fill::before {
- content: "\f53c";
-}
-
-.bi-shield-slash::before {
- content: "\f53d";
-}
-
-.bi-shield-x::before {
- content: "\f53e";
-}
-
-.bi-shield::before {
- content: "\f53f";
-}
-
-.bi-shift-fill::before {
- content: "\f540";
-}
-
-.bi-shift::before {
- content: "\f541";
-}
-
-.bi-shop-window::before {
- content: "\f542";
-}
-
-.bi-shop::before {
- content: "\f543";
-}
-
-.bi-shuffle::before {
- content: "\f544";
-}
-
-.bi-signpost-2-fill::before {
- content: "\f545";
-}
-
-.bi-signpost-2::before {
- content: "\f546";
-}
-
-.bi-signpost-fill::before {
- content: "\f547";
-}
-
-.bi-signpost-split-fill::before {
- content: "\f548";
-}
-
-.bi-signpost-split::before {
- content: "\f549";
-}
-
-.bi-signpost::before {
- content: "\f54a";
-}
-
-.bi-sim-fill::before {
- content: "\f54b";
-}
-
-.bi-sim::before {
- content: "\f54c";
-}
-
-.bi-skip-backward-btn-fill::before {
- content: "\f54d";
-}
-
-.bi-skip-backward-btn::before {
- content: "\f54e";
-}
-
-.bi-skip-backward-circle-fill::before {
- content: "\f54f";
-}
-
-.bi-skip-backward-circle::before {
- content: "\f550";
-}
-
-.bi-skip-backward-fill::before {
- content: "\f551";
-}
-
-.bi-skip-backward::before {
- content: "\f552";
-}
-
-.bi-skip-end-btn-fill::before {
- content: "\f553";
-}
-
-.bi-skip-end-btn::before {
- content: "\f554";
-}
-
-.bi-skip-end-circle-fill::before {
- content: "\f555";
-}
-
-.bi-skip-end-circle::before {
- content: "\f556";
-}
-
-.bi-skip-end-fill::before {
- content: "\f557";
-}
-
-.bi-skip-end::before {
- content: "\f558";
-}
-
-.bi-skip-forward-btn-fill::before {
- content: "\f559";
-}
-
-.bi-skip-forward-btn::before {
- content: "\f55a";
-}
-
-.bi-skip-forward-circle-fill::before {
- content: "\f55b";
-}
-
-.bi-skip-forward-circle::before {
- content: "\f55c";
-}
-
-.bi-skip-forward-fill::before {
- content: "\f55d";
-}
-
-.bi-skip-forward::before {
- content: "\f55e";
-}
-
-.bi-skip-start-btn-fill::before {
- content: "\f55f";
-}
-
-.bi-skip-start-btn::before {
- content: "\f560";
-}
-
-.bi-skip-start-circle-fill::before {
- content: "\f561";
-}
-
-.bi-skip-start-circle::before {
- content: "\f562";
-}
-
-.bi-skip-start-fill::before {
- content: "\f563";
-}
-
-.bi-skip-start::before {
- content: "\f564";
-}
-
-.bi-slack::before {
- content: "\f565";
-}
-
-.bi-slash-circle-fill::before {
- content: "\f566";
-}
-
-.bi-slash-circle::before {
- content: "\f567";
-}
-
-.bi-slash-square-fill::before {
- content: "\f568";
-}
-
-.bi-slash-square::before {
- content: "\f569";
-}
-
-.bi-slash::before {
- content: "\f56a";
-}
-
-.bi-sliders::before {
- content: "\f56b";
-}
-
-.bi-smartwatch::before {
- content: "\f56c";
-}
-
-.bi-snow::before {
- content: "\f56d";
-}
-
-.bi-snow2::before {
- content: "\f56e";
-}
-
-.bi-snow3::before {
- content: "\f56f";
-}
-
-.bi-sort-alpha-down-alt::before {
- content: "\f570";
-}
-
-.bi-sort-alpha-down::before {
- content: "\f571";
-}
-
-.bi-sort-alpha-up-alt::before {
- content: "\f572";
-}
-
-.bi-sort-alpha-up::before {
- content: "\f573";
-}
-
-.bi-sort-down-alt::before {
- content: "\f574";
-}
-
-.bi-sort-down::before {
- content: "\f575";
-}
-
-.bi-sort-numeric-down-alt::before {
- content: "\f576";
-}
-
-.bi-sort-numeric-down::before {
- content: "\f577";
-}
-
-.bi-sort-numeric-up-alt::before {
- content: "\f578";
-}
-
-.bi-sort-numeric-up::before {
- content: "\f579";
-}
-
-.bi-sort-up-alt::before {
- content: "\f57a";
-}
-
-.bi-sort-up::before {
- content: "\f57b";
-}
-
-.bi-soundwave::before {
- content: "\f57c";
-}
-
-.bi-speaker-fill::before {
- content: "\f57d";
-}
-
-.bi-speaker::before {
- content: "\f57e";
-}
-
-.bi-speedometer::before {
- content: "\f57f";
-}
-
-.bi-speedometer2::before {
- content: "\f580";
-}
-
-.bi-spellcheck::before {
- content: "\f581";
-}
-
-.bi-square-fill::before {
- content: "\f582";
-}
-
-.bi-square-half::before {
- content: "\f583";
-}
-
-.bi-square::before {
- content: "\f584";
-}
-
-.bi-stack::before {
- content: "\f585";
-}
-
-.bi-star-fill::before {
- content: "\f586";
-}
-
-.bi-star-half::before {
- content: "\f587";
-}
-
-.bi-star::before {
- content: "\f588";
-}
-
-.bi-stars::before {
- content: "\f589";
-}
-
-.bi-stickies-fill::before {
- content: "\f58a";
-}
-
-.bi-stickies::before {
- content: "\f58b";
-}
-
-.bi-sticky-fill::before {
- content: "\f58c";
-}
-
-.bi-sticky::before {
- content: "\f58d";
-}
-
-.bi-stop-btn-fill::before {
- content: "\f58e";
-}
-
-.bi-stop-btn::before {
- content: "\f58f";
-}
-
-.bi-stop-circle-fill::before {
- content: "\f590";
-}
-
-.bi-stop-circle::before {
- content: "\f591";
-}
-
-.bi-stop-fill::before {
- content: "\f592";
-}
-
-.bi-stop::before {
- content: "\f593";
-}
-
-.bi-stoplights-fill::before {
- content: "\f594";
-}
-
-.bi-stoplights::before {
- content: "\f595";
-}
-
-.bi-stopwatch-fill::before {
- content: "\f596";
-}
-
-.bi-stopwatch::before {
- content: "\f597";
-}
-
-.bi-subtract::before {
- content: "\f598";
-}
-
-.bi-suit-club-fill::before {
- content: "\f599";
-}
-
-.bi-suit-club::before {
- content: "\f59a";
-}
-
-.bi-suit-diamond-fill::before {
- content: "\f59b";
-}
-
-.bi-suit-diamond::before {
- content: "\f59c";
-}
-
-.bi-suit-heart-fill::before {
- content: "\f59d";
-}
-
-.bi-suit-heart::before {
- content: "\f59e";
-}
-
-.bi-suit-spade-fill::before {
- content: "\f59f";
-}
-
-.bi-suit-spade::before {
- content: "\f5a0";
-}
-
-.bi-sun-fill::before {
- content: "\f5a1";
-}
-
-.bi-sun::before {
- content: "\f5a2";
-}
-
-.bi-sunglasses::before {
- content: "\f5a3";
-}
-
-.bi-sunrise-fill::before {
- content: "\f5a4";
-}
-
-.bi-sunrise::before {
- content: "\f5a5";
-}
-
-.bi-sunset-fill::before {
- content: "\f5a6";
-}
-
-.bi-sunset::before {
- content: "\f5a7";
-}
-
-.bi-symmetry-horizontal::before {
- content: "\f5a8";
-}
-
-.bi-symmetry-vertical::before {
- content: "\f5a9";
-}
-
-.bi-table::before {
- content: "\f5aa";
-}
-
-.bi-tablet-fill::before {
- content: "\f5ab";
-}
-
-.bi-tablet-landscape-fill::before {
- content: "\f5ac";
-}
-
-.bi-tablet-landscape::before {
- content: "\f5ad";
-}
-
-.bi-tablet::before {
- content: "\f5ae";
-}
-
-.bi-tag-fill::before {
- content: "\f5af";
-}
-
-.bi-tag::before {
- content: "\f5b0";
-}
-
-.bi-tags-fill::before {
- content: "\f5b1";
-}
-
-.bi-tags::before {
- content: "\f5b2";
-}
-
-.bi-telegram::before {
- content: "\f5b3";
-}
-
-.bi-telephone-fill::before {
- content: "\f5b4";
-}
-
-.bi-telephone-forward-fill::before {
- content: "\f5b5";
-}
-
-.bi-telephone-forward::before {
- content: "\f5b6";
-}
-
-.bi-telephone-inbound-fill::before {
- content: "\f5b7";
-}
-
-.bi-telephone-inbound::before {
- content: "\f5b8";
-}
-
-.bi-telephone-minus-fill::before {
- content: "\f5b9";
-}
-
-.bi-telephone-minus::before {
- content: "\f5ba";
-}
-
-.bi-telephone-outbound-fill::before {
- content: "\f5bb";
-}
-
-.bi-telephone-outbound::before {
- content: "\f5bc";
-}
-
-.bi-telephone-plus-fill::before {
- content: "\f5bd";
-}
-
-.bi-telephone-plus::before {
- content: "\f5be";
-}
-
-.bi-telephone-x-fill::before {
- content: "\f5bf";
-}
-
-.bi-telephone-x::before {
- content: "\f5c0";
-}
-
-.bi-telephone::before {
- content: "\f5c1";
-}
-
-.bi-terminal-fill::before {
- content: "\f5c2";
-}
-
-.bi-terminal::before {
- content: "\f5c3";
-}
-
-.bi-text-center::before {
- content: "\f5c4";
-}
-
-.bi-text-indent-left::before {
- content: "\f5c5";
-}
-
-.bi-text-indent-right::before {
- content: "\f5c6";
-}
-
-.bi-text-left::before {
- content: "\f5c7";
-}
-
-.bi-text-paragraph::before {
- content: "\f5c8";
-}
-
-.bi-text-right::before {
- content: "\f5c9";
-}
-
-.bi-textarea-resize::before {
- content: "\f5ca";
-}
-
-.bi-textarea-t::before {
- content: "\f5cb";
-}
-
-.bi-textarea::before {
- content: "\f5cc";
-}
-
-.bi-thermometer-half::before {
- content: "\f5cd";
-}
-
-.bi-thermometer-high::before {
- content: "\f5ce";
-}
-
-.bi-thermometer-low::before {
- content: "\f5cf";
-}
-
-.bi-thermometer-snow::before {
- content: "\f5d0";
-}
-
-.bi-thermometer-sun::before {
- content: "\f5d1";
-}
-
-.bi-thermometer::before {
- content: "\f5d2";
-}
-
-.bi-three-dots-vertical::before {
- content: "\f5d3";
-}
-
-.bi-three-dots::before {
- content: "\f5d4";
-}
-
-.bi-toggle-off::before {
- content: "\f5d5";
-}
-
-.bi-toggle-on::before {
- content: "\f5d6";
-}
-
-.bi-toggle2-off::before {
- content: "\f5d7";
-}
-
-.bi-toggle2-on::before {
- content: "\f5d8";
-}
-
-.bi-toggles::before {
- content: "\f5d9";
-}
-
-.bi-toggles2::before {
- content: "\f5da";
-}
-
-.bi-tools::before {
- content: "\f5db";
-}
-
-.bi-tornado::before {
- content: "\f5dc";
-}
-
-.bi-trash-fill::before {
- content: "\f5dd";
-}
-
-.bi-trash::before {
- content: "\f5de";
-}
-
-.bi-trash2-fill::before {
- content: "\f5df";
-}
-
-.bi-trash2::before {
- content: "\f5e0";
-}
-
-.bi-tree-fill::before {
- content: "\f5e1";
-}
-
-.bi-tree::before {
- content: "\f5e2";
-}
-
-.bi-triangle-fill::before {
- content: "\f5e3";
-}
-
-.bi-triangle-half::before {
- content: "\f5e4";
-}
-
-.bi-triangle::before {
- content: "\f5e5";
-}
-
-.bi-trophy-fill::before {
- content: "\f5e6";
-}
-
-.bi-trophy::before {
- content: "\f5e7";
-}
-
-.bi-tropical-storm::before {
- content: "\f5e8";
-}
-
-.bi-truck-flatbed::before {
- content: "\f5e9";
-}
-
-.bi-truck::before {
- content: "\f5ea";
-}
-
-.bi-tsunami::before {
- content: "\f5eb";
-}
-
-.bi-tv-fill::before {
- content: "\f5ec";
-}
-
-.bi-tv::before {
- content: "\f5ed";
-}
-
-.bi-twitch::before {
- content: "\f5ee";
-}
-
-.bi-twitter::before {
- content: "\f5ef";
-}
-
-.bi-type-bold::before {
- content: "\f5f0";
-}
-
-.bi-type-h1::before {
- content: "\f5f1";
-}
-
-.bi-type-h2::before {
- content: "\f5f2";
-}
-
-.bi-type-h3::before {
- content: "\f5f3";
-}
-
-.bi-type-italic::before {
- content: "\f5f4";
-}
-
-.bi-type-strikethrough::before {
- content: "\f5f5";
-}
-
-.bi-type-underline::before {
- content: "\f5f6";
-}
-
-.bi-type::before {
- content: "\f5f7";
-}
-
-.bi-ui-checks-grid::before {
- content: "\f5f8";
-}
-
-.bi-ui-checks::before {
- content: "\f5f9";
-}
-
-.bi-ui-radios-grid::before {
- content: "\f5fa";
-}
-
-.bi-ui-radios::before {
- content: "\f5fb";
-}
-
-.bi-umbrella-fill::before {
- content: "\f5fc";
-}
-
-.bi-umbrella::before {
- content: "\f5fd";
-}
-
-.bi-union::before {
- content: "\f5fe";
-}
-
-.bi-unlock-fill::before {
- content: "\f5ff";
-}
-
-.bi-unlock::before {
- content: "\f600";
-}
-
-.bi-upc-scan::before {
- content: "\f601";
-}
-
-.bi-upc::before {
- content: "\f602";
-}
-
-.bi-upload::before {
- content: "\f603";
-}
-
-.bi-vector-pen::before {
- content: "\f604";
-}
-
-.bi-view-list::before {
- content: "\f605";
-}
-
-.bi-view-stacked::before {
- content: "\f606";
-}
-
-.bi-vinyl-fill::before {
- content: "\f607";
-}
-
-.bi-vinyl::before {
- content: "\f608";
-}
-
-.bi-voicemail::before {
- content: "\f609";
-}
-
-.bi-volume-down-fill::before {
- content: "\f60a";
-}
-
-.bi-volume-down::before {
- content: "\f60b";
-}
-
-.bi-volume-mute-fill::before {
- content: "\f60c";
-}
-
-.bi-volume-mute::before {
- content: "\f60d";
-}
-
-.bi-volume-off-fill::before {
- content: "\f60e";
-}
-
-.bi-volume-off::before {
- content: "\f60f";
-}
-
-.bi-volume-up-fill::before {
- content: "\f610";
-}
-
-.bi-volume-up::before {
- content: "\f611";
-}
-
-.bi-vr::before {
- content: "\f612";
-}
-
-.bi-wallet-fill::before {
- content: "\f613";
-}
-
-.bi-wallet::before {
- content: "\f614";
-}
-
-.bi-wallet2::before {
- content: "\f615";
-}
-
-.bi-watch::before {
- content: "\f616";
-}
-
-.bi-water::before {
- content: "\f617";
-}
-
-.bi-whatsapp::before {
- content: "\f618";
-}
-
-.bi-wifi-1::before {
- content: "\f619";
-}
-
-.bi-wifi-2::before {
- content: "\f61a";
-}
-
-.bi-wifi-off::before {
- content: "\f61b";
-}
-
-.bi-wifi::before {
- content: "\f61c";
-}
-
-.bi-wind::before {
- content: "\f61d";
-}
-
-.bi-window-dock::before {
- content: "\f61e";
-}
-
-.bi-window-sidebar::before {
- content: "\f61f";
-}
-
-.bi-window::before {
- content: "\f620";
-}
-
-.bi-wrench::before {
- content: "\f621";
-}
-
-.bi-x-circle-fill::before {
- content: "\f622";
-}
-
-.bi-x-circle::before {
- content: "\f623";
-}
-
-.bi-x-diamond-fill::before {
- content: "\f624";
-}
-
-.bi-x-diamond::before {
- content: "\f625";
-}
-
-.bi-x-octagon-fill::before {
- content: "\f626";
-}
-
-.bi-x-octagon::before {
- content: "\f627";
-}
-
-.bi-x-square-fill::before {
- content: "\f628";
-}
-
-.bi-x-square::before {
- content: "\f629";
-}
-
-.bi-x::before {
- content: "\f62a";
-}
-
-.bi-youtube::before {
- content: "\f62b";
-}
-
-.bi-zoom-in::before {
- content: "\f62c";
-}
-
-.bi-zoom-out::before {
- content: "\f62d";
-}
-
-.bi-bank::before {
- content: "\f62e";
-}
-
-.bi-bank2::before {
- content: "\f62f";
-}
-
-.bi-bell-slash-fill::before {
- content: "\f630";
-}
-
-.bi-bell-slash::before {
- content: "\f631";
-}
-
-.bi-cash-coin::before {
- content: "\f632";
-}
-
-.bi-check-lg::before {
- content: "\f633";
-}
-
-.bi-coin::before {
- content: "\f634";
-}
-
-.bi-currency-bitcoin::before {
- content: "\f635";
-}
-
-.bi-currency-dollar::before {
- content: "\f636";
-}
-
-.bi-currency-euro::before {
- content: "\f637";
-}
-
-.bi-currency-exchange::before {
- content: "\f638";
-}
-
-.bi-currency-pound::before {
- content: "\f639";
-}
-
-.bi-currency-yen::before {
- content: "\f63a";
-}
-
-.bi-dash-lg::before {
- content: "\f63b";
-}
-
-.bi-exclamation-lg::before {
- content: "\f63c";
-}
-
-.bi-file-earmark-pdf-fill::before {
- content: "\f63d";
-}
-
-.bi-file-earmark-pdf::before {
- content: "\f63e";
-}
-
-.bi-file-pdf-fill::before {
- content: "\f63f";
-}
-
-.bi-file-pdf::before {
- content: "\f640";
-}
-
-.bi-gender-ambiguous::before {
- content: "\f641";
-}
-
-.bi-gender-female::before {
- content: "\f642";
-}
-
-.bi-gender-male::before {
- content: "\f643";
-}
-
-.bi-gender-trans::before {
- content: "\f644";
-}
-
-.bi-headset-vr::before {
- content: "\f645";
-}
-
-.bi-info-lg::before {
- content: "\f646";
-}
-
-.bi-mastodon::before {
- content: "\f647";
-}
-
-.bi-messenger::before {
- content: "\f648";
-}
-
-.bi-piggy-bank-fill::before {
- content: "\f649";
-}
-
-.bi-piggy-bank::before {
- content: "\f64a";
-}
-
-.bi-pin-map-fill::before {
- content: "\f64b";
-}
-
-.bi-pin-map::before {
- content: "\f64c";
-}
-
-.bi-plus-lg::before {
- content: "\f64d";
-}
-
-.bi-question-lg::before {
- content: "\f64e";
-}
-
-.bi-recycle::before {
- content: "\f64f";
-}
-
-.bi-reddit::before {
- content: "\f650";
-}
-
-.bi-safe-fill::before {
- content: "\f651";
-}
-
-.bi-safe2-fill::before {
- content: "\f652";
-}
-
-.bi-safe2::before {
- content: "\f653";
-}
-
-.bi-sd-card-fill::before {
- content: "\f654";
-}
-
-.bi-sd-card::before {
- content: "\f655";
-}
-
-.bi-skype::before {
- content: "\f656";
-}
-
-.bi-slash-lg::before {
- content: "\f657";
-}
-
-.bi-translate::before {
- content: "\f658";
-}
-
-.bi-x-lg::before {
- content: "\f659";
-}
-
-.bi-safe::before {
- content: "\f65a";
-}
-
-.bi-apple::before {
- content: "\f65b";
-}
-
-.bi-microsoft::before {
- content: "\f65d";
-}
-
-.bi-windows::before {
- content: "\f65e";
-}
-
-.bi-behance::before {
- content: "\f65c";
-}
-
-.bi-dribbble::before {
- content: "\f65f";
-}
-
-.bi-line::before {
- content: "\f660";
-}
-
-.bi-medium::before {
- content: "\f661";
-}
-
-.bi-paypal::before {
- content: "\f662";
-}
-
-.bi-pinterest::before {
- content: "\f663";
-}
-
-.bi-signal::before {
- content: "\f664";
-}
-
-.bi-snapchat::before {
- content: "\f665";
-}
-
-.bi-spotify::before {
- content: "\f666";
-}
-
-.bi-stack-overflow::before {
- content: "\f667";
-}
-
-.bi-strava::before {
- content: "\f668";
-}
-
-.bi-wordpress::before {
- content: "\f669";
-}
-
-.bi-vimeo::before {
- content: "\f66a";
-}
-
-.bi-activity::before {
- content: "\f66b";
-}
-
-.bi-easel2-fill::before {
- content: "\f66c";
-}
-
-.bi-easel2::before {
- content: "\f66d";
-}
-
-.bi-easel3-fill::before {
- content: "\f66e";
-}
-
-.bi-easel3::before {
- content: "\f66f";
-}
-
-.bi-fan::before {
- content: "\f670";
-}
-
-.bi-fingerprint::before {
- content: "\f671";
-}
-
-.bi-graph-down-arrow::before {
- content: "\f672";
-}
-
-.bi-graph-up-arrow::before {
- content: "\f673";
-}
-
-.bi-hypnotize::before {
- content: "\f674";
-}
-
-.bi-magic::before {
- content: "\f675";
-}
-
-.bi-person-rolodex::before {
- content: "\f676";
-}
-
-.bi-person-video::before {
- content: "\f677";
-}
-
-.bi-person-video2::before {
- content: "\f678";
-}
-
-.bi-person-video3::before {
- content: "\f679";
-}
-
-.bi-person-workspace::before {
- content: "\f67a";
-}
-
-.bi-radioactive::before {
- content: "\f67b";
-}
-
-.bi-webcam-fill::before {
- content: "\f67c";
-}
-
-.bi-webcam::before {
- content: "\f67d";
-}
-
-.bi-yin-yang::before {
- content: "\f67e";
-}
-
-.bi-bandaid-fill::before {
- content: "\f680";
-}
-
-.bi-bandaid::before {
- content: "\f681";
-}
-
-.bi-bluetooth::before {
- content: "\f682";
-}
-
-.bi-body-text::before {
- content: "\f683";
-}
-
-.bi-boombox::before {
- content: "\f684";
-}
-
-.bi-boxes::before {
- content: "\f685";
-}
-
-.bi-dpad-fill::before {
- content: "\f686";
-}
-
-.bi-dpad::before {
- content: "\f687";
-}
-
-.bi-ear-fill::before {
- content: "\f688";
-}
-
-.bi-ear::before {
- content: "\f689";
-}
-
-.bi-envelope-check-fill::before {
- content: "\f68b";
-}
-
-.bi-envelope-check::before {
- content: "\f68c";
-}
-
-.bi-envelope-dash-fill::before {
- content: "\f68e";
-}
-
-.bi-envelope-dash::before {
- content: "\f68f";
-}
-
-.bi-envelope-exclamation-fill::before {
- content: "\f691";
-}
-
-.bi-envelope-exclamation::before {
- content: "\f692";
-}
-
-.bi-envelope-plus-fill::before {
- content: "\f693";
-}
-
-.bi-envelope-plus::before {
- content: "\f694";
-}
-
-.bi-envelope-slash-fill::before {
- content: "\f696";
-}
-
-.bi-envelope-slash::before {
- content: "\f697";
-}
-
-.bi-envelope-x-fill::before {
- content: "\f699";
-}
-
-.bi-envelope-x::before {
- content: "\f69a";
-}
-
-.bi-explicit-fill::before {
- content: "\f69b";
-}
-
-.bi-explicit::before {
- content: "\f69c";
-}
-
-.bi-git::before {
- content: "\f69d";
-}
-
-.bi-infinity::before {
- content: "\f69e";
-}
-
-.bi-list-columns-reverse::before {
- content: "\f69f";
-}
-
-.bi-list-columns::before {
- content: "\f6a0";
-}
-
-.bi-meta::before {
- content: "\f6a1";
-}
-
-.bi-nintendo-switch::before {
- content: "\f6a4";
-}
-
-.bi-pc-display-horizontal::before {
- content: "\f6a5";
-}
-
-.bi-pc-display::before {
- content: "\f6a6";
-}
-
-.bi-pc-horizontal::before {
- content: "\f6a7";
-}
-
-.bi-pc::before {
- content: "\f6a8";
-}
-
-.bi-playstation::before {
- content: "\f6a9";
-}
-
-.bi-plus-slash-minus::before {
- content: "\f6aa";
-}
-
-.bi-projector-fill::before {
- content: "\f6ab";
-}
-
-.bi-projector::before {
- content: "\f6ac";
-}
-
-.bi-qr-code-scan::before {
- content: "\f6ad";
-}
-
-.bi-qr-code::before {
- content: "\f6ae";
-}
-
-.bi-quora::before {
- content: "\f6af";
-}
-
-.bi-quote::before {
- content: "\f6b0";
-}
-
-.bi-robot::before {
- content: "\f6b1";
-}
-
-.bi-send-check-fill::before {
- content: "\f6b2";
-}
-
-.bi-send-check::before {
- content: "\f6b3";
-}
-
-.bi-send-dash-fill::before {
- content: "\f6b4";
-}
-
-.bi-send-dash::before {
- content: "\f6b5";
-}
-
-.bi-send-exclamation-fill::before {
- content: "\f6b7";
-}
-
-.bi-send-exclamation::before {
- content: "\f6b8";
-}
-
-.bi-send-fill::before {
- content: "\f6b9";
-}
-
-.bi-send-plus-fill::before {
- content: "\f6ba";
-}
-
-.bi-send-plus::before {
- content: "\f6bb";
-}
-
-.bi-send-slash-fill::before {
- content: "\f6bc";
-}
-
-.bi-send-slash::before {
- content: "\f6bd";
-}
-
-.bi-send-x-fill::before {
- content: "\f6be";
-}
-
-.bi-send-x::before {
- content: "\f6bf";
-}
-
-.bi-send::before {
- content: "\f6c0";
-}
-
-.bi-steam::before {
- content: "\f6c1";
-}
-
-.bi-terminal-dash::before {
- content: "\f6c3";
-}
-
-.bi-terminal-plus::before {
- content: "\f6c4";
-}
-
-.bi-terminal-split::before {
- content: "\f6c5";
-}
-
-.bi-ticket-detailed-fill::before {
- content: "\f6c6";
-}
-
-.bi-ticket-detailed::before {
- content: "\f6c7";
-}
-
-.bi-ticket-fill::before {
- content: "\f6c8";
-}
-
-.bi-ticket-perforated-fill::before {
- content: "\f6c9";
-}
-
-.bi-ticket-perforated::before {
- content: "\f6ca";
-}
-
-.bi-ticket::before {
- content: "\f6cb";
-}
-
-.bi-tiktok::before {
- content: "\f6cc";
-}
-
-.bi-window-dash::before {
- content: "\f6cd";
-}
-
-.bi-window-desktop::before {
- content: "\f6ce";
-}
-
-.bi-window-fullscreen::before {
- content: "\f6cf";
-}
-
-.bi-window-plus::before {
- content: "\f6d0";
-}
-
-.bi-window-split::before {
- content: "\f6d1";
-}
-
-.bi-window-stack::before {
- content: "\f6d2";
-}
-
-.bi-window-x::before {
- content: "\f6d3";
-}
-
-.bi-xbox::before {
- content: "\f6d4";
-}
-
-.bi-ethernet::before {
- content: "\f6d5";
-}
-
-.bi-hdmi-fill::before {
- content: "\f6d6";
-}
-
-.bi-hdmi::before {
- content: "\f6d7";
-}
-
-.bi-usb-c-fill::before {
- content: "\f6d8";
-}
-
-.bi-usb-c::before {
- content: "\f6d9";
-}
-
-.bi-usb-fill::before {
- content: "\f6da";
-}
-
-.bi-usb-plug-fill::before {
- content: "\f6db";
-}
-
-.bi-usb-plug::before {
- content: "\f6dc";
-}
-
-.bi-usb-symbol::before {
- content: "\f6dd";
-}
-
-.bi-usb::before {
- content: "\f6de";
-}
-
-.bi-boombox-fill::before {
- content: "\f6df";
-}
-
-.bi-displayport::before {
- content: "\f6e1";
-}
-
-.bi-gpu-card::before {
- content: "\f6e2";
-}
-
-.bi-memory::before {
- content: "\f6e3";
-}
-
-.bi-modem-fill::before {
- content: "\f6e4";
-}
-
-.bi-modem::before {
- content: "\f6e5";
-}
-
-.bi-motherboard-fill::before {
- content: "\f6e6";
-}
-
-.bi-motherboard::before {
- content: "\f6e7";
-}
-
-.bi-optical-audio-fill::before {
- content: "\f6e8";
-}
-
-.bi-optical-audio::before {
- content: "\f6e9";
-}
-
-.bi-pci-card::before {
- content: "\f6ea";
-}
-
-.bi-router-fill::before {
- content: "\f6eb";
-}
-
-.bi-router::before {
- content: "\f6ec";
-}
-
-.bi-thunderbolt-fill::before {
- content: "\f6ef";
-}
-
-.bi-thunderbolt::before {
- content: "\f6f0";
-}
-
-.bi-usb-drive-fill::before {
- content: "\f6f1";
-}
-
-.bi-usb-drive::before {
- content: "\f6f2";
-}
-
-.bi-usb-micro-fill::before {
- content: "\f6f3";
-}
-
-.bi-usb-micro::before {
- content: "\f6f4";
-}
-
-.bi-usb-mini-fill::before {
- content: "\f6f5";
-}
-
-.bi-usb-mini::before {
- content: "\f6f6";
-}
-
-.bi-cloud-haze2::before {
- content: "\f6f7";
-}
-
-.bi-device-hdd-fill::before {
- content: "\f6f8";
-}
-
-.bi-device-hdd::before {
- content: "\f6f9";
-}
-
-.bi-device-ssd-fill::before {
- content: "\f6fa";
-}
-
-.bi-device-ssd::before {
- content: "\f6fb";
-}
-
-.bi-displayport-fill::before {
- content: "\f6fc";
-}
-
-.bi-mortarboard-fill::before {
- content: "\f6fd";
-}
-
-.bi-mortarboard::before {
- content: "\f6fe";
-}
-
-.bi-terminal-x::before {
- content: "\f6ff";
-}
-
-.bi-arrow-through-heart-fill::before {
- content: "\f700";
-}
-
-.bi-arrow-through-heart::before {
- content: "\f701";
-}
-
-.bi-badge-sd-fill::before {
- content: "\f702";
-}
-
-.bi-badge-sd::before {
- content: "\f703";
-}
-
-.bi-bag-heart-fill::before {
- content: "\f704";
-}
-
-.bi-bag-heart::before {
- content: "\f705";
-}
-
-.bi-balloon-fill::before {
- content: "\f706";
-}
-
-.bi-balloon-heart-fill::before {
- content: "\f707";
-}
-
-.bi-balloon-heart::before {
- content: "\f708";
-}
-
-.bi-balloon::before {
- content: "\f709";
-}
-
-.bi-box2-fill::before {
- content: "\f70a";
-}
-
-.bi-box2-heart-fill::before {
- content: "\f70b";
-}
-
-.bi-box2-heart::before {
- content: "\f70c";
-}
-
-.bi-box2::before {
- content: "\f70d";
-}
-
-.bi-braces-asterisk::before {
- content: "\f70e";
-}
-
-.bi-calendar-heart-fill::before {
- content: "\f70f";
-}
-
-.bi-calendar-heart::before {
- content: "\f710";
-}
-
-.bi-calendar2-heart-fill::before {
- content: "\f711";
-}
-
-.bi-calendar2-heart::before {
- content: "\f712";
-}
-
-.bi-chat-heart-fill::before {
- content: "\f713";
-}
-
-.bi-chat-heart::before {
- content: "\f714";
-}
-
-.bi-chat-left-heart-fill::before {
- content: "\f715";
-}
-
-.bi-chat-left-heart::before {
- content: "\f716";
-}
-
-.bi-chat-right-heart-fill::before {
- content: "\f717";
-}
-
-.bi-chat-right-heart::before {
- content: "\f718";
-}
-
-.bi-chat-square-heart-fill::before {
- content: "\f719";
-}
-
-.bi-chat-square-heart::before {
- content: "\f71a";
-}
-
-.bi-clipboard-check-fill::before {
- content: "\f71b";
-}
-
-.bi-clipboard-data-fill::before {
- content: "\f71c";
-}
-
-.bi-clipboard-fill::before {
- content: "\f71d";
-}
-
-.bi-clipboard-heart-fill::before {
- content: "\f71e";
-}
-
-.bi-clipboard-heart::before {
- content: "\f71f";
-}
-
-.bi-clipboard-minus-fill::before {
- content: "\f720";
-}
-
-.bi-clipboard-plus-fill::before {
- content: "\f721";
-}
-
-.bi-clipboard-pulse::before {
- content: "\f722";
-}
-
-.bi-clipboard-x-fill::before {
- content: "\f723";
-}
-
-.bi-clipboard2-check-fill::before {
- content: "\f724";
-}
-
-.bi-clipboard2-check::before {
- content: "\f725";
-}
-
-.bi-clipboard2-data-fill::before {
- content: "\f726";
-}
-
-.bi-clipboard2-data::before {
- content: "\f727";
-}
-
-.bi-clipboard2-fill::before {
- content: "\f728";
-}
-
-.bi-clipboard2-heart-fill::before {
- content: "\f729";
-}
-
-.bi-clipboard2-heart::before {
- content: "\f72a";
-}
-
-.bi-clipboard2-minus-fill::before {
- content: "\f72b";
-}
-
-.bi-clipboard2-minus::before {
- content: "\f72c";
-}
-
-.bi-clipboard2-plus-fill::before {
- content: "\f72d";
-}
-
-.bi-clipboard2-plus::before {
- content: "\f72e";
-}
-
-.bi-clipboard2-pulse-fill::before {
- content: "\f72f";
-}
-
-.bi-clipboard2-pulse::before {
- content: "\f730";
-}
-
-.bi-clipboard2-x-fill::before {
- content: "\f731";
-}
-
-.bi-clipboard2-x::before {
- content: "\f732";
-}
-
-.bi-clipboard2::before {
- content: "\f733";
-}
-
-.bi-emoji-kiss-fill::before {
- content: "\f734";
-}
-
-.bi-emoji-kiss::before {
- content: "\f735";
-}
-
-.bi-envelope-heart-fill::before {
- content: "\f736";
-}
-
-.bi-envelope-heart::before {
- content: "\f737";
-}
-
-.bi-envelope-open-heart-fill::before {
- content: "\f738";
-}
-
-.bi-envelope-open-heart::before {
- content: "\f739";
-}
-
-.bi-envelope-paper-fill::before {
- content: "\f73a";
-}
-
-.bi-envelope-paper-heart-fill::before {
- content: "\f73b";
-}
-
-.bi-envelope-paper-heart::before {
- content: "\f73c";
-}
-
-.bi-envelope-paper::before {
- content: "\f73d";
-}
-
-.bi-filetype-aac::before {
- content: "\f73e";
-}
-
-.bi-filetype-ai::before {
- content: "\f73f";
-}
-
-.bi-filetype-bmp::before {
- content: "\f740";
-}
-
-.bi-filetype-cs::before {
- content: "\f741";
-}
-
-.bi-filetype-css::before {
- content: "\f742";
-}
-
-.bi-filetype-csv::before {
- content: "\f743";
-}
-
-.bi-filetype-doc::before {
- content: "\f744";
-}
-
-.bi-filetype-docx::before {
- content: "\f745";
-}
-
-.bi-filetype-exe::before {
- content: "\f746";
-}
-
-.bi-filetype-gif::before {
- content: "\f747";
-}
-
-.bi-filetype-heic::before {
- content: "\f748";
-}
-
-.bi-filetype-html::before {
- content: "\f749";
-}
-
-.bi-filetype-java::before {
- content: "\f74a";
-}
-
-.bi-filetype-jpg::before {
- content: "\f74b";
-}
-
-.bi-filetype-js::before {
- content: "\f74c";
-}
-
-.bi-filetype-jsx::before {
- content: "\f74d";
-}
-
-.bi-filetype-key::before {
- content: "\f74e";
-}
-
-.bi-filetype-m4p::before {
- content: "\f74f";
-}
-
-.bi-filetype-md::before {
- content: "\f750";
-}
-
-.bi-filetype-mdx::before {
- content: "\f751";
-}
-
-.bi-filetype-mov::before {
- content: "\f752";
-}
-
-.bi-filetype-mp3::before {
- content: "\f753";
-}
-
-.bi-filetype-mp4::before {
- content: "\f754";
-}
-
-.bi-filetype-otf::before {
- content: "\f755";
-}
-
-.bi-filetype-pdf::before {
- content: "\f756";
-}
-
-.bi-filetype-php::before {
- content: "\f757";
-}
-
-.bi-filetype-png::before {
- content: "\f758";
-}
-
-.bi-filetype-ppt::before {
- content: "\f75a";
-}
-
-.bi-filetype-psd::before {
- content: "\f75b";
-}
-
-.bi-filetype-py::before {
- content: "\f75c";
-}
-
-.bi-filetype-raw::before {
- content: "\f75d";
-}
-
-.bi-filetype-rb::before {
- content: "\f75e";
-}
-
-.bi-filetype-sass::before {
- content: "\f75f";
-}
-
-.bi-filetype-scss::before {
- content: "\f760";
-}
-
-.bi-filetype-sh::before {
- content: "\f761";
-}
-
-.bi-filetype-svg::before {
- content: "\f762";
-}
-
-.bi-filetype-tiff::before {
- content: "\f763";
-}
-
-.bi-filetype-tsx::before {
- content: "\f764";
-}
-
-.bi-filetype-ttf::before {
- content: "\f765";
-}
-
-.bi-filetype-txt::before {
- content: "\f766";
-}
-
-.bi-filetype-wav::before {
- content: "\f767";
-}
-
-.bi-filetype-woff::before {
- content: "\f768";
-}
-
-.bi-filetype-xls::before {
- content: "\f76a";
-}
-
-.bi-filetype-xml::before {
- content: "\f76b";
-}
-
-.bi-filetype-yml::before {
- content: "\f76c";
-}
-
-.bi-heart-arrow::before {
- content: "\f76d";
-}
-
-.bi-heart-pulse-fill::before {
- content: "\f76e";
-}
-
-.bi-heart-pulse::before {
- content: "\f76f";
-}
-
-.bi-heartbreak-fill::before {
- content: "\f770";
-}
-
-.bi-heartbreak::before {
- content: "\f771";
-}
-
-.bi-hearts::before {
- content: "\f772";
-}
-
-.bi-hospital-fill::before {
- content: "\f773";
-}
-
-.bi-hospital::before {
- content: "\f774";
-}
-
-.bi-house-heart-fill::before {
- content: "\f775";
-}
-
-.bi-house-heart::before {
- content: "\f776";
-}
-
-.bi-incognito::before {
- content: "\f777";
-}
-
-.bi-magnet-fill::before {
- content: "\f778";
-}
-
-.bi-magnet::before {
- content: "\f779";
-}
-
-.bi-person-heart::before {
- content: "\f77a";
-}
-
-.bi-person-hearts::before {
- content: "\f77b";
-}
-
-.bi-phone-flip::before {
- content: "\f77c";
-}
-
-.bi-plugin::before {
- content: "\f77d";
-}
-
-.bi-postage-fill::before {
- content: "\f77e";
-}
-
-.bi-postage-heart-fill::before {
- content: "\f77f";
-}
-
-.bi-postage-heart::before {
- content: "\f780";
-}
-
-.bi-postage::before {
- content: "\f781";
-}
-
-.bi-postcard-fill::before {
- content: "\f782";
-}
-
-.bi-postcard-heart-fill::before {
- content: "\f783";
-}
-
-.bi-postcard-heart::before {
- content: "\f784";
-}
-
-.bi-postcard::before {
- content: "\f785";
-}
-
-.bi-search-heart-fill::before {
- content: "\f786";
-}
-
-.bi-search-heart::before {
- content: "\f787";
-}
-
-.bi-sliders2-vertical::before {
- content: "\f788";
-}
-
-.bi-sliders2::before {
- content: "\f789";
-}
-
-.bi-trash3-fill::before {
- content: "\f78a";
-}
-
-.bi-trash3::before {
- content: "\f78b";
-}
-
-.bi-valentine::before {
- content: "\f78c";
-}
-
-.bi-valentine2::before {
- content: "\f78d";
-}
-
-.bi-wrench-adjustable-circle-fill::before {
- content: "\f78e";
-}
-
-.bi-wrench-adjustable-circle::before {
- content: "\f78f";
-}
-
-.bi-wrench-adjustable::before {
- content: "\f790";
-}
-
-.bi-filetype-json::before {
- content: "\f791";
-}
-
-.bi-filetype-pptx::before {
- content: "\f792";
-}
-
-.bi-filetype-xlsx::before {
- content: "\f793";
-}
-
-.bi-1-circle-fill::before {
- content: "\f796";
-}
-
-.bi-1-circle::before {
- content: "\f797";
-}
-
-.bi-1-square-fill::before {
- content: "\f798";
-}
-
-.bi-1-square::before {
- content: "\f799";
-}
-
-.bi-2-circle-fill::before {
- content: "\f79c";
-}
-
-.bi-2-circle::before {
- content: "\f79d";
-}
-
-.bi-2-square-fill::before {
- content: "\f79e";
-}
-
-.bi-2-square::before {
- content: "\f79f";
-}
-
-.bi-3-circle-fill::before {
- content: "\f7a2";
-}
-
-.bi-3-circle::before {
- content: "\f7a3";
-}
-
-.bi-3-square-fill::before {
- content: "\f7a4";
-}
-
-.bi-3-square::before {
- content: "\f7a5";
-}
-
-.bi-4-circle-fill::before {
- content: "\f7a8";
-}
-
-.bi-4-circle::before {
- content: "\f7a9";
-}
-
-.bi-4-square-fill::before {
- content: "\f7aa";
-}
-
-.bi-4-square::before {
- content: "\f7ab";
-}
-
-.bi-5-circle-fill::before {
- content: "\f7ae";
-}
-
-.bi-5-circle::before {
- content: "\f7af";
-}
-
-.bi-5-square-fill::before {
- content: "\f7b0";
-}
-
-.bi-5-square::before {
- content: "\f7b1";
-}
-
-.bi-6-circle-fill::before {
- content: "\f7b4";
-}
-
-.bi-6-circle::before {
- content: "\f7b5";
-}
-
-.bi-6-square-fill::before {
- content: "\f7b6";
-}
-
-.bi-6-square::before {
- content: "\f7b7";
-}
-
-.bi-7-circle-fill::before {
- content: "\f7ba";
-}
-
-.bi-7-circle::before {
- content: "\f7bb";
-}
-
-.bi-7-square-fill::before {
- content: "\f7bc";
-}
-
-.bi-7-square::before {
- content: "\f7bd";
-}
-
-.bi-8-circle-fill::before {
- content: "\f7c0";
-}
-
-.bi-8-circle::before {
- content: "\f7c1";
-}
-
-.bi-8-square-fill::before {
- content: "\f7c2";
-}
-
-.bi-8-square::before {
- content: "\f7c3";
-}
-
-.bi-9-circle-fill::before {
- content: "\f7c6";
-}
-
-.bi-9-circle::before {
- content: "\f7c7";
-}
-
-.bi-9-square-fill::before {
- content: "\f7c8";
-}
-
-.bi-9-square::before {
- content: "\f7c9";
-}
-
-.bi-airplane-engines-fill::before {
- content: "\f7ca";
-}
-
-.bi-airplane-engines::before {
- content: "\f7cb";
-}
-
-.bi-airplane-fill::before {
- content: "\f7cc";
-}
-
-.bi-airplane::before {
- content: "\f7cd";
-}
-
-.bi-alexa::before {
- content: "\f7ce";
-}
-
-.bi-alipay::before {
- content: "\f7cf";
-}
-
-.bi-android::before {
- content: "\f7d0";
-}
-
-.bi-android2::before {
- content: "\f7d1";
-}
-
-.bi-box-fill::before {
- content: "\f7d2";
-}
-
-.bi-box-seam-fill::before {
- content: "\f7d3";
-}
-
-.bi-browser-chrome::before {
- content: "\f7d4";
-}
-
-.bi-browser-edge::before {
- content: "\f7d5";
-}
-
-.bi-browser-firefox::before {
- content: "\f7d6";
-}
-
-.bi-browser-safari::before {
- content: "\f7d7";
-}
-
-.bi-c-circle-fill::before {
- content: "\f7da";
-}
-
-.bi-c-circle::before {
- content: "\f7db";
-}
-
-.bi-c-square-fill::before {
- content: "\f7dc";
-}
-
-.bi-c-square::before {
- content: "\f7dd";
-}
-
-.bi-capsule-pill::before {
- content: "\f7de";
-}
-
-.bi-capsule::before {
- content: "\f7df";
-}
-
-.bi-car-front-fill::before {
- content: "\f7e0";
-}
-
-.bi-car-front::before {
- content: "\f7e1";
-}
-
-.bi-cassette-fill::before {
- content: "\f7e2";
-}
-
-.bi-cassette::before {
- content: "\f7e3";
-}
-
-.bi-cc-circle-fill::before {
- content: "\f7e6";
-}
-
-.bi-cc-circle::before {
- content: "\f7e7";
-}
-
-.bi-cc-square-fill::before {
- content: "\f7e8";
-}
-
-.bi-cc-square::before {
- content: "\f7e9";
-}
-
-.bi-cup-hot-fill::before {
- content: "\f7ea";
-}
-
-.bi-cup-hot::before {
- content: "\f7eb";
-}
-
-.bi-currency-rupee::before {
- content: "\f7ec";
-}
-
-.bi-dropbox::before {
- content: "\f7ed";
-}
-
-.bi-escape::before {
- content: "\f7ee";
-}
-
-.bi-fast-forward-btn-fill::before {
- content: "\f7ef";
-}
-
-.bi-fast-forward-btn::before {
- content: "\f7f0";
-}
-
-.bi-fast-forward-circle-fill::before {
- content: "\f7f1";
-}
-
-.bi-fast-forward-circle::before {
- content: "\f7f2";
-}
-
-.bi-fast-forward-fill::before {
- content: "\f7f3";
-}
-
-.bi-fast-forward::before {
- content: "\f7f4";
-}
-
-.bi-filetype-sql::before {
- content: "\f7f5";
-}
-
-.bi-fire::before {
- content: "\f7f6";
-}
-
-.bi-google-play::before {
- content: "\f7f7";
-}
-
-.bi-h-circle-fill::before {
- content: "\f7fa";
-}
-
-.bi-h-circle::before {
- content: "\f7fb";
-}
-
-.bi-h-square-fill::before {
- content: "\f7fc";
-}
-
-.bi-h-square::before {
- content: "\f7fd";
-}
-
-.bi-indent::before {
- content: "\f7fe";
-}
-
-.bi-lungs-fill::before {
- content: "\f7ff";
-}
-
-.bi-lungs::before {
- content: "\f800";
-}
-
-.bi-microsoft-teams::before {
- content: "\f801";
-}
-
-.bi-p-circle-fill::before {
- content: "\f804";
-}
-
-.bi-p-circle::before {
- content: "\f805";
-}
-
-.bi-p-square-fill::before {
- content: "\f806";
-}
-
-.bi-p-square::before {
- content: "\f807";
-}
-
-.bi-pass-fill::before {
- content: "\f808";
-}
-
-.bi-pass::before {
- content: "\f809";
-}
-
-.bi-prescription::before {
- content: "\f80a";
-}
-
-.bi-prescription2::before {
- content: "\f80b";
-}
-
-.bi-r-circle-fill::before {
- content: "\f80e";
-}
-
-.bi-r-circle::before {
- content: "\f80f";
-}
-
-.bi-r-square-fill::before {
- content: "\f810";
-}
-
-.bi-r-square::before {
- content: "\f811";
-}
-
-.bi-repeat-1::before {
- content: "\f812";
-}
-
-.bi-repeat::before {
- content: "\f813";
-}
-
-.bi-rewind-btn-fill::before {
- content: "\f814";
-}
-
-.bi-rewind-btn::before {
- content: "\f815";
-}
-
-.bi-rewind-circle-fill::before {
- content: "\f816";
-}
-
-.bi-rewind-circle::before {
- content: "\f817";
-}
-
-.bi-rewind-fill::before {
- content: "\f818";
-}
-
-.bi-rewind::before {
- content: "\f819";
-}
-
-.bi-train-freight-front-fill::before {
- content: "\f81a";
-}
-
-.bi-train-freight-front::before {
- content: "\f81b";
-}
-
-.bi-train-front-fill::before {
- content: "\f81c";
-}
-
-.bi-train-front::before {
- content: "\f81d";
-}
-
-.bi-train-lightrail-front-fill::before {
- content: "\f81e";
-}
-
-.bi-train-lightrail-front::before {
- content: "\f81f";
-}
-
-.bi-truck-front-fill::before {
- content: "\f820";
-}
-
-.bi-truck-front::before {
- content: "\f821";
-}
-
-.bi-ubuntu::before {
- content: "\f822";
-}
-
-.bi-unindent::before {
- content: "\f823";
-}
-
-.bi-unity::before {
- content: "\f824";
-}
-
-.bi-universal-access-circle::before {
- content: "\f825";
-}
-
-.bi-universal-access::before {
- content: "\f826";
-}
-
-.bi-virus::before {
- content: "\f827";
-}
-
-.bi-virus2::before {
- content: "\f828";
-}
-
-.bi-wechat::before {
- content: "\f829";
-}
-
-.bi-yelp::before {
- content: "\f82a";
-}
-
-.bi-sign-stop-fill::before {
- content: "\f82b";
-}
-
-.bi-sign-stop-lights-fill::before {
- content: "\f82c";
-}
-
-.bi-sign-stop-lights::before {
- content: "\f82d";
-}
-
-.bi-sign-stop::before {
- content: "\f82e";
-}
-
-.bi-sign-turn-left-fill::before {
- content: "\f82f";
-}
-
-.bi-sign-turn-left::before {
- content: "\f830";
-}
-
-.bi-sign-turn-right-fill::before {
- content: "\f831";
-}
-
-.bi-sign-turn-right::before {
- content: "\f832";
-}
-
-.bi-sign-turn-slight-left-fill::before {
- content: "\f833";
-}
-
-.bi-sign-turn-slight-left::before {
- content: "\f834";
-}
-
-.bi-sign-turn-slight-right-fill::before {
- content: "\f835";
-}
-
-.bi-sign-turn-slight-right::before {
- content: "\f836";
-}
-
-.bi-sign-yield-fill::before {
- content: "\f837";
-}
-
-.bi-sign-yield::before {
- content: "\f838";
-}
-
-.bi-ev-station-fill::before {
- content: "\f839";
-}
-
-.bi-ev-station::before {
- content: "\f83a";
-}
-
-.bi-fuel-pump-diesel-fill::before {
- content: "\f83b";
-}
-
-.bi-fuel-pump-diesel::before {
- content: "\f83c";
-}
-
-.bi-fuel-pump-fill::before {
- content: "\f83d";
-}
-
-.bi-fuel-pump::before {
- content: "\f83e";
-}
-
-.bi-0-circle-fill::before {
- content: "\f83f";
-}
-
-.bi-0-circle::before {
- content: "\f840";
-}
-
-.bi-0-square-fill::before {
- content: "\f841";
-}
-
-.bi-0-square::before {
- content: "\f842";
-}
-
-.bi-rocket-fill::before {
- content: "\f843";
-}
-
-.bi-rocket-takeoff-fill::before {
- content: "\f844";
-}
-
-.bi-rocket-takeoff::before {
- content: "\f845";
-}
-
-.bi-rocket::before {
- content: "\f846";
-}
-
-.bi-stripe::before {
- content: "\f847";
-}
-
-.bi-subscript::before {
- content: "\f848";
-}
-
-.bi-superscript::before {
- content: "\f849";
-}
-
-.bi-trello::before {
- content: "\f84a";
-}
-
-.bi-envelope-at-fill::before {
- content: "\f84b";
-}
-
-.bi-envelope-at::before {
- content: "\f84c";
-}
-
-.bi-regex::before {
- content: "\f84d";
-}
-
-.bi-text-wrap::before {
- content: "\f84e";
-}
-
-.bi-sign-dead-end-fill::before {
- content: "\f84f";
-}
-
-.bi-sign-dead-end::before {
- content: "\f850";
-}
-
-.bi-sign-do-not-enter-fill::before {
- content: "\f851";
-}
-
-.bi-sign-do-not-enter::before {
- content: "\f852";
-}
-
-.bi-sign-intersection-fill::before {
- content: "\f853";
-}
-
-.bi-sign-intersection-side-fill::before {
- content: "\f854";
-}
-
-.bi-sign-intersection-side::before {
- content: "\f855";
-}
-
-.bi-sign-intersection-t-fill::before {
- content: "\f856";
-}
-
-.bi-sign-intersection-t::before {
- content: "\f857";
-}
-
-.bi-sign-intersection-y-fill::before {
- content: "\f858";
-}
-
-.bi-sign-intersection-y::before {
- content: "\f859";
-}
-
-.bi-sign-intersection::before {
- content: "\f85a";
-}
-
-.bi-sign-merge-left-fill::before {
- content: "\f85b";
-}
-
-.bi-sign-merge-left::before {
- content: "\f85c";
-}
-
-.bi-sign-merge-right-fill::before {
- content: "\f85d";
-}
-
-.bi-sign-merge-right::before {
- content: "\f85e";
-}
-
-.bi-sign-no-left-turn-fill::before {
- content: "\f85f";
-}
-
-.bi-sign-no-left-turn::before {
- content: "\f860";
-}
-
-.bi-sign-no-parking-fill::before {
- content: "\f861";
-}
-
-.bi-sign-no-parking::before {
- content: "\f862";
-}
-
-.bi-sign-no-right-turn-fill::before {
- content: "\f863";
-}
-
-.bi-sign-no-right-turn::before {
- content: "\f864";
-}
-
-.bi-sign-railroad-fill::before {
- content: "\f865";
-}
-
-.bi-sign-railroad::before {
- content: "\f866";
-}
-
-.bi-building-add::before {
- content: "\f867";
-}
-
-.bi-building-check::before {
- content: "\f868";
-}
-
-.bi-building-dash::before {
- content: "\f869";
-}
-
-.bi-building-down::before {
- content: "\f86a";
-}
-
-.bi-building-exclamation::before {
- content: "\f86b";
-}
-
-.bi-building-fill-add::before {
- content: "\f86c";
-}
-
-.bi-building-fill-check::before {
- content: "\f86d";
-}
-
-.bi-building-fill-dash::before {
- content: "\f86e";
-}
-
-.bi-building-fill-down::before {
- content: "\f86f";
-}
-
-.bi-building-fill-exclamation::before {
- content: "\f870";
-}
-
-.bi-building-fill-gear::before {
- content: "\f871";
-}
-
-.bi-building-fill-lock::before {
- content: "\f872";
-}
-
-.bi-building-fill-slash::before {
- content: "\f873";
-}
-
-.bi-building-fill-up::before {
- content: "\f874";
-}
-
-.bi-building-fill-x::before {
- content: "\f875";
-}
-
-.bi-building-fill::before {
- content: "\f876";
-}
-
-.bi-building-gear::before {
- content: "\f877";
-}
-
-.bi-building-lock::before {
- content: "\f878";
-}
-
-.bi-building-slash::before {
- content: "\f879";
-}
-
-.bi-building-up::before {
- content: "\f87a";
-}
-
-.bi-building-x::before {
- content: "\f87b";
-}
-
-.bi-buildings-fill::before {
- content: "\f87c";
-}
-
-.bi-buildings::before {
- content: "\f87d";
-}
-
-.bi-bus-front-fill::before {
- content: "\f87e";
-}
-
-.bi-bus-front::before {
- content: "\f87f";
-}
-
-.bi-ev-front-fill::before {
- content: "\f880";
-}
-
-.bi-ev-front::before {
- content: "\f881";
-}
-
-.bi-globe-americas::before {
- content: "\f882";
-}
-
-.bi-globe-asia-australia::before {
- content: "\f883";
-}
-
-.bi-globe-central-south-asia::before {
- content: "\f884";
-}
-
-.bi-globe-europe-africa::before {
- content: "\f885";
-}
-
-.bi-house-add-fill::before {
- content: "\f886";
-}
-
-.bi-house-add::before {
- content: "\f887";
-}
-
-.bi-house-check-fill::before {
- content: "\f888";
-}
-
-.bi-house-check::before {
- content: "\f889";
-}
-
-.bi-house-dash-fill::before {
- content: "\f88a";
-}
-
-.bi-house-dash::before {
- content: "\f88b";
-}
-
-.bi-house-down-fill::before {
- content: "\f88c";
-}
-
-.bi-house-down::before {
- content: "\f88d";
-}
-
-.bi-house-exclamation-fill::before {
- content: "\f88e";
-}
-
-.bi-house-exclamation::before {
- content: "\f88f";
-}
-
-.bi-house-gear-fill::before {
- content: "\f890";
-}
-
-.bi-house-gear::before {
- content: "\f891";
-}
-
-.bi-house-lock-fill::before {
- content: "\f892";
-}
-
-.bi-house-lock::before {
- content: "\f893";
-}
-
-.bi-house-slash-fill::before {
- content: "\f894";
-}
-
-.bi-house-slash::before {
- content: "\f895";
-}
-
-.bi-house-up-fill::before {
- content: "\f896";
-}
-
-.bi-house-up::before {
- content: "\f897";
-}
-
-.bi-house-x-fill::before {
- content: "\f898";
-}
-
-.bi-house-x::before {
- content: "\f899";
-}
-
-.bi-person-add::before {
- content: "\f89a";
-}
-
-.bi-person-down::before {
- content: "\f89b";
-}
-
-.bi-person-exclamation::before {
- content: "\f89c";
-}
-
-.bi-person-fill-add::before {
- content: "\f89d";
-}
-
-.bi-person-fill-check::before {
- content: "\f89e";
-}
-
-.bi-person-fill-dash::before {
- content: "\f89f";
-}
-
-.bi-person-fill-down::before {
- content: "\f8a0";
-}
-
-.bi-person-fill-exclamation::before {
- content: "\f8a1";
-}
-
-.bi-person-fill-gear::before {
- content: "\f8a2";
-}
-
-.bi-person-fill-lock::before {
- content: "\f8a3";
-}
-
-.bi-person-fill-slash::before {
- content: "\f8a4";
-}
-
-.bi-person-fill-up::before {
- content: "\f8a5";
-}
-
-.bi-person-fill-x::before {
- content: "\f8a6";
-}
-
-.bi-person-gear::before {
- content: "\f8a7";
-}
-
-.bi-person-lock::before {
- content: "\f8a8";
-}
-
-.bi-person-slash::before {
- content: "\f8a9";
-}
-
-.bi-person-up::before {
- content: "\f8aa";
-}
-
-.bi-scooter::before {
- content: "\f8ab";
-}
-
-.bi-taxi-front-fill::before {
- content: "\f8ac";
-}
-
-.bi-taxi-front::before {
- content: "\f8ad";
-}
-
-.bi-amd::before {
- content: "\f8ae";
-}
-
-.bi-database-add::before {
- content: "\f8af";
-}
-
-.bi-database-check::before {
- content: "\f8b0";
-}
-
-.bi-database-dash::before {
- content: "\f8b1";
-}
-
-.bi-database-down::before {
- content: "\f8b2";
-}
-
-.bi-database-exclamation::before {
- content: "\f8b3";
-}
-
-.bi-database-fill-add::before {
- content: "\f8b4";
-}
-
-.bi-database-fill-check::before {
- content: "\f8b5";
-}
-
-.bi-database-fill-dash::before {
- content: "\f8b6";
-}
-
-.bi-database-fill-down::before {
- content: "\f8b7";
-}
-
-.bi-database-fill-exclamation::before {
- content: "\f8b8";
-}
-
-.bi-database-fill-gear::before {
- content: "\f8b9";
-}
-
-.bi-database-fill-lock::before {
- content: "\f8ba";
-}
-
-.bi-database-fill-slash::before {
- content: "\f8bb";
-}
-
-.bi-database-fill-up::before {
- content: "\f8bc";
-}
-
-.bi-database-fill-x::before {
- content: "\f8bd";
-}
-
-.bi-database-fill::before {
- content: "\f8be";
-}
-
-.bi-database-gear::before {
- content: "\f8bf";
-}
-
-.bi-database-lock::before {
- content: "\f8c0";
-}
-
-.bi-database-slash::before {
- content: "\f8c1";
-}
-
-.bi-database-up::before {
- content: "\f8c2";
-}
-
-.bi-database-x::before {
- content: "\f8c3";
-}
-
-.bi-database::before {
- content: "\f8c4";
-}
-
-.bi-houses-fill::before {
- content: "\f8c5";
-}
-
-.bi-houses::before {
- content: "\f8c6";
-}
-
-.bi-nvidia::before {
- content: "\f8c7";
-}
-
-.bi-person-vcard-fill::before {
- content: "\f8c8";
-}
-
-.bi-person-vcard::before {
- content: "\f8c9";
-}
-
-.bi-sina-weibo::before {
- content: "\f8ca";
-}
-
-.bi-tencent-qq::before {
- content: "\f8cb";
-}
-
-.bi-wikipedia::before {
- content: "\f8cc";
-}
-
-.bi-alphabet-uppercase::before {
- content: "\f2a5";
-}
-
-.bi-alphabet::before {
- content: "\f68a";
-}
-
-.bi-amazon::before {
- content: "\f68d";
-}
-
-.bi-arrows-collapse-vertical::before {
- content: "\f690";
-}
-
-.bi-arrows-expand-vertical::before {
- content: "\f695";
-}
-
-.bi-arrows-vertical::before {
- content: "\f698";
-}
-
-.bi-arrows::before {
- content: "\f6a2";
-}
-
-.bi-ban-fill::before {
- content: "\f6a3";
-}
-
-.bi-ban::before {
- content: "\f6b6";
-}
-
-.bi-bing::before {
- content: "\f6c2";
-}
-
-.bi-cake::before {
- content: "\f6e0";
-}
-
-.bi-cake2::before {
- content: "\f6ed";
-}
-
-.bi-cookie::before {
- content: "\f6ee";
-}
-
-.bi-copy::before {
- content: "\f759";
-}
-
-.bi-crosshair::before {
- content: "\f769";
-}
-
-.bi-crosshair2::before {
- content: "\f794";
-}
-
-.bi-emoji-astonished-fill::before {
- content: "\f795";
-}
-
-.bi-emoji-astonished::before {
- content: "\f79a";
-}
-
-.bi-emoji-grimace-fill::before {
- content: "\f79b";
-}
-
-.bi-emoji-grimace::before {
- content: "\f7a0";
-}
-
-.bi-emoji-grin-fill::before {
- content: "\f7a1";
-}
-
-.bi-emoji-grin::before {
- content: "\f7a6";
-}
-
-.bi-emoji-surprise-fill::before {
- content: "\f7a7";
-}
-
-.bi-emoji-surprise::before {
- content: "\f7ac";
-}
-
-.bi-emoji-tear-fill::before {
- content: "\f7ad";
-}
-
-.bi-emoji-tear::before {
- content: "\f7b2";
-}
-
-.bi-envelope-arrow-down-fill::before {
- content: "\f7b3";
-}
-
-.bi-envelope-arrow-down::before {
- content: "\f7b8";
-}
-
-.bi-envelope-arrow-up-fill::before {
- content: "\f7b9";
-}
-
-.bi-envelope-arrow-up::before {
- content: "\f7be";
-}
-
-.bi-feather::before {
- content: "\f7bf";
-}
-
-.bi-feather2::before {
- content: "\f7c4";
-}
-
-.bi-floppy-fill::before {
- content: "\f7c5";
-}
-
-.bi-floppy::before {
- content: "\f7d8";
-}
-
-.bi-floppy2-fill::before {
- content: "\f7d9";
-}
-
-.bi-floppy2::before {
- content: "\f7e4";
-}
-
-.bi-gitlab::before {
- content: "\f7e5";
-}
-
-.bi-highlighter::before {
- content: "\f7f8";
-}
-
-.bi-marker-tip::before {
- content: "\f802";
-}
-
-.bi-nvme-fill::before {
- content: "\f803";
-}
-
-.bi-nvme::before {
- content: "\f80c";
-}
-
-.bi-opencollective::before {
- content: "\f80d";
-}
-
-.bi-pci-card-network::before {
- content: "\f8cd";
-}
-
-.bi-pci-card-sound::before {
- content: "\f8ce";
-}
-
-.bi-radar::before {
- content: "\f8cf";
-}
-
-.bi-send-arrow-down-fill::before {
- content: "\f8d0";
-}
-
-.bi-send-arrow-down::before {
- content: "\f8d1";
-}
-
-.bi-send-arrow-up-fill::before {
- content: "\f8d2";
-}
-
-.bi-send-arrow-up::before {
- content: "\f8d3";
-}
-
-.bi-sim-slash-fill::before {
- content: "\f8d4";
-}
-
-.bi-sim-slash::before {
- content: "\f8d5";
-}
-
-.bi-sourceforge::before {
- content: "\f8d6";
-}
-
-.bi-substack::before {
- content: "\f8d7";
-}
-
-.bi-threads-fill::before {
- content: "\f8d8";
-}
-
-.bi-threads::before {
- content: "\f8d9";
-}
-
-.bi-transparency::before {
- content: "\f8da";
-}
-
-.bi-twitter-x::before {
- content: "\f8db";
-}
-
-.bi-type-h4::before {
- content: "\f8dc";
-}
-
-.bi-type-h5::before {
- content: "\f8dd";
-}
-
-.bi-type-h6::before {
- content: "\f8de";
-}
-
-.bi-backpack-fill::before {
- content: "\f8df";
-}
-
-.bi-backpack::before {
- content: "\f8e0";
-}
-
-.bi-backpack2-fill::before {
- content: "\f8e1";
-}
-
-.bi-backpack2::before {
- content: "\f8e2";
-}
-
-.bi-backpack3-fill::before {
- content: "\f8e3";
-}
-
-.bi-backpack3::before {
- content: "\f8e4";
-}
-
-.bi-backpack4-fill::before {
- content: "\f8e5";
-}
-
-.bi-backpack4::before {
- content: "\f8e6";
-}
-
-.bi-brilliance::before {
- content: "\f8e7";
-}
-
-.bi-cake-fill::before {
- content: "\f8e8";
-}
-
-.bi-cake2-fill::before {
- content: "\f8e9";
-}
-
-.bi-duffle-fill::before {
- content: "\f8ea";
-}
-
-.bi-duffle::before {
- content: "\f8eb";
-}
-
-.bi-exposure::before {
- content: "\f8ec";
-}
-
-.bi-gender-neuter::before {
- content: "\f8ed";
-}
-
-.bi-highlights::before {
- content: "\f8ee";
-}
-
-.bi-luggage-fill::before {
- content: "\f8ef";
-}
-
-.bi-luggage::before {
- content: "\f8f0";
-}
-
-.bi-mailbox-flag::before {
- content: "\f8f1";
-}
-
-.bi-mailbox2-flag::before {
- content: "\f8f2";
-}
-
-.bi-noise-reduction::before {
- content: "\f8f3";
-}
-
-.bi-passport-fill::before {
- content: "\f8f4";
-}
-
-.bi-passport::before {
- content: "\f8f5";
-}
-
-.bi-person-arms-up::before {
- content: "\f8f6";
-}
-
-.bi-person-raised-hand::before {
- content: "\f8f7";
-}
-
-.bi-person-standing-dress::before {
- content: "\f8f8";
-}
-
-.bi-person-standing::before {
- content: "\f8f9";
-}
-
-.bi-person-walking::before {
- content: "\f8fa";
-}
-
-.bi-person-wheelchair::before {
- content: "\f8fb";
-}
-
-.bi-shadows::before {
- content: "\f8fc";
-}
-
-.bi-suitcase-fill::before {
- content: "\f8fd";
-}
-
-.bi-suitcase-lg-fill::before {
- content: "\f8fe";
-}
-
-.bi-suitcase-lg::before {
- content: "\f8ff";
-}
-
-.bi-suitcase::before {
- content: "\f900";
-}
-
-.bi-suitcase2-fill::before {
- content: "\f901";
-}
-
-.bi-suitcase2::before {
- content: "\f902";
-}
-
-.bi-vignette::before {
- content: "\f903";
-}
diff --git a/static/bootstrap/icons/bootstrap-icons.json b/static/bootstrap/icons/bootstrap-icons.json
deleted file mode 100644
index 56247e50..00000000
--- a/static/bootstrap/icons/bootstrap-icons.json
+++ /dev/null
@@ -1,2052 +0,0 @@
-{
- "123": 63103,
- "alarm-fill": 61697,
- "alarm": 61698,
- "align-bottom": 61699,
- "align-center": 61700,
- "align-end": 61701,
- "align-middle": 61702,
- "align-start": 61703,
- "align-top": 61704,
- "alt": 61705,
- "app-indicator": 61706,
- "app": 61707,
- "archive-fill": 61708,
- "archive": 61709,
- "arrow-90deg-down": 61710,
- "arrow-90deg-left": 61711,
- "arrow-90deg-right": 61712,
- "arrow-90deg-up": 61713,
- "arrow-bar-down": 61714,
- "arrow-bar-left": 61715,
- "arrow-bar-right": 61716,
- "arrow-bar-up": 61717,
- "arrow-clockwise": 61718,
- "arrow-counterclockwise": 61719,
- "arrow-down-circle-fill": 61720,
- "arrow-down-circle": 61721,
- "arrow-down-left-circle-fill": 61722,
- "arrow-down-left-circle": 61723,
- "arrow-down-left-square-fill": 61724,
- "arrow-down-left-square": 61725,
- "arrow-down-left": 61726,
- "arrow-down-right-circle-fill": 61727,
- "arrow-down-right-circle": 61728,
- "arrow-down-right-square-fill": 61729,
- "arrow-down-right-square": 61730,
- "arrow-down-right": 61731,
- "arrow-down-short": 61732,
- "arrow-down-square-fill": 61733,
- "arrow-down-square": 61734,
- "arrow-down-up": 61735,
- "arrow-down": 61736,
- "arrow-left-circle-fill": 61737,
- "arrow-left-circle": 61738,
- "arrow-left-right": 61739,
- "arrow-left-short": 61740,
- "arrow-left-square-fill": 61741,
- "arrow-left-square": 61742,
- "arrow-left": 61743,
- "arrow-repeat": 61744,
- "arrow-return-left": 61745,
- "arrow-return-right": 61746,
- "arrow-right-circle-fill": 61747,
- "arrow-right-circle": 61748,
- "arrow-right-short": 61749,
- "arrow-right-square-fill": 61750,
- "arrow-right-square": 61751,
- "arrow-right": 61752,
- "arrow-up-circle-fill": 61753,
- "arrow-up-circle": 61754,
- "arrow-up-left-circle-fill": 61755,
- "arrow-up-left-circle": 61756,
- "arrow-up-left-square-fill": 61757,
- "arrow-up-left-square": 61758,
- "arrow-up-left": 61759,
- "arrow-up-right-circle-fill": 61760,
- "arrow-up-right-circle": 61761,
- "arrow-up-right-square-fill": 61762,
- "arrow-up-right-square": 61763,
- "arrow-up-right": 61764,
- "arrow-up-short": 61765,
- "arrow-up-square-fill": 61766,
- "arrow-up-square": 61767,
- "arrow-up": 61768,
- "arrows-angle-contract": 61769,
- "arrows-angle-expand": 61770,
- "arrows-collapse": 61771,
- "arrows-expand": 61772,
- "arrows-fullscreen": 61773,
- "arrows-move": 61774,
- "aspect-ratio-fill": 61775,
- "aspect-ratio": 61776,
- "asterisk": 61777,
- "at": 61778,
- "award-fill": 61779,
- "award": 61780,
- "back": 61781,
- "backspace-fill": 61782,
- "backspace-reverse-fill": 61783,
- "backspace-reverse": 61784,
- "backspace": 61785,
- "badge-3d-fill": 61786,
- "badge-3d": 61787,
- "badge-4k-fill": 61788,
- "badge-4k": 61789,
- "badge-8k-fill": 61790,
- "badge-8k": 61791,
- "badge-ad-fill": 61792,
- "badge-ad": 61793,
- "badge-ar-fill": 61794,
- "badge-ar": 61795,
- "badge-cc-fill": 61796,
- "badge-cc": 61797,
- "badge-hd-fill": 61798,
- "badge-hd": 61799,
- "badge-tm-fill": 61800,
- "badge-tm": 61801,
- "badge-vo-fill": 61802,
- "badge-vo": 61803,
- "badge-vr-fill": 61804,
- "badge-vr": 61805,
- "badge-wc-fill": 61806,
- "badge-wc": 61807,
- "bag-check-fill": 61808,
- "bag-check": 61809,
- "bag-dash-fill": 61810,
- "bag-dash": 61811,
- "bag-fill": 61812,
- "bag-plus-fill": 61813,
- "bag-plus": 61814,
- "bag-x-fill": 61815,
- "bag-x": 61816,
- "bag": 61817,
- "bar-chart-fill": 61818,
- "bar-chart-line-fill": 61819,
- "bar-chart-line": 61820,
- "bar-chart-steps": 61821,
- "bar-chart": 61822,
- "basket-fill": 61823,
- "basket": 61824,
- "basket2-fill": 61825,
- "basket2": 61826,
- "basket3-fill": 61827,
- "basket3": 61828,
- "battery-charging": 61829,
- "battery-full": 61830,
- "battery-half": 61831,
- "battery": 61832,
- "bell-fill": 61833,
- "bell": 61834,
- "bezier": 61835,
- "bezier2": 61836,
- "bicycle": 61837,
- "binoculars-fill": 61838,
- "binoculars": 61839,
- "blockquote-left": 61840,
- "blockquote-right": 61841,
- "book-fill": 61842,
- "book-half": 61843,
- "book": 61844,
- "bookmark-check-fill": 61845,
- "bookmark-check": 61846,
- "bookmark-dash-fill": 61847,
- "bookmark-dash": 61848,
- "bookmark-fill": 61849,
- "bookmark-heart-fill": 61850,
- "bookmark-heart": 61851,
- "bookmark-plus-fill": 61852,
- "bookmark-plus": 61853,
- "bookmark-star-fill": 61854,
- "bookmark-star": 61855,
- "bookmark-x-fill": 61856,
- "bookmark-x": 61857,
- "bookmark": 61858,
- "bookmarks-fill": 61859,
- "bookmarks": 61860,
- "bookshelf": 61861,
- "bootstrap-fill": 61862,
- "bootstrap-reboot": 61863,
- "bootstrap": 61864,
- "border-all": 61865,
- "border-bottom": 61866,
- "border-center": 61867,
- "border-inner": 61868,
- "border-left": 61869,
- "border-middle": 61870,
- "border-outer": 61871,
- "border-right": 61872,
- "border-style": 61873,
- "border-top": 61874,
- "border-width": 61875,
- "border": 61876,
- "bounding-box-circles": 61877,
- "bounding-box": 61878,
- "box-arrow-down-left": 61879,
- "box-arrow-down-right": 61880,
- "box-arrow-down": 61881,
- "box-arrow-in-down-left": 61882,
- "box-arrow-in-down-right": 61883,
- "box-arrow-in-down": 61884,
- "box-arrow-in-left": 61885,
- "box-arrow-in-right": 61886,
- "box-arrow-in-up-left": 61887,
- "box-arrow-in-up-right": 61888,
- "box-arrow-in-up": 61889,
- "box-arrow-left": 61890,
- "box-arrow-right": 61891,
- "box-arrow-up-left": 61892,
- "box-arrow-up-right": 61893,
- "box-arrow-up": 61894,
- "box-seam": 61895,
- "box": 61896,
- "braces": 61897,
- "bricks": 61898,
- "briefcase-fill": 61899,
- "briefcase": 61900,
- "brightness-alt-high-fill": 61901,
- "brightness-alt-high": 61902,
- "brightness-alt-low-fill": 61903,
- "brightness-alt-low": 61904,
- "brightness-high-fill": 61905,
- "brightness-high": 61906,
- "brightness-low-fill": 61907,
- "brightness-low": 61908,
- "broadcast-pin": 61909,
- "broadcast": 61910,
- "brush-fill": 61911,
- "brush": 61912,
- "bucket-fill": 61913,
- "bucket": 61914,
- "bug-fill": 61915,
- "bug": 61916,
- "building": 61917,
- "bullseye": 61918,
- "calculator-fill": 61919,
- "calculator": 61920,
- "calendar-check-fill": 61921,
- "calendar-check": 61922,
- "calendar-date-fill": 61923,
- "calendar-date": 61924,
- "calendar-day-fill": 61925,
- "calendar-day": 61926,
- "calendar-event-fill": 61927,
- "calendar-event": 61928,
- "calendar-fill": 61929,
- "calendar-minus-fill": 61930,
- "calendar-minus": 61931,
- "calendar-month-fill": 61932,
- "calendar-month": 61933,
- "calendar-plus-fill": 61934,
- "calendar-plus": 61935,
- "calendar-range-fill": 61936,
- "calendar-range": 61937,
- "calendar-week-fill": 61938,
- "calendar-week": 61939,
- "calendar-x-fill": 61940,
- "calendar-x": 61941,
- "calendar": 61942,
- "calendar2-check-fill": 61943,
- "calendar2-check": 61944,
- "calendar2-date-fill": 61945,
- "calendar2-date": 61946,
- "calendar2-day-fill": 61947,
- "calendar2-day": 61948,
- "calendar2-event-fill": 61949,
- "calendar2-event": 61950,
- "calendar2-fill": 61951,
- "calendar2-minus-fill": 61952,
- "calendar2-minus": 61953,
- "calendar2-month-fill": 61954,
- "calendar2-month": 61955,
- "calendar2-plus-fill": 61956,
- "calendar2-plus": 61957,
- "calendar2-range-fill": 61958,
- "calendar2-range": 61959,
- "calendar2-week-fill": 61960,
- "calendar2-week": 61961,
- "calendar2-x-fill": 61962,
- "calendar2-x": 61963,
- "calendar2": 61964,
- "calendar3-event-fill": 61965,
- "calendar3-event": 61966,
- "calendar3-fill": 61967,
- "calendar3-range-fill": 61968,
- "calendar3-range": 61969,
- "calendar3-week-fill": 61970,
- "calendar3-week": 61971,
- "calendar3": 61972,
- "calendar4-event": 61973,
- "calendar4-range": 61974,
- "calendar4-week": 61975,
- "calendar4": 61976,
- "camera-fill": 61977,
- "camera-reels-fill": 61978,
- "camera-reels": 61979,
- "camera-video-fill": 61980,
- "camera-video-off-fill": 61981,
- "camera-video-off": 61982,
- "camera-video": 61983,
- "camera": 61984,
- "camera2": 61985,
- "capslock-fill": 61986,
- "capslock": 61987,
- "card-checklist": 61988,
- "card-heading": 61989,
- "card-image": 61990,
- "card-list": 61991,
- "card-text": 61992,
- "caret-down-fill": 61993,
- "caret-down-square-fill": 61994,
- "caret-down-square": 61995,
- "caret-down": 61996,
- "caret-left-fill": 61997,
- "caret-left-square-fill": 61998,
- "caret-left-square": 61999,
- "caret-left": 62000,
- "caret-right-fill": 62001,
- "caret-right-square-fill": 62002,
- "caret-right-square": 62003,
- "caret-right": 62004,
- "caret-up-fill": 62005,
- "caret-up-square-fill": 62006,
- "caret-up-square": 62007,
- "caret-up": 62008,
- "cart-check-fill": 62009,
- "cart-check": 62010,
- "cart-dash-fill": 62011,
- "cart-dash": 62012,
- "cart-fill": 62013,
- "cart-plus-fill": 62014,
- "cart-plus": 62015,
- "cart-x-fill": 62016,
- "cart-x": 62017,
- "cart": 62018,
- "cart2": 62019,
- "cart3": 62020,
- "cart4": 62021,
- "cash-stack": 62022,
- "cash": 62023,
- "cast": 62024,
- "chat-dots-fill": 62025,
- "chat-dots": 62026,
- "chat-fill": 62027,
- "chat-left-dots-fill": 62028,
- "chat-left-dots": 62029,
- "chat-left-fill": 62030,
- "chat-left-quote-fill": 62031,
- "chat-left-quote": 62032,
- "chat-left-text-fill": 62033,
- "chat-left-text": 62034,
- "chat-left": 62035,
- "chat-quote-fill": 62036,
- "chat-quote": 62037,
- "chat-right-dots-fill": 62038,
- "chat-right-dots": 62039,
- "chat-right-fill": 62040,
- "chat-right-quote-fill": 62041,
- "chat-right-quote": 62042,
- "chat-right-text-fill": 62043,
- "chat-right-text": 62044,
- "chat-right": 62045,
- "chat-square-dots-fill": 62046,
- "chat-square-dots": 62047,
- "chat-square-fill": 62048,
- "chat-square-quote-fill": 62049,
- "chat-square-quote": 62050,
- "chat-square-text-fill": 62051,
- "chat-square-text": 62052,
- "chat-square": 62053,
- "chat-text-fill": 62054,
- "chat-text": 62055,
- "chat": 62056,
- "check-all": 62057,
- "check-circle-fill": 62058,
- "check-circle": 62059,
- "check-square-fill": 62060,
- "check-square": 62061,
- "check": 62062,
- "check2-all": 62063,
- "check2-circle": 62064,
- "check2-square": 62065,
- "check2": 62066,
- "chevron-bar-contract": 62067,
- "chevron-bar-down": 62068,
- "chevron-bar-expand": 62069,
- "chevron-bar-left": 62070,
- "chevron-bar-right": 62071,
- "chevron-bar-up": 62072,
- "chevron-compact-down": 62073,
- "chevron-compact-left": 62074,
- "chevron-compact-right": 62075,
- "chevron-compact-up": 62076,
- "chevron-contract": 62077,
- "chevron-double-down": 62078,
- "chevron-double-left": 62079,
- "chevron-double-right": 62080,
- "chevron-double-up": 62081,
- "chevron-down": 62082,
- "chevron-expand": 62083,
- "chevron-left": 62084,
- "chevron-right": 62085,
- "chevron-up": 62086,
- "circle-fill": 62087,
- "circle-half": 62088,
- "circle-square": 62089,
- "circle": 62090,
- "clipboard-check": 62091,
- "clipboard-data": 62092,
- "clipboard-minus": 62093,
- "clipboard-plus": 62094,
- "clipboard-x": 62095,
- "clipboard": 62096,
- "clock-fill": 62097,
- "clock-history": 62098,
- "clock": 62099,
- "cloud-arrow-down-fill": 62100,
- "cloud-arrow-down": 62101,
- "cloud-arrow-up-fill": 62102,
- "cloud-arrow-up": 62103,
- "cloud-check-fill": 62104,
- "cloud-check": 62105,
- "cloud-download-fill": 62106,
- "cloud-download": 62107,
- "cloud-drizzle-fill": 62108,
- "cloud-drizzle": 62109,
- "cloud-fill": 62110,
- "cloud-fog-fill": 62111,
- "cloud-fog": 62112,
- "cloud-fog2-fill": 62113,
- "cloud-fog2": 62114,
- "cloud-hail-fill": 62115,
- "cloud-hail": 62116,
- "cloud-haze-fill": 62118,
- "cloud-haze": 62119,
- "cloud-haze2-fill": 62120,
- "cloud-lightning-fill": 62121,
- "cloud-lightning-rain-fill": 62122,
- "cloud-lightning-rain": 62123,
- "cloud-lightning": 62124,
- "cloud-minus-fill": 62125,
- "cloud-minus": 62126,
- "cloud-moon-fill": 62127,
- "cloud-moon": 62128,
- "cloud-plus-fill": 62129,
- "cloud-plus": 62130,
- "cloud-rain-fill": 62131,
- "cloud-rain-heavy-fill": 62132,
- "cloud-rain-heavy": 62133,
- "cloud-rain": 62134,
- "cloud-slash-fill": 62135,
- "cloud-slash": 62136,
- "cloud-sleet-fill": 62137,
- "cloud-sleet": 62138,
- "cloud-snow-fill": 62139,
- "cloud-snow": 62140,
- "cloud-sun-fill": 62141,
- "cloud-sun": 62142,
- "cloud-upload-fill": 62143,
- "cloud-upload": 62144,
- "cloud": 62145,
- "clouds-fill": 62146,
- "clouds": 62147,
- "cloudy-fill": 62148,
- "cloudy": 62149,
- "code-slash": 62150,
- "code-square": 62151,
- "code": 62152,
- "collection-fill": 62153,
- "collection-play-fill": 62154,
- "collection-play": 62155,
- "collection": 62156,
- "columns-gap": 62157,
- "columns": 62158,
- "command": 62159,
- "compass-fill": 62160,
- "compass": 62161,
- "cone-striped": 62162,
- "cone": 62163,
- "controller": 62164,
- "cpu-fill": 62165,
- "cpu": 62166,
- "credit-card-2-back-fill": 62167,
- "credit-card-2-back": 62168,
- "credit-card-2-front-fill": 62169,
- "credit-card-2-front": 62170,
- "credit-card-fill": 62171,
- "credit-card": 62172,
- "crop": 62173,
- "cup-fill": 62174,
- "cup-straw": 62175,
- "cup": 62176,
- "cursor-fill": 62177,
- "cursor-text": 62178,
- "cursor": 62179,
- "dash-circle-dotted": 62180,
- "dash-circle-fill": 62181,
- "dash-circle": 62182,
- "dash-square-dotted": 62183,
- "dash-square-fill": 62184,
- "dash-square": 62185,
- "dash": 62186,
- "diagram-2-fill": 62187,
- "diagram-2": 62188,
- "diagram-3-fill": 62189,
- "diagram-3": 62190,
- "diamond-fill": 62191,
- "diamond-half": 62192,
- "diamond": 62193,
- "dice-1-fill": 62194,
- "dice-1": 62195,
- "dice-2-fill": 62196,
- "dice-2": 62197,
- "dice-3-fill": 62198,
- "dice-3": 62199,
- "dice-4-fill": 62200,
- "dice-4": 62201,
- "dice-5-fill": 62202,
- "dice-5": 62203,
- "dice-6-fill": 62204,
- "dice-6": 62205,
- "disc-fill": 62206,
- "disc": 62207,
- "discord": 62208,
- "display-fill": 62209,
- "display": 62210,
- "distribute-horizontal": 62211,
- "distribute-vertical": 62212,
- "door-closed-fill": 62213,
- "door-closed": 62214,
- "door-open-fill": 62215,
- "door-open": 62216,
- "dot": 62217,
- "download": 62218,
- "droplet-fill": 62219,
- "droplet-half": 62220,
- "droplet": 62221,
- "earbuds": 62222,
- "easel-fill": 62223,
- "easel": 62224,
- "egg-fill": 62225,
- "egg-fried": 62226,
- "egg": 62227,
- "eject-fill": 62228,
- "eject": 62229,
- "emoji-angry-fill": 62230,
- "emoji-angry": 62231,
- "emoji-dizzy-fill": 62232,
- "emoji-dizzy": 62233,
- "emoji-expressionless-fill": 62234,
- "emoji-expressionless": 62235,
- "emoji-frown-fill": 62236,
- "emoji-frown": 62237,
- "emoji-heart-eyes-fill": 62238,
- "emoji-heart-eyes": 62239,
- "emoji-laughing-fill": 62240,
- "emoji-laughing": 62241,
- "emoji-neutral-fill": 62242,
- "emoji-neutral": 62243,
- "emoji-smile-fill": 62244,
- "emoji-smile-upside-down-fill": 62245,
- "emoji-smile-upside-down": 62246,
- "emoji-smile": 62247,
- "emoji-sunglasses-fill": 62248,
- "emoji-sunglasses": 62249,
- "emoji-wink-fill": 62250,
- "emoji-wink": 62251,
- "envelope-fill": 62252,
- "envelope-open-fill": 62253,
- "envelope-open": 62254,
- "envelope": 62255,
- "eraser-fill": 62256,
- "eraser": 62257,
- "exclamation-circle-fill": 62258,
- "exclamation-circle": 62259,
- "exclamation-diamond-fill": 62260,
- "exclamation-diamond": 62261,
- "exclamation-octagon-fill": 62262,
- "exclamation-octagon": 62263,
- "exclamation-square-fill": 62264,
- "exclamation-square": 62265,
- "exclamation-triangle-fill": 62266,
- "exclamation-triangle": 62267,
- "exclamation": 62268,
- "exclude": 62269,
- "eye-fill": 62270,
- "eye-slash-fill": 62271,
- "eye-slash": 62272,
- "eye": 62273,
- "eyedropper": 62274,
- "eyeglasses": 62275,
- "facebook": 62276,
- "file-arrow-down-fill": 62277,
- "file-arrow-down": 62278,
- "file-arrow-up-fill": 62279,
- "file-arrow-up": 62280,
- "file-bar-graph-fill": 62281,
- "file-bar-graph": 62282,
- "file-binary-fill": 62283,
- "file-binary": 62284,
- "file-break-fill": 62285,
- "file-break": 62286,
- "file-check-fill": 62287,
- "file-check": 62288,
- "file-code-fill": 62289,
- "file-code": 62290,
- "file-diff-fill": 62291,
- "file-diff": 62292,
- "file-earmark-arrow-down-fill": 62293,
- "file-earmark-arrow-down": 62294,
- "file-earmark-arrow-up-fill": 62295,
- "file-earmark-arrow-up": 62296,
- "file-earmark-bar-graph-fill": 62297,
- "file-earmark-bar-graph": 62298,
- "file-earmark-binary-fill": 62299,
- "file-earmark-binary": 62300,
- "file-earmark-break-fill": 62301,
- "file-earmark-break": 62302,
- "file-earmark-check-fill": 62303,
- "file-earmark-check": 62304,
- "file-earmark-code-fill": 62305,
- "file-earmark-code": 62306,
- "file-earmark-diff-fill": 62307,
- "file-earmark-diff": 62308,
- "file-earmark-easel-fill": 62309,
- "file-earmark-easel": 62310,
- "file-earmark-excel-fill": 62311,
- "file-earmark-excel": 62312,
- "file-earmark-fill": 62313,
- "file-earmark-font-fill": 62314,
- "file-earmark-font": 62315,
- "file-earmark-image-fill": 62316,
- "file-earmark-image": 62317,
- "file-earmark-lock-fill": 62318,
- "file-earmark-lock": 62319,
- "file-earmark-lock2-fill": 62320,
- "file-earmark-lock2": 62321,
- "file-earmark-medical-fill": 62322,
- "file-earmark-medical": 62323,
- "file-earmark-minus-fill": 62324,
- "file-earmark-minus": 62325,
- "file-earmark-music-fill": 62326,
- "file-earmark-music": 62327,
- "file-earmark-person-fill": 62328,
- "file-earmark-person": 62329,
- "file-earmark-play-fill": 62330,
- "file-earmark-play": 62331,
- "file-earmark-plus-fill": 62332,
- "file-earmark-plus": 62333,
- "file-earmark-post-fill": 62334,
- "file-earmark-post": 62335,
- "file-earmark-ppt-fill": 62336,
- "file-earmark-ppt": 62337,
- "file-earmark-richtext-fill": 62338,
- "file-earmark-richtext": 62339,
- "file-earmark-ruled-fill": 62340,
- "file-earmark-ruled": 62341,
- "file-earmark-slides-fill": 62342,
- "file-earmark-slides": 62343,
- "file-earmark-spreadsheet-fill": 62344,
- "file-earmark-spreadsheet": 62345,
- "file-earmark-text-fill": 62346,
- "file-earmark-text": 62347,
- "file-earmark-word-fill": 62348,
- "file-earmark-word": 62349,
- "file-earmark-x-fill": 62350,
- "file-earmark-x": 62351,
- "file-earmark-zip-fill": 62352,
- "file-earmark-zip": 62353,
- "file-earmark": 62354,
- "file-easel-fill": 62355,
- "file-easel": 62356,
- "file-excel-fill": 62357,
- "file-excel": 62358,
- "file-fill": 62359,
- "file-font-fill": 62360,
- "file-font": 62361,
- "file-image-fill": 62362,
- "file-image": 62363,
- "file-lock-fill": 62364,
- "file-lock": 62365,
- "file-lock2-fill": 62366,
- "file-lock2": 62367,
- "file-medical-fill": 62368,
- "file-medical": 62369,
- "file-minus-fill": 62370,
- "file-minus": 62371,
- "file-music-fill": 62372,
- "file-music": 62373,
- "file-person-fill": 62374,
- "file-person": 62375,
- "file-play-fill": 62376,
- "file-play": 62377,
- "file-plus-fill": 62378,
- "file-plus": 62379,
- "file-post-fill": 62380,
- "file-post": 62381,
- "file-ppt-fill": 62382,
- "file-ppt": 62383,
- "file-richtext-fill": 62384,
- "file-richtext": 62385,
- "file-ruled-fill": 62386,
- "file-ruled": 62387,
- "file-slides-fill": 62388,
- "file-slides": 62389,
- "file-spreadsheet-fill": 62390,
- "file-spreadsheet": 62391,
- "file-text-fill": 62392,
- "file-text": 62393,
- "file-word-fill": 62394,
- "file-word": 62395,
- "file-x-fill": 62396,
- "file-x": 62397,
- "file-zip-fill": 62398,
- "file-zip": 62399,
- "file": 62400,
- "files-alt": 62401,
- "files": 62402,
- "film": 62403,
- "filter-circle-fill": 62404,
- "filter-circle": 62405,
- "filter-left": 62406,
- "filter-right": 62407,
- "filter-square-fill": 62408,
- "filter-square": 62409,
- "filter": 62410,
- "flag-fill": 62411,
- "flag": 62412,
- "flower1": 62413,
- "flower2": 62414,
- "flower3": 62415,
- "folder-check": 62416,
- "folder-fill": 62417,
- "folder-minus": 62418,
- "folder-plus": 62419,
- "folder-symlink-fill": 62420,
- "folder-symlink": 62421,
- "folder-x": 62422,
- "folder": 62423,
- "folder2-open": 62424,
- "folder2": 62425,
- "fonts": 62426,
- "forward-fill": 62427,
- "forward": 62428,
- "front": 62429,
- "fullscreen-exit": 62430,
- "fullscreen": 62431,
- "funnel-fill": 62432,
- "funnel": 62433,
- "gear-fill": 62434,
- "gear-wide-connected": 62435,
- "gear-wide": 62436,
- "gear": 62437,
- "gem": 62438,
- "geo-alt-fill": 62439,
- "geo-alt": 62440,
- "geo-fill": 62441,
- "geo": 62442,
- "gift-fill": 62443,
- "gift": 62444,
- "github": 62445,
- "globe": 62446,
- "globe2": 62447,
- "google": 62448,
- "graph-down": 62449,
- "graph-up": 62450,
- "grid-1x2-fill": 62451,
- "grid-1x2": 62452,
- "grid-3x2-gap-fill": 62453,
- "grid-3x2-gap": 62454,
- "grid-3x2": 62455,
- "grid-3x3-gap-fill": 62456,
- "grid-3x3-gap": 62457,
- "grid-3x3": 62458,
- "grid-fill": 62459,
- "grid": 62460,
- "grip-horizontal": 62461,
- "grip-vertical": 62462,
- "hammer": 62463,
- "hand-index-fill": 62464,
- "hand-index-thumb-fill": 62465,
- "hand-index-thumb": 62466,
- "hand-index": 62467,
- "hand-thumbs-down-fill": 62468,
- "hand-thumbs-down": 62469,
- "hand-thumbs-up-fill": 62470,
- "hand-thumbs-up": 62471,
- "handbag-fill": 62472,
- "handbag": 62473,
- "hash": 62474,
- "hdd-fill": 62475,
- "hdd-network-fill": 62476,
- "hdd-network": 62477,
- "hdd-rack-fill": 62478,
- "hdd-rack": 62479,
- "hdd-stack-fill": 62480,
- "hdd-stack": 62481,
- "hdd": 62482,
- "headphones": 62483,
- "headset": 62484,
- "heart-fill": 62485,
- "heart-half": 62486,
- "heart": 62487,
- "heptagon-fill": 62488,
- "heptagon-half": 62489,
- "heptagon": 62490,
- "hexagon-fill": 62491,
- "hexagon-half": 62492,
- "hexagon": 62493,
- "hourglass-bottom": 62494,
- "hourglass-split": 62495,
- "hourglass-top": 62496,
- "hourglass": 62497,
- "house-door-fill": 62498,
- "house-door": 62499,
- "house-fill": 62500,
- "house": 62501,
- "hr": 62502,
- "hurricane": 62503,
- "image-alt": 62504,
- "image-fill": 62505,
- "image": 62506,
- "images": 62507,
- "inbox-fill": 62508,
- "inbox": 62509,
- "inboxes-fill": 62510,
- "inboxes": 62511,
- "info-circle-fill": 62512,
- "info-circle": 62513,
- "info-square-fill": 62514,
- "info-square": 62515,
- "info": 62516,
- "input-cursor-text": 62517,
- "input-cursor": 62518,
- "instagram": 62519,
- "intersect": 62520,
- "journal-album": 62521,
- "journal-arrow-down": 62522,
- "journal-arrow-up": 62523,
- "journal-bookmark-fill": 62524,
- "journal-bookmark": 62525,
- "journal-check": 62526,
- "journal-code": 62527,
- "journal-medical": 62528,
- "journal-minus": 62529,
- "journal-plus": 62530,
- "journal-richtext": 62531,
- "journal-text": 62532,
- "journal-x": 62533,
- "journal": 62534,
- "journals": 62535,
- "joystick": 62536,
- "justify-left": 62537,
- "justify-right": 62538,
- "justify": 62539,
- "kanban-fill": 62540,
- "kanban": 62541,
- "key-fill": 62542,
- "key": 62543,
- "keyboard-fill": 62544,
- "keyboard": 62545,
- "ladder": 62546,
- "lamp-fill": 62547,
- "lamp": 62548,
- "laptop-fill": 62549,
- "laptop": 62550,
- "layer-backward": 62551,
- "layer-forward": 62552,
- "layers-fill": 62553,
- "layers-half": 62554,
- "layers": 62555,
- "layout-sidebar-inset-reverse": 62556,
- "layout-sidebar-inset": 62557,
- "layout-sidebar-reverse": 62558,
- "layout-sidebar": 62559,
- "layout-split": 62560,
- "layout-text-sidebar-reverse": 62561,
- "layout-text-sidebar": 62562,
- "layout-text-window-reverse": 62563,
- "layout-text-window": 62564,
- "layout-three-columns": 62565,
- "layout-wtf": 62566,
- "life-preserver": 62567,
- "lightbulb-fill": 62568,
- "lightbulb-off-fill": 62569,
- "lightbulb-off": 62570,
- "lightbulb": 62571,
- "lightning-charge-fill": 62572,
- "lightning-charge": 62573,
- "lightning-fill": 62574,
- "lightning": 62575,
- "link-45deg": 62576,
- "link": 62577,
- "linkedin": 62578,
- "list-check": 62579,
- "list-nested": 62580,
- "list-ol": 62581,
- "list-stars": 62582,
- "list-task": 62583,
- "list-ul": 62584,
- "list": 62585,
- "lock-fill": 62586,
- "lock": 62587,
- "mailbox": 62588,
- "mailbox2": 62589,
- "map-fill": 62590,
- "map": 62591,
- "markdown-fill": 62592,
- "markdown": 62593,
- "mask": 62594,
- "megaphone-fill": 62595,
- "megaphone": 62596,
- "menu-app-fill": 62597,
- "menu-app": 62598,
- "menu-button-fill": 62599,
- "menu-button-wide-fill": 62600,
- "menu-button-wide": 62601,
- "menu-button": 62602,
- "menu-down": 62603,
- "menu-up": 62604,
- "mic-fill": 62605,
- "mic-mute-fill": 62606,
- "mic-mute": 62607,
- "mic": 62608,
- "minecart-loaded": 62609,
- "minecart": 62610,
- "moisture": 62611,
- "moon-fill": 62612,
- "moon-stars-fill": 62613,
- "moon-stars": 62614,
- "moon": 62615,
- "mouse-fill": 62616,
- "mouse": 62617,
- "mouse2-fill": 62618,
- "mouse2": 62619,
- "mouse3-fill": 62620,
- "mouse3": 62621,
- "music-note-beamed": 62622,
- "music-note-list": 62623,
- "music-note": 62624,
- "music-player-fill": 62625,
- "music-player": 62626,
- "newspaper": 62627,
- "node-minus-fill": 62628,
- "node-minus": 62629,
- "node-plus-fill": 62630,
- "node-plus": 62631,
- "nut-fill": 62632,
- "nut": 62633,
- "octagon-fill": 62634,
- "octagon-half": 62635,
- "octagon": 62636,
- "option": 62637,
- "outlet": 62638,
- "paint-bucket": 62639,
- "palette-fill": 62640,
- "palette": 62641,
- "palette2": 62642,
- "paperclip": 62643,
- "paragraph": 62644,
- "patch-check-fill": 62645,
- "patch-check": 62646,
- "patch-exclamation-fill": 62647,
- "patch-exclamation": 62648,
- "patch-minus-fill": 62649,
- "patch-minus": 62650,
- "patch-plus-fill": 62651,
- "patch-plus": 62652,
- "patch-question-fill": 62653,
- "patch-question": 62654,
- "pause-btn-fill": 62655,
- "pause-btn": 62656,
- "pause-circle-fill": 62657,
- "pause-circle": 62658,
- "pause-fill": 62659,
- "pause": 62660,
- "peace-fill": 62661,
- "peace": 62662,
- "pen-fill": 62663,
- "pen": 62664,
- "pencil-fill": 62665,
- "pencil-square": 62666,
- "pencil": 62667,
- "pentagon-fill": 62668,
- "pentagon-half": 62669,
- "pentagon": 62670,
- "people-fill": 62671,
- "people": 62672,
- "percent": 62673,
- "person-badge-fill": 62674,
- "person-badge": 62675,
- "person-bounding-box": 62676,
- "person-check-fill": 62677,
- "person-check": 62678,
- "person-circle": 62679,
- "person-dash-fill": 62680,
- "person-dash": 62681,
- "person-fill": 62682,
- "person-lines-fill": 62683,
- "person-plus-fill": 62684,
- "person-plus": 62685,
- "person-square": 62686,
- "person-x-fill": 62687,
- "person-x": 62688,
- "person": 62689,
- "phone-fill": 62690,
- "phone-landscape-fill": 62691,
- "phone-landscape": 62692,
- "phone-vibrate-fill": 62693,
- "phone-vibrate": 62694,
- "phone": 62695,
- "pie-chart-fill": 62696,
- "pie-chart": 62697,
- "pin-angle-fill": 62698,
- "pin-angle": 62699,
- "pin-fill": 62700,
- "pin": 62701,
- "pip-fill": 62702,
- "pip": 62703,
- "play-btn-fill": 62704,
- "play-btn": 62705,
- "play-circle-fill": 62706,
- "play-circle": 62707,
- "play-fill": 62708,
- "play": 62709,
- "plug-fill": 62710,
- "plug": 62711,
- "plus-circle-dotted": 62712,
- "plus-circle-fill": 62713,
- "plus-circle": 62714,
- "plus-square-dotted": 62715,
- "plus-square-fill": 62716,
- "plus-square": 62717,
- "plus": 62718,
- "power": 62719,
- "printer-fill": 62720,
- "printer": 62721,
- "puzzle-fill": 62722,
- "puzzle": 62723,
- "question-circle-fill": 62724,
- "question-circle": 62725,
- "question-diamond-fill": 62726,
- "question-diamond": 62727,
- "question-octagon-fill": 62728,
- "question-octagon": 62729,
- "question-square-fill": 62730,
- "question-square": 62731,
- "question": 62732,
- "rainbow": 62733,
- "receipt-cutoff": 62734,
- "receipt": 62735,
- "reception-0": 62736,
- "reception-1": 62737,
- "reception-2": 62738,
- "reception-3": 62739,
- "reception-4": 62740,
- "record-btn-fill": 62741,
- "record-btn": 62742,
- "record-circle-fill": 62743,
- "record-circle": 62744,
- "record-fill": 62745,
- "record": 62746,
- "record2-fill": 62747,
- "record2": 62748,
- "reply-all-fill": 62749,
- "reply-all": 62750,
- "reply-fill": 62751,
- "reply": 62752,
- "rss-fill": 62753,
- "rss": 62754,
- "rulers": 62755,
- "save-fill": 62756,
- "save": 62757,
- "save2-fill": 62758,
- "save2": 62759,
- "scissors": 62760,
- "screwdriver": 62761,
- "search": 62762,
- "segmented-nav": 62763,
- "server": 62764,
- "share-fill": 62765,
- "share": 62766,
- "shield-check": 62767,
- "shield-exclamation": 62768,
- "shield-fill-check": 62769,
- "shield-fill-exclamation": 62770,
- "shield-fill-minus": 62771,
- "shield-fill-plus": 62772,
- "shield-fill-x": 62773,
- "shield-fill": 62774,
- "shield-lock-fill": 62775,
- "shield-lock": 62776,
- "shield-minus": 62777,
- "shield-plus": 62778,
- "shield-shaded": 62779,
- "shield-slash-fill": 62780,
- "shield-slash": 62781,
- "shield-x": 62782,
- "shield": 62783,
- "shift-fill": 62784,
- "shift": 62785,
- "shop-window": 62786,
- "shop": 62787,
- "shuffle": 62788,
- "signpost-2-fill": 62789,
- "signpost-2": 62790,
- "signpost-fill": 62791,
- "signpost-split-fill": 62792,
- "signpost-split": 62793,
- "signpost": 62794,
- "sim-fill": 62795,
- "sim": 62796,
- "skip-backward-btn-fill": 62797,
- "skip-backward-btn": 62798,
- "skip-backward-circle-fill": 62799,
- "skip-backward-circle": 62800,
- "skip-backward-fill": 62801,
- "skip-backward": 62802,
- "skip-end-btn-fill": 62803,
- "skip-end-btn": 62804,
- "skip-end-circle-fill": 62805,
- "skip-end-circle": 62806,
- "skip-end-fill": 62807,
- "skip-end": 62808,
- "skip-forward-btn-fill": 62809,
- "skip-forward-btn": 62810,
- "skip-forward-circle-fill": 62811,
- "skip-forward-circle": 62812,
- "skip-forward-fill": 62813,
- "skip-forward": 62814,
- "skip-start-btn-fill": 62815,
- "skip-start-btn": 62816,
- "skip-start-circle-fill": 62817,
- "skip-start-circle": 62818,
- "skip-start-fill": 62819,
- "skip-start": 62820,
- "slack": 62821,
- "slash-circle-fill": 62822,
- "slash-circle": 62823,
- "slash-square-fill": 62824,
- "slash-square": 62825,
- "slash": 62826,
- "sliders": 62827,
- "smartwatch": 62828,
- "snow": 62829,
- "snow2": 62830,
- "snow3": 62831,
- "sort-alpha-down-alt": 62832,
- "sort-alpha-down": 62833,
- "sort-alpha-up-alt": 62834,
- "sort-alpha-up": 62835,
- "sort-down-alt": 62836,
- "sort-down": 62837,
- "sort-numeric-down-alt": 62838,
- "sort-numeric-down": 62839,
- "sort-numeric-up-alt": 62840,
- "sort-numeric-up": 62841,
- "sort-up-alt": 62842,
- "sort-up": 62843,
- "soundwave": 62844,
- "speaker-fill": 62845,
- "speaker": 62846,
- "speedometer": 62847,
- "speedometer2": 62848,
- "spellcheck": 62849,
- "square-fill": 62850,
- "square-half": 62851,
- "square": 62852,
- "stack": 62853,
- "star-fill": 62854,
- "star-half": 62855,
- "star": 62856,
- "stars": 62857,
- "stickies-fill": 62858,
- "stickies": 62859,
- "sticky-fill": 62860,
- "sticky": 62861,
- "stop-btn-fill": 62862,
- "stop-btn": 62863,
- "stop-circle-fill": 62864,
- "stop-circle": 62865,
- "stop-fill": 62866,
- "stop": 62867,
- "stoplights-fill": 62868,
- "stoplights": 62869,
- "stopwatch-fill": 62870,
- "stopwatch": 62871,
- "subtract": 62872,
- "suit-club-fill": 62873,
- "suit-club": 62874,
- "suit-diamond-fill": 62875,
- "suit-diamond": 62876,
- "suit-heart-fill": 62877,
- "suit-heart": 62878,
- "suit-spade-fill": 62879,
- "suit-spade": 62880,
- "sun-fill": 62881,
- "sun": 62882,
- "sunglasses": 62883,
- "sunrise-fill": 62884,
- "sunrise": 62885,
- "sunset-fill": 62886,
- "sunset": 62887,
- "symmetry-horizontal": 62888,
- "symmetry-vertical": 62889,
- "table": 62890,
- "tablet-fill": 62891,
- "tablet-landscape-fill": 62892,
- "tablet-landscape": 62893,
- "tablet": 62894,
- "tag-fill": 62895,
- "tag": 62896,
- "tags-fill": 62897,
- "tags": 62898,
- "telegram": 62899,
- "telephone-fill": 62900,
- "telephone-forward-fill": 62901,
- "telephone-forward": 62902,
- "telephone-inbound-fill": 62903,
- "telephone-inbound": 62904,
- "telephone-minus-fill": 62905,
- "telephone-minus": 62906,
- "telephone-outbound-fill": 62907,
- "telephone-outbound": 62908,
- "telephone-plus-fill": 62909,
- "telephone-plus": 62910,
- "telephone-x-fill": 62911,
- "telephone-x": 62912,
- "telephone": 62913,
- "terminal-fill": 62914,
- "terminal": 62915,
- "text-center": 62916,
- "text-indent-left": 62917,
- "text-indent-right": 62918,
- "text-left": 62919,
- "text-paragraph": 62920,
- "text-right": 62921,
- "textarea-resize": 62922,
- "textarea-t": 62923,
- "textarea": 62924,
- "thermometer-half": 62925,
- "thermometer-high": 62926,
- "thermometer-low": 62927,
- "thermometer-snow": 62928,
- "thermometer-sun": 62929,
- "thermometer": 62930,
- "three-dots-vertical": 62931,
- "three-dots": 62932,
- "toggle-off": 62933,
- "toggle-on": 62934,
- "toggle2-off": 62935,
- "toggle2-on": 62936,
- "toggles": 62937,
- "toggles2": 62938,
- "tools": 62939,
- "tornado": 62940,
- "trash-fill": 62941,
- "trash": 62942,
- "trash2-fill": 62943,
- "trash2": 62944,
- "tree-fill": 62945,
- "tree": 62946,
- "triangle-fill": 62947,
- "triangle-half": 62948,
- "triangle": 62949,
- "trophy-fill": 62950,
- "trophy": 62951,
- "tropical-storm": 62952,
- "truck-flatbed": 62953,
- "truck": 62954,
- "tsunami": 62955,
- "tv-fill": 62956,
- "tv": 62957,
- "twitch": 62958,
- "twitter": 62959,
- "type-bold": 62960,
- "type-h1": 62961,
- "type-h2": 62962,
- "type-h3": 62963,
- "type-italic": 62964,
- "type-strikethrough": 62965,
- "type-underline": 62966,
- "type": 62967,
- "ui-checks-grid": 62968,
- "ui-checks": 62969,
- "ui-radios-grid": 62970,
- "ui-radios": 62971,
- "umbrella-fill": 62972,
- "umbrella": 62973,
- "union": 62974,
- "unlock-fill": 62975,
- "unlock": 62976,
- "upc-scan": 62977,
- "upc": 62978,
- "upload": 62979,
- "vector-pen": 62980,
- "view-list": 62981,
- "view-stacked": 62982,
- "vinyl-fill": 62983,
- "vinyl": 62984,
- "voicemail": 62985,
- "volume-down-fill": 62986,
- "volume-down": 62987,
- "volume-mute-fill": 62988,
- "volume-mute": 62989,
- "volume-off-fill": 62990,
- "volume-off": 62991,
- "volume-up-fill": 62992,
- "volume-up": 62993,
- "vr": 62994,
- "wallet-fill": 62995,
- "wallet": 62996,
- "wallet2": 62997,
- "watch": 62998,
- "water": 62999,
- "whatsapp": 63000,
- "wifi-1": 63001,
- "wifi-2": 63002,
- "wifi-off": 63003,
- "wifi": 63004,
- "wind": 63005,
- "window-dock": 63006,
- "window-sidebar": 63007,
- "window": 63008,
- "wrench": 63009,
- "x-circle-fill": 63010,
- "x-circle": 63011,
- "x-diamond-fill": 63012,
- "x-diamond": 63013,
- "x-octagon-fill": 63014,
- "x-octagon": 63015,
- "x-square-fill": 63016,
- "x-square": 63017,
- "x": 63018,
- "youtube": 63019,
- "zoom-in": 63020,
- "zoom-out": 63021,
- "bank": 63022,
- "bank2": 63023,
- "bell-slash-fill": 63024,
- "bell-slash": 63025,
- "cash-coin": 63026,
- "check-lg": 63027,
- "coin": 63028,
- "currency-bitcoin": 63029,
- "currency-dollar": 63030,
- "currency-euro": 63031,
- "currency-exchange": 63032,
- "currency-pound": 63033,
- "currency-yen": 63034,
- "dash-lg": 63035,
- "exclamation-lg": 63036,
- "file-earmark-pdf-fill": 63037,
- "file-earmark-pdf": 63038,
- "file-pdf-fill": 63039,
- "file-pdf": 63040,
- "gender-ambiguous": 63041,
- "gender-female": 63042,
- "gender-male": 63043,
- "gender-trans": 63044,
- "headset-vr": 63045,
- "info-lg": 63046,
- "mastodon": 63047,
- "messenger": 63048,
- "piggy-bank-fill": 63049,
- "piggy-bank": 63050,
- "pin-map-fill": 63051,
- "pin-map": 63052,
- "plus-lg": 63053,
- "question-lg": 63054,
- "recycle": 63055,
- "reddit": 63056,
- "safe-fill": 63057,
- "safe2-fill": 63058,
- "safe2": 63059,
- "sd-card-fill": 63060,
- "sd-card": 63061,
- "skype": 63062,
- "slash-lg": 63063,
- "translate": 63064,
- "x-lg": 63065,
- "safe": 63066,
- "apple": 63067,
- "microsoft": 63069,
- "windows": 63070,
- "behance": 63068,
- "dribbble": 63071,
- "line": 63072,
- "medium": 63073,
- "paypal": 63074,
- "pinterest": 63075,
- "signal": 63076,
- "snapchat": 63077,
- "spotify": 63078,
- "stack-overflow": 63079,
- "strava": 63080,
- "wordpress": 63081,
- "vimeo": 63082,
- "activity": 63083,
- "easel2-fill": 63084,
- "easel2": 63085,
- "easel3-fill": 63086,
- "easel3": 63087,
- "fan": 63088,
- "fingerprint": 63089,
- "graph-down-arrow": 63090,
- "graph-up-arrow": 63091,
- "hypnotize": 63092,
- "magic": 63093,
- "person-rolodex": 63094,
- "person-video": 63095,
- "person-video2": 63096,
- "person-video3": 63097,
- "person-workspace": 63098,
- "radioactive": 63099,
- "webcam-fill": 63100,
- "webcam": 63101,
- "yin-yang": 63102,
- "bandaid-fill": 63104,
- "bandaid": 63105,
- "bluetooth": 63106,
- "body-text": 63107,
- "boombox": 63108,
- "boxes": 63109,
- "dpad-fill": 63110,
- "dpad": 63111,
- "ear-fill": 63112,
- "ear": 63113,
- "envelope-check-fill": 63115,
- "envelope-check": 63116,
- "envelope-dash-fill": 63118,
- "envelope-dash": 63119,
- "envelope-exclamation-fill": 63121,
- "envelope-exclamation": 63122,
- "envelope-plus-fill": 63123,
- "envelope-plus": 63124,
- "envelope-slash-fill": 63126,
- "envelope-slash": 63127,
- "envelope-x-fill": 63129,
- "envelope-x": 63130,
- "explicit-fill": 63131,
- "explicit": 63132,
- "git": 63133,
- "infinity": 63134,
- "list-columns-reverse": 63135,
- "list-columns": 63136,
- "meta": 63137,
- "nintendo-switch": 63140,
- "pc-display-horizontal": 63141,
- "pc-display": 63142,
- "pc-horizontal": 63143,
- "pc": 63144,
- "playstation": 63145,
- "plus-slash-minus": 63146,
- "projector-fill": 63147,
- "projector": 63148,
- "qr-code-scan": 63149,
- "qr-code": 63150,
- "quora": 63151,
- "quote": 63152,
- "robot": 63153,
- "send-check-fill": 63154,
- "send-check": 63155,
- "send-dash-fill": 63156,
- "send-dash": 63157,
- "send-exclamation-fill": 63159,
- "send-exclamation": 63160,
- "send-fill": 63161,
- "send-plus-fill": 63162,
- "send-plus": 63163,
- "send-slash-fill": 63164,
- "send-slash": 63165,
- "send-x-fill": 63166,
- "send-x": 63167,
- "send": 63168,
- "steam": 63169,
- "terminal-dash": 63171,
- "terminal-plus": 63172,
- "terminal-split": 63173,
- "ticket-detailed-fill": 63174,
- "ticket-detailed": 63175,
- "ticket-fill": 63176,
- "ticket-perforated-fill": 63177,
- "ticket-perforated": 63178,
- "ticket": 63179,
- "tiktok": 63180,
- "window-dash": 63181,
- "window-desktop": 63182,
- "window-fullscreen": 63183,
- "window-plus": 63184,
- "window-split": 63185,
- "window-stack": 63186,
- "window-x": 63187,
- "xbox": 63188,
- "ethernet": 63189,
- "hdmi-fill": 63190,
- "hdmi": 63191,
- "usb-c-fill": 63192,
- "usb-c": 63193,
- "usb-fill": 63194,
- "usb-plug-fill": 63195,
- "usb-plug": 63196,
- "usb-symbol": 63197,
- "usb": 63198,
- "boombox-fill": 63199,
- "displayport": 63201,
- "gpu-card": 63202,
- "memory": 63203,
- "modem-fill": 63204,
- "modem": 63205,
- "motherboard-fill": 63206,
- "motherboard": 63207,
- "optical-audio-fill": 63208,
- "optical-audio": 63209,
- "pci-card": 63210,
- "router-fill": 63211,
- "router": 63212,
- "thunderbolt-fill": 63215,
- "thunderbolt": 63216,
- "usb-drive-fill": 63217,
- "usb-drive": 63218,
- "usb-micro-fill": 63219,
- "usb-micro": 63220,
- "usb-mini-fill": 63221,
- "usb-mini": 63222,
- "cloud-haze2": 63223,
- "device-hdd-fill": 63224,
- "device-hdd": 63225,
- "device-ssd-fill": 63226,
- "device-ssd": 63227,
- "displayport-fill": 63228,
- "mortarboard-fill": 63229,
- "mortarboard": 63230,
- "terminal-x": 63231,
- "arrow-through-heart-fill": 63232,
- "arrow-through-heart": 63233,
- "badge-sd-fill": 63234,
- "badge-sd": 63235,
- "bag-heart-fill": 63236,
- "bag-heart": 63237,
- "balloon-fill": 63238,
- "balloon-heart-fill": 63239,
- "balloon-heart": 63240,
- "balloon": 63241,
- "box2-fill": 63242,
- "box2-heart-fill": 63243,
- "box2-heart": 63244,
- "box2": 63245,
- "braces-asterisk": 63246,
- "calendar-heart-fill": 63247,
- "calendar-heart": 63248,
- "calendar2-heart-fill": 63249,
- "calendar2-heart": 63250,
- "chat-heart-fill": 63251,
- "chat-heart": 63252,
- "chat-left-heart-fill": 63253,
- "chat-left-heart": 63254,
- "chat-right-heart-fill": 63255,
- "chat-right-heart": 63256,
- "chat-square-heart-fill": 63257,
- "chat-square-heart": 63258,
- "clipboard-check-fill": 63259,
- "clipboard-data-fill": 63260,
- "clipboard-fill": 63261,
- "clipboard-heart-fill": 63262,
- "clipboard-heart": 63263,
- "clipboard-minus-fill": 63264,
- "clipboard-plus-fill": 63265,
- "clipboard-pulse": 63266,
- "clipboard-x-fill": 63267,
- "clipboard2-check-fill": 63268,
- "clipboard2-check": 63269,
- "clipboard2-data-fill": 63270,
- "clipboard2-data": 63271,
- "clipboard2-fill": 63272,
- "clipboard2-heart-fill": 63273,
- "clipboard2-heart": 63274,
- "clipboard2-minus-fill": 63275,
- "clipboard2-minus": 63276,
- "clipboard2-plus-fill": 63277,
- "clipboard2-plus": 63278,
- "clipboard2-pulse-fill": 63279,
- "clipboard2-pulse": 63280,
- "clipboard2-x-fill": 63281,
- "clipboard2-x": 63282,
- "clipboard2": 63283,
- "emoji-kiss-fill": 63284,
- "emoji-kiss": 63285,
- "envelope-heart-fill": 63286,
- "envelope-heart": 63287,
- "envelope-open-heart-fill": 63288,
- "envelope-open-heart": 63289,
- "envelope-paper-fill": 63290,
- "envelope-paper-heart-fill": 63291,
- "envelope-paper-heart": 63292,
- "envelope-paper": 63293,
- "filetype-aac": 63294,
- "filetype-ai": 63295,
- "filetype-bmp": 63296,
- "filetype-cs": 63297,
- "filetype-css": 63298,
- "filetype-csv": 63299,
- "filetype-doc": 63300,
- "filetype-docx": 63301,
- "filetype-exe": 63302,
- "filetype-gif": 63303,
- "filetype-heic": 63304,
- "filetype-html": 63305,
- "filetype-java": 63306,
- "filetype-jpg": 63307,
- "filetype-js": 63308,
- "filetype-jsx": 63309,
- "filetype-key": 63310,
- "filetype-m4p": 63311,
- "filetype-md": 63312,
- "filetype-mdx": 63313,
- "filetype-mov": 63314,
- "filetype-mp3": 63315,
- "filetype-mp4": 63316,
- "filetype-otf": 63317,
- "filetype-pdf": 63318,
- "filetype-php": 63319,
- "filetype-png": 63320,
- "filetype-ppt": 63322,
- "filetype-psd": 63323,
- "filetype-py": 63324,
- "filetype-raw": 63325,
- "filetype-rb": 63326,
- "filetype-sass": 63327,
- "filetype-scss": 63328,
- "filetype-sh": 63329,
- "filetype-svg": 63330,
- "filetype-tiff": 63331,
- "filetype-tsx": 63332,
- "filetype-ttf": 63333,
- "filetype-txt": 63334,
- "filetype-wav": 63335,
- "filetype-woff": 63336,
- "filetype-xls": 63338,
- "filetype-xml": 63339,
- "filetype-yml": 63340,
- "heart-arrow": 63341,
- "heart-pulse-fill": 63342,
- "heart-pulse": 63343,
- "heartbreak-fill": 63344,
- "heartbreak": 63345,
- "hearts": 63346,
- "hospital-fill": 63347,
- "hospital": 63348,
- "house-heart-fill": 63349,
- "house-heart": 63350,
- "incognito": 63351,
- "magnet-fill": 63352,
- "magnet": 63353,
- "person-heart": 63354,
- "person-hearts": 63355,
- "phone-flip": 63356,
- "plugin": 63357,
- "postage-fill": 63358,
- "postage-heart-fill": 63359,
- "postage-heart": 63360,
- "postage": 63361,
- "postcard-fill": 63362,
- "postcard-heart-fill": 63363,
- "postcard-heart": 63364,
- "postcard": 63365,
- "search-heart-fill": 63366,
- "search-heart": 63367,
- "sliders2-vertical": 63368,
- "sliders2": 63369,
- "trash3-fill": 63370,
- "trash3": 63371,
- "valentine": 63372,
- "valentine2": 63373,
- "wrench-adjustable-circle-fill": 63374,
- "wrench-adjustable-circle": 63375,
- "wrench-adjustable": 63376,
- "filetype-json": 63377,
- "filetype-pptx": 63378,
- "filetype-xlsx": 63379,
- "1-circle-fill": 63382,
- "1-circle": 63383,
- "1-square-fill": 63384,
- "1-square": 63385,
- "2-circle-fill": 63388,
- "2-circle": 63389,
- "2-square-fill": 63390,
- "2-square": 63391,
- "3-circle-fill": 63394,
- "3-circle": 63395,
- "3-square-fill": 63396,
- "3-square": 63397,
- "4-circle-fill": 63400,
- "4-circle": 63401,
- "4-square-fill": 63402,
- "4-square": 63403,
- "5-circle-fill": 63406,
- "5-circle": 63407,
- "5-square-fill": 63408,
- "5-square": 63409,
- "6-circle-fill": 63412,
- "6-circle": 63413,
- "6-square-fill": 63414,
- "6-square": 63415,
- "7-circle-fill": 63418,
- "7-circle": 63419,
- "7-square-fill": 63420,
- "7-square": 63421,
- "8-circle-fill": 63424,
- "8-circle": 63425,
- "8-square-fill": 63426,
- "8-square": 63427,
- "9-circle-fill": 63430,
- "9-circle": 63431,
- "9-square-fill": 63432,
- "9-square": 63433,
- "airplane-engines-fill": 63434,
- "airplane-engines": 63435,
- "airplane-fill": 63436,
- "airplane": 63437,
- "alexa": 63438,
- "alipay": 63439,
- "android": 63440,
- "android2": 63441,
- "box-fill": 63442,
- "box-seam-fill": 63443,
- "browser-chrome": 63444,
- "browser-edge": 63445,
- "browser-firefox": 63446,
- "browser-safari": 63447,
- "c-circle-fill": 63450,
- "c-circle": 63451,
- "c-square-fill": 63452,
- "c-square": 63453,
- "capsule-pill": 63454,
- "capsule": 63455,
- "car-front-fill": 63456,
- "car-front": 63457,
- "cassette-fill": 63458,
- "cassette": 63459,
- "cc-circle-fill": 63462,
- "cc-circle": 63463,
- "cc-square-fill": 63464,
- "cc-square": 63465,
- "cup-hot-fill": 63466,
- "cup-hot": 63467,
- "currency-rupee": 63468,
- "dropbox": 63469,
- "escape": 63470,
- "fast-forward-btn-fill": 63471,
- "fast-forward-btn": 63472,
- "fast-forward-circle-fill": 63473,
- "fast-forward-circle": 63474,
- "fast-forward-fill": 63475,
- "fast-forward": 63476,
- "filetype-sql": 63477,
- "fire": 63478,
- "google-play": 63479,
- "h-circle-fill": 63482,
- "h-circle": 63483,
- "h-square-fill": 63484,
- "h-square": 63485,
- "indent": 63486,
- "lungs-fill": 63487,
- "lungs": 63488,
- "microsoft-teams": 63489,
- "p-circle-fill": 63492,
- "p-circle": 63493,
- "p-square-fill": 63494,
- "p-square": 63495,
- "pass-fill": 63496,
- "pass": 63497,
- "prescription": 63498,
- "prescription2": 63499,
- "r-circle-fill": 63502,
- "r-circle": 63503,
- "r-square-fill": 63504,
- "r-square": 63505,
- "repeat-1": 63506,
- "repeat": 63507,
- "rewind-btn-fill": 63508,
- "rewind-btn": 63509,
- "rewind-circle-fill": 63510,
- "rewind-circle": 63511,
- "rewind-fill": 63512,
- "rewind": 63513,
- "train-freight-front-fill": 63514,
- "train-freight-front": 63515,
- "train-front-fill": 63516,
- "train-front": 63517,
- "train-lightrail-front-fill": 63518,
- "train-lightrail-front": 63519,
- "truck-front-fill": 63520,
- "truck-front": 63521,
- "ubuntu": 63522,
- "unindent": 63523,
- "unity": 63524,
- "universal-access-circle": 63525,
- "universal-access": 63526,
- "virus": 63527,
- "virus2": 63528,
- "wechat": 63529,
- "yelp": 63530,
- "sign-stop-fill": 63531,
- "sign-stop-lights-fill": 63532,
- "sign-stop-lights": 63533,
- "sign-stop": 63534,
- "sign-turn-left-fill": 63535,
- "sign-turn-left": 63536,
- "sign-turn-right-fill": 63537,
- "sign-turn-right": 63538,
- "sign-turn-slight-left-fill": 63539,
- "sign-turn-slight-left": 63540,
- "sign-turn-slight-right-fill": 63541,
- "sign-turn-slight-right": 63542,
- "sign-yield-fill": 63543,
- "sign-yield": 63544,
- "ev-station-fill": 63545,
- "ev-station": 63546,
- "fuel-pump-diesel-fill": 63547,
- "fuel-pump-diesel": 63548,
- "fuel-pump-fill": 63549,
- "fuel-pump": 63550,
- "0-circle-fill": 63551,
- "0-circle": 63552,
- "0-square-fill": 63553,
- "0-square": 63554,
- "rocket-fill": 63555,
- "rocket-takeoff-fill": 63556,
- "rocket-takeoff": 63557,
- "rocket": 63558,
- "stripe": 63559,
- "subscript": 63560,
- "superscript": 63561,
- "trello": 63562,
- "envelope-at-fill": 63563,
- "envelope-at": 63564,
- "regex": 63565,
- "text-wrap": 63566,
- "sign-dead-end-fill": 63567,
- "sign-dead-end": 63568,
- "sign-do-not-enter-fill": 63569,
- "sign-do-not-enter": 63570,
- "sign-intersection-fill": 63571,
- "sign-intersection-side-fill": 63572,
- "sign-intersection-side": 63573,
- "sign-intersection-t-fill": 63574,
- "sign-intersection-t": 63575,
- "sign-intersection-y-fill": 63576,
- "sign-intersection-y": 63577,
- "sign-intersection": 63578,
- "sign-merge-left-fill": 63579,
- "sign-merge-left": 63580,
- "sign-merge-right-fill": 63581,
- "sign-merge-right": 63582,
- "sign-no-left-turn-fill": 63583,
- "sign-no-left-turn": 63584,
- "sign-no-parking-fill": 63585,
- "sign-no-parking": 63586,
- "sign-no-right-turn-fill": 63587,
- "sign-no-right-turn": 63588,
- "sign-railroad-fill": 63589,
- "sign-railroad": 63590,
- "building-add": 63591,
- "building-check": 63592,
- "building-dash": 63593,
- "building-down": 63594,
- "building-exclamation": 63595,
- "building-fill-add": 63596,
- "building-fill-check": 63597,
- "building-fill-dash": 63598,
- "building-fill-down": 63599,
- "building-fill-exclamation": 63600,
- "building-fill-gear": 63601,
- "building-fill-lock": 63602,
- "building-fill-slash": 63603,
- "building-fill-up": 63604,
- "building-fill-x": 63605,
- "building-fill": 63606,
- "building-gear": 63607,
- "building-lock": 63608,
- "building-slash": 63609,
- "building-up": 63610,
- "building-x": 63611,
- "buildings-fill": 63612,
- "buildings": 63613,
- "bus-front-fill": 63614,
- "bus-front": 63615,
- "ev-front-fill": 63616,
- "ev-front": 63617,
- "globe-americas": 63618,
- "globe-asia-australia": 63619,
- "globe-central-south-asia": 63620,
- "globe-europe-africa": 63621,
- "house-add-fill": 63622,
- "house-add": 63623,
- "house-check-fill": 63624,
- "house-check": 63625,
- "house-dash-fill": 63626,
- "house-dash": 63627,
- "house-down-fill": 63628,
- "house-down": 63629,
- "house-exclamation-fill": 63630,
- "house-exclamation": 63631,
- "house-gear-fill": 63632,
- "house-gear": 63633,
- "house-lock-fill": 63634,
- "house-lock": 63635,
- "house-slash-fill": 63636,
- "house-slash": 63637,
- "house-up-fill": 63638,
- "house-up": 63639,
- "house-x-fill": 63640,
- "house-x": 63641,
- "person-add": 63642,
- "person-down": 63643,
- "person-exclamation": 63644,
- "person-fill-add": 63645,
- "person-fill-check": 63646,
- "person-fill-dash": 63647,
- "person-fill-down": 63648,
- "person-fill-exclamation": 63649,
- "person-fill-gear": 63650,
- "person-fill-lock": 63651,
- "person-fill-slash": 63652,
- "person-fill-up": 63653,
- "person-fill-x": 63654,
- "person-gear": 63655,
- "person-lock": 63656,
- "person-slash": 63657,
- "person-up": 63658,
- "scooter": 63659,
- "taxi-front-fill": 63660,
- "taxi-front": 63661,
- "amd": 63662,
- "database-add": 63663,
- "database-check": 63664,
- "database-dash": 63665,
- "database-down": 63666,
- "database-exclamation": 63667,
- "database-fill-add": 63668,
- "database-fill-check": 63669,
- "database-fill-dash": 63670,
- "database-fill-down": 63671,
- "database-fill-exclamation": 63672,
- "database-fill-gear": 63673,
- "database-fill-lock": 63674,
- "database-fill-slash": 63675,
- "database-fill-up": 63676,
- "database-fill-x": 63677,
- "database-fill": 63678,
- "database-gear": 63679,
- "database-lock": 63680,
- "database-slash": 63681,
- "database-up": 63682,
- "database-x": 63683,
- "database": 63684,
- "houses-fill": 63685,
- "houses": 63686,
- "nvidia": 63687,
- "person-vcard-fill": 63688,
- "person-vcard": 63689,
- "sina-weibo": 63690,
- "tencent-qq": 63691,
- "wikipedia": 63692,
- "alphabet-uppercase": 62117,
- "alphabet": 63114,
- "amazon": 63117,
- "arrows-collapse-vertical": 63120,
- "arrows-expand-vertical": 63125,
- "arrows-vertical": 63128,
- "arrows": 63138,
- "ban-fill": 63139,
- "ban": 63158,
- "bing": 63170,
- "cake": 63200,
- "cake2": 63213,
- "cookie": 63214,
- "copy": 63321,
- "crosshair": 63337,
- "crosshair2": 63380,
- "emoji-astonished-fill": 63381,
- "emoji-astonished": 63386,
- "emoji-grimace-fill": 63387,
- "emoji-grimace": 63392,
- "emoji-grin-fill": 63393,
- "emoji-grin": 63398,
- "emoji-surprise-fill": 63399,
- "emoji-surprise": 63404,
- "emoji-tear-fill": 63405,
- "emoji-tear": 63410,
- "envelope-arrow-down-fill": 63411,
- "envelope-arrow-down": 63416,
- "envelope-arrow-up-fill": 63417,
- "envelope-arrow-up": 63422,
- "feather": 63423,
- "feather2": 63428,
- "floppy-fill": 63429,
- "floppy": 63448,
- "floppy2-fill": 63449,
- "floppy2": 63460,
- "gitlab": 63461,
- "highlighter": 63480,
- "marker-tip": 63490,
- "nvme-fill": 63491,
- "nvme": 63500,
- "opencollective": 63501,
- "pci-card-network": 63693,
- "pci-card-sound": 63694,
- "radar": 63695,
- "send-arrow-down-fill": 63696,
- "send-arrow-down": 63697,
- "send-arrow-up-fill": 63698,
- "send-arrow-up": 63699,
- "sim-slash-fill": 63700,
- "sim-slash": 63701,
- "sourceforge": 63702,
- "substack": 63703,
- "threads-fill": 63704,
- "threads": 63705,
- "transparency": 63706,
- "twitter-x": 63707,
- "type-h4": 63708,
- "type-h5": 63709,
- "type-h6": 63710,
- "backpack-fill": 63711,
- "backpack": 63712,
- "backpack2-fill": 63713,
- "backpack2": 63714,
- "backpack3-fill": 63715,
- "backpack3": 63716,
- "backpack4-fill": 63717,
- "backpack4": 63718,
- "brilliance": 63719,
- "cake-fill": 63720,
- "cake2-fill": 63721,
- "duffle-fill": 63722,
- "duffle": 63723,
- "exposure": 63724,
- "gender-neuter": 63725,
- "highlights": 63726,
- "luggage-fill": 63727,
- "luggage": 63728,
- "mailbox-flag": 63729,
- "mailbox2-flag": 63730,
- "noise-reduction": 63731,
- "passport-fill": 63732,
- "passport": 63733,
- "person-arms-up": 63734,
- "person-raised-hand": 63735,
- "person-standing-dress": 63736,
- "person-standing": 63737,
- "person-walking": 63738,
- "person-wheelchair": 63739,
- "shadows": 63740,
- "suitcase-fill": 63741,
- "suitcase-lg-fill": 63742,
- "suitcase-lg": 63743,
- "suitcase": 63744,
- "suitcase2-fill": 63745,
- "suitcase2": 63746,
- "vignette": 63747
-}
\ No newline at end of file
diff --git a/static/bootstrap/icons/bootstrap-icons.min.css b/static/bootstrap/icons/bootstrap-icons.min.css
deleted file mode 100644
index b882fd5a..00000000
--- a/static/bootstrap/icons/bootstrap-icons.min.css
+++ /dev/null
@@ -1,5 +0,0 @@
-/*!
- * Bootstrap Icons v1.11.2 (https://icons.getbootstrap.com/)
- * Copyright 2019-2023 The Bootstrap Authors
- * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
- */@font-face{font-display:block;font-family:bootstrap-icons;src:url("fonts/bootstrap-icons.woff2?7141511ac37f13e1a387fb9fc6646256") format("woff2"),url("fonts/bootstrap-icons.woff?7141511ac37f13e1a387fb9fc6646256") format("woff")}.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{display:inline-block;font-family:bootstrap-icons!important;font-style:normal;font-weight:400!important;font-variant:normal;text-transform:none;line-height:1;vertical-align:-.125em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bi-123::before{content:"\f67f"}.bi-alarm-fill::before{content:"\f101"}.bi-alarm::before{content:"\f102"}.bi-align-bottom::before{content:"\f103"}.bi-align-center::before{content:"\f104"}.bi-align-end::before{content:"\f105"}.bi-align-middle::before{content:"\f106"}.bi-align-start::before{content:"\f107"}.bi-align-top::before{content:"\f108"}.bi-alt::before{content:"\f109"}.bi-app-indicator::before{content:"\f10a"}.bi-app::before{content:"\f10b"}.bi-archive-fill::before{content:"\f10c"}.bi-archive::before{content:"\f10d"}.bi-arrow-90deg-down::before{content:"\f10e"}.bi-arrow-90deg-left::before{content:"\f10f"}.bi-arrow-90deg-right::before{content:"\f110"}.bi-arrow-90deg-up::before{content:"\f111"}.bi-arrow-bar-down::before{content:"\f112"}.bi-arrow-bar-left::before{content:"\f113"}.bi-arrow-bar-right::before{content:"\f114"}.bi-arrow-bar-up::before{content:"\f115"}.bi-arrow-clockwise::before{content:"\f116"}.bi-arrow-counterclockwise::before{content:"\f117"}.bi-arrow-down-circle-fill::before{content:"\f118"}.bi-arrow-down-circle::before{content:"\f119"}.bi-arrow-down-left-circle-fill::before{content:"\f11a"}.bi-arrow-down-left-circle::before{content:"\f11b"}.bi-arrow-down-left-square-fill::before{content:"\f11c"}.bi-arrow-down-left-square::before{content:"\f11d"}.bi-arrow-down-left::before{content:"\f11e"}.bi-arrow-down-right-circle-fill::before{content:"\f11f"}.bi-arrow-down-right-circle::before{content:"\f120"}.bi-arrow-down-right-square-fill::before{content:"\f121"}.bi-arrow-down-right-square::before{content:"\f122"}.bi-arrow-down-right::before{content:"\f123"}.bi-arrow-down-short::before{content:"\f124"}.bi-arrow-down-square-fill::before{content:"\f125"}.bi-arrow-down-square::before{content:"\f126"}.bi-arrow-down-up::before{content:"\f127"}.bi-arrow-down::before{content:"\f128"}.bi-arrow-left-circle-fill::before{content:"\f129"}.bi-arrow-left-circle::before{content:"\f12a"}.bi-arrow-left-right::before{content:"\f12b"}.bi-arrow-left-short::before{content:"\f12c"}.bi-arrow-left-square-fill::before{content:"\f12d"}.bi-arrow-left-square::before{content:"\f12e"}.bi-arrow-left::before{content:"\f12f"}.bi-arrow-repeat::before{content:"\f130"}.bi-arrow-return-left::before{content:"\f131"}.bi-arrow-return-right::before{content:"\f132"}.bi-arrow-right-circle-fill::before{content:"\f133"}.bi-arrow-right-circle::before{content:"\f134"}.bi-arrow-right-short::before{content:"\f135"}.bi-arrow-right-square-fill::before{content:"\f136"}.bi-arrow-right-square::before{content:"\f137"}.bi-arrow-right::before{content:"\f138"}.bi-arrow-up-circle-fill::before{content:"\f139"}.bi-arrow-up-circle::before{content:"\f13a"}.bi-arrow-up-left-circle-fill::before{content:"\f13b"}.bi-arrow-up-left-circle::before{content:"\f13c"}.bi-arrow-up-left-square-fill::before{content:"\f13d"}.bi-arrow-up-left-square::before{content:"\f13e"}.bi-arrow-up-left::before{content:"\f13f"}.bi-arrow-up-right-circle-fill::before{content:"\f140"}.bi-arrow-up-right-circle::before{content:"\f141"}.bi-arrow-up-right-square-fill::before{content:"\f142"}.bi-arrow-up-right-square::before{content:"\f143"}.bi-arrow-up-right::before{content:"\f144"}.bi-arrow-up-short::before{content:"\f145"}.bi-arrow-up-square-fill::before{content:"\f146"}.bi-arrow-up-square::before{content:"\f147"}.bi-arrow-up::before{content:"\f148"}.bi-arrows-angle-contract::before{content:"\f149"}.bi-arrows-angle-expand::before{content:"\f14a"}.bi-arrows-collapse::before{content:"\f14b"}.bi-arrows-expand::before{content:"\f14c"}.bi-arrows-fullscreen::before{content:"\f14d"}.bi-arrows-move::before{content:"\f14e"}.bi-aspect-ratio-fill::before{content:"\f14f"}.bi-aspect-ratio::before{content:"\f150"}.bi-asterisk::before{content:"\f151"}.bi-at::before{content:"\f152"}.bi-award-fill::before{content:"\f153"}.bi-award::before{content:"\f154"}.bi-back::before{content:"\f155"}.bi-backspace-fill::before{content:"\f156"}.bi-backspace-reverse-fill::before{content:"\f157"}.bi-backspace-reverse::before{content:"\f158"}.bi-backspace::before{content:"\f159"}.bi-badge-3d-fill::before{content:"\f15a"}.bi-badge-3d::before{content:"\f15b"}.bi-badge-4k-fill::before{content:"\f15c"}.bi-badge-4k::before{content:"\f15d"}.bi-badge-8k-fill::before{content:"\f15e"}.bi-badge-8k::before{content:"\f15f"}.bi-badge-ad-fill::before{content:"\f160"}.bi-badge-ad::before{content:"\f161"}.bi-badge-ar-fill::before{content:"\f162"}.bi-badge-ar::before{content:"\f163"}.bi-badge-cc-fill::before{content:"\f164"}.bi-badge-cc::before{content:"\f165"}.bi-badge-hd-fill::before{content:"\f166"}.bi-badge-hd::before{content:"\f167"}.bi-badge-tm-fill::before{content:"\f168"}.bi-badge-tm::before{content:"\f169"}.bi-badge-vo-fill::before{content:"\f16a"}.bi-badge-vo::before{content:"\f16b"}.bi-badge-vr-fill::before{content:"\f16c"}.bi-badge-vr::before{content:"\f16d"}.bi-badge-wc-fill::before{content:"\f16e"}.bi-badge-wc::before{content:"\f16f"}.bi-bag-check-fill::before{content:"\f170"}.bi-bag-check::before{content:"\f171"}.bi-bag-dash-fill::before{content:"\f172"}.bi-bag-dash::before{content:"\f173"}.bi-bag-fill::before{content:"\f174"}.bi-bag-plus-fill::before{content:"\f175"}.bi-bag-plus::before{content:"\f176"}.bi-bag-x-fill::before{content:"\f177"}.bi-bag-x::before{content:"\f178"}.bi-bag::before{content:"\f179"}.bi-bar-chart-fill::before{content:"\f17a"}.bi-bar-chart-line-fill::before{content:"\f17b"}.bi-bar-chart-line::before{content:"\f17c"}.bi-bar-chart-steps::before{content:"\f17d"}.bi-bar-chart::before{content:"\f17e"}.bi-basket-fill::before{content:"\f17f"}.bi-basket::before{content:"\f180"}.bi-basket2-fill::before{content:"\f181"}.bi-basket2::before{content:"\f182"}.bi-basket3-fill::before{content:"\f183"}.bi-basket3::before{content:"\f184"}.bi-battery-charging::before{content:"\f185"}.bi-battery-full::before{content:"\f186"}.bi-battery-half::before{content:"\f187"}.bi-battery::before{content:"\f188"}.bi-bell-fill::before{content:"\f189"}.bi-bell::before{content:"\f18a"}.bi-bezier::before{content:"\f18b"}.bi-bezier2::before{content:"\f18c"}.bi-bicycle::before{content:"\f18d"}.bi-binoculars-fill::before{content:"\f18e"}.bi-binoculars::before{content:"\f18f"}.bi-blockquote-left::before{content:"\f190"}.bi-blockquote-right::before{content:"\f191"}.bi-book-fill::before{content:"\f192"}.bi-book-half::before{content:"\f193"}.bi-book::before{content:"\f194"}.bi-bookmark-check-fill::before{content:"\f195"}.bi-bookmark-check::before{content:"\f196"}.bi-bookmark-dash-fill::before{content:"\f197"}.bi-bookmark-dash::before{content:"\f198"}.bi-bookmark-fill::before{content:"\f199"}.bi-bookmark-heart-fill::before{content:"\f19a"}.bi-bookmark-heart::before{content:"\f19b"}.bi-bookmark-plus-fill::before{content:"\f19c"}.bi-bookmark-plus::before{content:"\f19d"}.bi-bookmark-star-fill::before{content:"\f19e"}.bi-bookmark-star::before{content:"\f19f"}.bi-bookmark-x-fill::before{content:"\f1a0"}.bi-bookmark-x::before{content:"\f1a1"}.bi-bookmark::before{content:"\f1a2"}.bi-bookmarks-fill::before{content:"\f1a3"}.bi-bookmarks::before{content:"\f1a4"}.bi-bookshelf::before{content:"\f1a5"}.bi-bootstrap-fill::before{content:"\f1a6"}.bi-bootstrap-reboot::before{content:"\f1a7"}.bi-bootstrap::before{content:"\f1a8"}.bi-border-all::before{content:"\f1a9"}.bi-border-bottom::before{content:"\f1aa"}.bi-border-center::before{content:"\f1ab"}.bi-border-inner::before{content:"\f1ac"}.bi-border-left::before{content:"\f1ad"}.bi-border-middle::before{content:"\f1ae"}.bi-border-outer::before{content:"\f1af"}.bi-border-right::before{content:"\f1b0"}.bi-border-style::before{content:"\f1b1"}.bi-border-top::before{content:"\f1b2"}.bi-border-width::before{content:"\f1b3"}.bi-border::before{content:"\f1b4"}.bi-bounding-box-circles::before{content:"\f1b5"}.bi-bounding-box::before{content:"\f1b6"}.bi-box-arrow-down-left::before{content:"\f1b7"}.bi-box-arrow-down-right::before{content:"\f1b8"}.bi-box-arrow-down::before{content:"\f1b9"}.bi-box-arrow-in-down-left::before{content:"\f1ba"}.bi-box-arrow-in-down-right::before{content:"\f1bb"}.bi-box-arrow-in-down::before{content:"\f1bc"}.bi-box-arrow-in-left::before{content:"\f1bd"}.bi-box-arrow-in-right::before{content:"\f1be"}.bi-box-arrow-in-up-left::before{content:"\f1bf"}.bi-box-arrow-in-up-right::before{content:"\f1c0"}.bi-box-arrow-in-up::before{content:"\f1c1"}.bi-box-arrow-left::before{content:"\f1c2"}.bi-box-arrow-right::before{content:"\f1c3"}.bi-box-arrow-up-left::before{content:"\f1c4"}.bi-box-arrow-up-right::before{content:"\f1c5"}.bi-box-arrow-up::before{content:"\f1c6"}.bi-box-seam::before{content:"\f1c7"}.bi-box::before{content:"\f1c8"}.bi-braces::before{content:"\f1c9"}.bi-bricks::before{content:"\f1ca"}.bi-briefcase-fill::before{content:"\f1cb"}.bi-briefcase::before{content:"\f1cc"}.bi-brightness-alt-high-fill::before{content:"\f1cd"}.bi-brightness-alt-high::before{content:"\f1ce"}.bi-brightness-alt-low-fill::before{content:"\f1cf"}.bi-brightness-alt-low::before{content:"\f1d0"}.bi-brightness-high-fill::before{content:"\f1d1"}.bi-brightness-high::before{content:"\f1d2"}.bi-brightness-low-fill::before{content:"\f1d3"}.bi-brightness-low::before{content:"\f1d4"}.bi-broadcast-pin::before{content:"\f1d5"}.bi-broadcast::before{content:"\f1d6"}.bi-brush-fill::before{content:"\f1d7"}.bi-brush::before{content:"\f1d8"}.bi-bucket-fill::before{content:"\f1d9"}.bi-bucket::before{content:"\f1da"}.bi-bug-fill::before{content:"\f1db"}.bi-bug::before{content:"\f1dc"}.bi-building::before{content:"\f1dd"}.bi-bullseye::before{content:"\f1de"}.bi-calculator-fill::before{content:"\f1df"}.bi-calculator::before{content:"\f1e0"}.bi-calendar-check-fill::before{content:"\f1e1"}.bi-calendar-check::before{content:"\f1e2"}.bi-calendar-date-fill::before{content:"\f1e3"}.bi-calendar-date::before{content:"\f1e4"}.bi-calendar-day-fill::before{content:"\f1e5"}.bi-calendar-day::before{content:"\f1e6"}.bi-calendar-event-fill::before{content:"\f1e7"}.bi-calendar-event::before{content:"\f1e8"}.bi-calendar-fill::before{content:"\f1e9"}.bi-calendar-minus-fill::before{content:"\f1ea"}.bi-calendar-minus::before{content:"\f1eb"}.bi-calendar-month-fill::before{content:"\f1ec"}.bi-calendar-month::before{content:"\f1ed"}.bi-calendar-plus-fill::before{content:"\f1ee"}.bi-calendar-plus::before{content:"\f1ef"}.bi-calendar-range-fill::before{content:"\f1f0"}.bi-calendar-range::before{content:"\f1f1"}.bi-calendar-week-fill::before{content:"\f1f2"}.bi-calendar-week::before{content:"\f1f3"}.bi-calendar-x-fill::before{content:"\f1f4"}.bi-calendar-x::before{content:"\f1f5"}.bi-calendar::before{content:"\f1f6"}.bi-calendar2-check-fill::before{content:"\f1f7"}.bi-calendar2-check::before{content:"\f1f8"}.bi-calendar2-date-fill::before{content:"\f1f9"}.bi-calendar2-date::before{content:"\f1fa"}.bi-calendar2-day-fill::before{content:"\f1fb"}.bi-calendar2-day::before{content:"\f1fc"}.bi-calendar2-event-fill::before{content:"\f1fd"}.bi-calendar2-event::before{content:"\f1fe"}.bi-calendar2-fill::before{content:"\f1ff"}.bi-calendar2-minus-fill::before{content:"\f200"}.bi-calendar2-minus::before{content:"\f201"}.bi-calendar2-month-fill::before{content:"\f202"}.bi-calendar2-month::before{content:"\f203"}.bi-calendar2-plus-fill::before{content:"\f204"}.bi-calendar2-plus::before{content:"\f205"}.bi-calendar2-range-fill::before{content:"\f206"}.bi-calendar2-range::before{content:"\f207"}.bi-calendar2-week-fill::before{content:"\f208"}.bi-calendar2-week::before{content:"\f209"}.bi-calendar2-x-fill::before{content:"\f20a"}.bi-calendar2-x::before{content:"\f20b"}.bi-calendar2::before{content:"\f20c"}.bi-calendar3-event-fill::before{content:"\f20d"}.bi-calendar3-event::before{content:"\f20e"}.bi-calendar3-fill::before{content:"\f20f"}.bi-calendar3-range-fill::before{content:"\f210"}.bi-calendar3-range::before{content:"\f211"}.bi-calendar3-week-fill::before{content:"\f212"}.bi-calendar3-week::before{content:"\f213"}.bi-calendar3::before{content:"\f214"}.bi-calendar4-event::before{content:"\f215"}.bi-calendar4-range::before{content:"\f216"}.bi-calendar4-week::before{content:"\f217"}.bi-calendar4::before{content:"\f218"}.bi-camera-fill::before{content:"\f219"}.bi-camera-reels-fill::before{content:"\f21a"}.bi-camera-reels::before{content:"\f21b"}.bi-camera-video-fill::before{content:"\f21c"}.bi-camera-video-off-fill::before{content:"\f21d"}.bi-camera-video-off::before{content:"\f21e"}.bi-camera-video::before{content:"\f21f"}.bi-camera::before{content:"\f220"}.bi-camera2::before{content:"\f221"}.bi-capslock-fill::before{content:"\f222"}.bi-capslock::before{content:"\f223"}.bi-card-checklist::before{content:"\f224"}.bi-card-heading::before{content:"\f225"}.bi-card-image::before{content:"\f226"}.bi-card-list::before{content:"\f227"}.bi-card-text::before{content:"\f228"}.bi-caret-down-fill::before{content:"\f229"}.bi-caret-down-square-fill::before{content:"\f22a"}.bi-caret-down-square::before{content:"\f22b"}.bi-caret-down::before{content:"\f22c"}.bi-caret-left-fill::before{content:"\f22d"}.bi-caret-left-square-fill::before{content:"\f22e"}.bi-caret-left-square::before{content:"\f22f"}.bi-caret-left::before{content:"\f230"}.bi-caret-right-fill::before{content:"\f231"}.bi-caret-right-square-fill::before{content:"\f232"}.bi-caret-right-square::before{content:"\f233"}.bi-caret-right::before{content:"\f234"}.bi-caret-up-fill::before{content:"\f235"}.bi-caret-up-square-fill::before{content:"\f236"}.bi-caret-up-square::before{content:"\f237"}.bi-caret-up::before{content:"\f238"}.bi-cart-check-fill::before{content:"\f239"}.bi-cart-check::before{content:"\f23a"}.bi-cart-dash-fill::before{content:"\f23b"}.bi-cart-dash::before{content:"\f23c"}.bi-cart-fill::before{content:"\f23d"}.bi-cart-plus-fill::before{content:"\f23e"}.bi-cart-plus::before{content:"\f23f"}.bi-cart-x-fill::before{content:"\f240"}.bi-cart-x::before{content:"\f241"}.bi-cart::before{content:"\f242"}.bi-cart2::before{content:"\f243"}.bi-cart3::before{content:"\f244"}.bi-cart4::before{content:"\f245"}.bi-cash-stack::before{content:"\f246"}.bi-cash::before{content:"\f247"}.bi-cast::before{content:"\f248"}.bi-chat-dots-fill::before{content:"\f249"}.bi-chat-dots::before{content:"\f24a"}.bi-chat-fill::before{content:"\f24b"}.bi-chat-left-dots-fill::before{content:"\f24c"}.bi-chat-left-dots::before{content:"\f24d"}.bi-chat-left-fill::before{content:"\f24e"}.bi-chat-left-quote-fill::before{content:"\f24f"}.bi-chat-left-quote::before{content:"\f250"}.bi-chat-left-text-fill::before{content:"\f251"}.bi-chat-left-text::before{content:"\f252"}.bi-chat-left::before{content:"\f253"}.bi-chat-quote-fill::before{content:"\f254"}.bi-chat-quote::before{content:"\f255"}.bi-chat-right-dots-fill::before{content:"\f256"}.bi-chat-right-dots::before{content:"\f257"}.bi-chat-right-fill::before{content:"\f258"}.bi-chat-right-quote-fill::before{content:"\f259"}.bi-chat-right-quote::before{content:"\f25a"}.bi-chat-right-text-fill::before{content:"\f25b"}.bi-chat-right-text::before{content:"\f25c"}.bi-chat-right::before{content:"\f25d"}.bi-chat-square-dots-fill::before{content:"\f25e"}.bi-chat-square-dots::before{content:"\f25f"}.bi-chat-square-fill::before{content:"\f260"}.bi-chat-square-quote-fill::before{content:"\f261"}.bi-chat-square-quote::before{content:"\f262"}.bi-chat-square-text-fill::before{content:"\f263"}.bi-chat-square-text::before{content:"\f264"}.bi-chat-square::before{content:"\f265"}.bi-chat-text-fill::before{content:"\f266"}.bi-chat-text::before{content:"\f267"}.bi-chat::before{content:"\f268"}.bi-check-all::before{content:"\f269"}.bi-check-circle-fill::before{content:"\f26a"}.bi-check-circle::before{content:"\f26b"}.bi-check-square-fill::before{content:"\f26c"}.bi-check-square::before{content:"\f26d"}.bi-check::before{content:"\f26e"}.bi-check2-all::before{content:"\f26f"}.bi-check2-circle::before{content:"\f270"}.bi-check2-square::before{content:"\f271"}.bi-check2::before{content:"\f272"}.bi-chevron-bar-contract::before{content:"\f273"}.bi-chevron-bar-down::before{content:"\f274"}.bi-chevron-bar-expand::before{content:"\f275"}.bi-chevron-bar-left::before{content:"\f276"}.bi-chevron-bar-right::before{content:"\f277"}.bi-chevron-bar-up::before{content:"\f278"}.bi-chevron-compact-down::before{content:"\f279"}.bi-chevron-compact-left::before{content:"\f27a"}.bi-chevron-compact-right::before{content:"\f27b"}.bi-chevron-compact-up::before{content:"\f27c"}.bi-chevron-contract::before{content:"\f27d"}.bi-chevron-double-down::before{content:"\f27e"}.bi-chevron-double-left::before{content:"\f27f"}.bi-chevron-double-right::before{content:"\f280"}.bi-chevron-double-up::before{content:"\f281"}.bi-chevron-down::before{content:"\f282"}.bi-chevron-expand::before{content:"\f283"}.bi-chevron-left::before{content:"\f284"}.bi-chevron-right::before{content:"\f285"}.bi-chevron-up::before{content:"\f286"}.bi-circle-fill::before{content:"\f287"}.bi-circle-half::before{content:"\f288"}.bi-circle-square::before{content:"\f289"}.bi-circle::before{content:"\f28a"}.bi-clipboard-check::before{content:"\f28b"}.bi-clipboard-data::before{content:"\f28c"}.bi-clipboard-minus::before{content:"\f28d"}.bi-clipboard-plus::before{content:"\f28e"}.bi-clipboard-x::before{content:"\f28f"}.bi-clipboard::before{content:"\f290"}.bi-clock-fill::before{content:"\f291"}.bi-clock-history::before{content:"\f292"}.bi-clock::before{content:"\f293"}.bi-cloud-arrow-down-fill::before{content:"\f294"}.bi-cloud-arrow-down::before{content:"\f295"}.bi-cloud-arrow-up-fill::before{content:"\f296"}.bi-cloud-arrow-up::before{content:"\f297"}.bi-cloud-check-fill::before{content:"\f298"}.bi-cloud-check::before{content:"\f299"}.bi-cloud-download-fill::before{content:"\f29a"}.bi-cloud-download::before{content:"\f29b"}.bi-cloud-drizzle-fill::before{content:"\f29c"}.bi-cloud-drizzle::before{content:"\f29d"}.bi-cloud-fill::before{content:"\f29e"}.bi-cloud-fog-fill::before{content:"\f29f"}.bi-cloud-fog::before{content:"\f2a0"}.bi-cloud-fog2-fill::before{content:"\f2a1"}.bi-cloud-fog2::before{content:"\f2a2"}.bi-cloud-hail-fill::before{content:"\f2a3"}.bi-cloud-hail::before{content:"\f2a4"}.bi-cloud-haze-fill::before{content:"\f2a6"}.bi-cloud-haze::before{content:"\f2a7"}.bi-cloud-haze2-fill::before{content:"\f2a8"}.bi-cloud-lightning-fill::before{content:"\f2a9"}.bi-cloud-lightning-rain-fill::before{content:"\f2aa"}.bi-cloud-lightning-rain::before{content:"\f2ab"}.bi-cloud-lightning::before{content:"\f2ac"}.bi-cloud-minus-fill::before{content:"\f2ad"}.bi-cloud-minus::before{content:"\f2ae"}.bi-cloud-moon-fill::before{content:"\f2af"}.bi-cloud-moon::before{content:"\f2b0"}.bi-cloud-plus-fill::before{content:"\f2b1"}.bi-cloud-plus::before{content:"\f2b2"}.bi-cloud-rain-fill::before{content:"\f2b3"}.bi-cloud-rain-heavy-fill::before{content:"\f2b4"}.bi-cloud-rain-heavy::before{content:"\f2b5"}.bi-cloud-rain::before{content:"\f2b6"}.bi-cloud-slash-fill::before{content:"\f2b7"}.bi-cloud-slash::before{content:"\f2b8"}.bi-cloud-sleet-fill::before{content:"\f2b9"}.bi-cloud-sleet::before{content:"\f2ba"}.bi-cloud-snow-fill::before{content:"\f2bb"}.bi-cloud-snow::before{content:"\f2bc"}.bi-cloud-sun-fill::before{content:"\f2bd"}.bi-cloud-sun::before{content:"\f2be"}.bi-cloud-upload-fill::before{content:"\f2bf"}.bi-cloud-upload::before{content:"\f2c0"}.bi-cloud::before{content:"\f2c1"}.bi-clouds-fill::before{content:"\f2c2"}.bi-clouds::before{content:"\f2c3"}.bi-cloudy-fill::before{content:"\f2c4"}.bi-cloudy::before{content:"\f2c5"}.bi-code-slash::before{content:"\f2c6"}.bi-code-square::before{content:"\f2c7"}.bi-code::before{content:"\f2c8"}.bi-collection-fill::before{content:"\f2c9"}.bi-collection-play-fill::before{content:"\f2ca"}.bi-collection-play::before{content:"\f2cb"}.bi-collection::before{content:"\f2cc"}.bi-columns-gap::before{content:"\f2cd"}.bi-columns::before{content:"\f2ce"}.bi-command::before{content:"\f2cf"}.bi-compass-fill::before{content:"\f2d0"}.bi-compass::before{content:"\f2d1"}.bi-cone-striped::before{content:"\f2d2"}.bi-cone::before{content:"\f2d3"}.bi-controller::before{content:"\f2d4"}.bi-cpu-fill::before{content:"\f2d5"}.bi-cpu::before{content:"\f2d6"}.bi-credit-card-2-back-fill::before{content:"\f2d7"}.bi-credit-card-2-back::before{content:"\f2d8"}.bi-credit-card-2-front-fill::before{content:"\f2d9"}.bi-credit-card-2-front::before{content:"\f2da"}.bi-credit-card-fill::before{content:"\f2db"}.bi-credit-card::before{content:"\f2dc"}.bi-crop::before{content:"\f2dd"}.bi-cup-fill::before{content:"\f2de"}.bi-cup-straw::before{content:"\f2df"}.bi-cup::before{content:"\f2e0"}.bi-cursor-fill::before{content:"\f2e1"}.bi-cursor-text::before{content:"\f2e2"}.bi-cursor::before{content:"\f2e3"}.bi-dash-circle-dotted::before{content:"\f2e4"}.bi-dash-circle-fill::before{content:"\f2e5"}.bi-dash-circle::before{content:"\f2e6"}.bi-dash-square-dotted::before{content:"\f2e7"}.bi-dash-square-fill::before{content:"\f2e8"}.bi-dash-square::before{content:"\f2e9"}.bi-dash::before{content:"\f2ea"}.bi-diagram-2-fill::before{content:"\f2eb"}.bi-diagram-2::before{content:"\f2ec"}.bi-diagram-3-fill::before{content:"\f2ed"}.bi-diagram-3::before{content:"\f2ee"}.bi-diamond-fill::before{content:"\f2ef"}.bi-diamond-half::before{content:"\f2f0"}.bi-diamond::before{content:"\f2f1"}.bi-dice-1-fill::before{content:"\f2f2"}.bi-dice-1::before{content:"\f2f3"}.bi-dice-2-fill::before{content:"\f2f4"}.bi-dice-2::before{content:"\f2f5"}.bi-dice-3-fill::before{content:"\f2f6"}.bi-dice-3::before{content:"\f2f7"}.bi-dice-4-fill::before{content:"\f2f8"}.bi-dice-4::before{content:"\f2f9"}.bi-dice-5-fill::before{content:"\f2fa"}.bi-dice-5::before{content:"\f2fb"}.bi-dice-6-fill::before{content:"\f2fc"}.bi-dice-6::before{content:"\f2fd"}.bi-disc-fill::before{content:"\f2fe"}.bi-disc::before{content:"\f2ff"}.bi-discord::before{content:"\f300"}.bi-display-fill::before{content:"\f301"}.bi-display::before{content:"\f302"}.bi-distribute-horizontal::before{content:"\f303"}.bi-distribute-vertical::before{content:"\f304"}.bi-door-closed-fill::before{content:"\f305"}.bi-door-closed::before{content:"\f306"}.bi-door-open-fill::before{content:"\f307"}.bi-door-open::before{content:"\f308"}.bi-dot::before{content:"\f309"}.bi-download::before{content:"\f30a"}.bi-droplet-fill::before{content:"\f30b"}.bi-droplet-half::before{content:"\f30c"}.bi-droplet::before{content:"\f30d"}.bi-earbuds::before{content:"\f30e"}.bi-easel-fill::before{content:"\f30f"}.bi-easel::before{content:"\f310"}.bi-egg-fill::before{content:"\f311"}.bi-egg-fried::before{content:"\f312"}.bi-egg::before{content:"\f313"}.bi-eject-fill::before{content:"\f314"}.bi-eject::before{content:"\f315"}.bi-emoji-angry-fill::before{content:"\f316"}.bi-emoji-angry::before{content:"\f317"}.bi-emoji-dizzy-fill::before{content:"\f318"}.bi-emoji-dizzy::before{content:"\f319"}.bi-emoji-expressionless-fill::before{content:"\f31a"}.bi-emoji-expressionless::before{content:"\f31b"}.bi-emoji-frown-fill::before{content:"\f31c"}.bi-emoji-frown::before{content:"\f31d"}.bi-emoji-heart-eyes-fill::before{content:"\f31e"}.bi-emoji-heart-eyes::before{content:"\f31f"}.bi-emoji-laughing-fill::before{content:"\f320"}.bi-emoji-laughing::before{content:"\f321"}.bi-emoji-neutral-fill::before{content:"\f322"}.bi-emoji-neutral::before{content:"\f323"}.bi-emoji-smile-fill::before{content:"\f324"}.bi-emoji-smile-upside-down-fill::before{content:"\f325"}.bi-emoji-smile-upside-down::before{content:"\f326"}.bi-emoji-smile::before{content:"\f327"}.bi-emoji-sunglasses-fill::before{content:"\f328"}.bi-emoji-sunglasses::before{content:"\f329"}.bi-emoji-wink-fill::before{content:"\f32a"}.bi-emoji-wink::before{content:"\f32b"}.bi-envelope-fill::before{content:"\f32c"}.bi-envelope-open-fill::before{content:"\f32d"}.bi-envelope-open::before{content:"\f32e"}.bi-envelope::before{content:"\f32f"}.bi-eraser-fill::before{content:"\f330"}.bi-eraser::before{content:"\f331"}.bi-exclamation-circle-fill::before{content:"\f332"}.bi-exclamation-circle::before{content:"\f333"}.bi-exclamation-diamond-fill::before{content:"\f334"}.bi-exclamation-diamond::before{content:"\f335"}.bi-exclamation-octagon-fill::before{content:"\f336"}.bi-exclamation-octagon::before{content:"\f337"}.bi-exclamation-square-fill::before{content:"\f338"}.bi-exclamation-square::before{content:"\f339"}.bi-exclamation-triangle-fill::before{content:"\f33a"}.bi-exclamation-triangle::before{content:"\f33b"}.bi-exclamation::before{content:"\f33c"}.bi-exclude::before{content:"\f33d"}.bi-eye-fill::before{content:"\f33e"}.bi-eye-slash-fill::before{content:"\f33f"}.bi-eye-slash::before{content:"\f340"}.bi-eye::before{content:"\f341"}.bi-eyedropper::before{content:"\f342"}.bi-eyeglasses::before{content:"\f343"}.bi-facebook::before{content:"\f344"}.bi-file-arrow-down-fill::before{content:"\f345"}.bi-file-arrow-down::before{content:"\f346"}.bi-file-arrow-up-fill::before{content:"\f347"}.bi-file-arrow-up::before{content:"\f348"}.bi-file-bar-graph-fill::before{content:"\f349"}.bi-file-bar-graph::before{content:"\f34a"}.bi-file-binary-fill::before{content:"\f34b"}.bi-file-binary::before{content:"\f34c"}.bi-file-break-fill::before{content:"\f34d"}.bi-file-break::before{content:"\f34e"}.bi-file-check-fill::before{content:"\f34f"}.bi-file-check::before{content:"\f350"}.bi-file-code-fill::before{content:"\f351"}.bi-file-code::before{content:"\f352"}.bi-file-diff-fill::before{content:"\f353"}.bi-file-diff::before{content:"\f354"}.bi-file-earmark-arrow-down-fill::before{content:"\f355"}.bi-file-earmark-arrow-down::before{content:"\f356"}.bi-file-earmark-arrow-up-fill::before{content:"\f357"}.bi-file-earmark-arrow-up::before{content:"\f358"}.bi-file-earmark-bar-graph-fill::before{content:"\f359"}.bi-file-earmark-bar-graph::before{content:"\f35a"}.bi-file-earmark-binary-fill::before{content:"\f35b"}.bi-file-earmark-binary::before{content:"\f35c"}.bi-file-earmark-break-fill::before{content:"\f35d"}.bi-file-earmark-break::before{content:"\f35e"}.bi-file-earmark-check-fill::before{content:"\f35f"}.bi-file-earmark-check::before{content:"\f360"}.bi-file-earmark-code-fill::before{content:"\f361"}.bi-file-earmark-code::before{content:"\f362"}.bi-file-earmark-diff-fill::before{content:"\f363"}.bi-file-earmark-diff::before{content:"\f364"}.bi-file-earmark-easel-fill::before{content:"\f365"}.bi-file-earmark-easel::before{content:"\f366"}.bi-file-earmark-excel-fill::before{content:"\f367"}.bi-file-earmark-excel::before{content:"\f368"}.bi-file-earmark-fill::before{content:"\f369"}.bi-file-earmark-font-fill::before{content:"\f36a"}.bi-file-earmark-font::before{content:"\f36b"}.bi-file-earmark-image-fill::before{content:"\f36c"}.bi-file-earmark-image::before{content:"\f36d"}.bi-file-earmark-lock-fill::before{content:"\f36e"}.bi-file-earmark-lock::before{content:"\f36f"}.bi-file-earmark-lock2-fill::before{content:"\f370"}.bi-file-earmark-lock2::before{content:"\f371"}.bi-file-earmark-medical-fill::before{content:"\f372"}.bi-file-earmark-medical::before{content:"\f373"}.bi-file-earmark-minus-fill::before{content:"\f374"}.bi-file-earmark-minus::before{content:"\f375"}.bi-file-earmark-music-fill::before{content:"\f376"}.bi-file-earmark-music::before{content:"\f377"}.bi-file-earmark-person-fill::before{content:"\f378"}.bi-file-earmark-person::before{content:"\f379"}.bi-file-earmark-play-fill::before{content:"\f37a"}.bi-file-earmark-play::before{content:"\f37b"}.bi-file-earmark-plus-fill::before{content:"\f37c"}.bi-file-earmark-plus::before{content:"\f37d"}.bi-file-earmark-post-fill::before{content:"\f37e"}.bi-file-earmark-post::before{content:"\f37f"}.bi-file-earmark-ppt-fill::before{content:"\f380"}.bi-file-earmark-ppt::before{content:"\f381"}.bi-file-earmark-richtext-fill::before{content:"\f382"}.bi-file-earmark-richtext::before{content:"\f383"}.bi-file-earmark-ruled-fill::before{content:"\f384"}.bi-file-earmark-ruled::before{content:"\f385"}.bi-file-earmark-slides-fill::before{content:"\f386"}.bi-file-earmark-slides::before{content:"\f387"}.bi-file-earmark-spreadsheet-fill::before{content:"\f388"}.bi-file-earmark-spreadsheet::before{content:"\f389"}.bi-file-earmark-text-fill::before{content:"\f38a"}.bi-file-earmark-text::before{content:"\f38b"}.bi-file-earmark-word-fill::before{content:"\f38c"}.bi-file-earmark-word::before{content:"\f38d"}.bi-file-earmark-x-fill::before{content:"\f38e"}.bi-file-earmark-x::before{content:"\f38f"}.bi-file-earmark-zip-fill::before{content:"\f390"}.bi-file-earmark-zip::before{content:"\f391"}.bi-file-earmark::before{content:"\f392"}.bi-file-easel-fill::before{content:"\f393"}.bi-file-easel::before{content:"\f394"}.bi-file-excel-fill::before{content:"\f395"}.bi-file-excel::before{content:"\f396"}.bi-file-fill::before{content:"\f397"}.bi-file-font-fill::before{content:"\f398"}.bi-file-font::before{content:"\f399"}.bi-file-image-fill::before{content:"\f39a"}.bi-file-image::before{content:"\f39b"}.bi-file-lock-fill::before{content:"\f39c"}.bi-file-lock::before{content:"\f39d"}.bi-file-lock2-fill::before{content:"\f39e"}.bi-file-lock2::before{content:"\f39f"}.bi-file-medical-fill::before{content:"\f3a0"}.bi-file-medical::before{content:"\f3a1"}.bi-file-minus-fill::before{content:"\f3a2"}.bi-file-minus::before{content:"\f3a3"}.bi-file-music-fill::before{content:"\f3a4"}.bi-file-music::before{content:"\f3a5"}.bi-file-person-fill::before{content:"\f3a6"}.bi-file-person::before{content:"\f3a7"}.bi-file-play-fill::before{content:"\f3a8"}.bi-file-play::before{content:"\f3a9"}.bi-file-plus-fill::before{content:"\f3aa"}.bi-file-plus::before{content:"\f3ab"}.bi-file-post-fill::before{content:"\f3ac"}.bi-file-post::before{content:"\f3ad"}.bi-file-ppt-fill::before{content:"\f3ae"}.bi-file-ppt::before{content:"\f3af"}.bi-file-richtext-fill::before{content:"\f3b0"}.bi-file-richtext::before{content:"\f3b1"}.bi-file-ruled-fill::before{content:"\f3b2"}.bi-file-ruled::before{content:"\f3b3"}.bi-file-slides-fill::before{content:"\f3b4"}.bi-file-slides::before{content:"\f3b5"}.bi-file-spreadsheet-fill::before{content:"\f3b6"}.bi-file-spreadsheet::before{content:"\f3b7"}.bi-file-text-fill::before{content:"\f3b8"}.bi-file-text::before{content:"\f3b9"}.bi-file-word-fill::before{content:"\f3ba"}.bi-file-word::before{content:"\f3bb"}.bi-file-x-fill::before{content:"\f3bc"}.bi-file-x::before{content:"\f3bd"}.bi-file-zip-fill::before{content:"\f3be"}.bi-file-zip::before{content:"\f3bf"}.bi-file::before{content:"\f3c0"}.bi-files-alt::before{content:"\f3c1"}.bi-files::before{content:"\f3c2"}.bi-film::before{content:"\f3c3"}.bi-filter-circle-fill::before{content:"\f3c4"}.bi-filter-circle::before{content:"\f3c5"}.bi-filter-left::before{content:"\f3c6"}.bi-filter-right::before{content:"\f3c7"}.bi-filter-square-fill::before{content:"\f3c8"}.bi-filter-square::before{content:"\f3c9"}.bi-filter::before{content:"\f3ca"}.bi-flag-fill::before{content:"\f3cb"}.bi-flag::before{content:"\f3cc"}.bi-flower1::before{content:"\f3cd"}.bi-flower2::before{content:"\f3ce"}.bi-flower3::before{content:"\f3cf"}.bi-folder-check::before{content:"\f3d0"}.bi-folder-fill::before{content:"\f3d1"}.bi-folder-minus::before{content:"\f3d2"}.bi-folder-plus::before{content:"\f3d3"}.bi-folder-symlink-fill::before{content:"\f3d4"}.bi-folder-symlink::before{content:"\f3d5"}.bi-folder-x::before{content:"\f3d6"}.bi-folder::before{content:"\f3d7"}.bi-folder2-open::before{content:"\f3d8"}.bi-folder2::before{content:"\f3d9"}.bi-fonts::before{content:"\f3da"}.bi-forward-fill::before{content:"\f3db"}.bi-forward::before{content:"\f3dc"}.bi-front::before{content:"\f3dd"}.bi-fullscreen-exit::before{content:"\f3de"}.bi-fullscreen::before{content:"\f3df"}.bi-funnel-fill::before{content:"\f3e0"}.bi-funnel::before{content:"\f3e1"}.bi-gear-fill::before{content:"\f3e2"}.bi-gear-wide-connected::before{content:"\f3e3"}.bi-gear-wide::before{content:"\f3e4"}.bi-gear::before{content:"\f3e5"}.bi-gem::before{content:"\f3e6"}.bi-geo-alt-fill::before{content:"\f3e7"}.bi-geo-alt::before{content:"\f3e8"}.bi-geo-fill::before{content:"\f3e9"}.bi-geo::before{content:"\f3ea"}.bi-gift-fill::before{content:"\f3eb"}.bi-gift::before{content:"\f3ec"}.bi-github::before{content:"\f3ed"}.bi-globe::before{content:"\f3ee"}.bi-globe2::before{content:"\f3ef"}.bi-google::before{content:"\f3f0"}.bi-graph-down::before{content:"\f3f1"}.bi-graph-up::before{content:"\f3f2"}.bi-grid-1x2-fill::before{content:"\f3f3"}.bi-grid-1x2::before{content:"\f3f4"}.bi-grid-3x2-gap-fill::before{content:"\f3f5"}.bi-grid-3x2-gap::before{content:"\f3f6"}.bi-grid-3x2::before{content:"\f3f7"}.bi-grid-3x3-gap-fill::before{content:"\f3f8"}.bi-grid-3x3-gap::before{content:"\f3f9"}.bi-grid-3x3::before{content:"\f3fa"}.bi-grid-fill::before{content:"\f3fb"}.bi-grid::before{content:"\f3fc"}.bi-grip-horizontal::before{content:"\f3fd"}.bi-grip-vertical::before{content:"\f3fe"}.bi-hammer::before{content:"\f3ff"}.bi-hand-index-fill::before{content:"\f400"}.bi-hand-index-thumb-fill::before{content:"\f401"}.bi-hand-index-thumb::before{content:"\f402"}.bi-hand-index::before{content:"\f403"}.bi-hand-thumbs-down-fill::before{content:"\f404"}.bi-hand-thumbs-down::before{content:"\f405"}.bi-hand-thumbs-up-fill::before{content:"\f406"}.bi-hand-thumbs-up::before{content:"\f407"}.bi-handbag-fill::before{content:"\f408"}.bi-handbag::before{content:"\f409"}.bi-hash::before{content:"\f40a"}.bi-hdd-fill::before{content:"\f40b"}.bi-hdd-network-fill::before{content:"\f40c"}.bi-hdd-network::before{content:"\f40d"}.bi-hdd-rack-fill::before{content:"\f40e"}.bi-hdd-rack::before{content:"\f40f"}.bi-hdd-stack-fill::before{content:"\f410"}.bi-hdd-stack::before{content:"\f411"}.bi-hdd::before{content:"\f412"}.bi-headphones::before{content:"\f413"}.bi-headset::before{content:"\f414"}.bi-heart-fill::before{content:"\f415"}.bi-heart-half::before{content:"\f416"}.bi-heart::before{content:"\f417"}.bi-heptagon-fill::before{content:"\f418"}.bi-heptagon-half::before{content:"\f419"}.bi-heptagon::before{content:"\f41a"}.bi-hexagon-fill::before{content:"\f41b"}.bi-hexagon-half::before{content:"\f41c"}.bi-hexagon::before{content:"\f41d"}.bi-hourglass-bottom::before{content:"\f41e"}.bi-hourglass-split::before{content:"\f41f"}.bi-hourglass-top::before{content:"\f420"}.bi-hourglass::before{content:"\f421"}.bi-house-door-fill::before{content:"\f422"}.bi-house-door::before{content:"\f423"}.bi-house-fill::before{content:"\f424"}.bi-house::before{content:"\f425"}.bi-hr::before{content:"\f426"}.bi-hurricane::before{content:"\f427"}.bi-image-alt::before{content:"\f428"}.bi-image-fill::before{content:"\f429"}.bi-image::before{content:"\f42a"}.bi-images::before{content:"\f42b"}.bi-inbox-fill::before{content:"\f42c"}.bi-inbox::before{content:"\f42d"}.bi-inboxes-fill::before{content:"\f42e"}.bi-inboxes::before{content:"\f42f"}.bi-info-circle-fill::before{content:"\f430"}.bi-info-circle::before{content:"\f431"}.bi-info-square-fill::before{content:"\f432"}.bi-info-square::before{content:"\f433"}.bi-info::before{content:"\f434"}.bi-input-cursor-text::before{content:"\f435"}.bi-input-cursor::before{content:"\f436"}.bi-instagram::before{content:"\f437"}.bi-intersect::before{content:"\f438"}.bi-journal-album::before{content:"\f439"}.bi-journal-arrow-down::before{content:"\f43a"}.bi-journal-arrow-up::before{content:"\f43b"}.bi-journal-bookmark-fill::before{content:"\f43c"}.bi-journal-bookmark::before{content:"\f43d"}.bi-journal-check::before{content:"\f43e"}.bi-journal-code::before{content:"\f43f"}.bi-journal-medical::before{content:"\f440"}.bi-journal-minus::before{content:"\f441"}.bi-journal-plus::before{content:"\f442"}.bi-journal-richtext::before{content:"\f443"}.bi-journal-text::before{content:"\f444"}.bi-journal-x::before{content:"\f445"}.bi-journal::before{content:"\f446"}.bi-journals::before{content:"\f447"}.bi-joystick::before{content:"\f448"}.bi-justify-left::before{content:"\f449"}.bi-justify-right::before{content:"\f44a"}.bi-justify::before{content:"\f44b"}.bi-kanban-fill::before{content:"\f44c"}.bi-kanban::before{content:"\f44d"}.bi-key-fill::before{content:"\f44e"}.bi-key::before{content:"\f44f"}.bi-keyboard-fill::before{content:"\f450"}.bi-keyboard::before{content:"\f451"}.bi-ladder::before{content:"\f452"}.bi-lamp-fill::before{content:"\f453"}.bi-lamp::before{content:"\f454"}.bi-laptop-fill::before{content:"\f455"}.bi-laptop::before{content:"\f456"}.bi-layer-backward::before{content:"\f457"}.bi-layer-forward::before{content:"\f458"}.bi-layers-fill::before{content:"\f459"}.bi-layers-half::before{content:"\f45a"}.bi-layers::before{content:"\f45b"}.bi-layout-sidebar-inset-reverse::before{content:"\f45c"}.bi-layout-sidebar-inset::before{content:"\f45d"}.bi-layout-sidebar-reverse::before{content:"\f45e"}.bi-layout-sidebar::before{content:"\f45f"}.bi-layout-split::before{content:"\f460"}.bi-layout-text-sidebar-reverse::before{content:"\f461"}.bi-layout-text-sidebar::before{content:"\f462"}.bi-layout-text-window-reverse::before{content:"\f463"}.bi-layout-text-window::before{content:"\f464"}.bi-layout-three-columns::before{content:"\f465"}.bi-layout-wtf::before{content:"\f466"}.bi-life-preserver::before{content:"\f467"}.bi-lightbulb-fill::before{content:"\f468"}.bi-lightbulb-off-fill::before{content:"\f469"}.bi-lightbulb-off::before{content:"\f46a"}.bi-lightbulb::before{content:"\f46b"}.bi-lightning-charge-fill::before{content:"\f46c"}.bi-lightning-charge::before{content:"\f46d"}.bi-lightning-fill::before{content:"\f46e"}.bi-lightning::before{content:"\f46f"}.bi-link-45deg::before{content:"\f470"}.bi-link::before{content:"\f471"}.bi-linkedin::before{content:"\f472"}.bi-list-check::before{content:"\f473"}.bi-list-nested::before{content:"\f474"}.bi-list-ol::before{content:"\f475"}.bi-list-stars::before{content:"\f476"}.bi-list-task::before{content:"\f477"}.bi-list-ul::before{content:"\f478"}.bi-list::before{content:"\f479"}.bi-lock-fill::before{content:"\f47a"}.bi-lock::before{content:"\f47b"}.bi-mailbox::before{content:"\f47c"}.bi-mailbox2::before{content:"\f47d"}.bi-map-fill::before{content:"\f47e"}.bi-map::before{content:"\f47f"}.bi-markdown-fill::before{content:"\f480"}.bi-markdown::before{content:"\f481"}.bi-mask::before{content:"\f482"}.bi-megaphone-fill::before{content:"\f483"}.bi-megaphone::before{content:"\f484"}.bi-menu-app-fill::before{content:"\f485"}.bi-menu-app::before{content:"\f486"}.bi-menu-button-fill::before{content:"\f487"}.bi-menu-button-wide-fill::before{content:"\f488"}.bi-menu-button-wide::before{content:"\f489"}.bi-menu-button::before{content:"\f48a"}.bi-menu-down::before{content:"\f48b"}.bi-menu-up::before{content:"\f48c"}.bi-mic-fill::before{content:"\f48d"}.bi-mic-mute-fill::before{content:"\f48e"}.bi-mic-mute::before{content:"\f48f"}.bi-mic::before{content:"\f490"}.bi-minecart-loaded::before{content:"\f491"}.bi-minecart::before{content:"\f492"}.bi-moisture::before{content:"\f493"}.bi-moon-fill::before{content:"\f494"}.bi-moon-stars-fill::before{content:"\f495"}.bi-moon-stars::before{content:"\f496"}.bi-moon::before{content:"\f497"}.bi-mouse-fill::before{content:"\f498"}.bi-mouse::before{content:"\f499"}.bi-mouse2-fill::before{content:"\f49a"}.bi-mouse2::before{content:"\f49b"}.bi-mouse3-fill::before{content:"\f49c"}.bi-mouse3::before{content:"\f49d"}.bi-music-note-beamed::before{content:"\f49e"}.bi-music-note-list::before{content:"\f49f"}.bi-music-note::before{content:"\f4a0"}.bi-music-player-fill::before{content:"\f4a1"}.bi-music-player::before{content:"\f4a2"}.bi-newspaper::before{content:"\f4a3"}.bi-node-minus-fill::before{content:"\f4a4"}.bi-node-minus::before{content:"\f4a5"}.bi-node-plus-fill::before{content:"\f4a6"}.bi-node-plus::before{content:"\f4a7"}.bi-nut-fill::before{content:"\f4a8"}.bi-nut::before{content:"\f4a9"}.bi-octagon-fill::before{content:"\f4aa"}.bi-octagon-half::before{content:"\f4ab"}.bi-octagon::before{content:"\f4ac"}.bi-option::before{content:"\f4ad"}.bi-outlet::before{content:"\f4ae"}.bi-paint-bucket::before{content:"\f4af"}.bi-palette-fill::before{content:"\f4b0"}.bi-palette::before{content:"\f4b1"}.bi-palette2::before{content:"\f4b2"}.bi-paperclip::before{content:"\f4b3"}.bi-paragraph::before{content:"\f4b4"}.bi-patch-check-fill::before{content:"\f4b5"}.bi-patch-check::before{content:"\f4b6"}.bi-patch-exclamation-fill::before{content:"\f4b7"}.bi-patch-exclamation::before{content:"\f4b8"}.bi-patch-minus-fill::before{content:"\f4b9"}.bi-patch-minus::before{content:"\f4ba"}.bi-patch-plus-fill::before{content:"\f4bb"}.bi-patch-plus::before{content:"\f4bc"}.bi-patch-question-fill::before{content:"\f4bd"}.bi-patch-question::before{content:"\f4be"}.bi-pause-btn-fill::before{content:"\f4bf"}.bi-pause-btn::before{content:"\f4c0"}.bi-pause-circle-fill::before{content:"\f4c1"}.bi-pause-circle::before{content:"\f4c2"}.bi-pause-fill::before{content:"\f4c3"}.bi-pause::before{content:"\f4c4"}.bi-peace-fill::before{content:"\f4c5"}.bi-peace::before{content:"\f4c6"}.bi-pen-fill::before{content:"\f4c7"}.bi-pen::before{content:"\f4c8"}.bi-pencil-fill::before{content:"\f4c9"}.bi-pencil-square::before{content:"\f4ca"}.bi-pencil::before{content:"\f4cb"}.bi-pentagon-fill::before{content:"\f4cc"}.bi-pentagon-half::before{content:"\f4cd"}.bi-pentagon::before{content:"\f4ce"}.bi-people-fill::before{content:"\f4cf"}.bi-people::before{content:"\f4d0"}.bi-percent::before{content:"\f4d1"}.bi-person-badge-fill::before{content:"\f4d2"}.bi-person-badge::before{content:"\f4d3"}.bi-person-bounding-box::before{content:"\f4d4"}.bi-person-check-fill::before{content:"\f4d5"}.bi-person-check::before{content:"\f4d6"}.bi-person-circle::before{content:"\f4d7"}.bi-person-dash-fill::before{content:"\f4d8"}.bi-person-dash::before{content:"\f4d9"}.bi-person-fill::before{content:"\f4da"}.bi-person-lines-fill::before{content:"\f4db"}.bi-person-plus-fill::before{content:"\f4dc"}.bi-person-plus::before{content:"\f4dd"}.bi-person-square::before{content:"\f4de"}.bi-person-x-fill::before{content:"\f4df"}.bi-person-x::before{content:"\f4e0"}.bi-person::before{content:"\f4e1"}.bi-phone-fill::before{content:"\f4e2"}.bi-phone-landscape-fill::before{content:"\f4e3"}.bi-phone-landscape::before{content:"\f4e4"}.bi-phone-vibrate-fill::before{content:"\f4e5"}.bi-phone-vibrate::before{content:"\f4e6"}.bi-phone::before{content:"\f4e7"}.bi-pie-chart-fill::before{content:"\f4e8"}.bi-pie-chart::before{content:"\f4e9"}.bi-pin-angle-fill::before{content:"\f4ea"}.bi-pin-angle::before{content:"\f4eb"}.bi-pin-fill::before{content:"\f4ec"}.bi-pin::before{content:"\f4ed"}.bi-pip-fill::before{content:"\f4ee"}.bi-pip::before{content:"\f4ef"}.bi-play-btn-fill::before{content:"\f4f0"}.bi-play-btn::before{content:"\f4f1"}.bi-play-circle-fill::before{content:"\f4f2"}.bi-play-circle::before{content:"\f4f3"}.bi-play-fill::before{content:"\f4f4"}.bi-play::before{content:"\f4f5"}.bi-plug-fill::before{content:"\f4f6"}.bi-plug::before{content:"\f4f7"}.bi-plus-circle-dotted::before{content:"\f4f8"}.bi-plus-circle-fill::before{content:"\f4f9"}.bi-plus-circle::before{content:"\f4fa"}.bi-plus-square-dotted::before{content:"\f4fb"}.bi-plus-square-fill::before{content:"\f4fc"}.bi-plus-square::before{content:"\f4fd"}.bi-plus::before{content:"\f4fe"}.bi-power::before{content:"\f4ff"}.bi-printer-fill::before{content:"\f500"}.bi-printer::before{content:"\f501"}.bi-puzzle-fill::before{content:"\f502"}.bi-puzzle::before{content:"\f503"}.bi-question-circle-fill::before{content:"\f504"}.bi-question-circle::before{content:"\f505"}.bi-question-diamond-fill::before{content:"\f506"}.bi-question-diamond::before{content:"\f507"}.bi-question-octagon-fill::before{content:"\f508"}.bi-question-octagon::before{content:"\f509"}.bi-question-square-fill::before{content:"\f50a"}.bi-question-square::before{content:"\f50b"}.bi-question::before{content:"\f50c"}.bi-rainbow::before{content:"\f50d"}.bi-receipt-cutoff::before{content:"\f50e"}.bi-receipt::before{content:"\f50f"}.bi-reception-0::before{content:"\f510"}.bi-reception-1::before{content:"\f511"}.bi-reception-2::before{content:"\f512"}.bi-reception-3::before{content:"\f513"}.bi-reception-4::before{content:"\f514"}.bi-record-btn-fill::before{content:"\f515"}.bi-record-btn::before{content:"\f516"}.bi-record-circle-fill::before{content:"\f517"}.bi-record-circle::before{content:"\f518"}.bi-record-fill::before{content:"\f519"}.bi-record::before{content:"\f51a"}.bi-record2-fill::before{content:"\f51b"}.bi-record2::before{content:"\f51c"}.bi-reply-all-fill::before{content:"\f51d"}.bi-reply-all::before{content:"\f51e"}.bi-reply-fill::before{content:"\f51f"}.bi-reply::before{content:"\f520"}.bi-rss-fill::before{content:"\f521"}.bi-rss::before{content:"\f522"}.bi-rulers::before{content:"\f523"}.bi-save-fill::before{content:"\f524"}.bi-save::before{content:"\f525"}.bi-save2-fill::before{content:"\f526"}.bi-save2::before{content:"\f527"}.bi-scissors::before{content:"\f528"}.bi-screwdriver::before{content:"\f529"}.bi-search::before{content:"\f52a"}.bi-segmented-nav::before{content:"\f52b"}.bi-server::before{content:"\f52c"}.bi-share-fill::before{content:"\f52d"}.bi-share::before{content:"\f52e"}.bi-shield-check::before{content:"\f52f"}.bi-shield-exclamation::before{content:"\f530"}.bi-shield-fill-check::before{content:"\f531"}.bi-shield-fill-exclamation::before{content:"\f532"}.bi-shield-fill-minus::before{content:"\f533"}.bi-shield-fill-plus::before{content:"\f534"}.bi-shield-fill-x::before{content:"\f535"}.bi-shield-fill::before{content:"\f536"}.bi-shield-lock-fill::before{content:"\f537"}.bi-shield-lock::before{content:"\f538"}.bi-shield-minus::before{content:"\f539"}.bi-shield-plus::before{content:"\f53a"}.bi-shield-shaded::before{content:"\f53b"}.bi-shield-slash-fill::before{content:"\f53c"}.bi-shield-slash::before{content:"\f53d"}.bi-shield-x::before{content:"\f53e"}.bi-shield::before{content:"\f53f"}.bi-shift-fill::before{content:"\f540"}.bi-shift::before{content:"\f541"}.bi-shop-window::before{content:"\f542"}.bi-shop::before{content:"\f543"}.bi-shuffle::before{content:"\f544"}.bi-signpost-2-fill::before{content:"\f545"}.bi-signpost-2::before{content:"\f546"}.bi-signpost-fill::before{content:"\f547"}.bi-signpost-split-fill::before{content:"\f548"}.bi-signpost-split::before{content:"\f549"}.bi-signpost::before{content:"\f54a"}.bi-sim-fill::before{content:"\f54b"}.bi-sim::before{content:"\f54c"}.bi-skip-backward-btn-fill::before{content:"\f54d"}.bi-skip-backward-btn::before{content:"\f54e"}.bi-skip-backward-circle-fill::before{content:"\f54f"}.bi-skip-backward-circle::before{content:"\f550"}.bi-skip-backward-fill::before{content:"\f551"}.bi-skip-backward::before{content:"\f552"}.bi-skip-end-btn-fill::before{content:"\f553"}.bi-skip-end-btn::before{content:"\f554"}.bi-skip-end-circle-fill::before{content:"\f555"}.bi-skip-end-circle::before{content:"\f556"}.bi-skip-end-fill::before{content:"\f557"}.bi-skip-end::before{content:"\f558"}.bi-skip-forward-btn-fill::before{content:"\f559"}.bi-skip-forward-btn::before{content:"\f55a"}.bi-skip-forward-circle-fill::before{content:"\f55b"}.bi-skip-forward-circle::before{content:"\f55c"}.bi-skip-forward-fill::before{content:"\f55d"}.bi-skip-forward::before{content:"\f55e"}.bi-skip-start-btn-fill::before{content:"\f55f"}.bi-skip-start-btn::before{content:"\f560"}.bi-skip-start-circle-fill::before{content:"\f561"}.bi-skip-start-circle::before{content:"\f562"}.bi-skip-start-fill::before{content:"\f563"}.bi-skip-start::before{content:"\f564"}.bi-slack::before{content:"\f565"}.bi-slash-circle-fill::before{content:"\f566"}.bi-slash-circle::before{content:"\f567"}.bi-slash-square-fill::before{content:"\f568"}.bi-slash-square::before{content:"\f569"}.bi-slash::before{content:"\f56a"}.bi-sliders::before{content:"\f56b"}.bi-smartwatch::before{content:"\f56c"}.bi-snow::before{content:"\f56d"}.bi-snow2::before{content:"\f56e"}.bi-snow3::before{content:"\f56f"}.bi-sort-alpha-down-alt::before{content:"\f570"}.bi-sort-alpha-down::before{content:"\f571"}.bi-sort-alpha-up-alt::before{content:"\f572"}.bi-sort-alpha-up::before{content:"\f573"}.bi-sort-down-alt::before{content:"\f574"}.bi-sort-down::before{content:"\f575"}.bi-sort-numeric-down-alt::before{content:"\f576"}.bi-sort-numeric-down::before{content:"\f577"}.bi-sort-numeric-up-alt::before{content:"\f578"}.bi-sort-numeric-up::before{content:"\f579"}.bi-sort-up-alt::before{content:"\f57a"}.bi-sort-up::before{content:"\f57b"}.bi-soundwave::before{content:"\f57c"}.bi-speaker-fill::before{content:"\f57d"}.bi-speaker::before{content:"\f57e"}.bi-speedometer::before{content:"\f57f"}.bi-speedometer2::before{content:"\f580"}.bi-spellcheck::before{content:"\f581"}.bi-square-fill::before{content:"\f582"}.bi-square-half::before{content:"\f583"}.bi-square::before{content:"\f584"}.bi-stack::before{content:"\f585"}.bi-star-fill::before{content:"\f586"}.bi-star-half::before{content:"\f587"}.bi-star::before{content:"\f588"}.bi-stars::before{content:"\f589"}.bi-stickies-fill::before{content:"\f58a"}.bi-stickies::before{content:"\f58b"}.bi-sticky-fill::before{content:"\f58c"}.bi-sticky::before{content:"\f58d"}.bi-stop-btn-fill::before{content:"\f58e"}.bi-stop-btn::before{content:"\f58f"}.bi-stop-circle-fill::before{content:"\f590"}.bi-stop-circle::before{content:"\f591"}.bi-stop-fill::before{content:"\f592"}.bi-stop::before{content:"\f593"}.bi-stoplights-fill::before{content:"\f594"}.bi-stoplights::before{content:"\f595"}.bi-stopwatch-fill::before{content:"\f596"}.bi-stopwatch::before{content:"\f597"}.bi-subtract::before{content:"\f598"}.bi-suit-club-fill::before{content:"\f599"}.bi-suit-club::before{content:"\f59a"}.bi-suit-diamond-fill::before{content:"\f59b"}.bi-suit-diamond::before{content:"\f59c"}.bi-suit-heart-fill::before{content:"\f59d"}.bi-suit-heart::before{content:"\f59e"}.bi-suit-spade-fill::before{content:"\f59f"}.bi-suit-spade::before{content:"\f5a0"}.bi-sun-fill::before{content:"\f5a1"}.bi-sun::before{content:"\f5a2"}.bi-sunglasses::before{content:"\f5a3"}.bi-sunrise-fill::before{content:"\f5a4"}.bi-sunrise::before{content:"\f5a5"}.bi-sunset-fill::before{content:"\f5a6"}.bi-sunset::before{content:"\f5a7"}.bi-symmetry-horizontal::before{content:"\f5a8"}.bi-symmetry-vertical::before{content:"\f5a9"}.bi-table::before{content:"\f5aa"}.bi-tablet-fill::before{content:"\f5ab"}.bi-tablet-landscape-fill::before{content:"\f5ac"}.bi-tablet-landscape::before{content:"\f5ad"}.bi-tablet::before{content:"\f5ae"}.bi-tag-fill::before{content:"\f5af"}.bi-tag::before{content:"\f5b0"}.bi-tags-fill::before{content:"\f5b1"}.bi-tags::before{content:"\f5b2"}.bi-telegram::before{content:"\f5b3"}.bi-telephone-fill::before{content:"\f5b4"}.bi-telephone-forward-fill::before{content:"\f5b5"}.bi-telephone-forward::before{content:"\f5b6"}.bi-telephone-inbound-fill::before{content:"\f5b7"}.bi-telephone-inbound::before{content:"\f5b8"}.bi-telephone-minus-fill::before{content:"\f5b9"}.bi-telephone-minus::before{content:"\f5ba"}.bi-telephone-outbound-fill::before{content:"\f5bb"}.bi-telephone-outbound::before{content:"\f5bc"}.bi-telephone-plus-fill::before{content:"\f5bd"}.bi-telephone-plus::before{content:"\f5be"}.bi-telephone-x-fill::before{content:"\f5bf"}.bi-telephone-x::before{content:"\f5c0"}.bi-telephone::before{content:"\f5c1"}.bi-terminal-fill::before{content:"\f5c2"}.bi-terminal::before{content:"\f5c3"}.bi-text-center::before{content:"\f5c4"}.bi-text-indent-left::before{content:"\f5c5"}.bi-text-indent-right::before{content:"\f5c6"}.bi-text-left::before{content:"\f5c7"}.bi-text-paragraph::before{content:"\f5c8"}.bi-text-right::before{content:"\f5c9"}.bi-textarea-resize::before{content:"\f5ca"}.bi-textarea-t::before{content:"\f5cb"}.bi-textarea::before{content:"\f5cc"}.bi-thermometer-half::before{content:"\f5cd"}.bi-thermometer-high::before{content:"\f5ce"}.bi-thermometer-low::before{content:"\f5cf"}.bi-thermometer-snow::before{content:"\f5d0"}.bi-thermometer-sun::before{content:"\f5d1"}.bi-thermometer::before{content:"\f5d2"}.bi-three-dots-vertical::before{content:"\f5d3"}.bi-three-dots::before{content:"\f5d4"}.bi-toggle-off::before{content:"\f5d5"}.bi-toggle-on::before{content:"\f5d6"}.bi-toggle2-off::before{content:"\f5d7"}.bi-toggle2-on::before{content:"\f5d8"}.bi-toggles::before{content:"\f5d9"}.bi-toggles2::before{content:"\f5da"}.bi-tools::before{content:"\f5db"}.bi-tornado::before{content:"\f5dc"}.bi-trash-fill::before{content:"\f5dd"}.bi-trash::before{content:"\f5de"}.bi-trash2-fill::before{content:"\f5df"}.bi-trash2::before{content:"\f5e0"}.bi-tree-fill::before{content:"\f5e1"}.bi-tree::before{content:"\f5e2"}.bi-triangle-fill::before{content:"\f5e3"}.bi-triangle-half::before{content:"\f5e4"}.bi-triangle::before{content:"\f5e5"}.bi-trophy-fill::before{content:"\f5e6"}.bi-trophy::before{content:"\f5e7"}.bi-tropical-storm::before{content:"\f5e8"}.bi-truck-flatbed::before{content:"\f5e9"}.bi-truck::before{content:"\f5ea"}.bi-tsunami::before{content:"\f5eb"}.bi-tv-fill::before{content:"\f5ec"}.bi-tv::before{content:"\f5ed"}.bi-twitch::before{content:"\f5ee"}.bi-twitter::before{content:"\f5ef"}.bi-type-bold::before{content:"\f5f0"}.bi-type-h1::before{content:"\f5f1"}.bi-type-h2::before{content:"\f5f2"}.bi-type-h3::before{content:"\f5f3"}.bi-type-italic::before{content:"\f5f4"}.bi-type-strikethrough::before{content:"\f5f5"}.bi-type-underline::before{content:"\f5f6"}.bi-type::before{content:"\f5f7"}.bi-ui-checks-grid::before{content:"\f5f8"}.bi-ui-checks::before{content:"\f5f9"}.bi-ui-radios-grid::before{content:"\f5fa"}.bi-ui-radios::before{content:"\f5fb"}.bi-umbrella-fill::before{content:"\f5fc"}.bi-umbrella::before{content:"\f5fd"}.bi-union::before{content:"\f5fe"}.bi-unlock-fill::before{content:"\f5ff"}.bi-unlock::before{content:"\f600"}.bi-upc-scan::before{content:"\f601"}.bi-upc::before{content:"\f602"}.bi-upload::before{content:"\f603"}.bi-vector-pen::before{content:"\f604"}.bi-view-list::before{content:"\f605"}.bi-view-stacked::before{content:"\f606"}.bi-vinyl-fill::before{content:"\f607"}.bi-vinyl::before{content:"\f608"}.bi-voicemail::before{content:"\f609"}.bi-volume-down-fill::before{content:"\f60a"}.bi-volume-down::before{content:"\f60b"}.bi-volume-mute-fill::before{content:"\f60c"}.bi-volume-mute::before{content:"\f60d"}.bi-volume-off-fill::before{content:"\f60e"}.bi-volume-off::before{content:"\f60f"}.bi-volume-up-fill::before{content:"\f610"}.bi-volume-up::before{content:"\f611"}.bi-vr::before{content:"\f612"}.bi-wallet-fill::before{content:"\f613"}.bi-wallet::before{content:"\f614"}.bi-wallet2::before{content:"\f615"}.bi-watch::before{content:"\f616"}.bi-water::before{content:"\f617"}.bi-whatsapp::before{content:"\f618"}.bi-wifi-1::before{content:"\f619"}.bi-wifi-2::before{content:"\f61a"}.bi-wifi-off::before{content:"\f61b"}.bi-wifi::before{content:"\f61c"}.bi-wind::before{content:"\f61d"}.bi-window-dock::before{content:"\f61e"}.bi-window-sidebar::before{content:"\f61f"}.bi-window::before{content:"\f620"}.bi-wrench::before{content:"\f621"}.bi-x-circle-fill::before{content:"\f622"}.bi-x-circle::before{content:"\f623"}.bi-x-diamond-fill::before{content:"\f624"}.bi-x-diamond::before{content:"\f625"}.bi-x-octagon-fill::before{content:"\f626"}.bi-x-octagon::before{content:"\f627"}.bi-x-square-fill::before{content:"\f628"}.bi-x-square::before{content:"\f629"}.bi-x::before{content:"\f62a"}.bi-youtube::before{content:"\f62b"}.bi-zoom-in::before{content:"\f62c"}.bi-zoom-out::before{content:"\f62d"}.bi-bank::before{content:"\f62e"}.bi-bank2::before{content:"\f62f"}.bi-bell-slash-fill::before{content:"\f630"}.bi-bell-slash::before{content:"\f631"}.bi-cash-coin::before{content:"\f632"}.bi-check-lg::before{content:"\f633"}.bi-coin::before{content:"\f634"}.bi-currency-bitcoin::before{content:"\f635"}.bi-currency-dollar::before{content:"\f636"}.bi-currency-euro::before{content:"\f637"}.bi-currency-exchange::before{content:"\f638"}.bi-currency-pound::before{content:"\f639"}.bi-currency-yen::before{content:"\f63a"}.bi-dash-lg::before{content:"\f63b"}.bi-exclamation-lg::before{content:"\f63c"}.bi-file-earmark-pdf-fill::before{content:"\f63d"}.bi-file-earmark-pdf::before{content:"\f63e"}.bi-file-pdf-fill::before{content:"\f63f"}.bi-file-pdf::before{content:"\f640"}.bi-gender-ambiguous::before{content:"\f641"}.bi-gender-female::before{content:"\f642"}.bi-gender-male::before{content:"\f643"}.bi-gender-trans::before{content:"\f644"}.bi-headset-vr::before{content:"\f645"}.bi-info-lg::before{content:"\f646"}.bi-mastodon::before{content:"\f647"}.bi-messenger::before{content:"\f648"}.bi-piggy-bank-fill::before{content:"\f649"}.bi-piggy-bank::before{content:"\f64a"}.bi-pin-map-fill::before{content:"\f64b"}.bi-pin-map::before{content:"\f64c"}.bi-plus-lg::before{content:"\f64d"}.bi-question-lg::before{content:"\f64e"}.bi-recycle::before{content:"\f64f"}.bi-reddit::before{content:"\f650"}.bi-safe-fill::before{content:"\f651"}.bi-safe2-fill::before{content:"\f652"}.bi-safe2::before{content:"\f653"}.bi-sd-card-fill::before{content:"\f654"}.bi-sd-card::before{content:"\f655"}.bi-skype::before{content:"\f656"}.bi-slash-lg::before{content:"\f657"}.bi-translate::before{content:"\f658"}.bi-x-lg::before{content:"\f659"}.bi-safe::before{content:"\f65a"}.bi-apple::before{content:"\f65b"}.bi-microsoft::before{content:"\f65d"}.bi-windows::before{content:"\f65e"}.bi-behance::before{content:"\f65c"}.bi-dribbble::before{content:"\f65f"}.bi-line::before{content:"\f660"}.bi-medium::before{content:"\f661"}.bi-paypal::before{content:"\f662"}.bi-pinterest::before{content:"\f663"}.bi-signal::before{content:"\f664"}.bi-snapchat::before{content:"\f665"}.bi-spotify::before{content:"\f666"}.bi-stack-overflow::before{content:"\f667"}.bi-strava::before{content:"\f668"}.bi-wordpress::before{content:"\f669"}.bi-vimeo::before{content:"\f66a"}.bi-activity::before{content:"\f66b"}.bi-easel2-fill::before{content:"\f66c"}.bi-easel2::before{content:"\f66d"}.bi-easel3-fill::before{content:"\f66e"}.bi-easel3::before{content:"\f66f"}.bi-fan::before{content:"\f670"}.bi-fingerprint::before{content:"\f671"}.bi-graph-down-arrow::before{content:"\f672"}.bi-graph-up-arrow::before{content:"\f673"}.bi-hypnotize::before{content:"\f674"}.bi-magic::before{content:"\f675"}.bi-person-rolodex::before{content:"\f676"}.bi-person-video::before{content:"\f677"}.bi-person-video2::before{content:"\f678"}.bi-person-video3::before{content:"\f679"}.bi-person-workspace::before{content:"\f67a"}.bi-radioactive::before{content:"\f67b"}.bi-webcam-fill::before{content:"\f67c"}.bi-webcam::before{content:"\f67d"}.bi-yin-yang::before{content:"\f67e"}.bi-bandaid-fill::before{content:"\f680"}.bi-bandaid::before{content:"\f681"}.bi-bluetooth::before{content:"\f682"}.bi-body-text::before{content:"\f683"}.bi-boombox::before{content:"\f684"}.bi-boxes::before{content:"\f685"}.bi-dpad-fill::before{content:"\f686"}.bi-dpad::before{content:"\f687"}.bi-ear-fill::before{content:"\f688"}.bi-ear::before{content:"\f689"}.bi-envelope-check-fill::before{content:"\f68b"}.bi-envelope-check::before{content:"\f68c"}.bi-envelope-dash-fill::before{content:"\f68e"}.bi-envelope-dash::before{content:"\f68f"}.bi-envelope-exclamation-fill::before{content:"\f691"}.bi-envelope-exclamation::before{content:"\f692"}.bi-envelope-plus-fill::before{content:"\f693"}.bi-envelope-plus::before{content:"\f694"}.bi-envelope-slash-fill::before{content:"\f696"}.bi-envelope-slash::before{content:"\f697"}.bi-envelope-x-fill::before{content:"\f699"}.bi-envelope-x::before{content:"\f69a"}.bi-explicit-fill::before{content:"\f69b"}.bi-explicit::before{content:"\f69c"}.bi-git::before{content:"\f69d"}.bi-infinity::before{content:"\f69e"}.bi-list-columns-reverse::before{content:"\f69f"}.bi-list-columns::before{content:"\f6a0"}.bi-meta::before{content:"\f6a1"}.bi-nintendo-switch::before{content:"\f6a4"}.bi-pc-display-horizontal::before{content:"\f6a5"}.bi-pc-display::before{content:"\f6a6"}.bi-pc-horizontal::before{content:"\f6a7"}.bi-pc::before{content:"\f6a8"}.bi-playstation::before{content:"\f6a9"}.bi-plus-slash-minus::before{content:"\f6aa"}.bi-projector-fill::before{content:"\f6ab"}.bi-projector::before{content:"\f6ac"}.bi-qr-code-scan::before{content:"\f6ad"}.bi-qr-code::before{content:"\f6ae"}.bi-quora::before{content:"\f6af"}.bi-quote::before{content:"\f6b0"}.bi-robot::before{content:"\f6b1"}.bi-send-check-fill::before{content:"\f6b2"}.bi-send-check::before{content:"\f6b3"}.bi-send-dash-fill::before{content:"\f6b4"}.bi-send-dash::before{content:"\f6b5"}.bi-send-exclamation-fill::before{content:"\f6b7"}.bi-send-exclamation::before{content:"\f6b8"}.bi-send-fill::before{content:"\f6b9"}.bi-send-plus-fill::before{content:"\f6ba"}.bi-send-plus::before{content:"\f6bb"}.bi-send-slash-fill::before{content:"\f6bc"}.bi-send-slash::before{content:"\f6bd"}.bi-send-x-fill::before{content:"\f6be"}.bi-send-x::before{content:"\f6bf"}.bi-send::before{content:"\f6c0"}.bi-steam::before{content:"\f6c1"}.bi-terminal-dash::before{content:"\f6c3"}.bi-terminal-plus::before{content:"\f6c4"}.bi-terminal-split::before{content:"\f6c5"}.bi-ticket-detailed-fill::before{content:"\f6c6"}.bi-ticket-detailed::before{content:"\f6c7"}.bi-ticket-fill::before{content:"\f6c8"}.bi-ticket-perforated-fill::before{content:"\f6c9"}.bi-ticket-perforated::before{content:"\f6ca"}.bi-ticket::before{content:"\f6cb"}.bi-tiktok::before{content:"\f6cc"}.bi-window-dash::before{content:"\f6cd"}.bi-window-desktop::before{content:"\f6ce"}.bi-window-fullscreen::before{content:"\f6cf"}.bi-window-plus::before{content:"\f6d0"}.bi-window-split::before{content:"\f6d1"}.bi-window-stack::before{content:"\f6d2"}.bi-window-x::before{content:"\f6d3"}.bi-xbox::before{content:"\f6d4"}.bi-ethernet::before{content:"\f6d5"}.bi-hdmi-fill::before{content:"\f6d6"}.bi-hdmi::before{content:"\f6d7"}.bi-usb-c-fill::before{content:"\f6d8"}.bi-usb-c::before{content:"\f6d9"}.bi-usb-fill::before{content:"\f6da"}.bi-usb-plug-fill::before{content:"\f6db"}.bi-usb-plug::before{content:"\f6dc"}.bi-usb-symbol::before{content:"\f6dd"}.bi-usb::before{content:"\f6de"}.bi-boombox-fill::before{content:"\f6df"}.bi-displayport::before{content:"\f6e1"}.bi-gpu-card::before{content:"\f6e2"}.bi-memory::before{content:"\f6e3"}.bi-modem-fill::before{content:"\f6e4"}.bi-modem::before{content:"\f6e5"}.bi-motherboard-fill::before{content:"\f6e6"}.bi-motherboard::before{content:"\f6e7"}.bi-optical-audio-fill::before{content:"\f6e8"}.bi-optical-audio::before{content:"\f6e9"}.bi-pci-card::before{content:"\f6ea"}.bi-router-fill::before{content:"\f6eb"}.bi-router::before{content:"\f6ec"}.bi-thunderbolt-fill::before{content:"\f6ef"}.bi-thunderbolt::before{content:"\f6f0"}.bi-usb-drive-fill::before{content:"\f6f1"}.bi-usb-drive::before{content:"\f6f2"}.bi-usb-micro-fill::before{content:"\f6f3"}.bi-usb-micro::before{content:"\f6f4"}.bi-usb-mini-fill::before{content:"\f6f5"}.bi-usb-mini::before{content:"\f6f6"}.bi-cloud-haze2::before{content:"\f6f7"}.bi-device-hdd-fill::before{content:"\f6f8"}.bi-device-hdd::before{content:"\f6f9"}.bi-device-ssd-fill::before{content:"\f6fa"}.bi-device-ssd::before{content:"\f6fb"}.bi-displayport-fill::before{content:"\f6fc"}.bi-mortarboard-fill::before{content:"\f6fd"}.bi-mortarboard::before{content:"\f6fe"}.bi-terminal-x::before{content:"\f6ff"}.bi-arrow-through-heart-fill::before{content:"\f700"}.bi-arrow-through-heart::before{content:"\f701"}.bi-badge-sd-fill::before{content:"\f702"}.bi-badge-sd::before{content:"\f703"}.bi-bag-heart-fill::before{content:"\f704"}.bi-bag-heart::before{content:"\f705"}.bi-balloon-fill::before{content:"\f706"}.bi-balloon-heart-fill::before{content:"\f707"}.bi-balloon-heart::before{content:"\f708"}.bi-balloon::before{content:"\f709"}.bi-box2-fill::before{content:"\f70a"}.bi-box2-heart-fill::before{content:"\f70b"}.bi-box2-heart::before{content:"\f70c"}.bi-box2::before{content:"\f70d"}.bi-braces-asterisk::before{content:"\f70e"}.bi-calendar-heart-fill::before{content:"\f70f"}.bi-calendar-heart::before{content:"\f710"}.bi-calendar2-heart-fill::before{content:"\f711"}.bi-calendar2-heart::before{content:"\f712"}.bi-chat-heart-fill::before{content:"\f713"}.bi-chat-heart::before{content:"\f714"}.bi-chat-left-heart-fill::before{content:"\f715"}.bi-chat-left-heart::before{content:"\f716"}.bi-chat-right-heart-fill::before{content:"\f717"}.bi-chat-right-heart::before{content:"\f718"}.bi-chat-square-heart-fill::before{content:"\f719"}.bi-chat-square-heart::before{content:"\f71a"}.bi-clipboard-check-fill::before{content:"\f71b"}.bi-clipboard-data-fill::before{content:"\f71c"}.bi-clipboard-fill::before{content:"\f71d"}.bi-clipboard-heart-fill::before{content:"\f71e"}.bi-clipboard-heart::before{content:"\f71f"}.bi-clipboard-minus-fill::before{content:"\f720"}.bi-clipboard-plus-fill::before{content:"\f721"}.bi-clipboard-pulse::before{content:"\f722"}.bi-clipboard-x-fill::before{content:"\f723"}.bi-clipboard2-check-fill::before{content:"\f724"}.bi-clipboard2-check::before{content:"\f725"}.bi-clipboard2-data-fill::before{content:"\f726"}.bi-clipboard2-data::before{content:"\f727"}.bi-clipboard2-fill::before{content:"\f728"}.bi-clipboard2-heart-fill::before{content:"\f729"}.bi-clipboard2-heart::before{content:"\f72a"}.bi-clipboard2-minus-fill::before{content:"\f72b"}.bi-clipboard2-minus::before{content:"\f72c"}.bi-clipboard2-plus-fill::before{content:"\f72d"}.bi-clipboard2-plus::before{content:"\f72e"}.bi-clipboard2-pulse-fill::before{content:"\f72f"}.bi-clipboard2-pulse::before{content:"\f730"}.bi-clipboard2-x-fill::before{content:"\f731"}.bi-clipboard2-x::before{content:"\f732"}.bi-clipboard2::before{content:"\f733"}.bi-emoji-kiss-fill::before{content:"\f734"}.bi-emoji-kiss::before{content:"\f735"}.bi-envelope-heart-fill::before{content:"\f736"}.bi-envelope-heart::before{content:"\f737"}.bi-envelope-open-heart-fill::before{content:"\f738"}.bi-envelope-open-heart::before{content:"\f739"}.bi-envelope-paper-fill::before{content:"\f73a"}.bi-envelope-paper-heart-fill::before{content:"\f73b"}.bi-envelope-paper-heart::before{content:"\f73c"}.bi-envelope-paper::before{content:"\f73d"}.bi-filetype-aac::before{content:"\f73e"}.bi-filetype-ai::before{content:"\f73f"}.bi-filetype-bmp::before{content:"\f740"}.bi-filetype-cs::before{content:"\f741"}.bi-filetype-css::before{content:"\f742"}.bi-filetype-csv::before{content:"\f743"}.bi-filetype-doc::before{content:"\f744"}.bi-filetype-docx::before{content:"\f745"}.bi-filetype-exe::before{content:"\f746"}.bi-filetype-gif::before{content:"\f747"}.bi-filetype-heic::before{content:"\f748"}.bi-filetype-html::before{content:"\f749"}.bi-filetype-java::before{content:"\f74a"}.bi-filetype-jpg::before{content:"\f74b"}.bi-filetype-js::before{content:"\f74c"}.bi-filetype-jsx::before{content:"\f74d"}.bi-filetype-key::before{content:"\f74e"}.bi-filetype-m4p::before{content:"\f74f"}.bi-filetype-md::before{content:"\f750"}.bi-filetype-mdx::before{content:"\f751"}.bi-filetype-mov::before{content:"\f752"}.bi-filetype-mp3::before{content:"\f753"}.bi-filetype-mp4::before{content:"\f754"}.bi-filetype-otf::before{content:"\f755"}.bi-filetype-pdf::before{content:"\f756"}.bi-filetype-php::before{content:"\f757"}.bi-filetype-png::before{content:"\f758"}.bi-filetype-ppt::before{content:"\f75a"}.bi-filetype-psd::before{content:"\f75b"}.bi-filetype-py::before{content:"\f75c"}.bi-filetype-raw::before{content:"\f75d"}.bi-filetype-rb::before{content:"\f75e"}.bi-filetype-sass::before{content:"\f75f"}.bi-filetype-scss::before{content:"\f760"}.bi-filetype-sh::before{content:"\f761"}.bi-filetype-svg::before{content:"\f762"}.bi-filetype-tiff::before{content:"\f763"}.bi-filetype-tsx::before{content:"\f764"}.bi-filetype-ttf::before{content:"\f765"}.bi-filetype-txt::before{content:"\f766"}.bi-filetype-wav::before{content:"\f767"}.bi-filetype-woff::before{content:"\f768"}.bi-filetype-xls::before{content:"\f76a"}.bi-filetype-xml::before{content:"\f76b"}.bi-filetype-yml::before{content:"\f76c"}.bi-heart-arrow::before{content:"\f76d"}.bi-heart-pulse-fill::before{content:"\f76e"}.bi-heart-pulse::before{content:"\f76f"}.bi-heartbreak-fill::before{content:"\f770"}.bi-heartbreak::before{content:"\f771"}.bi-hearts::before{content:"\f772"}.bi-hospital-fill::before{content:"\f773"}.bi-hospital::before{content:"\f774"}.bi-house-heart-fill::before{content:"\f775"}.bi-house-heart::before{content:"\f776"}.bi-incognito::before{content:"\f777"}.bi-magnet-fill::before{content:"\f778"}.bi-magnet::before{content:"\f779"}.bi-person-heart::before{content:"\f77a"}.bi-person-hearts::before{content:"\f77b"}.bi-phone-flip::before{content:"\f77c"}.bi-plugin::before{content:"\f77d"}.bi-postage-fill::before{content:"\f77e"}.bi-postage-heart-fill::before{content:"\f77f"}.bi-postage-heart::before{content:"\f780"}.bi-postage::before{content:"\f781"}.bi-postcard-fill::before{content:"\f782"}.bi-postcard-heart-fill::before{content:"\f783"}.bi-postcard-heart::before{content:"\f784"}.bi-postcard::before{content:"\f785"}.bi-search-heart-fill::before{content:"\f786"}.bi-search-heart::before{content:"\f787"}.bi-sliders2-vertical::before{content:"\f788"}.bi-sliders2::before{content:"\f789"}.bi-trash3-fill::before{content:"\f78a"}.bi-trash3::before{content:"\f78b"}.bi-valentine::before{content:"\f78c"}.bi-valentine2::before{content:"\f78d"}.bi-wrench-adjustable-circle-fill::before{content:"\f78e"}.bi-wrench-adjustable-circle::before{content:"\f78f"}.bi-wrench-adjustable::before{content:"\f790"}.bi-filetype-json::before{content:"\f791"}.bi-filetype-pptx::before{content:"\f792"}.bi-filetype-xlsx::before{content:"\f793"}.bi-1-circle-fill::before{content:"\f796"}.bi-1-circle::before{content:"\f797"}.bi-1-square-fill::before{content:"\f798"}.bi-1-square::before{content:"\f799"}.bi-2-circle-fill::before{content:"\f79c"}.bi-2-circle::before{content:"\f79d"}.bi-2-square-fill::before{content:"\f79e"}.bi-2-square::before{content:"\f79f"}.bi-3-circle-fill::before{content:"\f7a2"}.bi-3-circle::before{content:"\f7a3"}.bi-3-square-fill::before{content:"\f7a4"}.bi-3-square::before{content:"\f7a5"}.bi-4-circle-fill::before{content:"\f7a8"}.bi-4-circle::before{content:"\f7a9"}.bi-4-square-fill::before{content:"\f7aa"}.bi-4-square::before{content:"\f7ab"}.bi-5-circle-fill::before{content:"\f7ae"}.bi-5-circle::before{content:"\f7af"}.bi-5-square-fill::before{content:"\f7b0"}.bi-5-square::before{content:"\f7b1"}.bi-6-circle-fill::before{content:"\f7b4"}.bi-6-circle::before{content:"\f7b5"}.bi-6-square-fill::before{content:"\f7b6"}.bi-6-square::before{content:"\f7b7"}.bi-7-circle-fill::before{content:"\f7ba"}.bi-7-circle::before{content:"\f7bb"}.bi-7-square-fill::before{content:"\f7bc"}.bi-7-square::before{content:"\f7bd"}.bi-8-circle-fill::before{content:"\f7c0"}.bi-8-circle::before{content:"\f7c1"}.bi-8-square-fill::before{content:"\f7c2"}.bi-8-square::before{content:"\f7c3"}.bi-9-circle-fill::before{content:"\f7c6"}.bi-9-circle::before{content:"\f7c7"}.bi-9-square-fill::before{content:"\f7c8"}.bi-9-square::before{content:"\f7c9"}.bi-airplane-engines-fill::before{content:"\f7ca"}.bi-airplane-engines::before{content:"\f7cb"}.bi-airplane-fill::before{content:"\f7cc"}.bi-airplane::before{content:"\f7cd"}.bi-alexa::before{content:"\f7ce"}.bi-alipay::before{content:"\f7cf"}.bi-android::before{content:"\f7d0"}.bi-android2::before{content:"\f7d1"}.bi-box-fill::before{content:"\f7d2"}.bi-box-seam-fill::before{content:"\f7d3"}.bi-browser-chrome::before{content:"\f7d4"}.bi-browser-edge::before{content:"\f7d5"}.bi-browser-firefox::before{content:"\f7d6"}.bi-browser-safari::before{content:"\f7d7"}.bi-c-circle-fill::before{content:"\f7da"}.bi-c-circle::before{content:"\f7db"}.bi-c-square-fill::before{content:"\f7dc"}.bi-c-square::before{content:"\f7dd"}.bi-capsule-pill::before{content:"\f7de"}.bi-capsule::before{content:"\f7df"}.bi-car-front-fill::before{content:"\f7e0"}.bi-car-front::before{content:"\f7e1"}.bi-cassette-fill::before{content:"\f7e2"}.bi-cassette::before{content:"\f7e3"}.bi-cc-circle-fill::before{content:"\f7e6"}.bi-cc-circle::before{content:"\f7e7"}.bi-cc-square-fill::before{content:"\f7e8"}.bi-cc-square::before{content:"\f7e9"}.bi-cup-hot-fill::before{content:"\f7ea"}.bi-cup-hot::before{content:"\f7eb"}.bi-currency-rupee::before{content:"\f7ec"}.bi-dropbox::before{content:"\f7ed"}.bi-escape::before{content:"\f7ee"}.bi-fast-forward-btn-fill::before{content:"\f7ef"}.bi-fast-forward-btn::before{content:"\f7f0"}.bi-fast-forward-circle-fill::before{content:"\f7f1"}.bi-fast-forward-circle::before{content:"\f7f2"}.bi-fast-forward-fill::before{content:"\f7f3"}.bi-fast-forward::before{content:"\f7f4"}.bi-filetype-sql::before{content:"\f7f5"}.bi-fire::before{content:"\f7f6"}.bi-google-play::before{content:"\f7f7"}.bi-h-circle-fill::before{content:"\f7fa"}.bi-h-circle::before{content:"\f7fb"}.bi-h-square-fill::before{content:"\f7fc"}.bi-h-square::before{content:"\f7fd"}.bi-indent::before{content:"\f7fe"}.bi-lungs-fill::before{content:"\f7ff"}.bi-lungs::before{content:"\f800"}.bi-microsoft-teams::before{content:"\f801"}.bi-p-circle-fill::before{content:"\f804"}.bi-p-circle::before{content:"\f805"}.bi-p-square-fill::before{content:"\f806"}.bi-p-square::before{content:"\f807"}.bi-pass-fill::before{content:"\f808"}.bi-pass::before{content:"\f809"}.bi-prescription::before{content:"\f80a"}.bi-prescription2::before{content:"\f80b"}.bi-r-circle-fill::before{content:"\f80e"}.bi-r-circle::before{content:"\f80f"}.bi-r-square-fill::before{content:"\f810"}.bi-r-square::before{content:"\f811"}.bi-repeat-1::before{content:"\f812"}.bi-repeat::before{content:"\f813"}.bi-rewind-btn-fill::before{content:"\f814"}.bi-rewind-btn::before{content:"\f815"}.bi-rewind-circle-fill::before{content:"\f816"}.bi-rewind-circle::before{content:"\f817"}.bi-rewind-fill::before{content:"\f818"}.bi-rewind::before{content:"\f819"}.bi-train-freight-front-fill::before{content:"\f81a"}.bi-train-freight-front::before{content:"\f81b"}.bi-train-front-fill::before{content:"\f81c"}.bi-train-front::before{content:"\f81d"}.bi-train-lightrail-front-fill::before{content:"\f81e"}.bi-train-lightrail-front::before{content:"\f81f"}.bi-truck-front-fill::before{content:"\f820"}.bi-truck-front::before{content:"\f821"}.bi-ubuntu::before{content:"\f822"}.bi-unindent::before{content:"\f823"}.bi-unity::before{content:"\f824"}.bi-universal-access-circle::before{content:"\f825"}.bi-universal-access::before{content:"\f826"}.bi-virus::before{content:"\f827"}.bi-virus2::before{content:"\f828"}.bi-wechat::before{content:"\f829"}.bi-yelp::before{content:"\f82a"}.bi-sign-stop-fill::before{content:"\f82b"}.bi-sign-stop-lights-fill::before{content:"\f82c"}.bi-sign-stop-lights::before{content:"\f82d"}.bi-sign-stop::before{content:"\f82e"}.bi-sign-turn-left-fill::before{content:"\f82f"}.bi-sign-turn-left::before{content:"\f830"}.bi-sign-turn-right-fill::before{content:"\f831"}.bi-sign-turn-right::before{content:"\f832"}.bi-sign-turn-slight-left-fill::before{content:"\f833"}.bi-sign-turn-slight-left::before{content:"\f834"}.bi-sign-turn-slight-right-fill::before{content:"\f835"}.bi-sign-turn-slight-right::before{content:"\f836"}.bi-sign-yield-fill::before{content:"\f837"}.bi-sign-yield::before{content:"\f838"}.bi-ev-station-fill::before{content:"\f839"}.bi-ev-station::before{content:"\f83a"}.bi-fuel-pump-diesel-fill::before{content:"\f83b"}.bi-fuel-pump-diesel::before{content:"\f83c"}.bi-fuel-pump-fill::before{content:"\f83d"}.bi-fuel-pump::before{content:"\f83e"}.bi-0-circle-fill::before{content:"\f83f"}.bi-0-circle::before{content:"\f840"}.bi-0-square-fill::before{content:"\f841"}.bi-0-square::before{content:"\f842"}.bi-rocket-fill::before{content:"\f843"}.bi-rocket-takeoff-fill::before{content:"\f844"}.bi-rocket-takeoff::before{content:"\f845"}.bi-rocket::before{content:"\f846"}.bi-stripe::before{content:"\f847"}.bi-subscript::before{content:"\f848"}.bi-superscript::before{content:"\f849"}.bi-trello::before{content:"\f84a"}.bi-envelope-at-fill::before{content:"\f84b"}.bi-envelope-at::before{content:"\f84c"}.bi-regex::before{content:"\f84d"}.bi-text-wrap::before{content:"\f84e"}.bi-sign-dead-end-fill::before{content:"\f84f"}.bi-sign-dead-end::before{content:"\f850"}.bi-sign-do-not-enter-fill::before{content:"\f851"}.bi-sign-do-not-enter::before{content:"\f852"}.bi-sign-intersection-fill::before{content:"\f853"}.bi-sign-intersection-side-fill::before{content:"\f854"}.bi-sign-intersection-side::before{content:"\f855"}.bi-sign-intersection-t-fill::before{content:"\f856"}.bi-sign-intersection-t::before{content:"\f857"}.bi-sign-intersection-y-fill::before{content:"\f858"}.bi-sign-intersection-y::before{content:"\f859"}.bi-sign-intersection::before{content:"\f85a"}.bi-sign-merge-left-fill::before{content:"\f85b"}.bi-sign-merge-left::before{content:"\f85c"}.bi-sign-merge-right-fill::before{content:"\f85d"}.bi-sign-merge-right::before{content:"\f85e"}.bi-sign-no-left-turn-fill::before{content:"\f85f"}.bi-sign-no-left-turn::before{content:"\f860"}.bi-sign-no-parking-fill::before{content:"\f861"}.bi-sign-no-parking::before{content:"\f862"}.bi-sign-no-right-turn-fill::before{content:"\f863"}.bi-sign-no-right-turn::before{content:"\f864"}.bi-sign-railroad-fill::before{content:"\f865"}.bi-sign-railroad::before{content:"\f866"}.bi-building-add::before{content:"\f867"}.bi-building-check::before{content:"\f868"}.bi-building-dash::before{content:"\f869"}.bi-building-down::before{content:"\f86a"}.bi-building-exclamation::before{content:"\f86b"}.bi-building-fill-add::before{content:"\f86c"}.bi-building-fill-check::before{content:"\f86d"}.bi-building-fill-dash::before{content:"\f86e"}.bi-building-fill-down::before{content:"\f86f"}.bi-building-fill-exclamation::before{content:"\f870"}.bi-building-fill-gear::before{content:"\f871"}.bi-building-fill-lock::before{content:"\f872"}.bi-building-fill-slash::before{content:"\f873"}.bi-building-fill-up::before{content:"\f874"}.bi-building-fill-x::before{content:"\f875"}.bi-building-fill::before{content:"\f876"}.bi-building-gear::before{content:"\f877"}.bi-building-lock::before{content:"\f878"}.bi-building-slash::before{content:"\f879"}.bi-building-up::before{content:"\f87a"}.bi-building-x::before{content:"\f87b"}.bi-buildings-fill::before{content:"\f87c"}.bi-buildings::before{content:"\f87d"}.bi-bus-front-fill::before{content:"\f87e"}.bi-bus-front::before{content:"\f87f"}.bi-ev-front-fill::before{content:"\f880"}.bi-ev-front::before{content:"\f881"}.bi-globe-americas::before{content:"\f882"}.bi-globe-asia-australia::before{content:"\f883"}.bi-globe-central-south-asia::before{content:"\f884"}.bi-globe-europe-africa::before{content:"\f885"}.bi-house-add-fill::before{content:"\f886"}.bi-house-add::before{content:"\f887"}.bi-house-check-fill::before{content:"\f888"}.bi-house-check::before{content:"\f889"}.bi-house-dash-fill::before{content:"\f88a"}.bi-house-dash::before{content:"\f88b"}.bi-house-down-fill::before{content:"\f88c"}.bi-house-down::before{content:"\f88d"}.bi-house-exclamation-fill::before{content:"\f88e"}.bi-house-exclamation::before{content:"\f88f"}.bi-house-gear-fill::before{content:"\f890"}.bi-house-gear::before{content:"\f891"}.bi-house-lock-fill::before{content:"\f892"}.bi-house-lock::before{content:"\f893"}.bi-house-slash-fill::before{content:"\f894"}.bi-house-slash::before{content:"\f895"}.bi-house-up-fill::before{content:"\f896"}.bi-house-up::before{content:"\f897"}.bi-house-x-fill::before{content:"\f898"}.bi-house-x::before{content:"\f899"}.bi-person-add::before{content:"\f89a"}.bi-person-down::before{content:"\f89b"}.bi-person-exclamation::before{content:"\f89c"}.bi-person-fill-add::before{content:"\f89d"}.bi-person-fill-check::before{content:"\f89e"}.bi-person-fill-dash::before{content:"\f89f"}.bi-person-fill-down::before{content:"\f8a0"}.bi-person-fill-exclamation::before{content:"\f8a1"}.bi-person-fill-gear::before{content:"\f8a2"}.bi-person-fill-lock::before{content:"\f8a3"}.bi-person-fill-slash::before{content:"\f8a4"}.bi-person-fill-up::before{content:"\f8a5"}.bi-person-fill-x::before{content:"\f8a6"}.bi-person-gear::before{content:"\f8a7"}.bi-person-lock::before{content:"\f8a8"}.bi-person-slash::before{content:"\f8a9"}.bi-person-up::before{content:"\f8aa"}.bi-scooter::before{content:"\f8ab"}.bi-taxi-front-fill::before{content:"\f8ac"}.bi-taxi-front::before{content:"\f8ad"}.bi-amd::before{content:"\f8ae"}.bi-database-add::before{content:"\f8af"}.bi-database-check::before{content:"\f8b0"}.bi-database-dash::before{content:"\f8b1"}.bi-database-down::before{content:"\f8b2"}.bi-database-exclamation::before{content:"\f8b3"}.bi-database-fill-add::before{content:"\f8b4"}.bi-database-fill-check::before{content:"\f8b5"}.bi-database-fill-dash::before{content:"\f8b6"}.bi-database-fill-down::before{content:"\f8b7"}.bi-database-fill-exclamation::before{content:"\f8b8"}.bi-database-fill-gear::before{content:"\f8b9"}.bi-database-fill-lock::before{content:"\f8ba"}.bi-database-fill-slash::before{content:"\f8bb"}.bi-database-fill-up::before{content:"\f8bc"}.bi-database-fill-x::before{content:"\f8bd"}.bi-database-fill::before{content:"\f8be"}.bi-database-gear::before{content:"\f8bf"}.bi-database-lock::before{content:"\f8c0"}.bi-database-slash::before{content:"\f8c1"}.bi-database-up::before{content:"\f8c2"}.bi-database-x::before{content:"\f8c3"}.bi-database::before{content:"\f8c4"}.bi-houses-fill::before{content:"\f8c5"}.bi-houses::before{content:"\f8c6"}.bi-nvidia::before{content:"\f8c7"}.bi-person-vcard-fill::before{content:"\f8c8"}.bi-person-vcard::before{content:"\f8c9"}.bi-sina-weibo::before{content:"\f8ca"}.bi-tencent-qq::before{content:"\f8cb"}.bi-wikipedia::before{content:"\f8cc"}.bi-alphabet-uppercase::before{content:"\f2a5"}.bi-alphabet::before{content:"\f68a"}.bi-amazon::before{content:"\f68d"}.bi-arrows-collapse-vertical::before{content:"\f690"}.bi-arrows-expand-vertical::before{content:"\f695"}.bi-arrows-vertical::before{content:"\f698"}.bi-arrows::before{content:"\f6a2"}.bi-ban-fill::before{content:"\f6a3"}.bi-ban::before{content:"\f6b6"}.bi-bing::before{content:"\f6c2"}.bi-cake::before{content:"\f6e0"}.bi-cake2::before{content:"\f6ed"}.bi-cookie::before{content:"\f6ee"}.bi-copy::before{content:"\f759"}.bi-crosshair::before{content:"\f769"}.bi-crosshair2::before{content:"\f794"}.bi-emoji-astonished-fill::before{content:"\f795"}.bi-emoji-astonished::before{content:"\f79a"}.bi-emoji-grimace-fill::before{content:"\f79b"}.bi-emoji-grimace::before{content:"\f7a0"}.bi-emoji-grin-fill::before{content:"\f7a1"}.bi-emoji-grin::before{content:"\f7a6"}.bi-emoji-surprise-fill::before{content:"\f7a7"}.bi-emoji-surprise::before{content:"\f7ac"}.bi-emoji-tear-fill::before{content:"\f7ad"}.bi-emoji-tear::before{content:"\f7b2"}.bi-envelope-arrow-down-fill::before{content:"\f7b3"}.bi-envelope-arrow-down::before{content:"\f7b8"}.bi-envelope-arrow-up-fill::before{content:"\f7b9"}.bi-envelope-arrow-up::before{content:"\f7be"}.bi-feather::before{content:"\f7bf"}.bi-feather2::before{content:"\f7c4"}.bi-floppy-fill::before{content:"\f7c5"}.bi-floppy::before{content:"\f7d8"}.bi-floppy2-fill::before{content:"\f7d9"}.bi-floppy2::before{content:"\f7e4"}.bi-gitlab::before{content:"\f7e5"}.bi-highlighter::before{content:"\f7f8"}.bi-marker-tip::before{content:"\f802"}.bi-nvme-fill::before{content:"\f803"}.bi-nvme::before{content:"\f80c"}.bi-opencollective::before{content:"\f80d"}.bi-pci-card-network::before{content:"\f8cd"}.bi-pci-card-sound::before{content:"\f8ce"}.bi-radar::before{content:"\f8cf"}.bi-send-arrow-down-fill::before{content:"\f8d0"}.bi-send-arrow-down::before{content:"\f8d1"}.bi-send-arrow-up-fill::before{content:"\f8d2"}.bi-send-arrow-up::before{content:"\f8d3"}.bi-sim-slash-fill::before{content:"\f8d4"}.bi-sim-slash::before{content:"\f8d5"}.bi-sourceforge::before{content:"\f8d6"}.bi-substack::before{content:"\f8d7"}.bi-threads-fill::before{content:"\f8d8"}.bi-threads::before{content:"\f8d9"}.bi-transparency::before{content:"\f8da"}.bi-twitter-x::before{content:"\f8db"}.bi-type-h4::before{content:"\f8dc"}.bi-type-h5::before{content:"\f8dd"}.bi-type-h6::before{content:"\f8de"}.bi-backpack-fill::before{content:"\f8df"}.bi-backpack::before{content:"\f8e0"}.bi-backpack2-fill::before{content:"\f8e1"}.bi-backpack2::before{content:"\f8e2"}.bi-backpack3-fill::before{content:"\f8e3"}.bi-backpack3::before{content:"\f8e4"}.bi-backpack4-fill::before{content:"\f8e5"}.bi-backpack4::before{content:"\f8e6"}.bi-brilliance::before{content:"\f8e7"}.bi-cake-fill::before{content:"\f8e8"}.bi-cake2-fill::before{content:"\f8e9"}.bi-duffle-fill::before{content:"\f8ea"}.bi-duffle::before{content:"\f8eb"}.bi-exposure::before{content:"\f8ec"}.bi-gender-neuter::before{content:"\f8ed"}.bi-highlights::before{content:"\f8ee"}.bi-luggage-fill::before{content:"\f8ef"}.bi-luggage::before{content:"\f8f0"}.bi-mailbox-flag::before{content:"\f8f1"}.bi-mailbox2-flag::before{content:"\f8f2"}.bi-noise-reduction::before{content:"\f8f3"}.bi-passport-fill::before{content:"\f8f4"}.bi-passport::before{content:"\f8f5"}.bi-person-arms-up::before{content:"\f8f6"}.bi-person-raised-hand::before{content:"\f8f7"}.bi-person-standing-dress::before{content:"\f8f8"}.bi-person-standing::before{content:"\f8f9"}.bi-person-walking::before{content:"\f8fa"}.bi-person-wheelchair::before{content:"\f8fb"}.bi-shadows::before{content:"\f8fc"}.bi-suitcase-fill::before{content:"\f8fd"}.bi-suitcase-lg-fill::before{content:"\f8fe"}.bi-suitcase-lg::before{content:"\f8ff"}.bi-suitcase::before{content:"\f900"}.bi-suitcase2-fill::before{content:"\f901"}.bi-suitcase2::before{content:"\f902"}.bi-vignette::before{content:"\f903"}
\ No newline at end of file
diff --git a/static/bootstrap/icons/bootstrap-icons.scss b/static/bootstrap/icons/bootstrap-icons.scss
deleted file mode 100644
index a34d6ba8..00000000
--- a/static/bootstrap/icons/bootstrap-icons.scss
+++ /dev/null
@@ -1,2090 +0,0 @@
-/*!
- * Bootstrap Icons v1.11.2 (https://icons.getbootstrap.com/)
- * Copyright 2019-2023 The Bootstrap Authors
- * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
- */
-
-$bootstrap-icons-font: "bootstrap-icons" !default;
-$bootstrap-icons-font-dir: "./fonts" !default;
-$bootstrap-icons-font-file: "#{$bootstrap-icons-font-dir}/#{$bootstrap-icons-font}" !default;
-$bootstrap-icons-font-hash: "24e3eb84d0bcaf83d77f904c78ac1f47" !default;
-$bootstrap-icons-font-src: url("#{$bootstrap-icons-font-file}.woff2?#{$bootstrap-icons-font-hash}") format("woff2"),
- url("#{$bootstrap-icons-font-file}.woff?#{$bootstrap-icons-font-hash}") format("woff") !default;
-
-@font-face {
- font-display: block;
- font-family: $bootstrap-icons-font;
- src: $bootstrap-icons-font-src;
-}
-
-.bi::before,
-[class^="bi-"]::before,
-[class*=" bi-"]::before {
- display: inline-block;
- font-family: $bootstrap-icons-font !important;
- font-style: normal;
- font-weight: normal !important;
- font-variant: normal;
- text-transform: none;
- line-height: 1;
- vertical-align: -.125em;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-$bootstrap-icons-map: (
- "123": "\f67f",
- "alarm-fill": "\f101",
- "alarm": "\f102",
- "align-bottom": "\f103",
- "align-center": "\f104",
- "align-end": "\f105",
- "align-middle": "\f106",
- "align-start": "\f107",
- "align-top": "\f108",
- "alt": "\f109",
- "app-indicator": "\f10a",
- "app": "\f10b",
- "archive-fill": "\f10c",
- "archive": "\f10d",
- "arrow-90deg-down": "\f10e",
- "arrow-90deg-left": "\f10f",
- "arrow-90deg-right": "\f110",
- "arrow-90deg-up": "\f111",
- "arrow-bar-down": "\f112",
- "arrow-bar-left": "\f113",
- "arrow-bar-right": "\f114",
- "arrow-bar-up": "\f115",
- "arrow-clockwise": "\f116",
- "arrow-counterclockwise": "\f117",
- "arrow-down-circle-fill": "\f118",
- "arrow-down-circle": "\f119",
- "arrow-down-left-circle-fill": "\f11a",
- "arrow-down-left-circle": "\f11b",
- "arrow-down-left-square-fill": "\f11c",
- "arrow-down-left-square": "\f11d",
- "arrow-down-left": "\f11e",
- "arrow-down-right-circle-fill": "\f11f",
- "arrow-down-right-circle": "\f120",
- "arrow-down-right-square-fill": "\f121",
- "arrow-down-right-square": "\f122",
- "arrow-down-right": "\f123",
- "arrow-down-short": "\f124",
- "arrow-down-square-fill": "\f125",
- "arrow-down-square": "\f126",
- "arrow-down-up": "\f127",
- "arrow-down": "\f128",
- "arrow-left-circle-fill": "\f129",
- "arrow-left-circle": "\f12a",
- "arrow-left-right": "\f12b",
- "arrow-left-short": "\f12c",
- "arrow-left-square-fill": "\f12d",
- "arrow-left-square": "\f12e",
- "arrow-left": "\f12f",
- "arrow-repeat": "\f130",
- "arrow-return-left": "\f131",
- "arrow-return-right": "\f132",
- "arrow-right-circle-fill": "\f133",
- "arrow-right-circle": "\f134",
- "arrow-right-short": "\f135",
- "arrow-right-square-fill": "\f136",
- "arrow-right-square": "\f137",
- "arrow-right": "\f138",
- "arrow-up-circle-fill": "\f139",
- "arrow-up-circle": "\f13a",
- "arrow-up-left-circle-fill": "\f13b",
- "arrow-up-left-circle": "\f13c",
- "arrow-up-left-square-fill": "\f13d",
- "arrow-up-left-square": "\f13e",
- "arrow-up-left": "\f13f",
- "arrow-up-right-circle-fill": "\f140",
- "arrow-up-right-circle": "\f141",
- "arrow-up-right-square-fill": "\f142",
- "arrow-up-right-square": "\f143",
- "arrow-up-right": "\f144",
- "arrow-up-short": "\f145",
- "arrow-up-square-fill": "\f146",
- "arrow-up-square": "\f147",
- "arrow-up": "\f148",
- "arrows-angle-contract": "\f149",
- "arrows-angle-expand": "\f14a",
- "arrows-collapse": "\f14b",
- "arrows-expand": "\f14c",
- "arrows-fullscreen": "\f14d",
- "arrows-move": "\f14e",
- "aspect-ratio-fill": "\f14f",
- "aspect-ratio": "\f150",
- "asterisk": "\f151",
- "at": "\f152",
- "award-fill": "\f153",
- "award": "\f154",
- "back": "\f155",
- "backspace-fill": "\f156",
- "backspace-reverse-fill": "\f157",
- "backspace-reverse": "\f158",
- "backspace": "\f159",
- "badge-3d-fill": "\f15a",
- "badge-3d": "\f15b",
- "badge-4k-fill": "\f15c",
- "badge-4k": "\f15d",
- "badge-8k-fill": "\f15e",
- "badge-8k": "\f15f",
- "badge-ad-fill": "\f160",
- "badge-ad": "\f161",
- "badge-ar-fill": "\f162",
- "badge-ar": "\f163",
- "badge-cc-fill": "\f164",
- "badge-cc": "\f165",
- "badge-hd-fill": "\f166",
- "badge-hd": "\f167",
- "badge-tm-fill": "\f168",
- "badge-tm": "\f169",
- "badge-vo-fill": "\f16a",
- "badge-vo": "\f16b",
- "badge-vr-fill": "\f16c",
- "badge-vr": "\f16d",
- "badge-wc-fill": "\f16e",
- "badge-wc": "\f16f",
- "bag-check-fill": "\f170",
- "bag-check": "\f171",
- "bag-dash-fill": "\f172",
- "bag-dash": "\f173",
- "bag-fill": "\f174",
- "bag-plus-fill": "\f175",
- "bag-plus": "\f176",
- "bag-x-fill": "\f177",
- "bag-x": "\f178",
- "bag": "\f179",
- "bar-chart-fill": "\f17a",
- "bar-chart-line-fill": "\f17b",
- "bar-chart-line": "\f17c",
- "bar-chart-steps": "\f17d",
- "bar-chart": "\f17e",
- "basket-fill": "\f17f",
- "basket": "\f180",
- "basket2-fill": "\f181",
- "basket2": "\f182",
- "basket3-fill": "\f183",
- "basket3": "\f184",
- "battery-charging": "\f185",
- "battery-full": "\f186",
- "battery-half": "\f187",
- "battery": "\f188",
- "bell-fill": "\f189",
- "bell": "\f18a",
- "bezier": "\f18b",
- "bezier2": "\f18c",
- "bicycle": "\f18d",
- "binoculars-fill": "\f18e",
- "binoculars": "\f18f",
- "blockquote-left": "\f190",
- "blockquote-right": "\f191",
- "book-fill": "\f192",
- "book-half": "\f193",
- "book": "\f194",
- "bookmark-check-fill": "\f195",
- "bookmark-check": "\f196",
- "bookmark-dash-fill": "\f197",
- "bookmark-dash": "\f198",
- "bookmark-fill": "\f199",
- "bookmark-heart-fill": "\f19a",
- "bookmark-heart": "\f19b",
- "bookmark-plus-fill": "\f19c",
- "bookmark-plus": "\f19d",
- "bookmark-star-fill": "\f19e",
- "bookmark-star": "\f19f",
- "bookmark-x-fill": "\f1a0",
- "bookmark-x": "\f1a1",
- "bookmark": "\f1a2",
- "bookmarks-fill": "\f1a3",
- "bookmarks": "\f1a4",
- "bookshelf": "\f1a5",
- "bootstrap-fill": "\f1a6",
- "bootstrap-reboot": "\f1a7",
- "bootstrap": "\f1a8",
- "border-all": "\f1a9",
- "border-bottom": "\f1aa",
- "border-center": "\f1ab",
- "border-inner": "\f1ac",
- "border-left": "\f1ad",
- "border-middle": "\f1ae",
- "border-outer": "\f1af",
- "border-right": "\f1b0",
- "border-style": "\f1b1",
- "border-top": "\f1b2",
- "border-width": "\f1b3",
- "border": "\f1b4",
- "bounding-box-circles": "\f1b5",
- "bounding-box": "\f1b6",
- "box-arrow-down-left": "\f1b7",
- "box-arrow-down-right": "\f1b8",
- "box-arrow-down": "\f1b9",
- "box-arrow-in-down-left": "\f1ba",
- "box-arrow-in-down-right": "\f1bb",
- "box-arrow-in-down": "\f1bc",
- "box-arrow-in-left": "\f1bd",
- "box-arrow-in-right": "\f1be",
- "box-arrow-in-up-left": "\f1bf",
- "box-arrow-in-up-right": "\f1c0",
- "box-arrow-in-up": "\f1c1",
- "box-arrow-left": "\f1c2",
- "box-arrow-right": "\f1c3",
- "box-arrow-up-left": "\f1c4",
- "box-arrow-up-right": "\f1c5",
- "box-arrow-up": "\f1c6",
- "box-seam": "\f1c7",
- "box": "\f1c8",
- "braces": "\f1c9",
- "bricks": "\f1ca",
- "briefcase-fill": "\f1cb",
- "briefcase": "\f1cc",
- "brightness-alt-high-fill": "\f1cd",
- "brightness-alt-high": "\f1ce",
- "brightness-alt-low-fill": "\f1cf",
- "brightness-alt-low": "\f1d0",
- "brightness-high-fill": "\f1d1",
- "brightness-high": "\f1d2",
- "brightness-low-fill": "\f1d3",
- "brightness-low": "\f1d4",
- "broadcast-pin": "\f1d5",
- "broadcast": "\f1d6",
- "brush-fill": "\f1d7",
- "brush": "\f1d8",
- "bucket-fill": "\f1d9",
- "bucket": "\f1da",
- "bug-fill": "\f1db",
- "bug": "\f1dc",
- "building": "\f1dd",
- "bullseye": "\f1de",
- "calculator-fill": "\f1df",
- "calculator": "\f1e0",
- "calendar-check-fill": "\f1e1",
- "calendar-check": "\f1e2",
- "calendar-date-fill": "\f1e3",
- "calendar-date": "\f1e4",
- "calendar-day-fill": "\f1e5",
- "calendar-day": "\f1e6",
- "calendar-event-fill": "\f1e7",
- "calendar-event": "\f1e8",
- "calendar-fill": "\f1e9",
- "calendar-minus-fill": "\f1ea",
- "calendar-minus": "\f1eb",
- "calendar-month-fill": "\f1ec",
- "calendar-month": "\f1ed",
- "calendar-plus-fill": "\f1ee",
- "calendar-plus": "\f1ef",
- "calendar-range-fill": "\f1f0",
- "calendar-range": "\f1f1",
- "calendar-week-fill": "\f1f2",
- "calendar-week": "\f1f3",
- "calendar-x-fill": "\f1f4",
- "calendar-x": "\f1f5",
- "calendar": "\f1f6",
- "calendar2-check-fill": "\f1f7",
- "calendar2-check": "\f1f8",
- "calendar2-date-fill": "\f1f9",
- "calendar2-date": "\f1fa",
- "calendar2-day-fill": "\f1fb",
- "calendar2-day": "\f1fc",
- "calendar2-event-fill": "\f1fd",
- "calendar2-event": "\f1fe",
- "calendar2-fill": "\f1ff",
- "calendar2-minus-fill": "\f200",
- "calendar2-minus": "\f201",
- "calendar2-month-fill": "\f202",
- "calendar2-month": "\f203",
- "calendar2-plus-fill": "\f204",
- "calendar2-plus": "\f205",
- "calendar2-range-fill": "\f206",
- "calendar2-range": "\f207",
- "calendar2-week-fill": "\f208",
- "calendar2-week": "\f209",
- "calendar2-x-fill": "\f20a",
- "calendar2-x": "\f20b",
- "calendar2": "\f20c",
- "calendar3-event-fill": "\f20d",
- "calendar3-event": "\f20e",
- "calendar3-fill": "\f20f",
- "calendar3-range-fill": "\f210",
- "calendar3-range": "\f211",
- "calendar3-week-fill": "\f212",
- "calendar3-week": "\f213",
- "calendar3": "\f214",
- "calendar4-event": "\f215",
- "calendar4-range": "\f216",
- "calendar4-week": "\f217",
- "calendar4": "\f218",
- "camera-fill": "\f219",
- "camera-reels-fill": "\f21a",
- "camera-reels": "\f21b",
- "camera-video-fill": "\f21c",
- "camera-video-off-fill": "\f21d",
- "camera-video-off": "\f21e",
- "camera-video": "\f21f",
- "camera": "\f220",
- "camera2": "\f221",
- "capslock-fill": "\f222",
- "capslock": "\f223",
- "card-checklist": "\f224",
- "card-heading": "\f225",
- "card-image": "\f226",
- "card-list": "\f227",
- "card-text": "\f228",
- "caret-down-fill": "\f229",
- "caret-down-square-fill": "\f22a",
- "caret-down-square": "\f22b",
- "caret-down": "\f22c",
- "caret-left-fill": "\f22d",
- "caret-left-square-fill": "\f22e",
- "caret-left-square": "\f22f",
- "caret-left": "\f230",
- "caret-right-fill": "\f231",
- "caret-right-square-fill": "\f232",
- "caret-right-square": "\f233",
- "caret-right": "\f234",
- "caret-up-fill": "\f235",
- "caret-up-square-fill": "\f236",
- "caret-up-square": "\f237",
- "caret-up": "\f238",
- "cart-check-fill": "\f239",
- "cart-check": "\f23a",
- "cart-dash-fill": "\f23b",
- "cart-dash": "\f23c",
- "cart-fill": "\f23d",
- "cart-plus-fill": "\f23e",
- "cart-plus": "\f23f",
- "cart-x-fill": "\f240",
- "cart-x": "\f241",
- "cart": "\f242",
- "cart2": "\f243",
- "cart3": "\f244",
- "cart4": "\f245",
- "cash-stack": "\f246",
- "cash": "\f247",
- "cast": "\f248",
- "chat-dots-fill": "\f249",
- "chat-dots": "\f24a",
- "chat-fill": "\f24b",
- "chat-left-dots-fill": "\f24c",
- "chat-left-dots": "\f24d",
- "chat-left-fill": "\f24e",
- "chat-left-quote-fill": "\f24f",
- "chat-left-quote": "\f250",
- "chat-left-text-fill": "\f251",
- "chat-left-text": "\f252",
- "chat-left": "\f253",
- "chat-quote-fill": "\f254",
- "chat-quote": "\f255",
- "chat-right-dots-fill": "\f256",
- "chat-right-dots": "\f257",
- "chat-right-fill": "\f258",
- "chat-right-quote-fill": "\f259",
- "chat-right-quote": "\f25a",
- "chat-right-text-fill": "\f25b",
- "chat-right-text": "\f25c",
- "chat-right": "\f25d",
- "chat-square-dots-fill": "\f25e",
- "chat-square-dots": "\f25f",
- "chat-square-fill": "\f260",
- "chat-square-quote-fill": "\f261",
- "chat-square-quote": "\f262",
- "chat-square-text-fill": "\f263",
- "chat-square-text": "\f264",
- "chat-square": "\f265",
- "chat-text-fill": "\f266",
- "chat-text": "\f267",
- "chat": "\f268",
- "check-all": "\f269",
- "check-circle-fill": "\f26a",
- "check-circle": "\f26b",
- "check-square-fill": "\f26c",
- "check-square": "\f26d",
- "check": "\f26e",
- "check2-all": "\f26f",
- "check2-circle": "\f270",
- "check2-square": "\f271",
- "check2": "\f272",
- "chevron-bar-contract": "\f273",
- "chevron-bar-down": "\f274",
- "chevron-bar-expand": "\f275",
- "chevron-bar-left": "\f276",
- "chevron-bar-right": "\f277",
- "chevron-bar-up": "\f278",
- "chevron-compact-down": "\f279",
- "chevron-compact-left": "\f27a",
- "chevron-compact-right": "\f27b",
- "chevron-compact-up": "\f27c",
- "chevron-contract": "\f27d",
- "chevron-double-down": "\f27e",
- "chevron-double-left": "\f27f",
- "chevron-double-right": "\f280",
- "chevron-double-up": "\f281",
- "chevron-down": "\f282",
- "chevron-expand": "\f283",
- "chevron-left": "\f284",
- "chevron-right": "\f285",
- "chevron-up": "\f286",
- "circle-fill": "\f287",
- "circle-half": "\f288",
- "circle-square": "\f289",
- "circle": "\f28a",
- "clipboard-check": "\f28b",
- "clipboard-data": "\f28c",
- "clipboard-minus": "\f28d",
- "clipboard-plus": "\f28e",
- "clipboard-x": "\f28f",
- "clipboard": "\f290",
- "clock-fill": "\f291",
- "clock-history": "\f292",
- "clock": "\f293",
- "cloud-arrow-down-fill": "\f294",
- "cloud-arrow-down": "\f295",
- "cloud-arrow-up-fill": "\f296",
- "cloud-arrow-up": "\f297",
- "cloud-check-fill": "\f298",
- "cloud-check": "\f299",
- "cloud-download-fill": "\f29a",
- "cloud-download": "\f29b",
- "cloud-drizzle-fill": "\f29c",
- "cloud-drizzle": "\f29d",
- "cloud-fill": "\f29e",
- "cloud-fog-fill": "\f29f",
- "cloud-fog": "\f2a0",
- "cloud-fog2-fill": "\f2a1",
- "cloud-fog2": "\f2a2",
- "cloud-hail-fill": "\f2a3",
- "cloud-hail": "\f2a4",
- "cloud-haze-fill": "\f2a6",
- "cloud-haze": "\f2a7",
- "cloud-haze2-fill": "\f2a8",
- "cloud-lightning-fill": "\f2a9",
- "cloud-lightning-rain-fill": "\f2aa",
- "cloud-lightning-rain": "\f2ab",
- "cloud-lightning": "\f2ac",
- "cloud-minus-fill": "\f2ad",
- "cloud-minus": "\f2ae",
- "cloud-moon-fill": "\f2af",
- "cloud-moon": "\f2b0",
- "cloud-plus-fill": "\f2b1",
- "cloud-plus": "\f2b2",
- "cloud-rain-fill": "\f2b3",
- "cloud-rain-heavy-fill": "\f2b4",
- "cloud-rain-heavy": "\f2b5",
- "cloud-rain": "\f2b6",
- "cloud-slash-fill": "\f2b7",
- "cloud-slash": "\f2b8",
- "cloud-sleet-fill": "\f2b9",
- "cloud-sleet": "\f2ba",
- "cloud-snow-fill": "\f2bb",
- "cloud-snow": "\f2bc",
- "cloud-sun-fill": "\f2bd",
- "cloud-sun": "\f2be",
- "cloud-upload-fill": "\f2bf",
- "cloud-upload": "\f2c0",
- "cloud": "\f2c1",
- "clouds-fill": "\f2c2",
- "clouds": "\f2c3",
- "cloudy-fill": "\f2c4",
- "cloudy": "\f2c5",
- "code-slash": "\f2c6",
- "code-square": "\f2c7",
- "code": "\f2c8",
- "collection-fill": "\f2c9",
- "collection-play-fill": "\f2ca",
- "collection-play": "\f2cb",
- "collection": "\f2cc",
- "columns-gap": "\f2cd",
- "columns": "\f2ce",
- "command": "\f2cf",
- "compass-fill": "\f2d0",
- "compass": "\f2d1",
- "cone-striped": "\f2d2",
- "cone": "\f2d3",
- "controller": "\f2d4",
- "cpu-fill": "\f2d5",
- "cpu": "\f2d6",
- "credit-card-2-back-fill": "\f2d7",
- "credit-card-2-back": "\f2d8",
- "credit-card-2-front-fill": "\f2d9",
- "credit-card-2-front": "\f2da",
- "credit-card-fill": "\f2db",
- "credit-card": "\f2dc",
- "crop": "\f2dd",
- "cup-fill": "\f2de",
- "cup-straw": "\f2df",
- "cup": "\f2e0",
- "cursor-fill": "\f2e1",
- "cursor-text": "\f2e2",
- "cursor": "\f2e3",
- "dash-circle-dotted": "\f2e4",
- "dash-circle-fill": "\f2e5",
- "dash-circle": "\f2e6",
- "dash-square-dotted": "\f2e7",
- "dash-square-fill": "\f2e8",
- "dash-square": "\f2e9",
- "dash": "\f2ea",
- "diagram-2-fill": "\f2eb",
- "diagram-2": "\f2ec",
- "diagram-3-fill": "\f2ed",
- "diagram-3": "\f2ee",
- "diamond-fill": "\f2ef",
- "diamond-half": "\f2f0",
- "diamond": "\f2f1",
- "dice-1-fill": "\f2f2",
- "dice-1": "\f2f3",
- "dice-2-fill": "\f2f4",
- "dice-2": "\f2f5",
- "dice-3-fill": "\f2f6",
- "dice-3": "\f2f7",
- "dice-4-fill": "\f2f8",
- "dice-4": "\f2f9",
- "dice-5-fill": "\f2fa",
- "dice-5": "\f2fb",
- "dice-6-fill": "\f2fc",
- "dice-6": "\f2fd",
- "disc-fill": "\f2fe",
- "disc": "\f2ff",
- "discord": "\f300",
- "display-fill": "\f301",
- "display": "\f302",
- "distribute-horizontal": "\f303",
- "distribute-vertical": "\f304",
- "door-closed-fill": "\f305",
- "door-closed": "\f306",
- "door-open-fill": "\f307",
- "door-open": "\f308",
- "dot": "\f309",
- "download": "\f30a",
- "droplet-fill": "\f30b",
- "droplet-half": "\f30c",
- "droplet": "\f30d",
- "earbuds": "\f30e",
- "easel-fill": "\f30f",
- "easel": "\f310",
- "egg-fill": "\f311",
- "egg-fried": "\f312",
- "egg": "\f313",
- "eject-fill": "\f314",
- "eject": "\f315",
- "emoji-angry-fill": "\f316",
- "emoji-angry": "\f317",
- "emoji-dizzy-fill": "\f318",
- "emoji-dizzy": "\f319",
- "emoji-expressionless-fill": "\f31a",
- "emoji-expressionless": "\f31b",
- "emoji-frown-fill": "\f31c",
- "emoji-frown": "\f31d",
- "emoji-heart-eyes-fill": "\f31e",
- "emoji-heart-eyes": "\f31f",
- "emoji-laughing-fill": "\f320",
- "emoji-laughing": "\f321",
- "emoji-neutral-fill": "\f322",
- "emoji-neutral": "\f323",
- "emoji-smile-fill": "\f324",
- "emoji-smile-upside-down-fill": "\f325",
- "emoji-smile-upside-down": "\f326",
- "emoji-smile": "\f327",
- "emoji-sunglasses-fill": "\f328",
- "emoji-sunglasses": "\f329",
- "emoji-wink-fill": "\f32a",
- "emoji-wink": "\f32b",
- "envelope-fill": "\f32c",
- "envelope-open-fill": "\f32d",
- "envelope-open": "\f32e",
- "envelope": "\f32f",
- "eraser-fill": "\f330",
- "eraser": "\f331",
- "exclamation-circle-fill": "\f332",
- "exclamation-circle": "\f333",
- "exclamation-diamond-fill": "\f334",
- "exclamation-diamond": "\f335",
- "exclamation-octagon-fill": "\f336",
- "exclamation-octagon": "\f337",
- "exclamation-square-fill": "\f338",
- "exclamation-square": "\f339",
- "exclamation-triangle-fill": "\f33a",
- "exclamation-triangle": "\f33b",
- "exclamation": "\f33c",
- "exclude": "\f33d",
- "eye-fill": "\f33e",
- "eye-slash-fill": "\f33f",
- "eye-slash": "\f340",
- "eye": "\f341",
- "eyedropper": "\f342",
- "eyeglasses": "\f343",
- "facebook": "\f344",
- "file-arrow-down-fill": "\f345",
- "file-arrow-down": "\f346",
- "file-arrow-up-fill": "\f347",
- "file-arrow-up": "\f348",
- "file-bar-graph-fill": "\f349",
- "file-bar-graph": "\f34a",
- "file-binary-fill": "\f34b",
- "file-binary": "\f34c",
- "file-break-fill": "\f34d",
- "file-break": "\f34e",
- "file-check-fill": "\f34f",
- "file-check": "\f350",
- "file-code-fill": "\f351",
- "file-code": "\f352",
- "file-diff-fill": "\f353",
- "file-diff": "\f354",
- "file-earmark-arrow-down-fill": "\f355",
- "file-earmark-arrow-down": "\f356",
- "file-earmark-arrow-up-fill": "\f357",
- "file-earmark-arrow-up": "\f358",
- "file-earmark-bar-graph-fill": "\f359",
- "file-earmark-bar-graph": "\f35a",
- "file-earmark-binary-fill": "\f35b",
- "file-earmark-binary": "\f35c",
- "file-earmark-break-fill": "\f35d",
- "file-earmark-break": "\f35e",
- "file-earmark-check-fill": "\f35f",
- "file-earmark-check": "\f360",
- "file-earmark-code-fill": "\f361",
- "file-earmark-code": "\f362",
- "file-earmark-diff-fill": "\f363",
- "file-earmark-diff": "\f364",
- "file-earmark-easel-fill": "\f365",
- "file-earmark-easel": "\f366",
- "file-earmark-excel-fill": "\f367",
- "file-earmark-excel": "\f368",
- "file-earmark-fill": "\f369",
- "file-earmark-font-fill": "\f36a",
- "file-earmark-font": "\f36b",
- "file-earmark-image-fill": "\f36c",
- "file-earmark-image": "\f36d",
- "file-earmark-lock-fill": "\f36e",
- "file-earmark-lock": "\f36f",
- "file-earmark-lock2-fill": "\f370",
- "file-earmark-lock2": "\f371",
- "file-earmark-medical-fill": "\f372",
- "file-earmark-medical": "\f373",
- "file-earmark-minus-fill": "\f374",
- "file-earmark-minus": "\f375",
- "file-earmark-music-fill": "\f376",
- "file-earmark-music": "\f377",
- "file-earmark-person-fill": "\f378",
- "file-earmark-person": "\f379",
- "file-earmark-play-fill": "\f37a",
- "file-earmark-play": "\f37b",
- "file-earmark-plus-fill": "\f37c",
- "file-earmark-plus": "\f37d",
- "file-earmark-post-fill": "\f37e",
- "file-earmark-post": "\f37f",
- "file-earmark-ppt-fill": "\f380",
- "file-earmark-ppt": "\f381",
- "file-earmark-richtext-fill": "\f382",
- "file-earmark-richtext": "\f383",
- "file-earmark-ruled-fill": "\f384",
- "file-earmark-ruled": "\f385",
- "file-earmark-slides-fill": "\f386",
- "file-earmark-slides": "\f387",
- "file-earmark-spreadsheet-fill": "\f388",
- "file-earmark-spreadsheet": "\f389",
- "file-earmark-text-fill": "\f38a",
- "file-earmark-text": "\f38b",
- "file-earmark-word-fill": "\f38c",
- "file-earmark-word": "\f38d",
- "file-earmark-x-fill": "\f38e",
- "file-earmark-x": "\f38f",
- "file-earmark-zip-fill": "\f390",
- "file-earmark-zip": "\f391",
- "file-earmark": "\f392",
- "file-easel-fill": "\f393",
- "file-easel": "\f394",
- "file-excel-fill": "\f395",
- "file-excel": "\f396",
- "file-fill": "\f397",
- "file-font-fill": "\f398",
- "file-font": "\f399",
- "file-image-fill": "\f39a",
- "file-image": "\f39b",
- "file-lock-fill": "\f39c",
- "file-lock": "\f39d",
- "file-lock2-fill": "\f39e",
- "file-lock2": "\f39f",
- "file-medical-fill": "\f3a0",
- "file-medical": "\f3a1",
- "file-minus-fill": "\f3a2",
- "file-minus": "\f3a3",
- "file-music-fill": "\f3a4",
- "file-music": "\f3a5",
- "file-person-fill": "\f3a6",
- "file-person": "\f3a7",
- "file-play-fill": "\f3a8",
- "file-play": "\f3a9",
- "file-plus-fill": "\f3aa",
- "file-plus": "\f3ab",
- "file-post-fill": "\f3ac",
- "file-post": "\f3ad",
- "file-ppt-fill": "\f3ae",
- "file-ppt": "\f3af",
- "file-richtext-fill": "\f3b0",
- "file-richtext": "\f3b1",
- "file-ruled-fill": "\f3b2",
- "file-ruled": "\f3b3",
- "file-slides-fill": "\f3b4",
- "file-slides": "\f3b5",
- "file-spreadsheet-fill": "\f3b6",
- "file-spreadsheet": "\f3b7",
- "file-text-fill": "\f3b8",
- "file-text": "\f3b9",
- "file-word-fill": "\f3ba",
- "file-word": "\f3bb",
- "file-x-fill": "\f3bc",
- "file-x": "\f3bd",
- "file-zip-fill": "\f3be",
- "file-zip": "\f3bf",
- "file": "\f3c0",
- "files-alt": "\f3c1",
- "files": "\f3c2",
- "film": "\f3c3",
- "filter-circle-fill": "\f3c4",
- "filter-circle": "\f3c5",
- "filter-left": "\f3c6",
- "filter-right": "\f3c7",
- "filter-square-fill": "\f3c8",
- "filter-square": "\f3c9",
- "filter": "\f3ca",
- "flag-fill": "\f3cb",
- "flag": "\f3cc",
- "flower1": "\f3cd",
- "flower2": "\f3ce",
- "flower3": "\f3cf",
- "folder-check": "\f3d0",
- "folder-fill": "\f3d1",
- "folder-minus": "\f3d2",
- "folder-plus": "\f3d3",
- "folder-symlink-fill": "\f3d4",
- "folder-symlink": "\f3d5",
- "folder-x": "\f3d6",
- "folder": "\f3d7",
- "folder2-open": "\f3d8",
- "folder2": "\f3d9",
- "fonts": "\f3da",
- "forward-fill": "\f3db",
- "forward": "\f3dc",
- "front": "\f3dd",
- "fullscreen-exit": "\f3de",
- "fullscreen": "\f3df",
- "funnel-fill": "\f3e0",
- "funnel": "\f3e1",
- "gear-fill": "\f3e2",
- "gear-wide-connected": "\f3e3",
- "gear-wide": "\f3e4",
- "gear": "\f3e5",
- "gem": "\f3e6",
- "geo-alt-fill": "\f3e7",
- "geo-alt": "\f3e8",
- "geo-fill": "\f3e9",
- "geo": "\f3ea",
- "gift-fill": "\f3eb",
- "gift": "\f3ec",
- "github": "\f3ed",
- "globe": "\f3ee",
- "globe2": "\f3ef",
- "google": "\f3f0",
- "graph-down": "\f3f1",
- "graph-up": "\f3f2",
- "grid-1x2-fill": "\f3f3",
- "grid-1x2": "\f3f4",
- "grid-3x2-gap-fill": "\f3f5",
- "grid-3x2-gap": "\f3f6",
- "grid-3x2": "\f3f7",
- "grid-3x3-gap-fill": "\f3f8",
- "grid-3x3-gap": "\f3f9",
- "grid-3x3": "\f3fa",
- "grid-fill": "\f3fb",
- "grid": "\f3fc",
- "grip-horizontal": "\f3fd",
- "grip-vertical": "\f3fe",
- "hammer": "\f3ff",
- "hand-index-fill": "\f400",
- "hand-index-thumb-fill": "\f401",
- "hand-index-thumb": "\f402",
- "hand-index": "\f403",
- "hand-thumbs-down-fill": "\f404",
- "hand-thumbs-down": "\f405",
- "hand-thumbs-up-fill": "\f406",
- "hand-thumbs-up": "\f407",
- "handbag-fill": "\f408",
- "handbag": "\f409",
- "hash": "\f40a",
- "hdd-fill": "\f40b",
- "hdd-network-fill": "\f40c",
- "hdd-network": "\f40d",
- "hdd-rack-fill": "\f40e",
- "hdd-rack": "\f40f",
- "hdd-stack-fill": "\f410",
- "hdd-stack": "\f411",
- "hdd": "\f412",
- "headphones": "\f413",
- "headset": "\f414",
- "heart-fill": "\f415",
- "heart-half": "\f416",
- "heart": "\f417",
- "heptagon-fill": "\f418",
- "heptagon-half": "\f419",
- "heptagon": "\f41a",
- "hexagon-fill": "\f41b",
- "hexagon-half": "\f41c",
- "hexagon": "\f41d",
- "hourglass-bottom": "\f41e",
- "hourglass-split": "\f41f",
- "hourglass-top": "\f420",
- "hourglass": "\f421",
- "house-door-fill": "\f422",
- "house-door": "\f423",
- "house-fill": "\f424",
- "house": "\f425",
- "hr": "\f426",
- "hurricane": "\f427",
- "image-alt": "\f428",
- "image-fill": "\f429",
- "image": "\f42a",
- "images": "\f42b",
- "inbox-fill": "\f42c",
- "inbox": "\f42d",
- "inboxes-fill": "\f42e",
- "inboxes": "\f42f",
- "info-circle-fill": "\f430",
- "info-circle": "\f431",
- "info-square-fill": "\f432",
- "info-square": "\f433",
- "info": "\f434",
- "input-cursor-text": "\f435",
- "input-cursor": "\f436",
- "instagram": "\f437",
- "intersect": "\f438",
- "journal-album": "\f439",
- "journal-arrow-down": "\f43a",
- "journal-arrow-up": "\f43b",
- "journal-bookmark-fill": "\f43c",
- "journal-bookmark": "\f43d",
- "journal-check": "\f43e",
- "journal-code": "\f43f",
- "journal-medical": "\f440",
- "journal-minus": "\f441",
- "journal-plus": "\f442",
- "journal-richtext": "\f443",
- "journal-text": "\f444",
- "journal-x": "\f445",
- "journal": "\f446",
- "journals": "\f447",
- "joystick": "\f448",
- "justify-left": "\f449",
- "justify-right": "\f44a",
- "justify": "\f44b",
- "kanban-fill": "\f44c",
- "kanban": "\f44d",
- "key-fill": "\f44e",
- "key": "\f44f",
- "keyboard-fill": "\f450",
- "keyboard": "\f451",
- "ladder": "\f452",
- "lamp-fill": "\f453",
- "lamp": "\f454",
- "laptop-fill": "\f455",
- "laptop": "\f456",
- "layer-backward": "\f457",
- "layer-forward": "\f458",
- "layers-fill": "\f459",
- "layers-half": "\f45a",
- "layers": "\f45b",
- "layout-sidebar-inset-reverse": "\f45c",
- "layout-sidebar-inset": "\f45d",
- "layout-sidebar-reverse": "\f45e",
- "layout-sidebar": "\f45f",
- "layout-split": "\f460",
- "layout-text-sidebar-reverse": "\f461",
- "layout-text-sidebar": "\f462",
- "layout-text-window-reverse": "\f463",
- "layout-text-window": "\f464",
- "layout-three-columns": "\f465",
- "layout-wtf": "\f466",
- "life-preserver": "\f467",
- "lightbulb-fill": "\f468",
- "lightbulb-off-fill": "\f469",
- "lightbulb-off": "\f46a",
- "lightbulb": "\f46b",
- "lightning-charge-fill": "\f46c",
- "lightning-charge": "\f46d",
- "lightning-fill": "\f46e",
- "lightning": "\f46f",
- "link-45deg": "\f470",
- "link": "\f471",
- "linkedin": "\f472",
- "list-check": "\f473",
- "list-nested": "\f474",
- "list-ol": "\f475",
- "list-stars": "\f476",
- "list-task": "\f477",
- "list-ul": "\f478",
- "list": "\f479",
- "lock-fill": "\f47a",
- "lock": "\f47b",
- "mailbox": "\f47c",
- "mailbox2": "\f47d",
- "map-fill": "\f47e",
- "map": "\f47f",
- "markdown-fill": "\f480",
- "markdown": "\f481",
- "mask": "\f482",
- "megaphone-fill": "\f483",
- "megaphone": "\f484",
- "menu-app-fill": "\f485",
- "menu-app": "\f486",
- "menu-button-fill": "\f487",
- "menu-button-wide-fill": "\f488",
- "menu-button-wide": "\f489",
- "menu-button": "\f48a",
- "menu-down": "\f48b",
- "menu-up": "\f48c",
- "mic-fill": "\f48d",
- "mic-mute-fill": "\f48e",
- "mic-mute": "\f48f",
- "mic": "\f490",
- "minecart-loaded": "\f491",
- "minecart": "\f492",
- "moisture": "\f493",
- "moon-fill": "\f494",
- "moon-stars-fill": "\f495",
- "moon-stars": "\f496",
- "moon": "\f497",
- "mouse-fill": "\f498",
- "mouse": "\f499",
- "mouse2-fill": "\f49a",
- "mouse2": "\f49b",
- "mouse3-fill": "\f49c",
- "mouse3": "\f49d",
- "music-note-beamed": "\f49e",
- "music-note-list": "\f49f",
- "music-note": "\f4a0",
- "music-player-fill": "\f4a1",
- "music-player": "\f4a2",
- "newspaper": "\f4a3",
- "node-minus-fill": "\f4a4",
- "node-minus": "\f4a5",
- "node-plus-fill": "\f4a6",
- "node-plus": "\f4a7",
- "nut-fill": "\f4a8",
- "nut": "\f4a9",
- "octagon-fill": "\f4aa",
- "octagon-half": "\f4ab",
- "octagon": "\f4ac",
- "option": "\f4ad",
- "outlet": "\f4ae",
- "paint-bucket": "\f4af",
- "palette-fill": "\f4b0",
- "palette": "\f4b1",
- "palette2": "\f4b2",
- "paperclip": "\f4b3",
- "paragraph": "\f4b4",
- "patch-check-fill": "\f4b5",
- "patch-check": "\f4b6",
- "patch-exclamation-fill": "\f4b7",
- "patch-exclamation": "\f4b8",
- "patch-minus-fill": "\f4b9",
- "patch-minus": "\f4ba",
- "patch-plus-fill": "\f4bb",
- "patch-plus": "\f4bc",
- "patch-question-fill": "\f4bd",
- "patch-question": "\f4be",
- "pause-btn-fill": "\f4bf",
- "pause-btn": "\f4c0",
- "pause-circle-fill": "\f4c1",
- "pause-circle": "\f4c2",
- "pause-fill": "\f4c3",
- "pause": "\f4c4",
- "peace-fill": "\f4c5",
- "peace": "\f4c6",
- "pen-fill": "\f4c7",
- "pen": "\f4c8",
- "pencil-fill": "\f4c9",
- "pencil-square": "\f4ca",
- "pencil": "\f4cb",
- "pentagon-fill": "\f4cc",
- "pentagon-half": "\f4cd",
- "pentagon": "\f4ce",
- "people-fill": "\f4cf",
- "people": "\f4d0",
- "percent": "\f4d1",
- "person-badge-fill": "\f4d2",
- "person-badge": "\f4d3",
- "person-bounding-box": "\f4d4",
- "person-check-fill": "\f4d5",
- "person-check": "\f4d6",
- "person-circle": "\f4d7",
- "person-dash-fill": "\f4d8",
- "person-dash": "\f4d9",
- "person-fill": "\f4da",
- "person-lines-fill": "\f4db",
- "person-plus-fill": "\f4dc",
- "person-plus": "\f4dd",
- "person-square": "\f4de",
- "person-x-fill": "\f4df",
- "person-x": "\f4e0",
- "person": "\f4e1",
- "phone-fill": "\f4e2",
- "phone-landscape-fill": "\f4e3",
- "phone-landscape": "\f4e4",
- "phone-vibrate-fill": "\f4e5",
- "phone-vibrate": "\f4e6",
- "phone": "\f4e7",
- "pie-chart-fill": "\f4e8",
- "pie-chart": "\f4e9",
- "pin-angle-fill": "\f4ea",
- "pin-angle": "\f4eb",
- "pin-fill": "\f4ec",
- "pin": "\f4ed",
- "pip-fill": "\f4ee",
- "pip": "\f4ef",
- "play-btn-fill": "\f4f0",
- "play-btn": "\f4f1",
- "play-circle-fill": "\f4f2",
- "play-circle": "\f4f3",
- "play-fill": "\f4f4",
- "play": "\f4f5",
- "plug-fill": "\f4f6",
- "plug": "\f4f7",
- "plus-circle-dotted": "\f4f8",
- "plus-circle-fill": "\f4f9",
- "plus-circle": "\f4fa",
- "plus-square-dotted": "\f4fb",
- "plus-square-fill": "\f4fc",
- "plus-square": "\f4fd",
- "plus": "\f4fe",
- "power": "\f4ff",
- "printer-fill": "\f500",
- "printer": "\f501",
- "puzzle-fill": "\f502",
- "puzzle": "\f503",
- "question-circle-fill": "\f504",
- "question-circle": "\f505",
- "question-diamond-fill": "\f506",
- "question-diamond": "\f507",
- "question-octagon-fill": "\f508",
- "question-octagon": "\f509",
- "question-square-fill": "\f50a",
- "question-square": "\f50b",
- "question": "\f50c",
- "rainbow": "\f50d",
- "receipt-cutoff": "\f50e",
- "receipt": "\f50f",
- "reception-0": "\f510",
- "reception-1": "\f511",
- "reception-2": "\f512",
- "reception-3": "\f513",
- "reception-4": "\f514",
- "record-btn-fill": "\f515",
- "record-btn": "\f516",
- "record-circle-fill": "\f517",
- "record-circle": "\f518",
- "record-fill": "\f519",
- "record": "\f51a",
- "record2-fill": "\f51b",
- "record2": "\f51c",
- "reply-all-fill": "\f51d",
- "reply-all": "\f51e",
- "reply-fill": "\f51f",
- "reply": "\f520",
- "rss-fill": "\f521",
- "rss": "\f522",
- "rulers": "\f523",
- "save-fill": "\f524",
- "save": "\f525",
- "save2-fill": "\f526",
- "save2": "\f527",
- "scissors": "\f528",
- "screwdriver": "\f529",
- "search": "\f52a",
- "segmented-nav": "\f52b",
- "server": "\f52c",
- "share-fill": "\f52d",
- "share": "\f52e",
- "shield-check": "\f52f",
- "shield-exclamation": "\f530",
- "shield-fill-check": "\f531",
- "shield-fill-exclamation": "\f532",
- "shield-fill-minus": "\f533",
- "shield-fill-plus": "\f534",
- "shield-fill-x": "\f535",
- "shield-fill": "\f536",
- "shield-lock-fill": "\f537",
- "shield-lock": "\f538",
- "shield-minus": "\f539",
- "shield-plus": "\f53a",
- "shield-shaded": "\f53b",
- "shield-slash-fill": "\f53c",
- "shield-slash": "\f53d",
- "shield-x": "\f53e",
- "shield": "\f53f",
- "shift-fill": "\f540",
- "shift": "\f541",
- "shop-window": "\f542",
- "shop": "\f543",
- "shuffle": "\f544",
- "signpost-2-fill": "\f545",
- "signpost-2": "\f546",
- "signpost-fill": "\f547",
- "signpost-split-fill": "\f548",
- "signpost-split": "\f549",
- "signpost": "\f54a",
- "sim-fill": "\f54b",
- "sim": "\f54c",
- "skip-backward-btn-fill": "\f54d",
- "skip-backward-btn": "\f54e",
- "skip-backward-circle-fill": "\f54f",
- "skip-backward-circle": "\f550",
- "skip-backward-fill": "\f551",
- "skip-backward": "\f552",
- "skip-end-btn-fill": "\f553",
- "skip-end-btn": "\f554",
- "skip-end-circle-fill": "\f555",
- "skip-end-circle": "\f556",
- "skip-end-fill": "\f557",
- "skip-end": "\f558",
- "skip-forward-btn-fill": "\f559",
- "skip-forward-btn": "\f55a",
- "skip-forward-circle-fill": "\f55b",
- "skip-forward-circle": "\f55c",
- "skip-forward-fill": "\f55d",
- "skip-forward": "\f55e",
- "skip-start-btn-fill": "\f55f",
- "skip-start-btn": "\f560",
- "skip-start-circle-fill": "\f561",
- "skip-start-circle": "\f562",
- "skip-start-fill": "\f563",
- "skip-start": "\f564",
- "slack": "\f565",
- "slash-circle-fill": "\f566",
- "slash-circle": "\f567",
- "slash-square-fill": "\f568",
- "slash-square": "\f569",
- "slash": "\f56a",
- "sliders": "\f56b",
- "smartwatch": "\f56c",
- "snow": "\f56d",
- "snow2": "\f56e",
- "snow3": "\f56f",
- "sort-alpha-down-alt": "\f570",
- "sort-alpha-down": "\f571",
- "sort-alpha-up-alt": "\f572",
- "sort-alpha-up": "\f573",
- "sort-down-alt": "\f574",
- "sort-down": "\f575",
- "sort-numeric-down-alt": "\f576",
- "sort-numeric-down": "\f577",
- "sort-numeric-up-alt": "\f578",
- "sort-numeric-up": "\f579",
- "sort-up-alt": "\f57a",
- "sort-up": "\f57b",
- "soundwave": "\f57c",
- "speaker-fill": "\f57d",
- "speaker": "\f57e",
- "speedometer": "\f57f",
- "speedometer2": "\f580",
- "spellcheck": "\f581",
- "square-fill": "\f582",
- "square-half": "\f583",
- "square": "\f584",
- "stack": "\f585",
- "star-fill": "\f586",
- "star-half": "\f587",
- "star": "\f588",
- "stars": "\f589",
- "stickies-fill": "\f58a",
- "stickies": "\f58b",
- "sticky-fill": "\f58c",
- "sticky": "\f58d",
- "stop-btn-fill": "\f58e",
- "stop-btn": "\f58f",
- "stop-circle-fill": "\f590",
- "stop-circle": "\f591",
- "stop-fill": "\f592",
- "stop": "\f593",
- "stoplights-fill": "\f594",
- "stoplights": "\f595",
- "stopwatch-fill": "\f596",
- "stopwatch": "\f597",
- "subtract": "\f598",
- "suit-club-fill": "\f599",
- "suit-club": "\f59a",
- "suit-diamond-fill": "\f59b",
- "suit-diamond": "\f59c",
- "suit-heart-fill": "\f59d",
- "suit-heart": "\f59e",
- "suit-spade-fill": "\f59f",
- "suit-spade": "\f5a0",
- "sun-fill": "\f5a1",
- "sun": "\f5a2",
- "sunglasses": "\f5a3",
- "sunrise-fill": "\f5a4",
- "sunrise": "\f5a5",
- "sunset-fill": "\f5a6",
- "sunset": "\f5a7",
- "symmetry-horizontal": "\f5a8",
- "symmetry-vertical": "\f5a9",
- "table": "\f5aa",
- "tablet-fill": "\f5ab",
- "tablet-landscape-fill": "\f5ac",
- "tablet-landscape": "\f5ad",
- "tablet": "\f5ae",
- "tag-fill": "\f5af",
- "tag": "\f5b0",
- "tags-fill": "\f5b1",
- "tags": "\f5b2",
- "telegram": "\f5b3",
- "telephone-fill": "\f5b4",
- "telephone-forward-fill": "\f5b5",
- "telephone-forward": "\f5b6",
- "telephone-inbound-fill": "\f5b7",
- "telephone-inbound": "\f5b8",
- "telephone-minus-fill": "\f5b9",
- "telephone-minus": "\f5ba",
- "telephone-outbound-fill": "\f5bb",
- "telephone-outbound": "\f5bc",
- "telephone-plus-fill": "\f5bd",
- "telephone-plus": "\f5be",
- "telephone-x-fill": "\f5bf",
- "telephone-x": "\f5c0",
- "telephone": "\f5c1",
- "terminal-fill": "\f5c2",
- "terminal": "\f5c3",
- "text-center": "\f5c4",
- "text-indent-left": "\f5c5",
- "text-indent-right": "\f5c6",
- "text-left": "\f5c7",
- "text-paragraph": "\f5c8",
- "text-right": "\f5c9",
- "textarea-resize": "\f5ca",
- "textarea-t": "\f5cb",
- "textarea": "\f5cc",
- "thermometer-half": "\f5cd",
- "thermometer-high": "\f5ce",
- "thermometer-low": "\f5cf",
- "thermometer-snow": "\f5d0",
- "thermometer-sun": "\f5d1",
- "thermometer": "\f5d2",
- "three-dots-vertical": "\f5d3",
- "three-dots": "\f5d4",
- "toggle-off": "\f5d5",
- "toggle-on": "\f5d6",
- "toggle2-off": "\f5d7",
- "toggle2-on": "\f5d8",
- "toggles": "\f5d9",
- "toggles2": "\f5da",
- "tools": "\f5db",
- "tornado": "\f5dc",
- "trash-fill": "\f5dd",
- "trash": "\f5de",
- "trash2-fill": "\f5df",
- "trash2": "\f5e0",
- "tree-fill": "\f5e1",
- "tree": "\f5e2",
- "triangle-fill": "\f5e3",
- "triangle-half": "\f5e4",
- "triangle": "\f5e5",
- "trophy-fill": "\f5e6",
- "trophy": "\f5e7",
- "tropical-storm": "\f5e8",
- "truck-flatbed": "\f5e9",
- "truck": "\f5ea",
- "tsunami": "\f5eb",
- "tv-fill": "\f5ec",
- "tv": "\f5ed",
- "twitch": "\f5ee",
- "twitter": "\f5ef",
- "type-bold": "\f5f0",
- "type-h1": "\f5f1",
- "type-h2": "\f5f2",
- "type-h3": "\f5f3",
- "type-italic": "\f5f4",
- "type-strikethrough": "\f5f5",
- "type-underline": "\f5f6",
- "type": "\f5f7",
- "ui-checks-grid": "\f5f8",
- "ui-checks": "\f5f9",
- "ui-radios-grid": "\f5fa",
- "ui-radios": "\f5fb",
- "umbrella-fill": "\f5fc",
- "umbrella": "\f5fd",
- "union": "\f5fe",
- "unlock-fill": "\f5ff",
- "unlock": "\f600",
- "upc-scan": "\f601",
- "upc": "\f602",
- "upload": "\f603",
- "vector-pen": "\f604",
- "view-list": "\f605",
- "view-stacked": "\f606",
- "vinyl-fill": "\f607",
- "vinyl": "\f608",
- "voicemail": "\f609",
- "volume-down-fill": "\f60a",
- "volume-down": "\f60b",
- "volume-mute-fill": "\f60c",
- "volume-mute": "\f60d",
- "volume-off-fill": "\f60e",
- "volume-off": "\f60f",
- "volume-up-fill": "\f610",
- "volume-up": "\f611",
- "vr": "\f612",
- "wallet-fill": "\f613",
- "wallet": "\f614",
- "wallet2": "\f615",
- "watch": "\f616",
- "water": "\f617",
- "whatsapp": "\f618",
- "wifi-1": "\f619",
- "wifi-2": "\f61a",
- "wifi-off": "\f61b",
- "wifi": "\f61c",
- "wind": "\f61d",
- "window-dock": "\f61e",
- "window-sidebar": "\f61f",
- "window": "\f620",
- "wrench": "\f621",
- "x-circle-fill": "\f622",
- "x-circle": "\f623",
- "x-diamond-fill": "\f624",
- "x-diamond": "\f625",
- "x-octagon-fill": "\f626",
- "x-octagon": "\f627",
- "x-square-fill": "\f628",
- "x-square": "\f629",
- "x": "\f62a",
- "youtube": "\f62b",
- "zoom-in": "\f62c",
- "zoom-out": "\f62d",
- "bank": "\f62e",
- "bank2": "\f62f",
- "bell-slash-fill": "\f630",
- "bell-slash": "\f631",
- "cash-coin": "\f632",
- "check-lg": "\f633",
- "coin": "\f634",
- "currency-bitcoin": "\f635",
- "currency-dollar": "\f636",
- "currency-euro": "\f637",
- "currency-exchange": "\f638",
- "currency-pound": "\f639",
- "currency-yen": "\f63a",
- "dash-lg": "\f63b",
- "exclamation-lg": "\f63c",
- "file-earmark-pdf-fill": "\f63d",
- "file-earmark-pdf": "\f63e",
- "file-pdf-fill": "\f63f",
- "file-pdf": "\f640",
- "gender-ambiguous": "\f641",
- "gender-female": "\f642",
- "gender-male": "\f643",
- "gender-trans": "\f644",
- "headset-vr": "\f645",
- "info-lg": "\f646",
- "mastodon": "\f647",
- "messenger": "\f648",
- "piggy-bank-fill": "\f649",
- "piggy-bank": "\f64a",
- "pin-map-fill": "\f64b",
- "pin-map": "\f64c",
- "plus-lg": "\f64d",
- "question-lg": "\f64e",
- "recycle": "\f64f",
- "reddit": "\f650",
- "safe-fill": "\f651",
- "safe2-fill": "\f652",
- "safe2": "\f653",
- "sd-card-fill": "\f654",
- "sd-card": "\f655",
- "skype": "\f656",
- "slash-lg": "\f657",
- "translate": "\f658",
- "x-lg": "\f659",
- "safe": "\f65a",
- "apple": "\f65b",
- "microsoft": "\f65d",
- "windows": "\f65e",
- "behance": "\f65c",
- "dribbble": "\f65f",
- "line": "\f660",
- "medium": "\f661",
- "paypal": "\f662",
- "pinterest": "\f663",
- "signal": "\f664",
- "snapchat": "\f665",
- "spotify": "\f666",
- "stack-overflow": "\f667",
- "strava": "\f668",
- "wordpress": "\f669",
- "vimeo": "\f66a",
- "activity": "\f66b",
- "easel2-fill": "\f66c",
- "easel2": "\f66d",
- "easel3-fill": "\f66e",
- "easel3": "\f66f",
- "fan": "\f670",
- "fingerprint": "\f671",
- "graph-down-arrow": "\f672",
- "graph-up-arrow": "\f673",
- "hypnotize": "\f674",
- "magic": "\f675",
- "person-rolodex": "\f676",
- "person-video": "\f677",
- "person-video2": "\f678",
- "person-video3": "\f679",
- "person-workspace": "\f67a",
- "radioactive": "\f67b",
- "webcam-fill": "\f67c",
- "webcam": "\f67d",
- "yin-yang": "\f67e",
- "bandaid-fill": "\f680",
- "bandaid": "\f681",
- "bluetooth": "\f682",
- "body-text": "\f683",
- "boombox": "\f684",
- "boxes": "\f685",
- "dpad-fill": "\f686",
- "dpad": "\f687",
- "ear-fill": "\f688",
- "ear": "\f689",
- "envelope-check-fill": "\f68b",
- "envelope-check": "\f68c",
- "envelope-dash-fill": "\f68e",
- "envelope-dash": "\f68f",
- "envelope-exclamation-fill": "\f691",
- "envelope-exclamation": "\f692",
- "envelope-plus-fill": "\f693",
- "envelope-plus": "\f694",
- "envelope-slash-fill": "\f696",
- "envelope-slash": "\f697",
- "envelope-x-fill": "\f699",
- "envelope-x": "\f69a",
- "explicit-fill": "\f69b",
- "explicit": "\f69c",
- "git": "\f69d",
- "infinity": "\f69e",
- "list-columns-reverse": "\f69f",
- "list-columns": "\f6a0",
- "meta": "\f6a1",
- "nintendo-switch": "\f6a4",
- "pc-display-horizontal": "\f6a5",
- "pc-display": "\f6a6",
- "pc-horizontal": "\f6a7",
- "pc": "\f6a8",
- "playstation": "\f6a9",
- "plus-slash-minus": "\f6aa",
- "projector-fill": "\f6ab",
- "projector": "\f6ac",
- "qr-code-scan": "\f6ad",
- "qr-code": "\f6ae",
- "quora": "\f6af",
- "quote": "\f6b0",
- "robot": "\f6b1",
- "send-check-fill": "\f6b2",
- "send-check": "\f6b3",
- "send-dash-fill": "\f6b4",
- "send-dash": "\f6b5",
- "send-exclamation-fill": "\f6b7",
- "send-exclamation": "\f6b8",
- "send-fill": "\f6b9",
- "send-plus-fill": "\f6ba",
- "send-plus": "\f6bb",
- "send-slash-fill": "\f6bc",
- "send-slash": "\f6bd",
- "send-x-fill": "\f6be",
- "send-x": "\f6bf",
- "send": "\f6c0",
- "steam": "\f6c1",
- "terminal-dash": "\f6c3",
- "terminal-plus": "\f6c4",
- "terminal-split": "\f6c5",
- "ticket-detailed-fill": "\f6c6",
- "ticket-detailed": "\f6c7",
- "ticket-fill": "\f6c8",
- "ticket-perforated-fill": "\f6c9",
- "ticket-perforated": "\f6ca",
- "ticket": "\f6cb",
- "tiktok": "\f6cc",
- "window-dash": "\f6cd",
- "window-desktop": "\f6ce",
- "window-fullscreen": "\f6cf",
- "window-plus": "\f6d0",
- "window-split": "\f6d1",
- "window-stack": "\f6d2",
- "window-x": "\f6d3",
- "xbox": "\f6d4",
- "ethernet": "\f6d5",
- "hdmi-fill": "\f6d6",
- "hdmi": "\f6d7",
- "usb-c-fill": "\f6d8",
- "usb-c": "\f6d9",
- "usb-fill": "\f6da",
- "usb-plug-fill": "\f6db",
- "usb-plug": "\f6dc",
- "usb-symbol": "\f6dd",
- "usb": "\f6de",
- "boombox-fill": "\f6df",
- "displayport": "\f6e1",
- "gpu-card": "\f6e2",
- "memory": "\f6e3",
- "modem-fill": "\f6e4",
- "modem": "\f6e5",
- "motherboard-fill": "\f6e6",
- "motherboard": "\f6e7",
- "optical-audio-fill": "\f6e8",
- "optical-audio": "\f6e9",
- "pci-card": "\f6ea",
- "router-fill": "\f6eb",
- "router": "\f6ec",
- "thunderbolt-fill": "\f6ef",
- "thunderbolt": "\f6f0",
- "usb-drive-fill": "\f6f1",
- "usb-drive": "\f6f2",
- "usb-micro-fill": "\f6f3",
- "usb-micro": "\f6f4",
- "usb-mini-fill": "\f6f5",
- "usb-mini": "\f6f6",
- "cloud-haze2": "\f6f7",
- "device-hdd-fill": "\f6f8",
- "device-hdd": "\f6f9",
- "device-ssd-fill": "\f6fa",
- "device-ssd": "\f6fb",
- "displayport-fill": "\f6fc",
- "mortarboard-fill": "\f6fd",
- "mortarboard": "\f6fe",
- "terminal-x": "\f6ff",
- "arrow-through-heart-fill": "\f700",
- "arrow-through-heart": "\f701",
- "badge-sd-fill": "\f702",
- "badge-sd": "\f703",
- "bag-heart-fill": "\f704",
- "bag-heart": "\f705",
- "balloon-fill": "\f706",
- "balloon-heart-fill": "\f707",
- "balloon-heart": "\f708",
- "balloon": "\f709",
- "box2-fill": "\f70a",
- "box2-heart-fill": "\f70b",
- "box2-heart": "\f70c",
- "box2": "\f70d",
- "braces-asterisk": "\f70e",
- "calendar-heart-fill": "\f70f",
- "calendar-heart": "\f710",
- "calendar2-heart-fill": "\f711",
- "calendar2-heart": "\f712",
- "chat-heart-fill": "\f713",
- "chat-heart": "\f714",
- "chat-left-heart-fill": "\f715",
- "chat-left-heart": "\f716",
- "chat-right-heart-fill": "\f717",
- "chat-right-heart": "\f718",
- "chat-square-heart-fill": "\f719",
- "chat-square-heart": "\f71a",
- "clipboard-check-fill": "\f71b",
- "clipboard-data-fill": "\f71c",
- "clipboard-fill": "\f71d",
- "clipboard-heart-fill": "\f71e",
- "clipboard-heart": "\f71f",
- "clipboard-minus-fill": "\f720",
- "clipboard-plus-fill": "\f721",
- "clipboard-pulse": "\f722",
- "clipboard-x-fill": "\f723",
- "clipboard2-check-fill": "\f724",
- "clipboard2-check": "\f725",
- "clipboard2-data-fill": "\f726",
- "clipboard2-data": "\f727",
- "clipboard2-fill": "\f728",
- "clipboard2-heart-fill": "\f729",
- "clipboard2-heart": "\f72a",
- "clipboard2-minus-fill": "\f72b",
- "clipboard2-minus": "\f72c",
- "clipboard2-plus-fill": "\f72d",
- "clipboard2-plus": "\f72e",
- "clipboard2-pulse-fill": "\f72f",
- "clipboard2-pulse": "\f730",
- "clipboard2-x-fill": "\f731",
- "clipboard2-x": "\f732",
- "clipboard2": "\f733",
- "emoji-kiss-fill": "\f734",
- "emoji-kiss": "\f735",
- "envelope-heart-fill": "\f736",
- "envelope-heart": "\f737",
- "envelope-open-heart-fill": "\f738",
- "envelope-open-heart": "\f739",
- "envelope-paper-fill": "\f73a",
- "envelope-paper-heart-fill": "\f73b",
- "envelope-paper-heart": "\f73c",
- "envelope-paper": "\f73d",
- "filetype-aac": "\f73e",
- "filetype-ai": "\f73f",
- "filetype-bmp": "\f740",
- "filetype-cs": "\f741",
- "filetype-css": "\f742",
- "filetype-csv": "\f743",
- "filetype-doc": "\f744",
- "filetype-docx": "\f745",
- "filetype-exe": "\f746",
- "filetype-gif": "\f747",
- "filetype-heic": "\f748",
- "filetype-html": "\f749",
- "filetype-java": "\f74a",
- "filetype-jpg": "\f74b",
- "filetype-js": "\f74c",
- "filetype-jsx": "\f74d",
- "filetype-key": "\f74e",
- "filetype-m4p": "\f74f",
- "filetype-md": "\f750",
- "filetype-mdx": "\f751",
- "filetype-mov": "\f752",
- "filetype-mp3": "\f753",
- "filetype-mp4": "\f754",
- "filetype-otf": "\f755",
- "filetype-pdf": "\f756",
- "filetype-php": "\f757",
- "filetype-png": "\f758",
- "filetype-ppt": "\f75a",
- "filetype-psd": "\f75b",
- "filetype-py": "\f75c",
- "filetype-raw": "\f75d",
- "filetype-rb": "\f75e",
- "filetype-sass": "\f75f",
- "filetype-scss": "\f760",
- "filetype-sh": "\f761",
- "filetype-svg": "\f762",
- "filetype-tiff": "\f763",
- "filetype-tsx": "\f764",
- "filetype-ttf": "\f765",
- "filetype-txt": "\f766",
- "filetype-wav": "\f767",
- "filetype-woff": "\f768",
- "filetype-xls": "\f76a",
- "filetype-xml": "\f76b",
- "filetype-yml": "\f76c",
- "heart-arrow": "\f76d",
- "heart-pulse-fill": "\f76e",
- "heart-pulse": "\f76f",
- "heartbreak-fill": "\f770",
- "heartbreak": "\f771",
- "hearts": "\f772",
- "hospital-fill": "\f773",
- "hospital": "\f774",
- "house-heart-fill": "\f775",
- "house-heart": "\f776",
- "incognito": "\f777",
- "magnet-fill": "\f778",
- "magnet": "\f779",
- "person-heart": "\f77a",
- "person-hearts": "\f77b",
- "phone-flip": "\f77c",
- "plugin": "\f77d",
- "postage-fill": "\f77e",
- "postage-heart-fill": "\f77f",
- "postage-heart": "\f780",
- "postage": "\f781",
- "postcard-fill": "\f782",
- "postcard-heart-fill": "\f783",
- "postcard-heart": "\f784",
- "postcard": "\f785",
- "search-heart-fill": "\f786",
- "search-heart": "\f787",
- "sliders2-vertical": "\f788",
- "sliders2": "\f789",
- "trash3-fill": "\f78a",
- "trash3": "\f78b",
- "valentine": "\f78c",
- "valentine2": "\f78d",
- "wrench-adjustable-circle-fill": "\f78e",
- "wrench-adjustable-circle": "\f78f",
- "wrench-adjustable": "\f790",
- "filetype-json": "\f791",
- "filetype-pptx": "\f792",
- "filetype-xlsx": "\f793",
- "1-circle-fill": "\f796",
- "1-circle": "\f797",
- "1-square-fill": "\f798",
- "1-square": "\f799",
- "2-circle-fill": "\f79c",
- "2-circle": "\f79d",
- "2-square-fill": "\f79e",
- "2-square": "\f79f",
- "3-circle-fill": "\f7a2",
- "3-circle": "\f7a3",
- "3-square-fill": "\f7a4",
- "3-square": "\f7a5",
- "4-circle-fill": "\f7a8",
- "4-circle": "\f7a9",
- "4-square-fill": "\f7aa",
- "4-square": "\f7ab",
- "5-circle-fill": "\f7ae",
- "5-circle": "\f7af",
- "5-square-fill": "\f7b0",
- "5-square": "\f7b1",
- "6-circle-fill": "\f7b4",
- "6-circle": "\f7b5",
- "6-square-fill": "\f7b6",
- "6-square": "\f7b7",
- "7-circle-fill": "\f7ba",
- "7-circle": "\f7bb",
- "7-square-fill": "\f7bc",
- "7-square": "\f7bd",
- "8-circle-fill": "\f7c0",
- "8-circle": "\f7c1",
- "8-square-fill": "\f7c2",
- "8-square": "\f7c3",
- "9-circle-fill": "\f7c6",
- "9-circle": "\f7c7",
- "9-square-fill": "\f7c8",
- "9-square": "\f7c9",
- "airplane-engines-fill": "\f7ca",
- "airplane-engines": "\f7cb",
- "airplane-fill": "\f7cc",
- "airplane": "\f7cd",
- "alexa": "\f7ce",
- "alipay": "\f7cf",
- "android": "\f7d0",
- "android2": "\f7d1",
- "box-fill": "\f7d2",
- "box-seam-fill": "\f7d3",
- "browser-chrome": "\f7d4",
- "browser-edge": "\f7d5",
- "browser-firefox": "\f7d6",
- "browser-safari": "\f7d7",
- "c-circle-fill": "\f7da",
- "c-circle": "\f7db",
- "c-square-fill": "\f7dc",
- "c-square": "\f7dd",
- "capsule-pill": "\f7de",
- "capsule": "\f7df",
- "car-front-fill": "\f7e0",
- "car-front": "\f7e1",
- "cassette-fill": "\f7e2",
- "cassette": "\f7e3",
- "cc-circle-fill": "\f7e6",
- "cc-circle": "\f7e7",
- "cc-square-fill": "\f7e8",
- "cc-square": "\f7e9",
- "cup-hot-fill": "\f7ea",
- "cup-hot": "\f7eb",
- "currency-rupee": "\f7ec",
- "dropbox": "\f7ed",
- "escape": "\f7ee",
- "fast-forward-btn-fill": "\f7ef",
- "fast-forward-btn": "\f7f0",
- "fast-forward-circle-fill": "\f7f1",
- "fast-forward-circle": "\f7f2",
- "fast-forward-fill": "\f7f3",
- "fast-forward": "\f7f4",
- "filetype-sql": "\f7f5",
- "fire": "\f7f6",
- "google-play": "\f7f7",
- "h-circle-fill": "\f7fa",
- "h-circle": "\f7fb",
- "h-square-fill": "\f7fc",
- "h-square": "\f7fd",
- "indent": "\f7fe",
- "lungs-fill": "\f7ff",
- "lungs": "\f800",
- "microsoft-teams": "\f801",
- "p-circle-fill": "\f804",
- "p-circle": "\f805",
- "p-square-fill": "\f806",
- "p-square": "\f807",
- "pass-fill": "\f808",
- "pass": "\f809",
- "prescription": "\f80a",
- "prescription2": "\f80b",
- "r-circle-fill": "\f80e",
- "r-circle": "\f80f",
- "r-square-fill": "\f810",
- "r-square": "\f811",
- "repeat-1": "\f812",
- "repeat": "\f813",
- "rewind-btn-fill": "\f814",
- "rewind-btn": "\f815",
- "rewind-circle-fill": "\f816",
- "rewind-circle": "\f817",
- "rewind-fill": "\f818",
- "rewind": "\f819",
- "train-freight-front-fill": "\f81a",
- "train-freight-front": "\f81b",
- "train-front-fill": "\f81c",
- "train-front": "\f81d",
- "train-lightrail-front-fill": "\f81e",
- "train-lightrail-front": "\f81f",
- "truck-front-fill": "\f820",
- "truck-front": "\f821",
- "ubuntu": "\f822",
- "unindent": "\f823",
- "unity": "\f824",
- "universal-access-circle": "\f825",
- "universal-access": "\f826",
- "virus": "\f827",
- "virus2": "\f828",
- "wechat": "\f829",
- "yelp": "\f82a",
- "sign-stop-fill": "\f82b",
- "sign-stop-lights-fill": "\f82c",
- "sign-stop-lights": "\f82d",
- "sign-stop": "\f82e",
- "sign-turn-left-fill": "\f82f",
- "sign-turn-left": "\f830",
- "sign-turn-right-fill": "\f831",
- "sign-turn-right": "\f832",
- "sign-turn-slight-left-fill": "\f833",
- "sign-turn-slight-left": "\f834",
- "sign-turn-slight-right-fill": "\f835",
- "sign-turn-slight-right": "\f836",
- "sign-yield-fill": "\f837",
- "sign-yield": "\f838",
- "ev-station-fill": "\f839",
- "ev-station": "\f83a",
- "fuel-pump-diesel-fill": "\f83b",
- "fuel-pump-diesel": "\f83c",
- "fuel-pump-fill": "\f83d",
- "fuel-pump": "\f83e",
- "0-circle-fill": "\f83f",
- "0-circle": "\f840",
- "0-square-fill": "\f841",
- "0-square": "\f842",
- "rocket-fill": "\f843",
- "rocket-takeoff-fill": "\f844",
- "rocket-takeoff": "\f845",
- "rocket": "\f846",
- "stripe": "\f847",
- "subscript": "\f848",
- "superscript": "\f849",
- "trello": "\f84a",
- "envelope-at-fill": "\f84b",
- "envelope-at": "\f84c",
- "regex": "\f84d",
- "text-wrap": "\f84e",
- "sign-dead-end-fill": "\f84f",
- "sign-dead-end": "\f850",
- "sign-do-not-enter-fill": "\f851",
- "sign-do-not-enter": "\f852",
- "sign-intersection-fill": "\f853",
- "sign-intersection-side-fill": "\f854",
- "sign-intersection-side": "\f855",
- "sign-intersection-t-fill": "\f856",
- "sign-intersection-t": "\f857",
- "sign-intersection-y-fill": "\f858",
- "sign-intersection-y": "\f859",
- "sign-intersection": "\f85a",
- "sign-merge-left-fill": "\f85b",
- "sign-merge-left": "\f85c",
- "sign-merge-right-fill": "\f85d",
- "sign-merge-right": "\f85e",
- "sign-no-left-turn-fill": "\f85f",
- "sign-no-left-turn": "\f860",
- "sign-no-parking-fill": "\f861",
- "sign-no-parking": "\f862",
- "sign-no-right-turn-fill": "\f863",
- "sign-no-right-turn": "\f864",
- "sign-railroad-fill": "\f865",
- "sign-railroad": "\f866",
- "building-add": "\f867",
- "building-check": "\f868",
- "building-dash": "\f869",
- "building-down": "\f86a",
- "building-exclamation": "\f86b",
- "building-fill-add": "\f86c",
- "building-fill-check": "\f86d",
- "building-fill-dash": "\f86e",
- "building-fill-down": "\f86f",
- "building-fill-exclamation": "\f870",
- "building-fill-gear": "\f871",
- "building-fill-lock": "\f872",
- "building-fill-slash": "\f873",
- "building-fill-up": "\f874",
- "building-fill-x": "\f875",
- "building-fill": "\f876",
- "building-gear": "\f877",
- "building-lock": "\f878",
- "building-slash": "\f879",
- "building-up": "\f87a",
- "building-x": "\f87b",
- "buildings-fill": "\f87c",
- "buildings": "\f87d",
- "bus-front-fill": "\f87e",
- "bus-front": "\f87f",
- "ev-front-fill": "\f880",
- "ev-front": "\f881",
- "globe-americas": "\f882",
- "globe-asia-australia": "\f883",
- "globe-central-south-asia": "\f884",
- "globe-europe-africa": "\f885",
- "house-add-fill": "\f886",
- "house-add": "\f887",
- "house-check-fill": "\f888",
- "house-check": "\f889",
- "house-dash-fill": "\f88a",
- "house-dash": "\f88b",
- "house-down-fill": "\f88c",
- "house-down": "\f88d",
- "house-exclamation-fill": "\f88e",
- "house-exclamation": "\f88f",
- "house-gear-fill": "\f890",
- "house-gear": "\f891",
- "house-lock-fill": "\f892",
- "house-lock": "\f893",
- "house-slash-fill": "\f894",
- "house-slash": "\f895",
- "house-up-fill": "\f896",
- "house-up": "\f897",
- "house-x-fill": "\f898",
- "house-x": "\f899",
- "person-add": "\f89a",
- "person-down": "\f89b",
- "person-exclamation": "\f89c",
- "person-fill-add": "\f89d",
- "person-fill-check": "\f89e",
- "person-fill-dash": "\f89f",
- "person-fill-down": "\f8a0",
- "person-fill-exclamation": "\f8a1",
- "person-fill-gear": "\f8a2",
- "person-fill-lock": "\f8a3",
- "person-fill-slash": "\f8a4",
- "person-fill-up": "\f8a5",
- "person-fill-x": "\f8a6",
- "person-gear": "\f8a7",
- "person-lock": "\f8a8",
- "person-slash": "\f8a9",
- "person-up": "\f8aa",
- "scooter": "\f8ab",
- "taxi-front-fill": "\f8ac",
- "taxi-front": "\f8ad",
- "amd": "\f8ae",
- "database-add": "\f8af",
- "database-check": "\f8b0",
- "database-dash": "\f8b1",
- "database-down": "\f8b2",
- "database-exclamation": "\f8b3",
- "database-fill-add": "\f8b4",
- "database-fill-check": "\f8b5",
- "database-fill-dash": "\f8b6",
- "database-fill-down": "\f8b7",
- "database-fill-exclamation": "\f8b8",
- "database-fill-gear": "\f8b9",
- "database-fill-lock": "\f8ba",
- "database-fill-slash": "\f8bb",
- "database-fill-up": "\f8bc",
- "database-fill-x": "\f8bd",
- "database-fill": "\f8be",
- "database-gear": "\f8bf",
- "database-lock": "\f8c0",
- "database-slash": "\f8c1",
- "database-up": "\f8c2",
- "database-x": "\f8c3",
- "database": "\f8c4",
- "houses-fill": "\f8c5",
- "houses": "\f8c6",
- "nvidia": "\f8c7",
- "person-vcard-fill": "\f8c8",
- "person-vcard": "\f8c9",
- "sina-weibo": "\f8ca",
- "tencent-qq": "\f8cb",
- "wikipedia": "\f8cc",
- "alphabet-uppercase": "\f2a5",
- "alphabet": "\f68a",
- "amazon": "\f68d",
- "arrows-collapse-vertical": "\f690",
- "arrows-expand-vertical": "\f695",
- "arrows-vertical": "\f698",
- "arrows": "\f6a2",
- "ban-fill": "\f6a3",
- "ban": "\f6b6",
- "bing": "\f6c2",
- "cake": "\f6e0",
- "cake2": "\f6ed",
- "cookie": "\f6ee",
- "copy": "\f759",
- "crosshair": "\f769",
- "crosshair2": "\f794",
- "emoji-astonished-fill": "\f795",
- "emoji-astonished": "\f79a",
- "emoji-grimace-fill": "\f79b",
- "emoji-grimace": "\f7a0",
- "emoji-grin-fill": "\f7a1",
- "emoji-grin": "\f7a6",
- "emoji-surprise-fill": "\f7a7",
- "emoji-surprise": "\f7ac",
- "emoji-tear-fill": "\f7ad",
- "emoji-tear": "\f7b2",
- "envelope-arrow-down-fill": "\f7b3",
- "envelope-arrow-down": "\f7b8",
- "envelope-arrow-up-fill": "\f7b9",
- "envelope-arrow-up": "\f7be",
- "feather": "\f7bf",
- "feather2": "\f7c4",
- "floppy-fill": "\f7c5",
- "floppy": "\f7d8",
- "floppy2-fill": "\f7d9",
- "floppy2": "\f7e4",
- "gitlab": "\f7e5",
- "highlighter": "\f7f8",
- "marker-tip": "\f802",
- "nvme-fill": "\f803",
- "nvme": "\f80c",
- "opencollective": "\f80d",
- "pci-card-network": "\f8cd",
- "pci-card-sound": "\f8ce",
- "radar": "\f8cf",
- "send-arrow-down-fill": "\f8d0",
- "send-arrow-down": "\f8d1",
- "send-arrow-up-fill": "\f8d2",
- "send-arrow-up": "\f8d3",
- "sim-slash-fill": "\f8d4",
- "sim-slash": "\f8d5",
- "sourceforge": "\f8d6",
- "substack": "\f8d7",
- "threads-fill": "\f8d8",
- "threads": "\f8d9",
- "transparency": "\f8da",
- "twitter-x": "\f8db",
- "type-h4": "\f8dc",
- "type-h5": "\f8dd",
- "type-h6": "\f8de",
- "backpack-fill": "\f8df",
- "backpack": "\f8e0",
- "backpack2-fill": "\f8e1",
- "backpack2": "\f8e2",
- "backpack3-fill": "\f8e3",
- "backpack3": "\f8e4",
- "backpack4-fill": "\f8e5",
- "backpack4": "\f8e6",
- "brilliance": "\f8e7",
- "cake-fill": "\f8e8",
- "cake2-fill": "\f8e9",
- "duffle-fill": "\f8ea",
- "duffle": "\f8eb",
- "exposure": "\f8ec",
- "gender-neuter": "\f8ed",
- "highlights": "\f8ee",
- "luggage-fill": "\f8ef",
- "luggage": "\f8f0",
- "mailbox-flag": "\f8f1",
- "mailbox2-flag": "\f8f2",
- "noise-reduction": "\f8f3",
- "passport-fill": "\f8f4",
- "passport": "\f8f5",
- "person-arms-up": "\f8f6",
- "person-raised-hand": "\f8f7",
- "person-standing-dress": "\f8f8",
- "person-standing": "\f8f9",
- "person-walking": "\f8fa",
- "person-wheelchair": "\f8fb",
- "shadows": "\f8fc",
- "suitcase-fill": "\f8fd",
- "suitcase-lg-fill": "\f8fe",
- "suitcase-lg": "\f8ff",
- "suitcase": "\f900",
- "suitcase2-fill": "\f901",
- "suitcase2": "\f902",
- "vignette": "\f903",
-);
-
-@each $icon, $codepoint in $bootstrap-icons-map {
- .bi-#{$icon}::before { content: $codepoint; }
-}
diff --git a/static/bootstrap/icons/fonts/bootstrap-icons.woff b/static/bootstrap/icons/fonts/bootstrap-icons.woff
deleted file mode 100644
index ae24c650..00000000
Binary files a/static/bootstrap/icons/fonts/bootstrap-icons.woff and /dev/null differ
diff --git a/static/bootstrap/icons/fonts/bootstrap-icons.woff2 b/static/bootstrap/icons/fonts/bootstrap-icons.woff2
deleted file mode 100644
index fff9d0cf..00000000
Binary files a/static/bootstrap/icons/fonts/bootstrap-icons.woff2 and /dev/null differ
diff --git a/static/bootstrap/js/bootstrap.min.js b/static/bootstrap/js/bootstrap.min.js
deleted file mode 100644
index c35e1da1..00000000
--- a/static/bootstrap/js/bootstrap.min.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/*!
- * Bootstrap v5.3.2 (https://getbootstrap.com/)
- * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
- */
-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,(function(t){"use strict";function e(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t)for(const i in t)if("default"!==i){const s=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,s.get?s:{enumerable:!0,get:()=>t[i]})}return e.default=t,Object.freeze(e)}const i=e(t),s=new Map,n={set(t,e,i){s.has(t)||s.set(t,new Map);const n=s.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>s.has(t)&&s.get(t).get(e)||null,remove(t,e){if(!s.has(t))return;const i=s.get(t);i.delete(e),0===i.size&&s.delete(t)}},o="transitionend",r=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),a=t=>{t.dispatchEvent(new Event(o))},l=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),c=t=>l(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(r(t)):null,h=t=>{if(!l(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},d=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),u=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?u(t.parentNode):null},_=()=>{},g=t=>{t.offsetHeight},f=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,m=[],p=()=>"rtl"===document.documentElement.dir,b=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,s=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=s,t.jQueryInterface)}},"loading"===document.readyState?(m.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of m)t()})),m.push(e)):e()},v=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,y=(t,e,i=!0)=>{if(!i)return void v(t);const s=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let n=!1;const r=({target:i})=>{i===e&&(n=!0,e.removeEventListener(o,r),v(t))};e.addEventListener(o,r),setTimeout((()=>{n||a(e)}),s)},w=(t,e,i,s)=>{const n=t.length;let o=t.indexOf(e);return-1===o?!i&&s?t[n-1]:t[0]:(o+=i?1:-1,s&&(o=(o+n)%n),t[Math.max(0,Math.min(o,n-1))])},A=/[^.]*(?=\..*)\.|.*/,E=/\..*/,C=/::\d+$/,T={};let k=1;const $={mouseenter:"mouseover",mouseleave:"mouseout"},S=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${k++}`||t.uidEvent||k++}function O(t){const e=L(t);return t.uidEvent=e,T[e]=T[e]||{},T[e]}function I(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function D(t,e,i){const s="string"==typeof e,n=s?i:e||i;let o=M(t);return S.has(o)||(o=t),[s,n,o]}function N(t,e,i,s,n){if("string"!=typeof e||!t)return;let[o,r,a]=D(e,i,s);if(e in $){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=O(t),c=l[a]||(l[a]={}),h=I(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=L(r,e.replace(A,"")),u=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return F(n,{delegateTarget:r}),s.oneOff&&j.off(t,n.type,e,i),i.apply(r,[n])}}(t,i,r):function(t,e){return function i(s){return F(s,{delegateTarget:t}),i.oneOff&&j.off(t,s.type,e),e.apply(t,[s])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function P(t,e,i,s,n){const o=I(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}function x(t,e,i,s){const n=e[i]||{};for(const[o,r]of Object.entries(n))o.includes(s)&&P(t,e,i,r.callable,r.delegationSelector)}function M(t){return t=t.replace(E,""),$[t]||t}const j={on(t,e,i,s){N(t,e,i,s,!1)},one(t,e,i,s){N(t,e,i,s,!0)},off(t,e,i,s){if("string"!=typeof e||!t)return;const[n,o,r]=D(e,i,s),a=r!==e,l=O(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))x(t,l,i,e.slice(1));for(const[i,s]of Object.entries(c)){const n=i.replace(C,"");a&&!e.includes(n)||P(t,l,r,s.callable,s.delegationSelector)}}else{if(!Object.keys(c).length)return;P(t,l,r,o,n?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const s=f();let n=null,o=!0,r=!0,a=!1;e!==M(e)&&s&&(n=s.Event(e,i),s(t).trigger(n),o=!n.isPropagationStopped(),r=!n.isImmediatePropagationStopped(),a=n.isDefaultPrevented());const l=F(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&n&&n.preventDefault(),l}};function F(t,e={}){for(const[i,s]of Object.entries(e))try{t[i]=s}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>s})}return t}function z(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function H(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const B={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${H(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${H(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const s of i){let i=s.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=z(t.dataset[s])}return e},getDataAttribute:(t,e)=>z(t.getAttribute(`data-bs-${H(e)}`))};class q{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=l(e)?B.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...l(e)?B.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[s,n]of Object.entries(e)){const e=t[s],o=l(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(n).test(o))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${s}" provided type "${o}" but expected type "${n}".`)}var i}}class W extends q{constructor(t,e){super(),(t=c(t))&&(this._element=t,this._config=this._getConfig(e),n.set(this._element,this.constructor.DATA_KEY,this))}dispose(){n.remove(this._element,this.constructor.DATA_KEY),j.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){y(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return n.get(c(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.2"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const R=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?r(i.trim()):null}return e},K={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let s=t.parentNode.closest(e);for(;s;)i.push(s),s=s.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!d(t)&&h(t)))},getSelectorFromElement(t){const e=R(t);return e&&K.findOne(e)?e:null},getElementFromSelector(t){const e=R(t);return e?K.findOne(e):null},getMultipleElementsFromSelector(t){const e=R(t);return e?K.find(e):[]}},V=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;j.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),d(this))return;const n=K.getElementFromSelector(this)||this.closest(`.${s}`);t.getOrCreateInstance(n)[e]()}))},Q=".bs.alert",X=`close${Q}`,Y=`closed${Q}`;class U extends W{static get NAME(){return"alert"}close(){if(j.trigger(this._element,X).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),j.trigger(this._element,Y),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=U.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}V(U,"close"),b(U);const G='[data-bs-toggle="button"]';class J extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=J.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}j.on(document,"click.bs.button.data-api",G,(t=>{t.preventDefault();const e=t.target.closest(G);J.getOrCreateInstance(e).toggle()})),b(J);const Z=".bs.swipe",tt=`touchstart${Z}`,et=`touchmove${Z}`,it=`touchend${Z}`,st=`pointerdown${Z}`,nt=`pointerup${Z}`,ot={endCallback:null,leftCallback:null,rightCallback:null},rt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class at extends q{constructor(t,e){super(),this._element=t,t&&at.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return ot}static get DefaultType(){return rt}static get NAME(){return"swipe"}dispose(){j.off(this._element,Z)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),v(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&v(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(j.on(this._element,st,(t=>this._start(t))),j.on(this._element,nt,(t=>this._end(t))),this._element.classList.add("pointer-event")):(j.on(this._element,tt,(t=>this._start(t))),j.on(this._element,et,(t=>this._move(t))),j.on(this._element,it,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const lt=".bs.carousel",ct=".data-api",ht="next",dt="prev",ut="left",_t="right",gt=`slide${lt}`,ft=`slid${lt}`,mt=`keydown${lt}`,pt=`mouseenter${lt}`,bt=`mouseleave${lt}`,vt=`dragstart${lt}`,yt=`load${lt}${ct}`,wt=`click${lt}${ct}`,At="carousel",Et="active",Ct=".active",Tt=".carousel-item",kt=Ct+Tt,$t={ArrowLeft:_t,ArrowRight:ut},St={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Lt={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class Ot extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=K.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===At&&this.cycle()}static get Default(){return St}static get DefaultType(){return Lt}static get NAME(){return"carousel"}next(){this._slide(ht)}nextWhenVisible(){!document.hidden&&h(this._element)&&this.next()}prev(){this._slide(dt)}pause(){this._isSliding&&a(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?j.one(this._element,ft,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void j.one(this._element,ft,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const s=t>i?ht:dt;this._slide(s,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&j.on(this._element,mt,(t=>this._keydown(t))),"hover"===this._config.pause&&(j.on(this._element,pt,(()=>this.pause())),j.on(this._element,bt,(()=>this._maybeEnableCycle()))),this._config.touch&&at.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of K.find(".carousel-item img",this._element))j.on(t,vt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ut)),rightCallback:()=>this._slide(this._directionToOrder(_t)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new at(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=$t[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=K.findOne(Ct,this._indicatorsElement);e.classList.remove(Et),e.removeAttribute("aria-current");const i=K.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(Et),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),s=t===ht,n=e||w(this._getItems(),i,s,this._config.wrap);if(n===i)return;const o=this._getItemIndex(n),r=e=>j.trigger(this._element,e,{relatedTarget:n,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(gt).defaultPrevented)return;if(!i||!n)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=n;const l=s?"carousel-item-start":"carousel-item-end",c=s?"carousel-item-next":"carousel-item-prev";n.classList.add(c),g(n),i.classList.add(l),n.classList.add(l),this._queueCallback((()=>{n.classList.remove(l,c),n.classList.add(Et),i.classList.remove(Et,c,l),this._isSliding=!1,r(ft)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return K.findOne(kt,this._element)}_getItems(){return K.find(Tt,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ut?dt:ht:t===ut?ht:dt}_orderToDirection(t){return p()?t===dt?ut:_t:t===dt?_t:ut}static jQueryInterface(t){return this.each((function(){const e=Ot.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}j.on(document,wt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=K.getElementFromSelector(this);if(!e||!e.classList.contains(At))return;t.preventDefault();const i=Ot.getOrCreateInstance(e),s=this.getAttribute("data-bs-slide-to");return s?(i.to(s),void i._maybeEnableCycle()):"next"===B.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),j.on(window,yt,(()=>{const t=K.find('[data-bs-ride="carousel"]');for(const e of t)Ot.getOrCreateInstance(e)})),b(Ot);const It=".bs.collapse",Dt=`show${It}`,Nt=`shown${It}`,Pt=`hide${It}`,xt=`hidden${It}`,Mt=`click${It}.data-api`,jt="show",Ft="collapse",zt="collapsing",Ht=`:scope .${Ft} .${Ft}`,Bt='[data-bs-toggle="collapse"]',qt={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Rt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=K.find(Bt);for(const t of i){const e=K.getSelectorFromElement(t),i=K.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return qt}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Rt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(j.trigger(this._element,Dt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Ft),this._element.classList.add(zt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(zt),this._element.classList.add(Ft,jt),this._element.style[e]="",j.trigger(this._element,Nt)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(j.trigger(this._element,Pt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,g(this._element),this._element.classList.add(zt),this._element.classList.remove(Ft,jt);for(const t of this._triggerArray){const e=K.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(zt),this._element.classList.add(Ft),j.trigger(this._element,xt)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(jt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=c(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Bt);for(const e of t){const t=K.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=K.find(Ht,this._config.parent);return K.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Rt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}j.on(document,Mt,Bt,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of K.getMultipleElementsFromSelector(this))Rt.getOrCreateInstance(t,{toggle:!1}).toggle()})),b(Rt);const Kt="dropdown",Vt=".bs.dropdown",Qt=".data-api",Xt="ArrowUp",Yt="ArrowDown",Ut=`hide${Vt}`,Gt=`hidden${Vt}`,Jt=`show${Vt}`,Zt=`shown${Vt}`,te=`click${Vt}${Qt}`,ee=`keydown${Vt}${Qt}`,ie=`keyup${Vt}${Qt}`,se="show",ne='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',oe=`${ne}.${se}`,re=".dropdown-menu",ae=p()?"top-end":"top-start",le=p()?"top-start":"top-end",ce=p()?"bottom-end":"bottom-start",he=p()?"bottom-start":"bottom-end",de=p()?"left-start":"right-start",ue=p()?"right-start":"left-start",_e={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},ge={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class fe extends W{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=K.next(this._element,re)[0]||K.prev(this._element,re)[0]||K.findOne(re,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return _e}static get DefaultType(){return ge}static get NAME(){return Kt}toggle(){return this._isShown()?this.hide():this.show()}show(){if(d(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!j.trigger(this._element,Jt,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))j.on(t,"mouseover",_);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(se),this._element.classList.add(se),j.trigger(this._element,Zt,t)}}hide(){if(d(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!j.trigger(this._element,Ut,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.off(t,"mouseover",_);this._popper&&this._popper.destroy(),this._menu.classList.remove(se),this._element.classList.remove(se),this._element.setAttribute("aria-expanded","false"),B.removeDataAttribute(this._menu,"popper"),j.trigger(this._element,Gt,t)}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!l(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Kt.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===i)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let t=this._element;"parent"===this._config.reference?t=this._parent:l(this._config.reference)?t=c(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const e=this._getPopperConfig();this._popper=i.createPopper(t,this._menu,e)}_isShown(){return this._menu.classList.contains(se)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return de;if(t.classList.contains("dropstart"))return ue;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?le:ae:e?he:ce}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(B.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...v(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=K.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>h(t)));i.length&&w(i,e,t===Yt,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=fe.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=K.find(oe);for(const i of e){const e=fe.getInstance(i);if(!e||!1===e._config.autoClose)continue;const s=t.composedPath(),n=s.includes(e._menu);if(s.includes(e._element)||"inside"===e._config.autoClose&&!n||"outside"===e._config.autoClose&&n)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,s=[Xt,Yt].includes(t.key);if(!s&&!i)return;if(e&&!i)return;t.preventDefault();const n=this.matches(ne)?this:K.prev(this,ne)[0]||K.next(this,ne)[0]||K.findOne(ne,t.delegateTarget.parentNode),o=fe.getOrCreateInstance(n);if(s)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),n.focus())}}j.on(document,ee,ne,fe.dataApiKeydownHandler),j.on(document,ee,re,fe.dataApiKeydownHandler),j.on(document,te,fe.clearMenus),j.on(document,ie,fe.clearMenus),j.on(document,te,ne,(function(t){t.preventDefault(),fe.getOrCreateInstance(this).toggle()})),b(fe);const me="backdrop",pe="show",be=`mousedown.bs.${me}`,ve={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},ye={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class we extends q{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return ve}static get DefaultType(){return ye}static get NAME(){return me}show(t){if(!this._config.isVisible)return void v(t);this._append();const e=this._getElement();this._config.isAnimated&&g(e),e.classList.add(pe),this._emulateAnimation((()=>{v(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(pe),this._emulateAnimation((()=>{this.dispose(),v(t)}))):v(t)}dispose(){this._isAppended&&(j.off(this._element,be),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=c(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),j.on(t,be,(()=>{v(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){y(t,this._getElement(),this._config.isAnimated)}}const Ae=".bs.focustrap",Ee=`focusin${Ae}`,Ce=`keydown.tab${Ae}`,Te="backward",ke={autofocus:!0,trapElement:null},$e={autofocus:"boolean",trapElement:"element"};class Se extends q{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return ke}static get DefaultType(){return $e}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),j.off(document,Ae),j.on(document,Ee,(t=>this._handleFocusin(t))),j.on(document,Ce,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,j.off(document,Ae))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=K.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===Te?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Te:"forward")}}const Le=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",Oe=".sticky-top",Ie="padding-right",De="margin-right";class Ne{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,Ie,(e=>e+t)),this._setElementAttributes(Le,Ie,(e=>e+t)),this._setElementAttributes(Oe,De,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,Ie),this._resetElementAttributes(Le,Ie),this._resetElementAttributes(Oe,De)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const s=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+s)return;this._saveInitialAttribute(t,e);const n=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(n))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&B.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=B.getDataAttribute(t,e);null!==i?(B.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(l(t))e(t);else for(const i of K.find(t,this._element))e(i)}}const Pe=".bs.modal",xe=`hide${Pe}`,Me=`hidePrevented${Pe}`,je=`hidden${Pe}`,Fe=`show${Pe}`,ze=`shown${Pe}`,He=`resize${Pe}`,Be=`click.dismiss${Pe}`,qe=`mousedown.dismiss${Pe}`,We=`keydown.dismiss${Pe}`,Re=`click${Pe}.data-api`,Ke="modal-open",Ve="show",Qe="modal-static",Xe={backdrop:!0,focus:!0,keyboard:!0},Ye={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Ue extends W{constructor(t,e){super(t,e),this._dialog=K.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new Ne,this._addEventListeners()}static get Default(){return Xe}static get DefaultType(){return Ye}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||j.trigger(this._element,Fe,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Ke),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(j.trigger(this._element,xe).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Ve),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){j.off(window,Pe),j.off(this._dialog,Pe),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new we({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Se({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=K.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),g(this._element),this._element.classList.add(Ve),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,j.trigger(this._element,ze,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){j.on(this._element,We,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),j.on(window,He,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),j.on(this._element,qe,(t=>{j.one(this._element,Be,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Ke),this._resetAdjustments(),this._scrollBar.reset(),j.trigger(this._element,je)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(j.trigger(this._element,Me).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Qe)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Qe),this._queueCallback((()=>{this._element.classList.remove(Qe),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Ue.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}j.on(document,Re,'[data-bs-toggle="modal"]',(function(t){const e=K.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),j.one(e,Fe,(t=>{t.defaultPrevented||j.one(e,je,(()=>{h(this)&&this.focus()}))}));const i=K.findOne(".modal.show");i&&Ue.getInstance(i).hide(),Ue.getOrCreateInstance(e).toggle(this)})),V(Ue),b(Ue);const Ge=".bs.offcanvas",Je=".data-api",Ze=`load${Ge}${Je}`,ti="show",ei="showing",ii="hiding",si=".offcanvas.show",ni=`show${Ge}`,oi=`shown${Ge}`,ri=`hide${Ge}`,ai=`hidePrevented${Ge}`,li=`hidden${Ge}`,ci=`resize${Ge}`,hi=`click${Ge}${Je}`,di=`keydown.dismiss${Ge}`,ui={backdrop:!0,keyboard:!0,scroll:!1},_i={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class gi extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return ui}static get DefaultType(){return _i}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||j.trigger(this._element,ni,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new Ne).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(ei),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(ti),this._element.classList.remove(ei),j.trigger(this._element,oi,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(j.trigger(this._element,ri).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(ii),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(ti,ii),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new Ne).reset(),j.trigger(this._element,li)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new we({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():j.trigger(this._element,ai)}:null})}_initializeFocusTrap(){return new Se({trapElement:this._element})}_addEventListeners(){j.on(this._element,di,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():j.trigger(this._element,ai))}))}static jQueryInterface(t){return this.each((function(){const e=gi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}j.on(document,hi,'[data-bs-toggle="offcanvas"]',(function(t){const e=K.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this))return;j.one(e,li,(()=>{h(this)&&this.focus()}));const i=K.findOne(si);i&&i!==e&&gi.getInstance(i).hide(),gi.getOrCreateInstance(e).toggle(this)})),j.on(window,Ze,(()=>{for(const t of K.find(si))gi.getOrCreateInstance(t).show()})),j.on(window,ci,(()=>{for(const t of K.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&gi.getOrCreateInstance(t).hide()})),V(gi),b(gi);const fi={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},mi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),pi=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,bi=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!mi.has(i)||Boolean(pi.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},vi={allowList:fi,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:""},yi={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},wi={entry:"(string|element|function|null)",selector:"(string|element)"};class Ai extends q{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return vi}static get DefaultType(){return yi}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},wi)}_setContent(t,e,i){const s=K.findOne(i,t);s&&((e=this._resolvePossibleFunction(e))?l(e)?this._putElementInTemplate(c(e),s):this._config.html?s.innerHTML=this._maybeSanitize(e):s.textContent=e:s.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const s=(new window.DOMParser).parseFromString(t,"text/html"),n=[].concat(...s.body.querySelectorAll("*"));for(const t of n){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const s=[].concat(...t.attributes),n=[].concat(e["*"]||[],e[i]||[]);for(const e of s)bi(e,n)||t.removeAttribute(e.nodeName)}return s.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return v(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Ei=new Set(["sanitize","allowList","sanitizeFn"]),Ci="fade",Ti="show",ki=".modal",$i="hide.bs.modal",Si="hover",Li="focus",Oi={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},Ii={allowList:fi,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},Di={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class Ni extends W{constructor(t,e){if(void 0===i)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Ii}static get DefaultType(){return Di}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),j.off(this._element.closest(ki),$i,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=j.trigger(this._element,this.constructor.eventName("show")),e=(u(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:s}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(s.append(i),j.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(Ti),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.on(t,"mouseover",_);this._queueCallback((()=>{j.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!j.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(Ti),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.off(t,"mouseover",_);this._activeTrigger.click=!1,this._activeTrigger[Li]=!1,this._activeTrigger[Si]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),j.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(Ci,Ti),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(Ci),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Ai({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(Ci)}_isShown(){return this.tip&&this.tip.classList.contains(Ti)}_createPopper(t){const e=v(this._config.placement,[this,t,this._element]),s=Oi[e.toUpperCase()];return i.createPopper(this._element,t,this._getPopperConfig(s))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return v(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...v(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)j.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===Si?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===Si?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");j.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?Li:Si]=!0,e._enter()})),j.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?Li:Si]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},j.on(this._element.closest(ki),$i,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=B.getDataAttributes(this._element);for(const t of Object.keys(e))Ei.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:c(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=Ni.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(Ni);const Pi={...Ni.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},xi={...Ni.DefaultType,content:"(null|string|element|function)"};class Mi extends Ni{static get Default(){return Pi}static get DefaultType(){return xi}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=Mi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(Mi);const ji=".bs.scrollspy",Fi=`activate${ji}`,zi=`click${ji}`,Hi=`load${ji}.data-api`,Bi="active",qi="[href]",Wi=".nav-link",Ri=`${Wi}, .nav-item > ${Wi}, .list-group-item`,Ki={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Vi={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Qi extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return Ki}static get DefaultType(){return Vi}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=c(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(j.off(this._config.target,zi),j.on(this._config.target,zi,qi,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,s=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:s,behavior:"smooth"});i.scrollTop=s}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},s=(this._rootElement||document.documentElement).scrollTop,n=s>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=s;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(n&&t){if(i(o),!s)return}else n||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=K.find(qi,this._config.target);for(const e of t){if(!e.hash||d(e))continue;const t=K.findOne(decodeURI(e.hash),this._element);h(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(Bi),this._activateParents(t),j.trigger(this._element,Fi,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))K.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(Bi);else for(const e of K.parents(t,".nav, .list-group"))for(const t of K.prev(e,Ri))t.classList.add(Bi)}_clearActiveClass(t){t.classList.remove(Bi);const e=K.find(`${qi}.${Bi}`,t);for(const t of e)t.classList.remove(Bi)}static jQueryInterface(t){return this.each((function(){const e=Qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(window,Hi,(()=>{for(const t of K.find('[data-bs-spy="scroll"]'))Qi.getOrCreateInstance(t)})),b(Qi);const Xi=".bs.tab",Yi=`hide${Xi}`,Ui=`hidden${Xi}`,Gi=`show${Xi}`,Ji=`shown${Xi}`,Zi=`click${Xi}`,ts=`keydown${Xi}`,es=`load${Xi}`,is="ArrowLeft",ss="ArrowRight",ns="ArrowUp",os="ArrowDown",rs="Home",as="End",ls="active",cs="fade",hs="show",ds=".dropdown-toggle",us=`:not(${ds})`,_s='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',gs=`.nav-link${us}, .list-group-item${us}, [role="tab"]${us}, ${_s}`,fs=`.${ls}[data-bs-toggle="tab"], .${ls}[data-bs-toggle="pill"], .${ls}[data-bs-toggle="list"]`;class ms extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),j.on(this._element,ts,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?j.trigger(e,Yi,{relatedTarget:t}):null;j.trigger(t,Gi,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(ls),this._activate(K.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),j.trigger(t,Ji,{relatedTarget:e})):t.classList.add(hs)}),t,t.classList.contains(cs)))}_deactivate(t,e){t&&(t.classList.remove(ls),t.blur(),this._deactivate(K.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),j.trigger(t,Ui,{relatedTarget:e})):t.classList.remove(hs)}),t,t.classList.contains(cs)))}_keydown(t){if(![is,ss,ns,os,rs,as].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!d(t)));let i;if([rs,as].includes(t.key))i=e[t.key===rs?0:e.length-1];else{const s=[ss,os].includes(t.key);i=w(e,t.target,s,!0)}i&&(i.focus({preventScroll:!0}),ms.getOrCreateInstance(i).show())}_getChildren(){return K.find(gs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=K.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const s=(t,s)=>{const n=K.findOne(t,i);n&&n.classList.toggle(s,e)};s(ds,ls),s(".dropdown-menu",hs),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(ls)}_getInnerElement(t){return t.matches(gs)?t:K.findOne(gs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=ms.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(document,Zi,_s,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this)||ms.getOrCreateInstance(this).show()})),j.on(window,es,(()=>{for(const t of K.find(fs))ms.getOrCreateInstance(t)})),b(ms);const ps=".bs.toast",bs=`mouseover${ps}`,vs=`mouseout${ps}`,ys=`focusin${ps}`,ws=`focusout${ps}`,As=`hide${ps}`,Es=`hidden${ps}`,Cs=`show${ps}`,Ts=`shown${ps}`,ks="hide",$s="show",Ss="showing",Ls={animation:"boolean",autohide:"boolean",delay:"number"},Os={animation:!0,autohide:!0,delay:5e3};class Is extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Os}static get DefaultType(){return Ls}static get NAME(){return"toast"}show(){j.trigger(this._element,Cs).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(ks),g(this._element),this._element.classList.add($s,Ss),this._queueCallback((()=>{this._element.classList.remove(Ss),j.trigger(this._element,Ts),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(j.trigger(this._element,As).defaultPrevented||(this._element.classList.add(Ss),this._queueCallback((()=>{this._element.classList.add(ks),this._element.classList.remove(Ss,$s),j.trigger(this._element,Es)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove($s),super.dispose()}isShown(){return this._element.classList.contains($s)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){j.on(this._element,bs,(t=>this._onInteraction(t,!0))),j.on(this._element,vs,(t=>this._onInteraction(t,!1))),j.on(this._element,ys,(t=>this._onInteraction(t,!0))),j.on(this._element,ws,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Is.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return V(Is),b(Is),{Alert:U,Button:J,Carousel:Ot,Collapse:Rt,Dropdown:fe,Modal:Ue,Offcanvas:gi,Popover:Mi,ScrollSpy:Qi,Tab:ms,Toast:Is,Tooltip:Ni}}));
-//# sourceMappingURL=bootstrap.min.js.map
\ No newline at end of file
diff --git a/version b/version
index 535b2f4d..a4b6ac3d 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-v2.1.3
+v2.2.0