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 + License: AGPLv3 - + + +一个主打便捷的 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