commit 218687a1ee04ef8606f7246ac1a810d83b03f5e7 Author: Owen Date: Mon Aug 17 13:34:40 2026 -0500 Initial implementation of inventree A small IT asset inventory system built around a nested folder tree, matching the project brief: Go + html/template + Bootstrap 5 + vanilla JS, SQLite storage (WAL, FKs, migrations), embedded templates/static assets, single compiled binary. - AJAX-driven inventory tree: lazy-loaded folder expansion with per-user persisted expand state, drag-and-drop move for folders/assets, safe delete-with-relocate-or-cascade confirmation, search with breadcrumbs. - Assets: CRUD, duplicate, move, auto-generated IT-000001-style IDs, CSV import (with optional folder auto-create) and export. - Admin-defined item types with custom fields (text/long_text/number/date/ boolean/dropdown/url/ip_address). - Configurable statuses, session-based auth (bcrypt, secure cookies), admin/user roles, user management. - CLI: serve, version, database migrate, database backup; /healthz; graceful shutdown; Dockerfile/docker-compose/systemd unit; Go test suite. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e7ef3ac --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +data/ +*.db +*.db-shm +*.db-wal +.git/ +inventree diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2712213 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/inventree +/data/ +*.db +*.db-shm +*.db-wal +*.db-journal +backup-*.db +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f8630a4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +syntax=docker/dockerfile:1 + +FROM golang:1.25-alpine AS build +WORKDIR /src +ENV CGO_ENABLED=0 + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +ARG VERSION=dev +RUN go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" -o /out/inventree ./cmd/inventree + +FROM alpine:3.20 +RUN apk add --no-cache ca-certificates tzdata && \ + addgroup -S inventree && adduser -S inventree -G inventree +WORKDIR /app +COPY --from=build /out/inventree /usr/local/bin/inventree +RUN mkdir -p /app/data && chown -R inventree:inventree /app/data + +USER inventree +VOLUME ["/app/data"] +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s CMD wget -qO- http://127.0.0.1:8080/healthz || exit 1 + +ENTRYPOINT ["inventree"] +CMD ["serve", "-addr", ":8080", "-db", "/app/data/inventree.db"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..16e26bb --- /dev/null +++ b/README.md @@ -0,0 +1,172 @@ +# inventree + +A small, self-contained IT asset inventory system. The heart of inventree is a +nested folder tree — like a file manager — for organizing physical and +virtual IT assets (servers, switches, laptops, printers, etc.). + +- Single compiled Go binary, embedded HTML templates and static assets +- SQLite storage (WAL mode, foreign keys, migrations) +- Bootstrap 5 UI, minimal vanilla JavaScript (no Node/React/build step) +- Session-based auth with two roles: Admin and User +- Custom, admin-defined asset types with custom fields +- CSV import/export, full-text-ish search with breadcrumbs + +## Quick start + +```sh +go build -o inventree ./cmd/inventree +./inventree serve +``` + +Then open http://localhost:8080 and log in with: + +``` +Username: admin +Password: admin +``` + +(This default account is created intentionally on first run. Change or +disable it via the Users admin page once you're up and running.) + +The database file is created automatically at `data/inventree.db` on first +run, with migrations applied and seed data (default statuses, item types, +and the admin account) inserted. + +## CLI + +```sh +inventree # same as `inventree serve` +inventree serve # start the web server +inventree version # print the version +inventree database migrate # apply pending migrations and exit +inventree database backup [path] # write a consistent VACUUM INTO snapshot +``` + +`serve` flags: + +| Flag | Default | Description | +|---------------------|------------------------|--------------------------------------------| +| `-addr` | `:8080` | Listen address | +| `-db` | `data/inventree.db` | Path to the SQLite database file | +| `-secure-cookies` | `false` | Mark the session cookie `Secure` (enable when serving over HTTPS) | + +`/healthz` is available for load balancer / container health checks. + +## The inventory tree + +The Inventory page is the default page after login and the center of the +application: a folder/asset tree on the left, and a detail panel on the +right that shows whatever is selected (a folder's contents, or a single +asset). + +- **Folders** can nest arbitrarily deep, and contain both subfolders and + assets. +- **Expand/collapse** state is remembered per user (persisted server-side), + and folder contents are fetched lazily via small AJAX requests — the page + never fully reloads when you browse the tree. +- **Drag and drop** folders and assets onto another folder (or onto + "Inventory" itself) to move them. Admin only. +- **Right-click-style actions**: each folder has a "…" action menu (New + Subfolder, Add Asset, Rename, Delete). +- **Deleting a folder** that still contains subfolders or assets requires + confirmation, and offers a safe "move contents elsewhere" option instead + of an accidental cascade. A "permanently delete everything inside" option + is also available for when that's genuinely what you want. +- **Search** (top of the tree pane) looks across asset ID, name, folder + name, item type, assigned user, notes, and custom field values, and shows + each result's full folder breadcrumb. + +## Item types and custom fields + +Admins can define arbitrary asset types (Laptop, Switch, Firewall, …) under +**Item Types**, each with its own set of custom fields (text, long text, +number, date, boolean, dropdown, URL, IP address), marked required or not, +with a default value and display order. A representative starter set of +types and fields is seeded on first run — feel free to edit or delete it. + +## CSV import / export + +- **Export**: from the Inventory toolbar, or `/export/assets.csv` (optionally + scoped to a folder, with `?recursive=1` to include subfolders). Includes + one `cf_` column per distinct custom field in use. +- **Import**: CSV with columns `Asset ID` (optional — auto-generated if + blank), `Name`, `Item Type`, `Folder` (a `/`-separated path, e.g. + `Main Office/Network`), `Status`, `Assigned User`, `Notes`, plus + `cf_` columns for custom fields. Existing assets are matched + and updated by Asset ID; everything else is created. Missing folders in + the path can optionally be created automatically. + +A full backup/restore of everything (folders, assets, types, custom fields, +statuses) is just `inventree database backup` — it's a consistent snapshot +of the whole SQLite database. + +## Project layout + +``` +cmd/inventree/ CLI entrypoint (serve, version, database migrate/backup) +internal/ + auth/ session management, secure cookies + database/ SQLite connection, pragmas, migrations + migrations/ embedded .sql migration files + models/ shared domain types + folders/ nested folder tree + assets/ asset CRUD, search, CSV import/export + itemtypes/ admin-defined types + custom field definitions + users/ accounts + statuses/ configurable asset statuses + settings/ key/value app settings + web/ HTTP router, handlers, template rendering +web/ + templates/ Go html/template pages and partials (embedded) + static/ Bootstrap 5, Bootstrap Icons, app.css, app.js (embedded) +deploy/ systemd unit example +``` + +## Development + +```sh +go build ./... +go vet ./... +go test ./... +``` + +The SQLite driver is [modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite), +a pure-Go implementation, so `CGO_ENABLED=0` builds work out of the box and +the compiled binary has no C library dependencies. + +## Docker + +```sh +docker compose up --build +``` + +This builds the image, starts inventree on port 8080, and persists the +database in a named volume (`inventree-data`, mounted at `/app/data`). + +Or directly: + +```sh +docker build -t inventree . +docker run -p 8080:8080 -v inventree-data:/app/data inventree +``` + +## systemd + +See `deploy/inventree.service` for an example unit. Adjust the paths, then: + +```sh +sudo useradd --system --home /opt/inventree --shell /usr/sbin/nologin inventree +sudo mkdir -p /opt/inventree/data +sudo cp inventree /opt/inventree/inventree +sudo chown -R inventree:inventree /opt/inventree +sudo cp deploy/inventree.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now inventree +``` + +## Scope + +inventree is intentionally a focused MVP: folders, assets, custom types, +statuses, users, search, and CSV import/export. It does not include +ticketing, purchasing, monitoring, RMM, MDM, or accounting features — if you +need those, look elsewhere and integrate via CSV. diff --git a/cmd/inventree/main.go b/cmd/inventree/main.go new file mode 100644 index 0000000..5d7aab8 --- /dev/null +++ b/cmd/inventree/main.go @@ -0,0 +1,205 @@ +// Command inventree runs the inventree IT asset inventory server, or one of +// its supporting subcommands (version, database migrate, database backup). +package main + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "inventree/internal/database" + "inventree/internal/web" +) + +// version is set at build time via -ldflags "-X main.version=...". +var version = "dev" + +func main() { + if err := run(os.Args[1:]); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + os.Exit(1) + } +} + +func run(args []string) error { + if len(args) == 0 { + return runServe(args) + } + switch args[0] { + case "serve": + return runServe(args[1:]) + case "version": + fmt.Println("inventree", version) + return nil + case "database": + if len(args) < 2 { + return fmt.Errorf("usage: inventree database ") + } + switch args[1] { + case "migrate": + return runDatabaseMigrate(args[2:]) + case "backup": + return runDatabaseBackup(args[2:]) + default: + return fmt.Errorf("unknown database subcommand %q", args[1]) + } + case "help", "-h", "--help": + printUsage() + return nil + default: + printUsage() + return fmt.Errorf("unknown command %q", args[0]) + } +} + +func printUsage() { + fmt.Println(`inventree - a small IT asset inventory system + +Usage: + inventree [serve] Start the web server (default command) + inventree version Print the version + inventree database migrate Apply pending database migrations + inventree database backup [path] Write a consistent backup snapshot + +Flags for "serve": + -addr string listen address (default ":8080") + -db string path to the SQLite database file (default "data/inventree.db") + -secure-cookies mark the session cookie Secure (enable when serving over HTTPS)`) +} + +func dbPathFlag(args []string) (dbPath string, rest []string) { + dbPath = "data/inventree.db" + for i := 0; i < len(args); i++ { + if args[i] == "-db" && i+1 < len(args) { + dbPath = args[i+1] + i++ + continue + } + rest = append(rest, args[i]) + } + return +} + +func runDatabaseMigrate(args []string) error { + dbPath, _ := dbPathFlag(args) + if err := ensureDir(dbPath); err != nil { + return err + } + db, err := database.Open(dbPath) + if err != nil { + return err + } + defer db.Close() + fmt.Println("migrations applied to", dbPath) + return nil +} + +func runDatabaseBackup(args []string) error { + dbPath, rest := dbPathFlag(args) + dest := fmt.Sprintf("backup-%s.db", time.Now().Format("20060102-150405")) + if len(rest) > 0 { + dest = rest[0] + } + if err := ensureDir(dbPath); err != nil { + return err + } + db, err := database.Open(dbPath) + if err != nil { + return err + } + defer db.Close() + + if err := ensureDir(dest); err != nil { + return err + } + if _, err := db.Exec(`VACUUM INTO ?`, dest); err != nil { + return fmt.Errorf("backup failed: %w", err) + } + fmt.Println("backup written to", dest) + return nil +} + +func ensureDir(path string) error { + dir := filepath.Dir(path) + if dir == "" || dir == "." { + return nil + } + return os.MkdirAll(dir, 0o755) +} + +func runServe(args []string) error { + var ( + addr = ":8080" + dbPath = "data/inventree.db" + secureCookies = false + ) + for i := 0; i < len(args); i++ { + switch args[i] { + case "-addr": + if i+1 < len(args) { + addr = args[i+1] + i++ + } + case "-db": + if i+1 < len(args) { + dbPath = args[i+1] + i++ + } + case "-secure-cookies": + secureCookies = true + } + } + + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + if err := ensureDir(dbPath); err != nil { + return err + } + db, err := database.Open(dbPath) + if err != nil { + return fmt.Errorf("open database: %w", err) + } + defer db.Close() + + app, err := web.NewApp(db, log, secureCookies) + if err != nil { + return fmt.Errorf("initialize app: %w", err) + } + + stop := make(chan struct{}) + app.StartSessionJanitor(stop) + defer close(stop) + + srv := &http.Server{ + Addr: addr, + Handler: app.Router(), + ReadHeaderTimeout: 10 * time.Second, + } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + go func() { + <-ctx.Done() + log.Info("shutting down") + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownCancel() + if err := srv.Shutdown(shutdownCtx); err != nil { + log.Error("graceful shutdown failed", "error", err) + } + }() + + log.Info("inventree listening", "addr", addr, "db", dbPath) + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + log.Info("server stopped") + return nil +} diff --git a/deploy/inventree.service b/deploy/inventree.service new file mode 100644 index 0000000..274282e --- /dev/null +++ b/deploy/inventree.service @@ -0,0 +1,22 @@ +[Unit] +Description=inventree - IT asset inventory +After=network.target + +[Service] +Type=simple +User=inventree +Group=inventree +WorkingDirectory=/opt/inventree +ExecStart=/opt/inventree/inventree serve -addr :8080 -db /opt/inventree/data/inventree.db +Restart=on-failure +RestartSec=2 + +# Hardening (safe defaults for a small single-binary service) +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/opt/inventree/data +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..61589d0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +services: + inventree: + build: . + image: inventree:latest + container_name: inventree + restart: unless-stopped + ports: + - "8080:8080" + volumes: + - inventree-data:/app/data + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + +volumes: + inventree-data: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7f6dc33 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module inventree + +go 1.25.0 + +require ( + golang.org/x/crypto v0.55.0 + modernc.org/sqlite v1.56.0 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/sys v0.47.0 // indirect + modernc.org/libc v1.74.4 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e1dbd7b --- /dev/null +++ b/go.sum @@ -0,0 +1,52 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI= +modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= +modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= +modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k= +modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0= +modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/assets/assets.go b/internal/assets/assets.go new file mode 100644 index 0000000..f702f2a --- /dev/null +++ b/internal/assets/assets.go @@ -0,0 +1,549 @@ +// Package assets implements CRUD, search, and CSV import/export for IT +// assets. +package assets + +import ( + "context" + "database/sql" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "inventree/internal/folders" + "inventree/internal/models" + "inventree/internal/settings" +) + +var ( + ErrNotFound = errors.New("asset not found") + ErrNameRequired = errors.New("asset name is required") + ErrTagRequired = errors.New("asset ID is required") + ErrTagTaken = errors.New("asset ID is already in use") + ErrFieldRequired = errors.New("a required custom field is missing a value") +) + +type Store struct { + db *sql.DB + settings *settings.Store + folders *folders.Store +} + +func NewStore(db *sql.DB, st *settings.Store, fs *folders.Store) *Store { + return &Store{db: db, settings: st, folders: fs} +} + +const timeLayout = "2006-01-02T15:04:05.000Z" + +func parseTime(s string) time.Time { + t, _ := time.Parse(timeLayout, s) + return t +} + +// GenerateAssetTag produces the next available asset ID such as IT-000042, +// based on the configured prefix/padding, retrying past any manually +// assigned collisions. +func (s *Store) GenerateAssetTag(ctx context.Context) (string, error) { + prefix, err := s.settings.Get(ctx, "asset_id_prefix", "IT-") + if err != nil { + return "", err + } + paddingStr, err := s.settings.Get(ctx, "asset_id_padding", "6") + if err != nil { + return "", err + } + padding, err := strconv.Atoi(paddingStr) + if err != nil || padding <= 0 { + padding = 6 + } + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return "", err + } + defer tx.Rollback() + + var seq int64 + var seqStr string + if err := tx.QueryRowContext(ctx, `SELECT value FROM settings WHERE key = 'asset_id_seq'`).Scan(&seqStr); err != nil && !errors.Is(err, sql.ErrNoRows) { + return "", err + } + if seqStr != "" { + if n, err := strconv.ParseInt(seqStr, 10, 64); err == nil { + seq = n + } + } + + var candidate string + for { + seq++ + candidate = fmt.Sprintf("%s%0*d", prefix, padding, seq) + var exists int + if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM assets WHERE asset_id = ?`, candidate).Scan(&exists); err != nil { + return "", err + } + if exists == 0 { + break + } + } + + if _, err := tx.ExecContext(ctx, + `INSERT INTO settings (key, value, updated_at) VALUES ('asset_id_seq', ?, strftime('%Y-%m-%dT%H:%M:%fZ','now')) + ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`, + strconv.FormatInt(seq, 10)); err != nil { + return "", err + } + if err := tx.Commit(); err != nil { + return "", err + } + return candidate, nil +} + +func (s *Store) tagExists(ctx context.Context, tag string, excludeID int64) (bool, error) { + var count int + err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM assets WHERE asset_id = ? AND id != ?`, tag, excludeID).Scan(&count) + return count > 0, err +} + +// Create inserts a new asset. If a.AssetTag is empty, one is generated. +// fieldValues maps custom_field_defs.id to the submitted value. +func (s *Store) Create(ctx context.Context, a *models.Asset, fieldValues map[int64]string) error { + if a.Name == "" { + return ErrNameRequired + } + if a.AssetTag == "" { + tag, err := s.GenerateAssetTag(ctx) + if err != nil { + return err + } + a.AssetTag = tag + } else if taken, err := s.tagExists(ctx, a.AssetTag, 0); err != nil { + return err + } else if taken { + return ErrTagTaken + } + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + res, err := tx.ExecContext(ctx, + `INSERT INTO assets (asset_id, name, item_type_id, folder_id, status_id, assigned_user_id, notes) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + a.AssetTag, a.Name, a.ItemTypeID, a.FolderID, a.StatusID, a.AssignedUserID, a.Notes) + if err != nil { + return err + } + id, err := res.LastInsertId() + if err != nil { + return err + } + a.ID = id + + if err := saveFieldValues(ctx, tx, id, fieldValues); err != nil { + return err + } + return tx.Commit() +} + +func saveFieldValues(ctx context.Context, tx *sql.Tx, assetID int64, values map[int64]string) error { + for defID, val := range values { + if _, err := tx.ExecContext(ctx, + `INSERT INTO custom_field_values (asset_id, field_def_id, value) VALUES (?, ?, ?) + ON CONFLICT(asset_id, field_def_id) DO UPDATE SET value = excluded.value`, + assetID, defID, val); err != nil { + return err + } + } + return nil +} + +func (s *Store) Update(ctx context.Context, a *models.Asset, fieldValues map[int64]string) error { + if a.Name == "" { + return ErrNameRequired + } + if a.AssetTag == "" { + return ErrTagRequired + } + if taken, err := s.tagExists(ctx, a.AssetTag, a.ID); err != nil { + return err + } else if taken { + return ErrTagTaken + } + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + _, err = tx.ExecContext(ctx, + `UPDATE assets SET asset_id = ?, name = ?, item_type_id = ?, folder_id = ?, status_id = ?, assigned_user_id = ?, notes = ?, + updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, + a.AssetTag, a.Name, a.ItemTypeID, a.FolderID, a.StatusID, a.AssignedUserID, a.Notes, a.ID) + if err != nil { + return err + } + + if _, err := tx.ExecContext(ctx, `DELETE FROM custom_field_values WHERE asset_id = ?`, a.ID); err != nil { + return err + } + if err := saveFieldValues(ctx, tx, a.ID, fieldValues); err != nil { + return err + } + return tx.Commit() +} + +func (s *Store) Delete(ctx context.Context, id int64) error { + _, err := s.db.ExecContext(ctx, `DELETE FROM assets WHERE id = ?`, id) + return err +} + +func (s *Store) Move(ctx context.Context, id int64, folderID *int64) error { + _, err := s.db.ExecContext(ctx, + `UPDATE assets SET folder_id = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, + folderID, id) + return err +} + +// Duplicate copies an asset's fields and custom field values into a new +// asset with a freshly generated asset tag. +func (s *Store) Duplicate(ctx context.Context, id int64) (*models.Asset, error) { + orig, err := s.Get(ctx, id) + if err != nil { + return nil, err + } + tag, err := s.GenerateAssetTag(ctx) + if err != nil { + return nil, err + } + copyAsset := &models.Asset{ + AssetTag: tag, + Name: orig.Name + " (Copy)", + ItemTypeID: orig.ItemTypeID, + FolderID: orig.FolderID, + StatusID: orig.StatusID, + AssignedUserID: orig.AssignedUserID, + Notes: orig.Notes, + } + values := map[int64]string{} + for _, fv := range orig.CustomFields { + if fv.Value != "" { + values[fv.Def.ID] = fv.Value + } + } + if err := s.Create(ctx, copyAsset, values); err != nil { + return nil, err + } + return copyAsset, nil +} + +const assetSelect = ` + SELECT a.id, a.asset_id, a.name, a.item_type_id, a.folder_id, a.status_id, a.assigned_user_id, a.notes, a.created_at, a.updated_at, + it.name, it.icon, st.name, st.color, COALESCE(u.username, '') + FROM assets a + JOIN item_types it ON it.id = a.item_type_id + JOIN statuses st ON st.id = a.status_id + LEFT JOIN users u ON u.id = a.assigned_user_id` + +func scanAsset(row interface{ Scan(...any) error }) (*models.Asset, error) { + var a models.Asset + var folderID, assignedUserID sql.NullInt64 + var created, updated string + if err := row.Scan(&a.ID, &a.AssetTag, &a.Name, &a.ItemTypeID, &folderID, &a.StatusID, &assignedUserID, &a.Notes, &created, &updated, + &a.ItemTypeName, &a.ItemTypeIcon, &a.StatusName, &a.StatusColor, &a.AssignedUsername); err != nil { + return nil, err + } + if folderID.Valid { + a.FolderID = &folderID.Int64 + } + if assignedUserID.Valid { + a.AssignedUserID = &assignedUserID.Int64 + } + a.CreatedAt = parseTime(created) + a.UpdatedAt = parseTime(updated) + return &a, nil +} + +// Get loads an asset with its custom field values (including empty values +// for fields with no data yet) and folder breadcrumb path. +func (s *Store) Get(ctx context.Context, id int64) (*models.Asset, error) { + row := s.db.QueryRowContext(ctx, assetSelect+` WHERE a.id = ?`, id) + a, err := scanAsset(row) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + if err != nil { + return nil, err + } + if err := s.loadExtras(ctx, a); err != nil { + return nil, err + } + return a, nil +} + +func (s *Store) GetByTag(ctx context.Context, tag string) (*models.Asset, error) { + row := s.db.QueryRowContext(ctx, assetSelect+` WHERE a.asset_id = ?`, tag) + a, err := scanAsset(row) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + if err != nil { + return nil, err + } + if err := s.loadExtras(ctx, a); err != nil { + return nil, err + } + return a, nil +} + +func (s *Store) loadExtras(ctx context.Context, a *models.Asset) error { + fields, err := s.fieldValues(ctx, a.ID, a.ItemTypeID) + if err != nil { + return err + } + a.CustomFields = fields + + if a.FolderID != nil { + path, err := s.folders.Path(ctx, *a.FolderID) + if err != nil { + return err + } + a.FolderPath = path + } + return nil +} + +func (s *Store) fieldValues(ctx context.Context, assetID, itemTypeID int64) ([]models.AssetFieldValue, error) { + rows, err := s.db.QueryContext(ctx, ` + SELECT d.id, d.item_type_id, d.name, d.field_key, d.field_type, d.required, d.default_value, d.options, d.sort_order, + COALESCE(v.value, '') + FROM custom_field_defs d + LEFT JOIN custom_field_values v ON v.field_def_id = d.id AND v.asset_id = ? + WHERE d.item_type_id = ? + ORDER BY d.sort_order ASC, d.name COLLATE NOCASE ASC`, assetID, itemTypeID) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []models.AssetFieldValue + for rows.Next() { + var fv models.AssetFieldValue + var options string + if err := rows.Scan(&fv.Def.ID, &fv.Def.ItemTypeID, &fv.Def.Name, &fv.Def.FieldKey, &fv.Def.FieldType, + &fv.Def.Required, &fv.Def.DefaultValue, &options, &fv.Def.SortOrder, &fv.Value); err != nil { + return nil, err + } + if options != "" { + fv.Def.Options = strings.Split(options, "\n") + } + out = append(out, fv) + } + return out, rows.Err() +} + +// ListFilter narrows a List query. A nil pointer means "don't filter on +// this field"; FolderID pointing at a zero value with FolderSet=true means +// "top-level / unfiled only". +type ListFilter struct { + FolderID *int64 + FolderSet bool + IncludeSub bool // if true and FolderID set, include assets in descendant folders too + ItemTypeID int64 + StatusID int64 + AssignedUserID int64 + Query string + Page int + PageSize int +} + +// List returns assets matching the filter plus the total matching row count +// (for pagination), ordered by name. +func (s *Store) List(ctx context.Context, f ListFilter) ([]models.Asset, int, error) { + var where []string + var args []any + + if f.FolderSet { + if f.IncludeSub && f.FolderID != nil { + ids, err := s.descendantFolderIDs(ctx, *f.FolderID) + if err != nil { + return nil, 0, err + } + ids = append(ids, *f.FolderID) + placeholders := make([]string, len(ids)) + for i, id := range ids { + placeholders[i] = "?" + args = append(args, id) + } + where = append(where, "a.folder_id IN ("+strings.Join(placeholders, ",")+")") + } else if f.FolderID == nil { + where = append(where, "a.folder_id IS NULL") + } else { + where = append(where, "a.folder_id = ?") + args = append(args, *f.FolderID) + } + } + if f.ItemTypeID != 0 { + where = append(where, "a.item_type_id = ?") + args = append(args, f.ItemTypeID) + } + if f.StatusID != 0 { + where = append(where, "a.status_id = ?") + args = append(args, f.StatusID) + } + if f.AssignedUserID != 0 { + where = append(where, "a.assigned_user_id = ?") + args = append(args, f.AssignedUserID) + } + if f.Query != "" { + where = append(where, "(a.name LIKE '%'||?||'%' COLLATE NOCASE OR a.asset_id LIKE '%'||?||'%' COLLATE NOCASE)") + args = append(args, f.Query, f.Query) + } + + whereClause := "" + if len(where) > 0 { + whereClause = "WHERE " + strings.Join(where, " AND ") + } + + var total int + countQuery := `SELECT COUNT(*) FROM assets a ` + whereClause + if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil { + return nil, 0, err + } + + page, pageSize := f.Page, f.PageSize + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 500 { + pageSize = 50 + } + offset := (page - 1) * pageSize + + query := assetSelect + " " + whereClause + " ORDER BY a.name COLLATE NOCASE ASC LIMIT ? OFFSET ?" + args = append(args, pageSize, offset) + + rows, err := s.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, 0, err + } + defer rows.Close() + + var out []models.Asset + for rows.Next() { + a, err := scanAsset(rows) + if err != nil { + return nil, 0, err + } + out = append(out, *a) + } + return out, total, rows.Err() +} + +func (s *Store) descendantFolderIDs(ctx context.Context, rootID int64) ([]int64, error) { + all, err := s.folders.All(ctx) + if err != nil { + return nil, err + } + childrenOf := map[int64][]int64{} + for _, f := range all { + if f.ParentID != nil { + childrenOf[*f.ParentID] = append(childrenOf[*f.ParentID], f.ID) + } + } + var out []int64 + var walk func(int64) + walk = func(id int64) { + for _, c := range childrenOf[id] { + out = append(out, c) + walk(c) + } + } + walk(rootID) + return out, nil +} + +// DashboardStats summarizes inventory counts for the small dashboard. +type DashboardStats struct { + Total int + ByStatus []StatusCount + ByType []TypeCount + RecentlyAdded []models.Asset +} + +type StatusCount struct { + Name string + Color string + Count int +} + +type TypeCount struct { + Name string + Icon string + Count int +} + +func (s *Store) Stats(ctx context.Context) (*DashboardStats, error) { + stats := &DashboardStats{} + if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM assets`).Scan(&stats.Total); err != nil { + return nil, err + } + + rows, err := s.db.QueryContext(ctx, ` + SELECT st.name, st.color, COUNT(a.id) + FROM statuses st LEFT JOIN assets a ON a.status_id = st.id + GROUP BY st.id ORDER BY st.sort_order ASC`) + if err != nil { + return nil, err + } + for rows.Next() { + var sc StatusCount + if err := rows.Scan(&sc.Name, &sc.Color, &sc.Count); err != nil { + rows.Close() + return nil, err + } + stats.ByStatus = append(stats.ByStatus, sc) + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + + rows, err = s.db.QueryContext(ctx, ` + SELECT it.name, it.icon, COUNT(a.id) + FROM item_types it LEFT JOIN assets a ON a.item_type_id = it.id + GROUP BY it.id HAVING COUNT(a.id) > 0 ORDER BY COUNT(a.id) DESC`) + if err != nil { + return nil, err + } + for rows.Next() { + var tc TypeCount + if err := rows.Scan(&tc.Name, &tc.Icon, &tc.Count); err != nil { + rows.Close() + return nil, err + } + stats.ByType = append(stats.ByType, tc) + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + + recentRows, err := s.db.QueryContext(ctx, assetSelect+` ORDER BY a.created_at DESC LIMIT 8`) + if err != nil { + return nil, err + } + defer recentRows.Close() + for recentRows.Next() { + a, err := scanAsset(recentRows) + if err != nil { + return nil, err + } + stats.RecentlyAdded = append(stats.RecentlyAdded, *a) + } + return stats, recentRows.Err() +} diff --git a/internal/assets/assets_test.go b/internal/assets/assets_test.go new file mode 100644 index 0000000..27d8c5a --- /dev/null +++ b/internal/assets/assets_test.go @@ -0,0 +1,202 @@ +package assets_test + +import ( + "bytes" + "context" + "database/sql" + "path/filepath" + "strings" + "testing" + + "inventree/internal/assets" + "inventree/internal/database" + "inventree/internal/folders" + "inventree/internal/models" + "inventree/internal/settings" +) + +// Seed data from migrations/0002_seed.sql gives us stable IDs to rely on: +// item types 1=Laptop 2=Desktop 3=Server 4=Switch ... statuses 1=In Use 2=Spare ... +const ( + itemTypeLaptop = 1 + itemTypeSwitch = 4 + statusInUse = 1 + statusSpare = 2 +) + +func testStores(t *testing.T) (*assets.Store, *folders.Store, *sql.DB) { + t.Helper() + dir := t.TempDir() + db, err := database.Open(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("open db: %v", err) + } + t.Cleanup(func() { db.Close() }) + fs := folders.NewStore(db) + st := settings.NewStore(db) + return assets.NewStore(db, st, fs), fs, db +} + +func TestAssetCreateAutoGeneratesID(t *testing.T) { + ctx := context.Background() + as, _, _ := testStores(t) + + a1 := &models.Asset{Name: "Core Switch", ItemTypeID: itemTypeSwitch, StatusID: statusInUse} + if err := as.Create(ctx, a1, nil); err != nil { + t.Fatalf("create: %v", err) + } + if a1.AssetTag != "IT-000001" { + t.Fatalf("expected IT-000001, got %s", a1.AssetTag) + } + + a2 := &models.Asset{Name: "Firewall", ItemTypeID: itemTypeSwitch, StatusID: statusInUse} + if err := as.Create(ctx, a2, nil); err != nil { + t.Fatalf("create: %v", err) + } + if a2.AssetTag != "IT-000002" { + t.Fatalf("expected IT-000002, got %s", a2.AssetTag) + } +} + +func TestAssetCreateWithManualIDAndCollision(t *testing.T) { + ctx := context.Background() + as, _, _ := testStores(t) + + a1 := &models.Asset{AssetTag: "CUSTOM-1", Name: "Core Switch", ItemTypeID: itemTypeSwitch, StatusID: statusInUse} + if err := as.Create(ctx, a1, nil); err != nil { + t.Fatalf("create: %v", err) + } + + dup := &models.Asset{AssetTag: "CUSTOM-1", Name: "Other", ItemTypeID: itemTypeSwitch, StatusID: statusInUse} + if err := as.Create(ctx, dup, nil); err != assets.ErrTagTaken { + t.Fatalf("expected ErrTagTaken, got %v", err) + } +} + +func TestAssetDuplicateGeneratesNewID(t *testing.T) { + ctx := context.Background() + as, _, _ := testStores(t) + + orig := &models.Asset{Name: "Laptop 1", ItemTypeID: itemTypeLaptop, StatusID: statusInUse, Notes: "hello"} + if err := as.Create(ctx, orig, nil); err != nil { + t.Fatalf("create: %v", err) + } + + dup, err := as.Duplicate(ctx, orig.ID) + if err != nil { + t.Fatalf("duplicate: %v", err) + } + if dup.AssetTag == orig.AssetTag { + t.Fatalf("expected different asset tag, got same: %s", dup.AssetTag) + } + if dup.Notes != "hello" { + t.Fatalf("expected notes copied, got %q", dup.Notes) + } +} + +func TestAssetListFilterByFolder(t *testing.T) { + ctx := context.Background() + as, fs, _ := testStores(t) + + f1 := &models.Folder{Name: "Office"} + must(t, fs.Create(ctx, f1)) + f2 := &models.Folder{Name: "Warehouse"} + must(t, fs.Create(ctx, f2)) + + a1 := &models.Asset{Name: "A1", ItemTypeID: itemTypeLaptop, StatusID: statusInUse, FolderID: &f1.ID} + must(t, as.Create(ctx, a1, nil)) + a2 := &models.Asset{Name: "A2", ItemTypeID: itemTypeLaptop, StatusID: statusInUse, FolderID: &f2.ID} + must(t, as.Create(ctx, a2, nil)) + + list, total, err := as.List(ctx, assets.ListFilter{FolderID: &f1.ID, FolderSet: true, Page: 1, PageSize: 10}) + if err != nil { + t.Fatalf("list: %v", err) + } + if total != 1 || len(list) != 1 || list[0].Name != "A1" { + t.Fatalf("unexpected list result: total=%d list=%+v", total, list) + } +} + +func TestAssetCSVExportImportRoundTrip(t *testing.T) { + ctx := context.Background() + as, fs, db := testStores(t) + + office := &models.Folder{Name: "Main Office"} + must(t, fs.Create(ctx, office)) + orig := &models.Asset{Name: "Core Switch", ItemTypeID: itemTypeSwitch, StatusID: statusInUse, FolderID: &office.ID} + must(t, as.Create(ctx, orig, nil)) + + var buf bytes.Buffer + if err := as.Export(ctx, &buf, assets.ListFilter{}); err != nil { + t.Fatalf("export: %v", err) + } + if !strings.Contains(buf.String(), "Core Switch") { + t.Fatalf("expected exported CSV to contain asset name, got: %s", buf.String()) + } + + // Import a new asset referencing a not-yet-existing nested folder path, + // with CreateMissingFolders enabled. + csvData := "Name,Item Type,Folder,Status\nBackup Router,Switch,Main Office/Network,In Use\n" + + itemTypes := seedItemTypes(t, db) + statuses := seedStatuses(t, db) + + result, err := as.Import(ctx, strings.NewReader(csvData), assets.ImportOptions{CreateMissingFolders: true}, itemTypes, statuses, nil) + if err != nil { + t.Fatalf("import: %v", err) + } + if result.Created != 1 { + t.Fatalf("expected 1 created, got %+v", result) + } + + all, total, err := as.List(ctx, assets.ListFilter{Page: 1, PageSize: 10}) + if err != nil { + t.Fatalf("list: %v", err) + } + if total != 2 { + t.Fatalf("expected 2 assets total after import, got %d (%+v)", total, all) + } +} + +func seedItemTypes(t *testing.T, db *sql.DB) []models.ItemType { + t.Helper() + rows, err := db.Query(`SELECT id, name, icon, enabled, sort_order FROM item_types`) + if err != nil { + t.Fatalf("query item types: %v", err) + } + defer rows.Close() + var out []models.ItemType + for rows.Next() { + var it models.ItemType + if err := rows.Scan(&it.ID, &it.Name, &it.Icon, &it.Enabled, &it.SortOrder); err != nil { + t.Fatalf("scan: %v", err) + } + out = append(out, it) + } + return out +} + +func seedStatuses(t *testing.T, db *sql.DB) []models.Status { + t.Helper() + rows, err := db.Query(`SELECT id, name, color, sort_order FROM statuses`) + if err != nil { + t.Fatalf("query statuses: %v", err) + } + defer rows.Close() + var out []models.Status + for rows.Next() { + var st models.Status + if err := rows.Scan(&st.ID, &st.Name, &st.Color, &st.SortOrder); err != nil { + t.Fatalf("scan: %v", err) + } + out = append(out, st) + } + return out +} + +func must(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/internal/assets/csv.go b/internal/assets/csv.go new file mode 100644 index 0000000..c666144 --- /dev/null +++ b/internal/assets/csv.go @@ -0,0 +1,349 @@ +package assets + +import ( + "context" + "encoding/csv" + "fmt" + "io" + "sort" + "strings" + + "inventree/internal/models" +) + +const ( + colAssetID = "asset id" + colName = "name" + colItemType = "item type" + colFolder = "folder" + colStatus = "status" + colAssignedUser = "assigned user" + colNotes = "notes" + cfColPrefix = "cf_" +) + +// ImportOptions controls CSV import behavior. +type ImportOptions struct { + CreateMissingFolders bool +} + +type ImportResult struct { + Created int + Updated int + Errors []string +} + +// lookups bundles reference data needed to resolve CSV rows to foreign keys. +type lookups struct { + itemTypesByName map[string]*models.ItemType + statusesByName map[string]int64 + usersByName map[string]int64 + folderPathToID map[string]int64 +} + +func normalize(s string) string { return strings.ToLower(strings.TrimSpace(s)) } + +// Import reads assets from CSV data, creating or updating rows. Matching is +// done by Asset ID: if a row's Asset ID matches an existing asset, it is +// updated in place; otherwise a new asset is created (auto-generating an ID +// if the column is blank). +func (s *Store) Import(ctx context.Context, r io.Reader, opts ImportOptions, + itemTypesList []models.ItemType, statusesList []models.Status, usersList []models.User) (*ImportResult, error) { + + lk := &lookups{ + itemTypesByName: map[string]*models.ItemType{}, + statusesByName: map[string]int64{}, + usersByName: map[string]int64{}, + folderPathToID: map[string]int64{}, + } + for i := range itemTypesList { + lk.itemTypesByName[normalize(itemTypesList[i].Name)] = &itemTypesList[i] + } + for _, st := range statusesList { + lk.statusesByName[normalize(st.Name)] = st.ID + } + for _, u := range usersList { + lk.usersByName[normalize(u.Username)] = u.ID + } + allFolders, err := s.folders.All(ctx) + if err != nil { + return nil, err + } + byID := map[int64]models.Folder{} + for _, f := range allFolders { + byID[f.ID] = f + } + var pathOf func(id int64) string + pathOf = func(id int64) string { + f, ok := byID[id] + if !ok { + return "" + } + if f.ParentID == nil { + return f.Name + } + return pathOf(*f.ParentID) + "/" + f.Name + } + for _, f := range allFolders { + lk.folderPathToID[normalize(pathOf(f.ID))] = f.ID + } + + cr := csv.NewReader(r) + cr.FieldsPerRecord = -1 + header, err := cr.Read() + if err != nil { + if err == io.EOF { + return &ImportResult{}, nil + } + return nil, fmt.Errorf("read CSV header: %w", err) + } + + colIdx := map[string]int{} + var cfCols []struct { + key string + idx int + } + for i, h := range header { + n := normalize(h) + if strings.HasPrefix(n, cfColPrefix) { + cfCols = append(cfCols, struct { + key string + idx int + }{key: strings.TrimPrefix(n, cfColPrefix), idx: i}) + continue + } + colIdx[n] = i + } + + get := func(row []string, key string) string { + idx, ok := colIdx[key] + if !ok || idx >= len(row) { + return "" + } + return strings.TrimSpace(row[idx]) + } + + result := &ImportResult{} + rowNum := 1 + for { + row, err := cr.Read() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("read CSV row %d: %w", rowNum, err) + } + rowNum++ + + name := get(row, colName) + if name == "" { + result.Errors = append(result.Errors, fmt.Sprintf("row %d: name is required, skipped", rowNum)) + continue + } + itemTypeName := get(row, colItemType) + it, ok := lk.itemTypesByName[normalize(itemTypeName)] + if !ok { + result.Errors = append(result.Errors, fmt.Sprintf("row %d: unknown item type %q, skipped", rowNum, itemTypeName)) + continue + } + statusName := get(row, colStatus) + statusID, ok := lk.statusesByName[normalize(statusName)] + if !ok { + if len(statusesList) == 0 { + result.Errors = append(result.Errors, fmt.Sprintf("row %d: no statuses configured, skipped", rowNum)) + continue + } + statusID = statusesList[0].ID + } + + var folderID *int64 + folderPath := get(row, colFolder) + if folderPath != "" { + id, err := s.resolveFolderPath(ctx, lk, folderPath, opts.CreateMissingFolders) + if err != nil { + result.Errors = append(result.Errors, fmt.Sprintf("row %d: folder %q: %v", rowNum, folderPath, err)) + continue + } + folderID = &id + } + + var assignedUserID *int64 + if uname := get(row, colAssignedUser); uname != "" { + if id, ok := lk.usersByName[normalize(uname)]; ok { + assignedUserID = &id + } else { + result.Errors = append(result.Errors, fmt.Sprintf("row %d: unknown assigned user %q, leaving unassigned", rowNum, uname)) + } + } + + fieldValues := map[int64]string{} + for _, cf := range cfCols { + if cf.idx >= len(row) { + continue + } + val := strings.TrimSpace(row[cf.idx]) + if val == "" { + continue + } + for _, def := range it.Fields { + if normalize(def.FieldKey) == cf.key { + fieldValues[def.ID] = val + break + } + } + } + + asset := &models.Asset{ + AssetTag: get(row, colAssetID), + Name: name, + ItemTypeID: it.ID, + FolderID: folderID, + StatusID: statusID, + AssignedUserID: assignedUserID, + Notes: get(row, colNotes), + } + + if asset.AssetTag != "" { + existing, err := s.GetByTag(ctx, asset.AssetTag) + if err == nil { + asset.ID = existing.ID + if err := s.Update(ctx, asset, fieldValues); err != nil { + result.Errors = append(result.Errors, fmt.Sprintf("row %d: update failed: %v", rowNum, err)) + continue + } + result.Updated++ + continue + } + } + + if err := s.Create(ctx, asset, fieldValues); err != nil { + result.Errors = append(result.Errors, fmt.Sprintf("row %d: create failed: %v", rowNum, err)) + continue + } + result.Created++ + } + + return result, nil +} + +func (s *Store) resolveFolderPath(ctx context.Context, lk *lookups, path string, createMissing bool) (int64, error) { + norm := normalize(path) + if id, ok := lk.folderPathToID[norm]; ok { + return id, nil + } + if !createMissing { + return 0, fmt.Errorf("folder not found") + } + + segments := strings.Split(path, "/") + var parentID *int64 + built := "" + for _, seg := range segments { + seg = strings.TrimSpace(seg) + if seg == "" { + continue + } + if built == "" { + built = seg + } else { + built = built + "/" + seg + } + if id, ok := lk.folderPathToID[normalize(built)]; ok { + parentID = &id + continue + } + f := &models.Folder{Name: seg, ParentID: parentID} + if err := s.folders.Create(ctx, f); err != nil { + return 0, err + } + lk.folderPathToID[normalize(built)] = f.ID + parentID = &f.ID + } + if parentID == nil { + return 0, fmt.Errorf("empty folder path") + } + return *parentID, nil +} + +// Export writes all assets (optionally filtered) to CSV, with one column per +// distinct custom field key used across all item types. +func (s *Store) Export(ctx context.Context, w io.Writer, f ListFilter) error { + fieldDefs, err := s.allFieldKeys(ctx) + if err != nil { + return err + } + + cw := csv.NewWriter(w) + header := []string{"Asset ID", "Name", "Item Type", "Folder", "Status", "Assigned User", "Notes", "Created At", "Updated At"} + for _, k := range fieldDefs { + header = append(header, cfColPrefix+k) + } + if err := cw.Write(header); err != nil { + return err + } + + f.Page = 1 + f.PageSize = 1000 + for { + batch, total, err := s.List(ctx, f) + if err != nil { + return err + } + for i := range batch { + a, err := s.Get(ctx, batch[i].ID) + if err != nil { + return err + } + row := []string{ + a.AssetTag, a.Name, a.ItemTypeName, folderPathString(a.FolderPath), a.StatusName, a.AssignedUsername, a.Notes, + a.CreatedAt.Format("2006-01-02 15:04:05"), a.UpdatedAt.Format("2006-01-02 15:04:05"), + } + values := map[string]string{} + for _, fv := range a.CustomFields { + values[fv.Def.FieldKey] = fv.Value + } + for _, k := range fieldDefs { + row = append(row, values[k]) + } + if err := cw.Write(row); err != nil { + return err + } + } + if f.Page*f.PageSize >= total || len(batch) == 0 { + break + } + f.Page++ + } + cw.Flush() + return cw.Error() +} + +func folderPathString(path []models.BreadcrumbItem) string { + names := make([]string, len(path)) + for i, p := range path { + names[i] = p.Name + } + return strings.Join(names, "/") +} + +func (s *Store) allFieldKeys(ctx context.Context) ([]string, error) { + rows, err := s.db.QueryContext(ctx, `SELECT DISTINCT field_key FROM custom_field_defs`) + if err != nil { + return nil, err + } + defer rows.Close() + seen := map[string]bool{} + var out []string + for rows.Next() { + var k string + if err := rows.Scan(&k); err != nil { + return nil, err + } + if !seen[k] { + seen[k] = true + out = append(out, k) + } + } + sort.Strings(out) + return out, rows.Err() +} diff --git a/internal/assets/search.go b/internal/assets/search.go new file mode 100644 index 0000000..9a6afca --- /dev/null +++ b/internal/assets/search.go @@ -0,0 +1,64 @@ +package assets + +import ( + "context" + + "inventree/internal/models" +) + +// Search performs a global, case-insensitive substring search across asset +// ID, name, notes, folder name, item type name, assigned username, and +// custom field values, returning fully-loaded assets (with folder path and +// custom fields) ready for display with breadcrumbs. +func (s *Store) Search(ctx context.Context, query string, limit int) ([]models.Asset, error) { + if query == "" { + return nil, nil + } + if limit <= 0 || limit > 200 { + limit = 50 + } + like := "%" + query + "%" + + rows, err := s.db.QueryContext(ctx, ` + SELECT DISTINCT a.id + FROM assets a + LEFT JOIN folders fo ON fo.id = a.folder_id + LEFT JOIN users au ON au.id = a.assigned_user_id + JOIN item_types it ON it.id = a.item_type_id + WHERE a.asset_id LIKE ? COLLATE NOCASE + OR a.name LIKE ? COLLATE NOCASE + OR a.notes LIKE ? COLLATE NOCASE + OR fo.name LIKE ? COLLATE NOCASE + OR it.name LIKE ? COLLATE NOCASE + OR au.username LIKE ? COLLATE NOCASE + OR EXISTS (SELECT 1 FROM custom_field_values v WHERE v.asset_id = a.id AND v.value LIKE ? COLLATE NOCASE) + ORDER BY a.name COLLATE NOCASE ASC + LIMIT ?`, + like, like, like, like, like, like, like, limit) + if err != nil { + return nil, err + } + var ids []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + rows.Close() + return nil, err + } + ids = append(ids, id) + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + + var out []models.Asset + for _, id := range ids { + a, err := s.Get(ctx, id) + if err != nil { + return nil, err + } + out = append(out, *a) + } + return out, nil +} diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..2073e10 --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,133 @@ +// Package auth implements session-based authentication with secure cookies. +package auth + +import ( + "context" + "crypto/rand" + "database/sql" + "encoding/hex" + "errors" + "net/http" + "time" + + "inventree/internal/models" + "inventree/internal/users" +) + +const ( + CookieName = "inventree_session" + SessionTTL = 7 * 24 * time.Hour + contextUserK = ctxKey("current_user") +) + +type ctxKey string + +var ErrNoSession = errors.New("no valid session") + +type Store struct { + db *sql.DB + users *users.Store +} + +func NewStore(db *sql.DB, u *users.Store) *Store { return &Store{db: db, users: u} } + +func newToken() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} + +// CreateSession issues a new session for the given user and returns the +// opaque session token to be stored in a cookie. +func (s *Store) CreateSession(ctx context.Context, userID int64) (string, time.Time, error) { + token, err := newToken() + if err != nil { + return "", time.Time{}, err + } + expires := time.Now().Add(SessionTTL) + _, err = s.db.ExecContext(ctx, `INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)`, + token, userID, expires.UTC().Format("2006-01-02T15:04:05.000Z")) + if err != nil { + return "", time.Time{}, err + } + return token, expires, nil +} + +func (s *Store) DestroySession(ctx context.Context, token string) error { + _, err := s.db.ExecContext(ctx, `DELETE FROM sessions WHERE id = ?`, token) + return err +} + +// UserForToken resolves a session token to the current, enabled user. Expired +// sessions are treated as invalid (and lazily cleaned up). +func (s *Store) UserForToken(ctx context.Context, token string) (*models.User, error) { + if token == "" { + return nil, ErrNoSession + } + var userID int64 + var expiresAt string + err := s.db.QueryRowContext(ctx, `SELECT user_id, expires_at FROM sessions WHERE id = ?`, token).Scan(&userID, &expiresAt) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNoSession + } + if err != nil { + return nil, err + } + exp, _ := time.Parse("2006-01-02T15:04:05.000Z", expiresAt) + if time.Now().After(exp) { + _, _ = s.db.ExecContext(ctx, `DELETE FROM sessions WHERE id = ?`, token) + return nil, ErrNoSession + } + u, err := s.users.Get(ctx, userID) + if err != nil { + return nil, ErrNoSession + } + if !u.Enabled { + return nil, ErrNoSession + } + return u, nil +} + +// CleanExpired removes expired sessions; safe to call periodically. +func (s *Store) CleanExpired(ctx context.Context) error { + _, err := s.db.ExecContext(ctx, `DELETE FROM sessions WHERE expires_at < ?`, time.Now().UTC().Format("2006-01-02T15:04:05.000Z")) + return err +} + +func SetCookie(w http.ResponseWriter, token string, expires time.Time, secure bool) { + http.SetCookie(w, &http.Cookie{ + Name: CookieName, + Value: token, + Path: "/", + Expires: expires, + HttpOnly: true, + Secure: secure, + SameSite: http.SameSiteLaxMode, + }) +} + +func ClearCookie(w http.ResponseWriter, secure bool) { + http.SetCookie(w, &http.Cookie{ + Name: CookieName, + Value: "", + Path: "/", + Expires: time.Unix(0, 0), + MaxAge: -1, + HttpOnly: true, + Secure: secure, + SameSite: http.SameSiteLaxMode, + }) +} + +// WithUser stores the current user in the request context. +func WithUser(ctx context.Context, u *models.User) context.Context { + return context.WithValue(ctx, contextUserK, u) +} + +// UserFromContext returns the current authenticated user, if any. +func UserFromContext(ctx context.Context) (*models.User, bool) { + u, ok := ctx.Value(contextUserK).(*models.User) + return u, ok && u != nil +} diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go new file mode 100644 index 0000000..424c7c9 --- /dev/null +++ b/internal/auth/auth_test.go @@ -0,0 +1,63 @@ +package auth_test + +import ( + "context" + "database/sql" + "path/filepath" + "testing" + + "inventree/internal/auth" + "inventree/internal/database" + "inventree/internal/users" +) + +func testDB(t *testing.T) *sql.DB { + t.Helper() + dir := t.TempDir() + db, err := database.Open(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("open db: %v", err) + } + t.Cleanup(func() { db.Close() }) + return db +} + +func TestSessionLifecycle(t *testing.T) { + ctx := context.Background() + db := testDB(t) + userStore := users.NewStore(db) + authStore := auth.NewStore(db, userStore) + + admin, err := userStore.GetByUsername(ctx, "admin") + if err != nil { + t.Fatalf("get admin: %v", err) + } + + token, _, err := authStore.CreateSession(ctx, admin.ID) + if err != nil { + t.Fatalf("create session: %v", err) + } + + u, err := authStore.UserForToken(ctx, token) + if err != nil { + t.Fatalf("resolve session: %v", err) + } + if u.Username != "admin" { + t.Fatalf("expected admin, got %s", u.Username) + } + + if err := authStore.DestroySession(ctx, token); err != nil { + t.Fatalf("destroy session: %v", err) + } + if _, err := authStore.UserForToken(ctx, token); err != auth.ErrNoSession { + t.Fatalf("expected ErrNoSession after destroy, got %v", err) + } +} + +func TestUserForTokenRejectsUnknownToken(t *testing.T) { + db := testDB(t) + authStore := auth.NewStore(db, users.NewStore(db)) + if _, err := authStore.UserForToken(context.Background(), "does-not-exist"); err != auth.ErrNoSession { + t.Fatalf("expected ErrNoSession, got %v", err) + } +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..922b24f --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,124 @@ +// Package database manages the SQLite connection and schema migrations. +package database + +import ( + "database/sql" + "embed" + "fmt" + "io/fs" + "log/slog" + "path/filepath" + "sort" + "strings" + + _ "modernc.org/sqlite" +) + +//go:embed migrations/*.sql +var migrationsFS embed.FS + +// Open opens the SQLite database at path, applies pragmas suited for a +// concurrent web application, and runs any pending migrations. +func Open(path string) (*sql.DB, error) { + dsn := fmt.Sprintf("file:%s?_pragma=busy_timeout(5000)", path) + db, err := sql.Open("sqlite", dsn) + if err != nil { + return nil, fmt.Errorf("open database: %w", err) + } + + // SQLite only supports one writer at a time; a single connection avoids + // SQLITE_BUSY under concurrent goroutines while WAL still allows + // concurrent readers. + db.SetMaxOpenConns(1) + + pragmas := []string{ + "PRAGMA journal_mode = WAL", + "PRAGMA foreign_keys = ON", + "PRAGMA synchronous = NORMAL", + "PRAGMA busy_timeout = 5000", + } + for _, p := range pragmas { + if _, err := db.Exec(p); err != nil { + db.Close() + return nil, fmt.Errorf("set pragma %q: %w", p, err) + } + } + + if err := Migrate(db); err != nil { + db.Close() + return nil, fmt.Errorf("migrate: %w", err) + } + + return db, nil +} + +// Migrate applies any migrations that have not yet been recorded in the +// schema_migrations table, in filename order, each inside its own +// transaction. +func Migrate(db *sql.DB) error { + if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations ( + version TEXT PRIMARY KEY, + applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) + )`); err != nil { + return fmt.Errorf("create schema_migrations: %w", err) + } + + applied := map[string]bool{} + rows, err := db.Query(`SELECT version FROM schema_migrations`) + if err != nil { + return fmt.Errorf("query schema_migrations: %w", err) + } + for rows.Next() { + var v string + if err := rows.Scan(&v); err != nil { + rows.Close() + return err + } + applied[v] = true + } + if err := rows.Err(); err != nil { + return err + } + rows.Close() + + entries, err := fs.ReadDir(migrationsFS, "migrations") + if err != nil { + return fmt.Errorf("read migrations dir: %w", err) + } + var names []string + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") { + names = append(names, e.Name()) + } + } + sort.Strings(names) + + for _, name := range names { + if applied[name] { + continue + } + content, err := fs.ReadFile(migrationsFS, filepath.Join("migrations", name)) + if err != nil { + return fmt.Errorf("read migration %s: %w", name, err) + } + + tx, err := db.Begin() + if err != nil { + return fmt.Errorf("begin tx for %s: %w", name, err) + } + if _, err := tx.Exec(string(content)); err != nil { + tx.Rollback() + return fmt.Errorf("apply migration %s: %w", name, err) + } + if _, err := tx.Exec(`INSERT INTO schema_migrations (version) VALUES (?)`, name); err != nil { + tx.Rollback() + return fmt.Errorf("record migration %s: %w", name, err) + } + if err := tx.Commit(); err != nil { + return fmt.Errorf("commit migration %s: %w", name, err) + } + slog.Info("applied migration", "name", name) + } + + return nil +} diff --git a/internal/database/database_test.go b/internal/database/database_test.go new file mode 100644 index 0000000..195ef4f --- /dev/null +++ b/internal/database/database_test.go @@ -0,0 +1,76 @@ +package database_test + +import ( + "path/filepath" + "testing" + + "inventree/internal/database" +) + +func TestOpenSetsPragmasAndMigrates(t *testing.T) { + dir := t.TempDir() + db, err := database.Open(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("open: %v", err) + } + defer db.Close() + + var journalMode string + if err := db.QueryRow(`PRAGMA journal_mode`).Scan(&journalMode); err != nil { + t.Fatalf("query journal_mode: %v", err) + } + if journalMode != "wal" { + t.Fatalf("expected WAL journal mode, got %q", journalMode) + } + + var fk int + if err := db.QueryRow(`PRAGMA foreign_keys`).Scan(&fk); err != nil { + t.Fatalf("query foreign_keys: %v", err) + } + if fk != 1 { + t.Fatalf("expected foreign_keys=1, got %d", fk) + } + + var userCount int + if err := db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&userCount); err != nil { + t.Fatalf("query users: %v", err) + } + if userCount != 1 { + t.Fatalf("expected 1 seeded admin user, got %d", userCount) + } + + var statusCount int + if err := db.QueryRow(`SELECT COUNT(*) FROM statuses`).Scan(&statusCount); err != nil { + t.Fatalf("query statuses: %v", err) + } + if statusCount != 5 { + t.Fatalf("expected 5 seeded statuses, got %d", statusCount) + } +} + +func TestMigrateIsIdempotent(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "test.db") + + db1, err := database.Open(path) + if err != nil { + t.Fatalf("first open: %v", err) + } + db1.Close() + + // Re-opening (and re-migrating) an already-migrated database must not + // error or duplicate seed data. + db2, err := database.Open(path) + if err != nil { + t.Fatalf("second open: %v", err) + } + defer db2.Close() + + var userCount int + if err := db2.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&userCount); err != nil { + t.Fatalf("query users: %v", err) + } + if userCount != 1 { + t.Fatalf("expected still 1 seeded admin user after reopen, got %d", userCount) + } +} diff --git a/internal/database/migrations/0001_init.sql b/internal/database/migrations/0001_init.sql new file mode 100644 index 0000000..0f7ceb0 --- /dev/null +++ b/internal/database/migrations/0001_init.sql @@ -0,0 +1,108 @@ +-- Initial schema for inventree + +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + role TEXT NOT NULL CHECK (role IN ('admin', 'user')), + enabled INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); + +CREATE TABLE sessions ( + id TEXT PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + expires_at TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); +CREATE INDEX idx_sessions_user_id ON sessions(user_id); +CREATE INDEX idx_sessions_expires_at ON sessions(expires_at); + +CREATE TABLE folders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + parent_id INTEGER REFERENCES folders(id) ON DELETE RESTRICT, + description TEXT NOT NULL DEFAULT '', + icon TEXT NOT NULL DEFAULT '', + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); +CREATE INDEX idx_folders_parent_id ON folders(parent_id); + +CREATE TABLE statuses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + color TEXT NOT NULL DEFAULT 'secondary', + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); + +CREATE TABLE item_types ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT NOT NULL DEFAULT '', + icon TEXT NOT NULL DEFAULT 'bi-box-seam', + enabled INTEGER NOT NULL DEFAULT 1, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); + +CREATE TABLE custom_field_defs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + item_type_id INTEGER NOT NULL REFERENCES item_types(id) ON DELETE CASCADE, + name TEXT NOT NULL, + field_key TEXT NOT NULL, + field_type TEXT NOT NULL CHECK (field_type IN ('text','long_text','number','date','boolean','dropdown','url','ip_address')), + required INTEGER NOT NULL DEFAULT 0, + default_value TEXT NOT NULL DEFAULT '', + options TEXT NOT NULL DEFAULT '', + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + UNIQUE(item_type_id, field_key) +); +CREATE INDEX idx_custom_field_defs_item_type_id ON custom_field_defs(item_type_id); + +CREATE TABLE assets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + asset_id TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + item_type_id INTEGER NOT NULL REFERENCES item_types(id) ON DELETE RESTRICT, + folder_id INTEGER REFERENCES folders(id) ON DELETE RESTRICT, + status_id INTEGER NOT NULL REFERENCES statuses(id) ON DELETE RESTRICT, + assigned_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, + notes TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); +CREATE INDEX idx_assets_folder_id ON assets(folder_id); +CREATE INDEX idx_assets_item_type_id ON assets(item_type_id); +CREATE INDEX idx_assets_status_id ON assets(status_id); +CREATE INDEX idx_assets_assigned_user_id ON assets(assigned_user_id); +CREATE INDEX idx_assets_name ON assets(name); + +CREATE TABLE custom_field_values ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE, + field_def_id INTEGER NOT NULL REFERENCES custom_field_defs(id) ON DELETE CASCADE, + value TEXT NOT NULL DEFAULT '', + UNIQUE(asset_id, field_def_id) +); +CREATE INDEX idx_custom_field_values_asset_id ON custom_field_values(asset_id); +CREATE INDEX idx_custom_field_values_field_def_id ON custom_field_values(field_def_id); + +CREATE TABLE settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); + +CREATE TABLE user_tree_state ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + folder_id INTEGER NOT NULL REFERENCES folders(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, folder_id) +); diff --git a/internal/database/migrations/0002_seed.sql b/internal/database/migrations/0002_seed.sql new file mode 100644 index 0000000..1598cdc --- /dev/null +++ b/internal/database/migrations/0002_seed.sql @@ -0,0 +1,172 @@ +-- Seed data: default admin user, statuses, item types with custom fields, settings + +INSERT INTO users (username, password_hash, role, enabled) VALUES + ('admin', '$2a$10$AdcPKrFFnDcKh4QVagYcNOKlyyGYYLarBPSFzbKHvVyNaotVjLl0C', 'admin', 1); + +INSERT INTO statuses (name, color, sort_order) VALUES + ('In Use', 'success', 1), + ('Spare', 'info', 2), + ('Repair', 'warning', 3), + ('Retired', 'secondary', 4), + ('Lost', 'danger', 5); + +INSERT INTO settings (key, value) VALUES + ('asset_id_prefix', 'IT-'), + ('asset_id_padding', '6'), + ('asset_id_seq', '0'), + ('site_name', 'inventree'); + +INSERT INTO item_types (name, description, icon, enabled, sort_order) VALUES + ('Laptop', 'Portable computer', 'bi-laptop', 1, 1), + ('Desktop', 'Desktop workstation', 'bi-pc-display', 1, 2), + ('Server', 'Physical or virtual server', 'bi-hdd-rack', 1, 3), + ('Switch', 'Network switch', 'bi-diagram-3', 1, 4), + ('Router', 'Network router', 'bi-router', 1, 5), + ('Firewall', 'Network firewall appliance', 'bi-shield-lock', 1, 6), + ('Access Point', 'Wireless access point', 'bi-wifi', 1, 7), + ('Printer', 'Printer or MFP', 'bi-printer', 1, 8), + ('Monitor', 'Display monitor', 'bi-display', 1, 9), + ('Phone', 'Desk or mobile phone', 'bi-telephone', 1, 10), + ('UPS', 'Uninterruptible power supply', 'bi-battery-charging', 1, 11); + +-- Laptop +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Manufacturer', 'manufacturer', 'text', 0, '', '', 1 FROM item_types WHERE name = 'Laptop'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Model', 'model', 'text', 0, '', '', 2 FROM item_types WHERE name = 'Laptop'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Serial Number', 'serial_number', 'text', 0, '', '', 3 FROM item_types WHERE name = 'Laptop'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'CPU', 'cpu', 'text', 0, '', '', 4 FROM item_types WHERE name = 'Laptop'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'RAM', 'ram', 'text', 0, '', '', 5 FROM item_types WHERE name = 'Laptop'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Storage', 'storage', 'text', 0, '', '', 6 FROM item_types WHERE name = 'Laptop'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Operating System', 'operating_system', 'text', 0, '', '', 7 FROM item_types WHERE name = 'Laptop'; + +-- Desktop +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Manufacturer', 'manufacturer', 'text', 0, '', '', 1 FROM item_types WHERE name = 'Desktop'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Model', 'model', 'text', 0, '', '', 2 FROM item_types WHERE name = 'Desktop'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Serial Number', 'serial_number', 'text', 0, '', '', 3 FROM item_types WHERE name = 'Desktop'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'CPU', 'cpu', 'text', 0, '', '', 4 FROM item_types WHERE name = 'Desktop'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'RAM', 'ram', 'text', 0, '', '', 5 FROM item_types WHERE name = 'Desktop'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Storage', 'storage', 'text', 0, '', '', 6 FROM item_types WHERE name = 'Desktop'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Operating System', 'operating_system', 'text', 0, '', '', 7 FROM item_types WHERE name = 'Desktop'; + +-- Server +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Manufacturer', 'manufacturer', 'text', 0, '', '', 1 FROM item_types WHERE name = 'Server'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Model', 'model', 'text', 0, '', '', 2 FROM item_types WHERE name = 'Server'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Serial Number', 'serial_number', 'text', 0, '', '', 3 FROM item_types WHERE name = 'Server'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'CPU', 'cpu', 'text', 0, '', '', 4 FROM item_types WHERE name = 'Server'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'RAM', 'ram', 'text', 0, '', '', 5 FROM item_types WHERE name = 'Server'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Storage', 'storage', 'text', 0, '', '', 6 FROM item_types WHERE name = 'Server'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Operating System', 'operating_system', 'text', 0, '', '', 7 FROM item_types WHERE name = 'Server'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Management IP', 'management_ip', 'ip_address', 0, '', '', 8 FROM item_types WHERE name = 'Server'; + +-- Switch +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Manufacturer', 'manufacturer', 'text', 0, '', '', 1 FROM item_types WHERE name = 'Switch'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Model', 'model', 'text', 0, '', '', 2 FROM item_types WHERE name = 'Switch'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Serial Number', 'serial_number', 'text', 0, '', '', 3 FROM item_types WHERE name = 'Switch'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Management IP', 'management_ip', 'ip_address', 0, '', '', 4 FROM item_types WHERE name = 'Switch'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Port Count', 'port_count', 'number', 0, '', '', 5 FROM item_types WHERE name = 'Switch'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Firmware Version', 'firmware_version', 'text', 0, '', '', 6 FROM item_types WHERE name = 'Switch'; + +-- Router +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Manufacturer', 'manufacturer', 'text', 0, '', '', 1 FROM item_types WHERE name = 'Router'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Model', 'model', 'text', 0, '', '', 2 FROM item_types WHERE name = 'Router'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Serial Number', 'serial_number', 'text', 0, '', '', 3 FROM item_types WHERE name = 'Router'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Management IP', 'management_ip', 'ip_address', 0, '', '', 4 FROM item_types WHERE name = 'Router'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Firmware Version', 'firmware_version', 'text', 0, '', '', 5 FROM item_types WHERE name = 'Router'; + +-- Firewall +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Manufacturer', 'manufacturer', 'text', 0, '', '', 1 FROM item_types WHERE name = 'Firewall'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Model', 'model', 'text', 0, '', '', 2 FROM item_types WHERE name = 'Firewall'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Serial Number', 'serial_number', 'text', 0, '', '', 3 FROM item_types WHERE name = 'Firewall'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Management IP', 'management_ip', 'ip_address', 0, '', '', 4 FROM item_types WHERE name = 'Firewall'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Firmware Version', 'firmware_version', 'text', 0, '', '', 5 FROM item_types WHERE name = 'Firewall'; + +-- Access Point +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Manufacturer', 'manufacturer', 'text', 0, '', '', 1 FROM item_types WHERE name = 'Access Point'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Model', 'model', 'text', 0, '', '', 2 FROM item_types WHERE name = 'Access Point'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Serial Number', 'serial_number', 'text', 0, '', '', 3 FROM item_types WHERE name = 'Access Point'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Management IP', 'management_ip', 'ip_address', 0, '', '', 4 FROM item_types WHERE name = 'Access Point'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Firmware Version', 'firmware_version', 'text', 0, '', '', 5 FROM item_types WHERE name = 'Access Point'; + +-- Printer +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Manufacturer', 'manufacturer', 'text', 0, '', '', 1 FROM item_types WHERE name = 'Printer'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Model', 'model', 'text', 0, '', '', 2 FROM item_types WHERE name = 'Printer'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Serial Number', 'serial_number', 'text', 0, '', '', 3 FROM item_types WHERE name = 'Printer'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Management IP', 'management_ip', 'ip_address', 0, '', '', 4 FROM item_types WHERE name = 'Printer'; + +-- Monitor +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Manufacturer', 'manufacturer', 'text', 0, '', '', 1 FROM item_types WHERE name = 'Monitor'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Model', 'model', 'text', 0, '', '', 2 FROM item_types WHERE name = 'Monitor'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Serial Number', 'serial_number', 'text', 0, '', '', 3 FROM item_types WHERE name = 'Monitor'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Screen Size', 'screen_size', 'text', 0, '', '', 4 FROM item_types WHERE name = 'Monitor'; + +-- Phone +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Manufacturer', 'manufacturer', 'text', 0, '', '', 1 FROM item_types WHERE name = 'Phone'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Model', 'model', 'text', 0, '', '', 2 FROM item_types WHERE name = 'Phone'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Serial Number', 'serial_number', 'text', 0, '', '', 3 FROM item_types WHERE name = 'Phone'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Phone Number', 'phone_number', 'text', 0, '', '', 4 FROM item_types WHERE name = 'Phone'; + +-- UPS +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Manufacturer', 'manufacturer', 'text', 0, '', '', 1 FROM item_types WHERE name = 'UPS'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Model', 'model', 'text', 0, '', '', 2 FROM item_types WHERE name = 'UPS'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Serial Number', 'serial_number', 'text', 0, '', '', 3 FROM item_types WHERE name = 'UPS'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'VA Rating', 'va_rating', 'text', 0, '', '', 4 FROM item_types WHERE name = 'UPS'; +INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) +SELECT id, 'Battery Replacement Date', 'battery_replacement_date', 'date', 0, '', '', 5 FROM item_types WHERE name = 'UPS'; diff --git a/internal/folders/folders.go b/internal/folders/folders.go new file mode 100644 index 0000000..c88c30c --- /dev/null +++ b/internal/folders/folders.go @@ -0,0 +1,342 @@ +// Package folders implements the nested folder tree that organizes assets. +package folders + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "inventree/internal/models" +) + +var ( + ErrNotFound = errors.New("folder not found") + ErrInvalidParent = errors.New("cannot move a folder into itself or one of its own descendants") + ErrNameRequired = errors.New("folder name is required") + ErrNotEmpty = errors.New("folder still contains child folders or assets") +) + +type Store struct { + db *sql.DB +} + +func NewStore(db *sql.DB) *Store { return &Store{db: db} } + +const timeLayout = "2006-01-02T15:04:05.000Z" + +func parseTime(s string) time.Time { + t, _ := time.Parse(timeLayout, s) + return t +} + +func scanFolder(row interface{ Scan(...any) error }) (*models.Folder, error) { + var f models.Folder + var parentID sql.NullInt64 + var created, updated string + if err := row.Scan(&f.ID, &f.Name, &parentID, &f.Description, &f.Icon, &f.SortOrder, &created, &updated); err != nil { + return nil, err + } + if parentID.Valid { + f.ParentID = &parentID.Int64 + } + f.CreatedAt = parseTime(created) + f.UpdatedAt = parseTime(updated) + return &f, nil +} + +func (s *Store) Get(ctx context.Context, id int64) (*models.Folder, error) { + row := s.db.QueryRowContext(ctx, `SELECT id, name, parent_id, description, icon, sort_order, created_at, updated_at FROM folders WHERE id = ?`, id) + f, err := scanFolder(row) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + if err != nil { + return nil, err + } + return f, nil +} + +// Children returns the direct child folders of parentID (nil for top level), +// ordered by sort_order then name, annotated with folder/asset counts. +func (s *Store) Children(ctx context.Context, parentID *int64) ([]models.Folder, error) { + query := ` + SELECT f.id, f.name, f.parent_id, f.description, f.icon, f.sort_order, f.created_at, f.updated_at, + (SELECT COUNT(*) FROM folders c WHERE c.parent_id = f.id) AS folder_count, + (SELECT COUNT(*) FROM assets a WHERE a.folder_id = f.id) AS asset_count + FROM folders f + WHERE f.parent_id IS ? + ORDER BY f.sort_order ASC, f.name COLLATE NOCASE ASC` + rows, err := s.db.QueryContext(ctx, query, parentID) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []models.Folder + for rows.Next() { + var f models.Folder + var pid sql.NullInt64 + var created, updated string + if err := rows.Scan(&f.ID, &f.Name, &pid, &f.Description, &f.Icon, &f.SortOrder, &created, &updated, &f.FolderCount, &f.AssetCount); err != nil { + return nil, err + } + if pid.Valid { + f.ParentID = &pid.Int64 + } + f.CreatedAt = parseTime(created) + f.UpdatedAt = parseTime(updated) + out = append(out, f) + } + return out, rows.Err() +} + +// All returns every folder in the system (id, name, parent_id only) for +// building the full in-memory tree or search indexes. +func (s *Store) All(ctx context.Context) ([]models.Folder, error) { + rows, err := s.db.QueryContext(ctx, `SELECT id, name, parent_id, description, icon, sort_order, created_at, updated_at FROM folders ORDER BY sort_order ASC, name COLLATE NOCASE ASC`) + if err != nil { + return nil, err + } + defer rows.Close() + var out []models.Folder + for rows.Next() { + f, err := scanFolder(rowsScanner{rows}) + if err != nil { + return nil, err + } + out = append(out, *f) + } + return out, rows.Err() +} + +type rowsScanner struct{ r *sql.Rows } + +func (rs rowsScanner) Scan(dest ...any) error { return rs.r.Scan(dest...) } + +func (s *Store) Create(ctx context.Context, f *models.Folder) error { + if f.Name == "" { + return ErrNameRequired + } + if f.ParentID != nil { + if _, err := s.Get(ctx, *f.ParentID); err != nil { + return fmt.Errorf("parent folder: %w", err) + } + } + res, err := s.db.ExecContext(ctx, + `INSERT INTO folders (name, parent_id, description, icon, sort_order) VALUES (?, ?, ?, ?, ?)`, + f.Name, f.ParentID, f.Description, f.Icon, f.SortOrder) + if err != nil { + return err + } + id, err := res.LastInsertId() + if err != nil { + return err + } + f.ID = id + return nil +} + +func (s *Store) Update(ctx context.Context, f *models.Folder) error { + if f.Name == "" { + return ErrNameRequired + } + _, err := s.db.ExecContext(ctx, + `UPDATE folders SET name = ?, description = ?, icon = ?, sort_order = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, + f.Name, f.Description, f.Icon, f.SortOrder, f.ID) + return err +} + +// IsDescendant reports whether candidateID is descendantOf ancestorID (or +// equal to it), by walking parent pointers. +func (s *Store) isSelfOrDescendant(ctx context.Context, folderID int64, targetParentID *int64) (bool, error) { + if targetParentID == nil { + return false, nil + } + cur := *targetParentID + for { + if cur == folderID { + return true, nil + } + var parentID sql.NullInt64 + err := s.db.QueryRowContext(ctx, `SELECT parent_id FROM folders WHERE id = ?`, cur).Scan(&parentID) + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + if err != nil { + return false, err + } + if !parentID.Valid { + return false, nil + } + cur = parentID.Int64 + } +} + +// Move relocates a folder to a new parent (nil = top level), rejecting moves +// into itself or one of its own descendants. +func (s *Store) Move(ctx context.Context, id int64, newParentID *int64) error { + if newParentID != nil && *newParentID == id { + return ErrInvalidParent + } + bad, err := s.isSelfOrDescendant(ctx, id, newParentID) + if err != nil { + return err + } + if bad { + return ErrInvalidParent + } + if newParentID != nil { + if _, err := s.Get(ctx, *newParentID); err != nil { + return fmt.Errorf("parent folder: %w", err) + } + } + _, err = s.db.ExecContext(ctx, + `UPDATE folders SET parent_id = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, + newParentID, id) + return err +} + +// Counts returns the number of direct child folders and direct assets in a +// folder, used to decide whether deletion needs confirmation. +func (s *Store) Counts(ctx context.Context, id int64) (folderCount, assetCount int, err error) { + err = s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM folders WHERE parent_id = ?`, id).Scan(&folderCount) + if err != nil { + return + } + err = s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM assets WHERE folder_id = ?`, id).Scan(&assetCount) + return +} + +// Delete removes an empty folder. It fails if the folder still has children +// or assets (enforced both here and by the FK constraint). +func (s *Store) Delete(ctx context.Context, id int64) error { + fc, ac, err := s.Counts(ctx, id) + if err != nil { + return err + } + if fc > 0 || ac > 0 { + return ErrNotEmpty + } + _, err = s.db.ExecContext(ctx, `DELETE FROM folders WHERE id = ?`, id) + return err +} + +// DeleteAndRelocate moves all direct child folders and assets of id to +// destParentID/destFolderID (nil = top level / unfiled) and then deletes the +// now-empty folder id. Runs in a single transaction. +func (s *Store) DeleteAndRelocate(ctx context.Context, id int64, dest *int64) error { + if dest != nil { + bad, err := s.isSelfOrDescendant(ctx, id, dest) + if err != nil { + return err + } + if bad || *dest == id { + return ErrInvalidParent + } + } + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + if _, err := tx.ExecContext(ctx, `UPDATE folders SET parent_id = ? WHERE parent_id = ?`, dest, id); err != nil { + return err + } + if _, err := tx.ExecContext(ctx, `UPDATE assets SET folder_id = ? WHERE folder_id = ?`, dest, id); err != nil { + return err + } + if _, err := tx.ExecContext(ctx, `DELETE FROM folders WHERE id = ?`, id); err != nil { + return err + } + return tx.Commit() +} + +// CascadeDelete permanently deletes a folder along with every descendant +// folder and asset inside it. Descendant folders are removed deepest-first +// so the ON DELETE RESTRICT foreign key is always satisfied. +func (s *Store) CascadeDelete(ctx context.Context, id int64) error { + all, err := s.All(ctx) + if err != nil { + return err + } + childrenOf := map[int64][]int64{} + for _, f := range all { + if f.ParentID != nil { + childrenOf[*f.ParentID] = append(childrenOf[*f.ParentID], f.ID) + } + } + // Depth-first order, id last, so children are deleted before parents. + var order []int64 + var walk func(int64) + walk = func(fid int64) { + for _, c := range childrenOf[fid] { + walk(c) + } + order = append(order, fid) + } + walk(id) + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + for _, fid := range order { + if _, err := tx.ExecContext(ctx, `DELETE FROM assets WHERE folder_id = ?`, fid); err != nil { + return err + } + if _, err := tx.ExecContext(ctx, `DELETE FROM folders WHERE id = ?`, fid); err != nil { + return err + } + } + return tx.Commit() +} + +// Path returns breadcrumb items from the root down to (and including) id. +func (s *Store) Path(ctx context.Context, id int64) ([]models.BreadcrumbItem, error) { + var path []models.BreadcrumbItem + cur := &id + for cur != nil { + var name string + var parentID sql.NullInt64 + err := s.db.QueryRowContext(ctx, `SELECT name, parent_id FROM folders WHERE id = ?`, *cur).Scan(&name, &parentID) + if errors.Is(err, sql.ErrNoRows) { + break + } + if err != nil { + return nil, err + } + path = append([]models.BreadcrumbItem{{ID: *cur, Name: name}}, path...) + if parentID.Valid { + cur = &parentID.Int64 + } else { + cur = nil + } + } + return path, nil +} + +// Search returns folders whose name matches the query (case-insensitive +// substring match). +func (s *Store) Search(ctx context.Context, query string) ([]models.Folder, error) { + rows, err := s.db.QueryContext(ctx, + `SELECT id, name, parent_id, description, icon, sort_order, created_at, updated_at + FROM folders WHERE name LIKE '%' || ? || '%' COLLATE NOCASE ORDER BY name COLLATE NOCASE ASC LIMIT 50`, query) + if err != nil { + return nil, err + } + defer rows.Close() + var out []models.Folder + for rows.Next() { + f, err := scanFolder(rowsScanner{rows}) + if err != nil { + return nil, err + } + out = append(out, *f) + } + return out, rows.Err() +} diff --git a/internal/folders/folders_test.go b/internal/folders/folders_test.go new file mode 100644 index 0000000..1c4abda --- /dev/null +++ b/internal/folders/folders_test.go @@ -0,0 +1,159 @@ +package folders_test + +import ( + "context" + "database/sql" + "path/filepath" + "testing" + + "inventree/internal/database" + "inventree/internal/folders" + "inventree/internal/models" +) + +func testDB(t *testing.T) *sql.DB { + t.Helper() + dir := t.TempDir() + db, err := database.Open(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("open db: %v", err) + } + t.Cleanup(func() { db.Close() }) + return db +} + +func TestFolderCreateAndChildren(t *testing.T) { + ctx := context.Background() + s := folders.NewStore(testDB(t)) + + root := &models.Folder{Name: "Main Office"} + if err := s.Create(ctx, root); err != nil { + t.Fatalf("create root: %v", err) + } + if root.ID == 0 { + t.Fatal("expected assigned ID") + } + + child := &models.Folder{Name: "Network", ParentID: &root.ID} + if err := s.Create(ctx, child); err != nil { + t.Fatalf("create child: %v", err) + } + + children, err := s.Children(ctx, &root.ID) + if err != nil { + t.Fatalf("children: %v", err) + } + if len(children) != 1 || children[0].Name != "Network" { + t.Fatalf("unexpected children: %+v", children) + } + if children[0].FolderCount != 0 { + t.Fatalf("expected 0 sub-folders, got %d", children[0].FolderCount) + } +} + +func TestFolderCreateRequiresName(t *testing.T) { + s := folders.NewStore(testDB(t)) + err := s.Create(context.Background(), &models.Folder{}) + if err != folders.ErrNameRequired { + t.Fatalf("expected ErrNameRequired, got %v", err) + } +} + +func TestFolderMovePreventsCycles(t *testing.T) { + ctx := context.Background() + s := folders.NewStore(testDB(t)) + + a := &models.Folder{Name: "A"} + must(t, s.Create(ctx, a)) + b := &models.Folder{Name: "B", ParentID: &a.ID} + must(t, s.Create(ctx, b)) + c := &models.Folder{Name: "C", ParentID: &b.ID} + must(t, s.Create(ctx, c)) + + // Moving A under its own descendant C must fail. + if err := s.Move(ctx, a.ID, &c.ID); err != folders.ErrInvalidParent { + t.Fatalf("expected ErrInvalidParent, got %v", err) + } + // Moving A under itself must fail. + if err := s.Move(ctx, a.ID, &a.ID); err != folders.ErrInvalidParent { + t.Fatalf("expected ErrInvalidParent for self-move, got %v", err) + } + // A legitimate move should succeed. + if err := s.Move(ctx, c.ID, &a.ID); err != nil { + t.Fatalf("expected valid move to succeed: %v", err) + } +} + +func TestFolderDeleteRequiresEmpty(t *testing.T) { + ctx := context.Background() + s := folders.NewStore(testDB(t)) + + a := &models.Folder{Name: "A"} + must(t, s.Create(ctx, a)) + b := &models.Folder{Name: "B", ParentID: &a.ID} + must(t, s.Create(ctx, b)) + + if err := s.Delete(ctx, a.ID); err != folders.ErrNotEmpty { + t.Fatalf("expected ErrNotEmpty, got %v", err) + } + if err := s.Delete(ctx, b.ID); err != nil { + t.Fatalf("delete empty folder: %v", err) + } + if err := s.Delete(ctx, a.ID); err != nil { + t.Fatalf("delete now-empty folder: %v", err) + } +} + +func TestFolderDeleteAndRelocate(t *testing.T) { + ctx := context.Background() + s := folders.NewStore(testDB(t)) + + a := &models.Folder{Name: "A"} + must(t, s.Create(ctx, a)) + b := &models.Folder{Name: "B", ParentID: &a.ID} + must(t, s.Create(ctx, b)) + dest := &models.Folder{Name: "Dest"} + must(t, s.Create(ctx, dest)) + + if err := s.DeleteAndRelocate(ctx, a.ID, &dest.ID); err != nil { + t.Fatalf("delete and relocate: %v", err) + } + children, err := s.Children(ctx, &dest.ID) + if err != nil { + t.Fatalf("children: %v", err) + } + if len(children) != 1 || children[0].ID != b.ID { + t.Fatalf("expected B relocated under Dest, got %+v", children) + } + if _, err := s.Get(ctx, a.ID); err != folders.ErrNotFound { + t.Fatalf("expected A to be deleted, got %v", err) + } +} + +func TestFolderCascadeDelete(t *testing.T) { + ctx := context.Background() + s := folders.NewStore(testDB(t)) + + a := &models.Folder{Name: "A"} + must(t, s.Create(ctx, a)) + b := &models.Folder{Name: "B", ParentID: &a.ID} + must(t, s.Create(ctx, b)) + c := &models.Folder{Name: "C", ParentID: &b.ID} + must(t, s.Create(ctx, c)) + + if err := s.CascadeDelete(ctx, a.ID); err != nil { + t.Fatalf("cascade delete: %v", err) + } + for _, id := range []int64{a.ID, b.ID, c.ID} { + if _, err := s.Get(ctx, id); err != folders.ErrNotFound { + t.Fatalf("expected folder %d deleted, got %v", id, err) + } + } +} + +func must(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/internal/itemtypes/itemtypes.go b/internal/itemtypes/itemtypes.go new file mode 100644 index 0000000..b417410 --- /dev/null +++ b/internal/itemtypes/itemtypes.go @@ -0,0 +1,247 @@ +// Package itemtypes manages admin-defined asset types and their custom fields. +package itemtypes + +import ( + "context" + "database/sql" + "errors" + "regexp" + "strings" + "time" + + "inventree/internal/models" +) + +var ( + ErrNotFound = errors.New("item type not found") + ErrNameRequired = errors.New("item type name is required") + ErrInUse = errors.New("item type is assigned to one or more assets") + + ErrFieldNotFound = errors.New("custom field not found") + ErrFieldName = errors.New("field name is required") + ErrFieldType = errors.New("invalid field type") +) + +type Store struct{ db *sql.DB } + +func NewStore(db *sql.DB) *Store { return &Store{db: db} } + +const timeLayout = "2006-01-02T15:04:05.000Z" + +func parseTime(s string) time.Time { + t, _ := time.Parse(timeLayout, s) + return t +} + +var slugRe = regexp.MustCompile(`[^a-z0-9_]+`) + +// Slugify converts a field name into a snake_case key suitable as field_key. +func Slugify(name string) string { + s := strings.ToLower(strings.TrimSpace(name)) + s = slugRe.ReplaceAllString(s, "_") + s = strings.Trim(s, "_") + if s == "" { + s = "field" + } + return s +} + +func validFieldType(t models.FieldType) bool { + for _, ft := range models.AllFieldTypes { + if ft == t { + return true + } + } + return false +} + +// List returns all item types, optionally including disabled ones, each with +// its custom field definitions loaded. +func (s *Store) List(ctx context.Context, includeDisabled bool) ([]models.ItemType, error) { + q := `SELECT id, name, description, icon, enabled, sort_order, created_at, updated_at FROM item_types` + if !includeDisabled { + q += ` WHERE enabled = 1` + } + q += ` ORDER BY sort_order ASC, name COLLATE NOCASE ASC` + rows, err := s.db.QueryContext(ctx, q) + if err != nil { + return nil, err + } + defer rows.Close() + var out []models.ItemType + for rows.Next() { + var it models.ItemType + var created, updated string + if err := rows.Scan(&it.ID, &it.Name, &it.Description, &it.Icon, &it.Enabled, &it.SortOrder, &created, &updated); err != nil { + return nil, err + } + it.CreatedAt = parseTime(created) + it.UpdatedAt = parseTime(updated) + out = append(out, it) + } + if err := rows.Err(); err != nil { + return nil, err + } + for i := range out { + fields, err := s.Fields(ctx, out[i].ID) + if err != nil { + return nil, err + } + out[i].Fields = fields + } + return out, nil +} + +func (s *Store) Get(ctx context.Context, id int64) (*models.ItemType, error) { + row := s.db.QueryRowContext(ctx, `SELECT id, name, description, icon, enabled, sort_order, created_at, updated_at FROM item_types WHERE id = ?`, id) + var it models.ItemType + var created, updated string + err := row.Scan(&it.ID, &it.Name, &it.Description, &it.Icon, &it.Enabled, &it.SortOrder, &created, &updated) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + if err != nil { + return nil, err + } + it.CreatedAt = parseTime(created) + it.UpdatedAt = parseTime(updated) + fields, err := s.Fields(ctx, it.ID) + if err != nil { + return nil, err + } + it.Fields = fields + return &it, nil +} + +func (s *Store) Fields(ctx context.Context, itemTypeID int64) ([]models.CustomFieldDef, error) { + rows, err := s.db.QueryContext(ctx, + `SELECT id, item_type_id, name, field_key, field_type, required, default_value, options, sort_order, created_at, updated_at + FROM custom_field_defs WHERE item_type_id = ? ORDER BY sort_order ASC, name COLLATE NOCASE ASC`, itemTypeID) + if err != nil { + return nil, err + } + defer rows.Close() + var out []models.CustomFieldDef + for rows.Next() { + var f models.CustomFieldDef + var created, updated, options string + if err := rows.Scan(&f.ID, &f.ItemTypeID, &f.Name, &f.FieldKey, &f.FieldType, &f.Required, &f.DefaultValue, &options, &f.SortOrder, &created, &updated); err != nil { + return nil, err + } + if options != "" { + f.Options = strings.Split(options, "\n") + } + f.CreatedAt = parseTime(created) + f.UpdatedAt = parseTime(updated) + out = append(out, f) + } + return out, rows.Err() +} + +func (s *Store) Create(ctx context.Context, it *models.ItemType) error { + if it.Name == "" { + return ErrNameRequired + } + if it.Icon == "" { + it.Icon = "bi-box-seam" + } + res, err := s.db.ExecContext(ctx, + `INSERT INTO item_types (name, description, icon, enabled, sort_order) VALUES (?, ?, ?, ?, ?)`, + it.Name, it.Description, it.Icon, it.Enabled, it.SortOrder) + if err != nil { + return err + } + id, err := res.LastInsertId() + if err != nil { + return err + } + it.ID = id + return nil +} + +func (s *Store) Update(ctx context.Context, it *models.ItemType) error { + if it.Name == "" { + return ErrNameRequired + } + _, err := s.db.ExecContext(ctx, + `UPDATE item_types SET name = ?, description = ?, icon = ?, enabled = ?, sort_order = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, + it.Name, it.Description, it.Icon, it.Enabled, it.SortOrder, it.ID) + return err +} + +func (s *Store) Delete(ctx context.Context, id int64) error { + var count int + if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM assets WHERE item_type_id = ?`, id).Scan(&count); err != nil { + return err + } + if count > 0 { + return ErrInUse + } + _, err := s.db.ExecContext(ctx, `DELETE FROM item_types WHERE id = ?`, id) + return err +} + +func (s *Store) CreateField(ctx context.Context, f *models.CustomFieldDef) error { + if f.Name == "" { + return ErrFieldName + } + if !validFieldType(f.FieldType) { + return ErrFieldType + } + if f.FieldKey == "" { + f.FieldKey = Slugify(f.Name) + } + res, err := s.db.ExecContext(ctx, + `INSERT INTO custom_field_defs (item_type_id, name, field_key, field_type, required, default_value, options, sort_order) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + f.ItemTypeID, f.Name, f.FieldKey, f.FieldType, f.Required, f.DefaultValue, strings.Join(f.Options, "\n"), f.SortOrder) + if err != nil { + return err + } + id, err := res.LastInsertId() + if err != nil { + return err + } + f.ID = id + return nil +} + +func (s *Store) UpdateField(ctx context.Context, f *models.CustomFieldDef) error { + if f.Name == "" { + return ErrFieldName + } + if !validFieldType(f.FieldType) { + return ErrFieldType + } + _, err := s.db.ExecContext(ctx, + `UPDATE custom_field_defs SET name = ?, field_type = ?, required = ?, default_value = ?, options = ?, sort_order = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') + WHERE id = ?`, + f.Name, f.FieldType, f.Required, f.DefaultValue, strings.Join(f.Options, "\n"), f.SortOrder, f.ID) + return err +} + +func (s *Store) DeleteField(ctx context.Context, id int64) error { + _, err := s.db.ExecContext(ctx, `DELETE FROM custom_field_defs WHERE id = ?`, id) + return err +} + +func (s *Store) GetField(ctx context.Context, id int64) (*models.CustomFieldDef, error) { + row := s.db.QueryRowContext(ctx, + `SELECT id, item_type_id, name, field_key, field_type, required, default_value, options, sort_order, created_at, updated_at + FROM custom_field_defs WHERE id = ?`, id) + var f models.CustomFieldDef + var created, updated, options string + err := row.Scan(&f.ID, &f.ItemTypeID, &f.Name, &f.FieldKey, &f.FieldType, &f.Required, &f.DefaultValue, &options, &f.SortOrder, &created, &updated) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrFieldNotFound + } + if err != nil { + return nil, err + } + if options != "" { + f.Options = strings.Split(options, "\n") + } + f.CreatedAt = parseTime(created) + f.UpdatedAt = parseTime(updated) + return &f, nil +} diff --git a/internal/itemtypes/itemtypes_test.go b/internal/itemtypes/itemtypes_test.go new file mode 100644 index 0000000..ea014c2 --- /dev/null +++ b/internal/itemtypes/itemtypes_test.go @@ -0,0 +1,107 @@ +package itemtypes_test + +import ( + "context" + "database/sql" + "path/filepath" + "testing" + + "inventree/internal/database" + "inventree/internal/itemtypes" + "inventree/internal/models" +) + +func testDB(t *testing.T) *sql.DB { + t.Helper() + dir := t.TempDir() + db, err := database.Open(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("open db: %v", err) + } + t.Cleanup(func() { db.Close() }) + return db +} + +func TestSlugify(t *testing.T) { + cases := map[string]string{ + "Management IP": "management_ip", + "CPU": "cpu", + " Serial Number ": "serial_number", + "": "field", + } + for in, want := range cases { + if got := itemtypes.Slugify(in); got != want { + t.Errorf("Slugify(%q) = %q, want %q", in, got, want) + } + } +} + +func TestCreateItemTypeWithFields(t *testing.T) { + ctx := context.Background() + s := itemtypes.NewStore(testDB(t)) + + it := &models.ItemType{Name: "Tablet", Enabled: true} + if err := s.Create(ctx, it); err != nil { + t.Fatalf("create: %v", err) + } + + field := &models.CustomFieldDef{ItemTypeID: it.ID, Name: "Screen Size", FieldType: models.FieldTypeText} + if err := s.CreateField(ctx, field); err != nil { + t.Fatalf("create field: %v", err) + } + if field.FieldKey != "screen_size" { + t.Fatalf("expected auto-slugified key, got %q", field.FieldKey) + } + + loaded, err := s.Get(ctx, it.ID) + if err != nil { + t.Fatalf("get: %v", err) + } + if len(loaded.Fields) != 1 || loaded.Fields[0].Name != "Screen Size" { + t.Fatalf("expected 1 field, got %+v", loaded.Fields) + } +} + +func TestDeleteItemTypeInUseFails(t *testing.T) { + ctx := context.Background() + db := testDB(t) + s := itemtypes.NewStore(db) + + // item_type_id 1 (Laptop, seeded) is referenced by no assets yet, so + // deletion should succeed; insert a fake asset row to simulate usage. + if _, err := db.Exec(`INSERT INTO assets (asset_id, name, item_type_id, status_id) VALUES ('IT-1', 'x', 1, 1)`); err != nil { + t.Fatalf("seed asset: %v", err) + } + if err := s.Delete(ctx, 1); err != itemtypes.ErrInUse { + t.Fatalf("expected ErrInUse, got %v", err) + } +} + +func TestDropdownFieldOptionsRoundTrip(t *testing.T) { + ctx := context.Background() + s := itemtypes.NewStore(testDB(t)) + + it := &models.ItemType{Name: "Printer2", Enabled: true} + must(t, s.Create(ctx, it)) + + field := &models.CustomFieldDef{ + ItemTypeID: it.ID, Name: "Color Mode", FieldType: models.FieldTypeDropdown, + Options: []string{"Color", "Black & White"}, + } + must(t, s.CreateField(ctx, field)) + + got, err := s.GetField(ctx, field.ID) + if err != nil { + t.Fatalf("get field: %v", err) + } + if len(got.Options) != 2 || got.Options[0] != "Color" || got.Options[1] != "Black & White" { + t.Fatalf("unexpected options: %+v", got.Options) + } +} + +func must(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..6bef54f --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,173 @@ +// Package models defines the core domain types shared across inventree. +package models + +import "time" + +type Role string + +const ( + RoleAdmin Role = "admin" + RoleUser Role = "user" +) + +type User struct { + ID int64 + Username string + PasswordHash string + Role Role + Enabled bool + CreatedAt time.Time + UpdatedAt time.Time +} + +func (u User) IsAdmin() bool { return u.Role == RoleAdmin } + +type Folder struct { + ID int64 + Name string + ParentID *int64 + Description string + Icon string + SortOrder int + CreatedAt time.Time + UpdatedAt time.Time + + // Populated by list/tree queries; not persisted directly on this table. + FolderCount int + AssetCount int +} + +type Status struct { + ID int64 + Name string + Color string + SortOrder int + CreatedAt time.Time + UpdatedAt time.Time +} + +type FieldType string + +const ( + FieldTypeText FieldType = "text" + FieldTypeLongText FieldType = "long_text" + FieldTypeNumber FieldType = "number" + FieldTypeDate FieldType = "date" + FieldTypeBoolean FieldType = "boolean" + FieldTypeDropdown FieldType = "dropdown" + FieldTypeURL FieldType = "url" + FieldTypeIPAddress FieldType = "ip_address" +) + +var AllFieldTypes = []FieldType{ + FieldTypeText, FieldTypeLongText, FieldTypeNumber, FieldTypeDate, + FieldTypeBoolean, FieldTypeDropdown, FieldTypeURL, FieldTypeIPAddress, +} + +func (t FieldType) Label() string { + switch t { + case FieldTypeText: + return "Text" + case FieldTypeLongText: + return "Long Text" + case FieldTypeNumber: + return "Number" + case FieldTypeDate: + return "Date" + case FieldTypeBoolean: + return "Boolean" + case FieldTypeDropdown: + return "Dropdown" + case FieldTypeURL: + return "URL" + case FieldTypeIPAddress: + return "IP Address" + } + return string(t) +} + +type ItemType struct { + ID int64 + Name string + Description string + Icon string + Enabled bool + SortOrder int + CreatedAt time.Time + UpdatedAt time.Time + + Fields []CustomFieldDef +} + +type CustomFieldDef struct { + ID int64 + ItemTypeID int64 + Name string + FieldKey string + FieldType FieldType + Required bool + DefaultValue string + Options []string + SortOrder int + CreatedAt time.Time + UpdatedAt time.Time +} + +type CustomFieldValue struct { + ID int64 + AssetID int64 + FieldDefID int64 + Value string +} + +type Asset struct { + ID int64 + AssetTag string + Name string + ItemTypeID int64 + FolderID *int64 + StatusID int64 + AssignedUserID *int64 + Notes string + CreatedAt time.Time + UpdatedAt time.Time + + // Populated by joined list/detail queries. + ItemTypeName string + ItemTypeIcon string + StatusName string + StatusColor string + AssignedUsername string + FolderPath []BreadcrumbItem + CustomFields []AssetFieldValue +} + +type AssetFieldValue struct { + Def CustomFieldDef + Value string +} + +type BreadcrumbItem struct { + ID int64 + Name string +} + +// TreeNode is a folder-or-asset node used to render the inventory tree. +type TreeNode struct { + Type string // "folder" or "asset" + ID int64 + Name string + Icon string + ParentID *int64 + HasChildren bool + AssetTag string + StatusName string + StatusColor string + + // Populated only when the node is pre-expanded for server-side + // recursive rendering (see web.buildTree); empty otherwise, in which + // case the client lazy-loads children via /app/tree/children. + Expanded bool + Children []TreeNode + MoreAssets int // count of additional assets beyond the tree display cap, if any +} diff --git a/internal/settings/settings.go b/internal/settings/settings.go new file mode 100644 index 0000000..ee7577a --- /dev/null +++ b/internal/settings/settings.go @@ -0,0 +1,49 @@ +// Package settings provides access to the application's key/value settings. +package settings + +import ( + "context" + "database/sql" + "errors" +) + +type Store struct{ db *sql.DB } + +func NewStore(db *sql.DB) *Store { return &Store{db: db} } + +func (s *Store) Get(ctx context.Context, key, fallback string) (string, error) { + var v string + err := s.db.QueryRowContext(ctx, `SELECT value FROM settings WHERE key = ?`, key).Scan(&v) + if errors.Is(err, sql.ErrNoRows) { + return fallback, nil + } + if err != nil { + return "", err + } + return v, nil +} + +func (s *Store) Set(ctx context.Context, key, value string) error { + _, err := s.db.ExecContext(ctx, + `INSERT INTO settings (key, value, updated_at) VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now')) + ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`, + key, value) + return err +} + +func (s *Store) All(ctx context.Context) (map[string]string, error) { + rows, err := s.db.QueryContext(ctx, `SELECT key, value FROM settings`) + if err != nil { + return nil, err + } + defer rows.Close() + out := map[string]string{} + for rows.Next() { + var k, v string + if err := rows.Scan(&k, &v); err != nil { + return nil, err + } + out[k] = v + } + return out, rows.Err() +} diff --git a/internal/statuses/statuses.go b/internal/statuses/statuses.go new file mode 100644 index 0000000..439be90 --- /dev/null +++ b/internal/statuses/statuses.go @@ -0,0 +1,121 @@ +// Package statuses manages the configurable list of asset statuses. +package statuses + +import ( + "context" + "database/sql" + "errors" + "time" + + "inventree/internal/models" +) + +var ( + ErrNotFound = errors.New("status not found") + ErrNameRequired = errors.New("status name is required") + ErrInUse = errors.New("status is assigned to one or more assets") +) + +type Store struct{ db *sql.DB } + +func NewStore(db *sql.DB) *Store { return &Store{db: db} } + +const timeLayout = "2006-01-02T15:04:05.000Z" + +func parseTime(s string) time.Time { + t, _ := time.Parse(timeLayout, s) + return t +} + +func scan(row interface{ Scan(...any) error }) (*models.Status, error) { + var st models.Status + var created, updated string + if err := row.Scan(&st.ID, &st.Name, &st.Color, &st.SortOrder, &created, &updated); err != nil { + return nil, err + } + st.CreatedAt = parseTime(created) + st.UpdatedAt = parseTime(updated) + return &st, nil +} + +func (s *Store) List(ctx context.Context) ([]models.Status, error) { + rows, err := s.db.QueryContext(ctx, `SELECT id, name, color, sort_order, created_at, updated_at FROM statuses ORDER BY sort_order ASC, name ASC`) + if err != nil { + return nil, err + } + defer rows.Close() + var out []models.Status + for rows.Next() { + st, err := scan(rows) + if err != nil { + return nil, err + } + out = append(out, *st) + } + return out, rows.Err() +} + +func (s *Store) Get(ctx context.Context, id int64) (*models.Status, error) { + row := s.db.QueryRowContext(ctx, `SELECT id, name, color, sort_order, created_at, updated_at FROM statuses WHERE id = ?`, id) + st, err := scan(row) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + return st, err +} + +func (s *Store) Create(ctx context.Context, st *models.Status) error { + if st.Name == "" { + return ErrNameRequired + } + if st.Color == "" { + st.Color = "secondary" + } + res, err := s.db.ExecContext(ctx, `INSERT INTO statuses (name, color, sort_order) VALUES (?, ?, ?)`, st.Name, st.Color, st.SortOrder) + if err != nil { + return err + } + id, err := res.LastInsertId() + if err != nil { + return err + } + st.ID = id + return nil +} + +func (s *Store) Update(ctx context.Context, st *models.Status) error { + if st.Name == "" { + return ErrNameRequired + } + _, err := s.db.ExecContext(ctx, + `UPDATE statuses SET name = ?, color = ?, sort_order = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, + st.Name, st.Color, st.SortOrder, st.ID) + return err +} + +func (s *Store) Delete(ctx context.Context, id int64) error { + var count int + if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM assets WHERE status_id = ?`, id).Scan(&count); err != nil { + return err + } + if count > 0 { + return ErrInUse + } + _, err := s.db.ExecContext(ctx, `DELETE FROM statuses WHERE id = ?`, id) + return err +} + +// Reorder sets sort_order for each status ID in the given order (1-indexed). +func (s *Store) Reorder(ctx context.Context, orderedIDs []int64) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + for i, id := range orderedIDs { + if _, err := tx.ExecContext(ctx, `UPDATE statuses SET sort_order = ? WHERE id = ?`, i+1, id); err != nil { + return err + } + } + return tx.Commit() +} diff --git a/internal/users/users.go b/internal/users/users.go new file mode 100644 index 0000000..7687393 --- /dev/null +++ b/internal/users/users.go @@ -0,0 +1,186 @@ +// Package users manages application accounts. +package users + +import ( + "context" + "database/sql" + "errors" + "time" + + "golang.org/x/crypto/bcrypt" + + "inventree/internal/models" +) + +var ( + ErrNotFound = errors.New("user not found") + ErrUsernameRequired = errors.New("username is required") + ErrUsernameTaken = errors.New("username is already taken") + ErrInvalidRole = errors.New("role must be admin or user") + ErrLastAdmin = errors.New("cannot remove the last enabled admin") +) + +type Store struct{ db *sql.DB } + +func NewStore(db *sql.DB) *Store { return &Store{db: db} } + +const timeLayout = "2006-01-02T15:04:05.000Z" + +func parseTime(s string) time.Time { + t, _ := time.Parse(timeLayout, s) + return t +} + +func scan(row interface{ Scan(...any) error }) (*models.User, error) { + var u models.User + var created, updated string + if err := row.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Role, &u.Enabled, &created, &updated); err != nil { + return nil, err + } + u.CreatedAt = parseTime(created) + u.UpdatedAt = parseTime(updated) + return &u, nil +} + +func (s *Store) List(ctx context.Context) ([]models.User, error) { + rows, err := s.db.QueryContext(ctx, `SELECT id, username, password_hash, role, enabled, created_at, updated_at FROM users ORDER BY username COLLATE NOCASE ASC`) + if err != nil { + return nil, err + } + defer rows.Close() + var out []models.User + for rows.Next() { + u, err := scan(rows) + if err != nil { + return nil, err + } + out = append(out, *u) + } + return out, rows.Err() +} + +func (s *Store) Get(ctx context.Context, id int64) (*models.User, error) { + row := s.db.QueryRowContext(ctx, `SELECT id, username, password_hash, role, enabled, created_at, updated_at FROM users WHERE id = ?`, id) + u, err := scan(row) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + return u, err +} + +func (s *Store) GetByUsername(ctx context.Context, username string) (*models.User, error) { + row := s.db.QueryRowContext(ctx, `SELECT id, username, password_hash, role, enabled, created_at, updated_at FROM users WHERE username = ?`, username) + u, err := scan(row) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + return u, err +} + +// Create adds a user with the given plaintext password (hashed with bcrypt). +func (s *Store) Create(ctx context.Context, u *models.User, plaintextPassword string) error { + if u.Username == "" { + return ErrUsernameRequired + } + if u.Role != models.RoleAdmin && u.Role != models.RoleUser { + return ErrInvalidRole + } + if _, err := s.GetByUsername(ctx, u.Username); err == nil { + return ErrUsernameTaken + } else if !errors.Is(err, ErrNotFound) { + return err + } + hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), bcrypt.DefaultCost) + if err != nil { + return err + } + res, err := s.db.ExecContext(ctx, + `INSERT INTO users (username, password_hash, role, enabled) VALUES (?, ?, ?, ?)`, + u.Username, string(hash), u.Role, u.Enabled) + if err != nil { + return err + } + id, err := res.LastInsertId() + if err != nil { + return err + } + u.ID = id + return nil +} + +// Update changes username, role, and enabled state (not the password). +func (s *Store) Update(ctx context.Context, u *models.User) error { + if u.Username == "" { + return ErrUsernameRequired + } + if u.Role != models.RoleAdmin && u.Role != models.RoleUser { + return ErrInvalidRole + } + if existing, err := s.GetByUsername(ctx, u.Username); err == nil && existing.ID != u.ID { + return ErrUsernameTaken + } + if u.Role != models.RoleAdmin || !u.Enabled { + if err := s.assertNotLastAdmin(ctx, u.ID); err != nil { + return err + } + } + _, err := s.db.ExecContext(ctx, + `UPDATE users SET username = ?, role = ?, enabled = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, + u.Username, u.Role, u.Enabled, u.ID) + return err +} + +func (s *Store) SetPassword(ctx context.Context, id int64, plaintextPassword string) error { + hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), bcrypt.DefaultCost) + if err != nil { + return err + } + _, err = s.db.ExecContext(ctx, + `UPDATE users SET password_hash = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, + string(hash), id) + return err +} + +func (s *Store) Delete(ctx context.Context, id int64) error { + if err := s.assertNotLastAdmin(ctx, id); err != nil { + return err + } + _, err := s.db.ExecContext(ctx, `DELETE FROM users WHERE id = ?`, id) + return err +} + +// assertNotLastAdmin returns ErrLastAdmin if id is currently an enabled +// admin and is the only one remaining. +func (s *Store) assertNotLastAdmin(ctx context.Context, id int64) error { + u, err := s.Get(ctx, id) + if err != nil { + return err + } + if u.Role != models.RoleAdmin || !u.Enabled { + return nil + } + var count int + if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE role = 'admin' AND enabled = 1`).Scan(&count); err != nil { + return err + } + if count <= 1 { + return ErrLastAdmin + } + return nil +} + +// Authenticate verifies a username/password pair and returns the user if +// valid, enabled, and the password matches. +func (s *Store) Authenticate(ctx context.Context, username, password string) (*models.User, error) { + u, err := s.GetByUsername(ctx, username) + if err != nil { + return nil, err + } + if !u.Enabled { + return nil, errors.New("account is disabled") + } + if err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)); err != nil { + return nil, errors.New("invalid credentials") + } + return u, nil +} diff --git a/internal/users/users_test.go b/internal/users/users_test.go new file mode 100644 index 0000000..6148f94 --- /dev/null +++ b/internal/users/users_test.go @@ -0,0 +1,96 @@ +package users_test + +import ( + "context" + "database/sql" + "path/filepath" + "testing" + + "inventree/internal/database" + "inventree/internal/models" + "inventree/internal/users" +) + +func testDB(t *testing.T) *sql.DB { + t.Helper() + dir := t.TempDir() + db, err := database.Open(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("open db: %v", err) + } + t.Cleanup(func() { db.Close() }) + return db +} + +func TestDefaultAdminSeeded(t *testing.T) { + ctx := context.Background() + s := users.NewStore(testDB(t)) + + u, err := s.Authenticate(ctx, "admin", "admin") + if err != nil { + t.Fatalf("authenticate default admin: %v", err) + } + if !u.IsAdmin() { + t.Fatal("expected default admin to have admin role") + } +} + +func TestAuthenticateRejectsBadPassword(t *testing.T) { + s := users.NewStore(testDB(t)) + if _, err := s.Authenticate(context.Background(), "admin", "wrong"); err == nil { + t.Fatal("expected authentication to fail") + } +} + +func TestCreateUserRejectsDuplicateUsername(t *testing.T) { + ctx := context.Background() + s := users.NewStore(testDB(t)) + + u := &models.User{Username: "admin", Role: models.RoleUser, Enabled: true} + if err := s.Create(ctx, u, "whatever"); err != users.ErrUsernameTaken { + t.Fatalf("expected ErrUsernameTaken, got %v", err) + } +} + +func TestCannotRemoveLastAdmin(t *testing.T) { + ctx := context.Background() + s := users.NewStore(testDB(t)) + + admin, err := s.GetByUsername(ctx, "admin") + if err != nil { + t.Fatalf("get admin: %v", err) + } + + if err := s.Delete(ctx, admin.ID); err != users.ErrLastAdmin { + t.Fatalf("expected ErrLastAdmin on delete, got %v", err) + } + + admin.Enabled = false + if err := s.Update(ctx, admin); err != users.ErrLastAdmin { + t.Fatalf("expected ErrLastAdmin on disable, got %v", err) + } + + admin.Role = models.RoleUser + admin.Enabled = true + if err := s.Update(ctx, admin); err != users.ErrLastAdmin { + t.Fatalf("expected ErrLastAdmin on demote, got %v", err) + } +} + +func TestSecondAdminAllowsRemovingFirst(t *testing.T) { + ctx := context.Background() + s := users.NewStore(testDB(t)) + + second := &models.User{Username: "second-admin", Role: models.RoleAdmin, Enabled: true} + if err := s.Create(ctx, second, "password123"); err != nil { + t.Fatalf("create second admin: %v", err) + } + + admin, err := s.GetByUsername(ctx, "admin") + if err != nil { + t.Fatalf("get admin: %v", err) + } + if err := s.Delete(ctx, admin.ID); err != nil { + t.Fatalf("expected delete to succeed with a second admin present: %v", err) + } +} diff --git a/internal/web/app.go b/internal/web/app.go new file mode 100644 index 0000000..3e6ccf2 --- /dev/null +++ b/internal/web/app.go @@ -0,0 +1,91 @@ +// Package web implements the HTTP server: routing, session middleware, +// template rendering, and all page/API handlers for inventree. +package web + +import ( + "context" + "database/sql" + "log/slog" + "net/http" + "time" + + "inventree/internal/assets" + "inventree/internal/auth" + "inventree/internal/folders" + "inventree/internal/itemtypes" + "inventree/internal/settings" + "inventree/internal/statuses" + "inventree/internal/users" +) + +// App holds every dependency HTTP handlers need: data stores, templates, and +// server-wide configuration. +type App struct { + DB *sql.DB + + Auth *auth.Store + Users *users.Store + Folders *folders.Store + Assets *assets.Store + ItemTypes *itemtypes.Store + Statuses *statuses.Store + Settings *settings.Store + + templates *templateSet + Log *slog.Logger + + // SecureCookies controls the Secure flag on the session cookie; set + // true when serving over HTTPS. + SecureCookies bool +} + +// NewApp wires all stores together from a shared database handle. +func NewApp(db *sql.DB, log *slog.Logger, secureCookies bool) (*App, error) { + userStore := users.NewStore(db) + folderStore := folders.NewStore(db) + settingsStore := settings.NewStore(db) + + app := &App{ + DB: db, + Users: userStore, + Folders: folderStore, + ItemTypes: itemtypes.NewStore(db), + Statuses: statuses.NewStore(db), + Settings: settingsStore, + Assets: assets.NewStore(db, settingsStore, folderStore), + Auth: auth.NewStore(db, userStore), + Log: log, + SecureCookies: secureCookies, + } + + tmpl, err := loadTemplates() + if err != nil { + return nil, err + } + app.templates = tmpl + + return app, nil +} + +// StartSessionJanitor periodically removes expired sessions until ctx (via +// the returned stop channel) signals shutdown. +func (a *App) StartSessionJanitor(stop <-chan struct{}) { + ticker := time.NewTicker(1 * time.Hour) + go func() { + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := a.Auth.CleanExpired(context.Background()); err != nil { + a.Log.Warn("session cleanup failed", "error", err) + } + case <-stop: + return + } + } + }() +} + +func writeErr(w http.ResponseWriter, status int, msg string) { + http.Error(w, msg, status) +} diff --git a/internal/web/folderoptions.go b/internal/web/folderoptions.go new file mode 100644 index 0000000..b24e25a --- /dev/null +++ b/internal/web/folderoptions.go @@ -0,0 +1,56 @@ +package web + +import "context" + +// FolderOption is a flattened, indented folder choice for . +func (a *App) folderOptions(ctx context.Context) ([]FolderOption, error) { + all, err := a.Folders.All(ctx) + if err != nil { + return nil, err + } + byParent := map[int64][]struct { + ID int64 + Name string + }{} + var top []struct { + ID int64 + Name string + } + for _, f := range all { + item := struct { + ID int64 + Name string + }{f.ID, f.Name} + if f.ParentID == nil { + top = append(top, item) + } else { + byParent[*f.ParentID] = append(byParent[*f.ParentID], item) + } + } + + var out []FolderOption + var walk func(id int64, name, prefix string, depth int) + walk = func(id int64, name, prefix string, depth int) { + path := name + if prefix != "" { + path = prefix + " / " + name + } + out = append(out, FolderOption{ID: id, Path: path, Depth: depth}) + for _, child := range byParent[id] { + walk(child.ID, child.Name, path, depth+1) + } + } + for _, t := range top { + walk(t.ID, t.Name, "", 0) + } + return out, nil +} diff --git a/internal/web/handlers_assets.go b/internal/web/handlers_assets.go new file mode 100644 index 0000000..4ebc03c --- /dev/null +++ b/internal/web/handlers_assets.go @@ -0,0 +1,129 @@ +package web + +import ( + "net/http" + "strconv" + + "inventree/internal/models" +) + +type assetRequest struct { + AssetTag string `json:"asset_id"` + Name string `json:"name"` + ItemTypeID int64 `json:"item_type_id"` + FolderID *int64 `json:"folder_id"` + StatusID int64 `json:"status_id"` + AssignedUserID *int64 `json:"assigned_user_id"` + Notes string `json:"notes"` + Fields map[string]string `json:"fields"` +} + +func (req assetRequest) fieldValues() map[int64]string { + out := make(map[int64]string, len(req.Fields)) + for k, v := range req.Fields { + id, err := strconv.ParseInt(k, 10, 64) + if err != nil { + continue + } + out[id] = v + } + return out +} + +func (a *App) handleAssetCreate(w http.ResponseWriter, r *http.Request) { + var req assetRequest + if err := readJSON(r, &req); err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + asset := &models.Asset{ + AssetTag: req.AssetTag, Name: req.Name, ItemTypeID: req.ItemTypeID, FolderID: req.FolderID, + StatusID: req.StatusID, AssignedUserID: req.AssignedUserID, Notes: req.Notes, + } + if err := a.Assets.Create(r.Context(), asset, req.fieldValues()); err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + jsonOK(w, map[string]any{"id": asset.ID, "asset_id": asset.AssetTag, "folder_id": asset.FolderID}) +} + +func (a *App) handleAssetUpdate(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + var req assetRequest + if err := readJSON(r, &req); err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + existing, err := a.Assets.Get(r.Context(), id) + if err != nil { + jsonErr(w, http.StatusNotFound, err) + return + } + asset := &models.Asset{ + ID: id, AssetTag: req.AssetTag, Name: req.Name, ItemTypeID: existing.ItemTypeID, FolderID: req.FolderID, + StatusID: req.StatusID, AssignedUserID: req.AssignedUserID, Notes: req.Notes, + } + if err := a.Assets.Update(r.Context(), asset, req.fieldValues()); err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + jsonOK(w, map[string]any{"id": asset.ID, "asset_id": asset.AssetTag, "folder_id": asset.FolderID}) +} + +func (a *App) handleAssetDelete(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + asset, err := a.Assets.Get(r.Context(), id) + if err != nil { + jsonErr(w, http.StatusNotFound, err) + return + } + if err := a.Assets.Delete(r.Context(), id); err != nil { + jsonErr(w, http.StatusInternalServerError, err) + return + } + jsonOK(w, map[string]any{"id": id, "folder_id": asset.FolderID}) +} + +func (a *App) handleAssetDuplicate(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + copyAsset, err := a.Assets.Duplicate(r.Context(), id) + if err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + jsonOK(w, map[string]any{"id": copyAsset.ID, "asset_id": copyAsset.AssetTag, "folder_id": copyAsset.FolderID}) +} + +type assetMoveRequest struct { + FolderID *int64 `json:"folder_id"` +} + +func (a *App) handleAssetMove(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + var req assetMoveRequest + if err := readJSON(r, &req); err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + if err := a.Assets.Move(r.Context(), id, req.FolderID); err != nil { + jsonErr(w, http.StatusInternalServerError, err) + return + } + jsonOK(w, map[string]any{"id": id, "folder_id": req.FolderID}) +} diff --git a/internal/web/handlers_auth.go b/internal/web/handlers_auth.go new file mode 100644 index 0000000..b732982 --- /dev/null +++ b/internal/web/handlers_auth.go @@ -0,0 +1,57 @@ +package web + +import ( + "net/http" + + "inventree/internal/auth" +) + +type loginPageData struct { + Error string + Next string +} + +func (a *App) handleLoginForm(w http.ResponseWriter, r *http.Request) { + if _, ok := auth.UserFromContext(r.Context()); ok { + http.Redirect(w, r, "/inventory", http.StatusSeeOther) + return + } + a.Render(w, http.StatusOK, "login", loginPageData{Next: r.URL.Query().Get("next")}) +} + +func (a *App) handleLoginSubmit(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + a.Render(w, http.StatusBadRequest, "login", loginPageData{Error: "Invalid form submission."}) + return + } + username := r.FormValue("username") + password := r.FormValue("password") + next := r.FormValue("next") + + u, err := a.Users.Authenticate(r.Context(), username, password) + if err != nil { + a.Render(w, http.StatusUnauthorized, "login", loginPageData{Error: "Invalid username or password.", Next: next}) + return + } + + token, expires, err := a.Auth.CreateSession(r.Context(), u.ID) + if err != nil { + a.Render(w, http.StatusInternalServerError, "login", loginPageData{Error: "Could not start a session. Please try again."}) + return + } + auth.SetCookie(w, token, expires, a.SecureCookies) + + dest := "/inventory" + if next != "" { + dest = next + } + http.Redirect(w, r, dest, http.StatusSeeOther) +} + +func (a *App) handleLogout(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie(auth.CookieName); err == nil { + _ = a.Auth.DestroySession(r.Context(), cookie.Value) + } + auth.ClearCookie(w, a.SecureCookies) + http.Redirect(w, r, "/login", http.StatusSeeOther) +} diff --git a/internal/web/handlers_csv.go b/internal/web/handlers_csv.go new file mode 100644 index 0000000..96d51a9 --- /dev/null +++ b/internal/web/handlers_csv.go @@ -0,0 +1,70 @@ +package web + +import ( + "fmt" + "net/http" + "time" + + "inventree/internal/assets" +) + +func (a *App) handleExportCSV(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + filter := assets.ListFilter{ + ItemTypeID: queryInt64(r, "item_type_id"), + StatusID: queryInt64(r, "status_id"), + AssignedUserID: queryInt64(r, "assigned_user_id"), + Query: r.URL.Query().Get("q"), + } + if folderID, err := queryOptionalID(r, "folder_id"); err == nil && folderID != nil { + filter.FolderID = folderID + filter.FolderSet = true + filter.IncludeSub = r.URL.Query().Get("recursive") == "1" + } + + filename := fmt.Sprintf("inventree-assets-%s.csv", time.Now().Format("2006-01-02")) + w.Header().Set("Content-Type", "text/csv; charset=utf-8") + w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"") + if err := a.Assets.Export(ctx, w, filter); err != nil { + a.Log.Error("csv export failed", "error", err) + } +} + +func (a *App) handleImportCSV(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if err := r.ParseMultipartForm(32 << 20); err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + file, _, err := r.FormFile("file") + if err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + defer file.Close() + + createMissing := r.FormValue("create_missing_folders") == "1" + + itemTypes, err := a.ItemTypes.List(ctx, true) + if err != nil { + jsonErr(w, http.StatusInternalServerError, err) + return + } + statusList, err := a.Statuses.List(ctx) + if err != nil { + jsonErr(w, http.StatusInternalServerError, err) + return + } + userList, err := a.Users.List(ctx) + if err != nil { + jsonErr(w, http.StatusInternalServerError, err) + return + } + + result, err := a.Assets.Import(ctx, file, assets.ImportOptions{CreateMissingFolders: createMissing}, itemTypes, statusList, userList) + if err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + jsonOK(w, result) +} diff --git a/internal/web/handlers_folders.go b/internal/web/handlers_folders.go new file mode 100644 index 0000000..1161071 --- /dev/null +++ b/internal/web/handlers_folders.go @@ -0,0 +1,214 @@ +package web + +import ( + "errors" + "net/http" + + "inventree/internal/folders" + "inventree/internal/models" +) + +func (a *App) handleFolderGet(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + f, err := a.Folders.Get(r.Context(), id) + if err != nil { + jsonErr(w, http.StatusNotFound, err) + return + } + jsonOK(w, f) +} + +type folderCreateRequest struct { + Name string `json:"name"` + ParentID *int64 `json:"parent_id"` + Description string `json:"description"` + Icon string `json:"icon"` + SortOrder int `json:"sort_order"` +} + +func (a *App) handleFolderCreate(w http.ResponseWriter, r *http.Request) { + var req folderCreateRequest + if err := readJSON(r, &req); err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + f := &models.Folder{Name: req.Name, ParentID: req.ParentID, Description: req.Description, Icon: req.Icon, SortOrder: req.SortOrder} + if err := a.Folders.Create(r.Context(), f); err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + jsonOK(w, f) +} + +type folderUpdateRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon"` + SortOrder int `json:"sort_order"` +} + +func (a *App) handleFolderUpdate(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + var req folderUpdateRequest + if err := readJSON(r, &req); err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + f, err := a.Folders.Get(r.Context(), id) + if err != nil { + jsonErr(w, http.StatusNotFound, err) + return + } + f.Name, f.Description, f.Icon, f.SortOrder = req.Name, req.Description, req.Icon, req.SortOrder + if err := a.Folders.Update(r.Context(), f); err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + jsonOK(w, f) +} + +type folderMoveRequest struct { + ParentID *int64 `json:"parent_id"` +} + +func (a *App) handleFolderMove(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + var req folderMoveRequest + if err := readJSON(r, &req); err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + if err := a.Folders.Move(r.Context(), id, req.ParentID); err != nil { + status := http.StatusBadRequest + if errors.Is(err, folders.ErrInvalidParent) { + status = http.StatusConflict + } + jsonErr(w, status, err) + return + } + jsonOK(w, map[string]any{"id": id, "parent_id": req.ParentID}) +} + +func (a *App) handleFolderDeleteInfo(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id, err := pathID(r, "id") + if err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + folderCount, assetCount, err := a.Folders.Counts(ctx, id) + if err != nil { + jsonErr(w, http.StatusInternalServerError, err) + return + } + f, err := a.Folders.Get(ctx, id) + if err != nil { + jsonErr(w, http.StatusNotFound, err) + return + } + + all, err := a.Folders.All(ctx) + if err != nil { + jsonErr(w, http.StatusInternalServerError, err) + return + } + descendants := map[int64]bool{id: true} + changed := true + for changed { + changed = false + for _, cand := range all { + if cand.ParentID != nil && descendants[*cand.ParentID] && !descendants[cand.ID] { + descendants[cand.ID] = true + changed = true + } + } + } + byID := map[int64]models.Folder{} + for _, cand := range all { + byID[cand.ID] = cand + } + var pathOf func(int64) string + pathOf = func(fid int64) string { + ff, ok := byID[fid] + if !ok { + return "" + } + if ff.ParentID == nil { + return ff.Name + } + return pathOf(*ff.ParentID) + " / " + ff.Name + } + + type dest struct { + ID int64 `json:"id"` + Path string `json:"path"` + } + destinations := []dest{{ID: 0, Path: "Inventory (top level)"}} + for _, cand := range all { + if descendants[cand.ID] { + continue + } + destinations = append(destinations, dest{ID: cand.ID, Path: pathOf(cand.ID)}) + } + + jsonOK(w, map[string]any{ + "folder_name": f.Name, + "folder_count": folderCount, + "asset_count": assetCount, + "destinations": destinations, + }) +} + +func (a *App) handleFolderDelete(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id, err := pathID(r, "id") + if err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + + mode := r.URL.Query().Get("mode") + switch mode { + case "relocate": + var dest *int64 + if raw := r.URL.Query().Get("dest"); raw != "" && raw != "0" { + d, err := queryOptionalID(r, "dest") + if err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + dest = d + } + if err := a.Folders.DeleteAndRelocate(ctx, id, dest); err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + case "cascade": + if err := a.Folders.CascadeDelete(ctx, id); err != nil { + jsonErr(w, http.StatusBadRequest, err) + return + } + default: + if err := a.Folders.Delete(ctx, id); err != nil { + status := http.StatusBadRequest + if errors.Is(err, folders.ErrNotEmpty) { + status = http.StatusConflict + } + jsonErr(w, status, err) + return + } + } + jsonOK(w, map[string]any{"id": id}) +} diff --git a/internal/web/handlers_inventory.go b/internal/web/handlers_inventory.go new file mode 100644 index 0000000..51202bc --- /dev/null +++ b/internal/web/handlers_inventory.go @@ -0,0 +1,353 @@ +package web + +import ( + "context" + "net/http" + + "inventree/internal/assets" + "inventree/internal/models" +) + +// InventoryPageData is the top-level data passed to templates/pages/inventory.html. +type InventoryPageData struct { + User *models.User + Tree []models.TreeNode + Root *RootPanelData + ItemTypes []models.ItemType + Statuses []models.Status +} + +func (a *App) handleInventoryPage(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u := currentUser(r) + + expanded, err := a.expandedFolderIDs(ctx, u.ID) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + tree, err := a.buildTree(ctx, nil, expanded) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + root, err := a.rootPanelData(ctx) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + itemTypes, err := a.ItemTypes.List(ctx, false) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + statusList, err := a.Statuses.List(ctx) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + + a.Render(w, http.StatusOK, "inventory", InventoryPageData{ + User: u, Tree: tree, Root: root, ItemTypes: itemTypes, Statuses: statusList, + }) +} + +// handleTreeChildren serves one level of the tree lazily for AJAX expand. +// When persist=1, the expanded state is remembered for the current user. +func (a *App) handleTreeChildren(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u := currentUser(r) + + parentID, err := queryOptionalID(r, "parent_id") + if err != nil { + writeErr(w, http.StatusBadRequest, "invalid parent_id") + return + } + if r.URL.Query().Get("persist") == "1" && parentID != nil { + if err := a.persistExpand(ctx, u.ID, *parentID); err != nil { + a.Log.Warn("persist expand failed", "error", err) + } + } + + // Honor already-persisted expand state for descendants so that + // re-fetching a branch (e.g. after a drag-and-drop move) doesn't + // visually collapse folders the user had already opened. + expanded, err := a.expandedFolderIDs(ctx, u.ID) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + nodes, err := a.buildTree(ctx, parentID, expanded) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + a.RenderFragment(w, http.StatusOK, "tree_children", nodes) +} + +func (a *App) handleTreeCollapse(w http.ResponseWriter, r *http.Request) { + u := currentUser(r) + if err := r.ParseForm(); err != nil { + writeErr(w, http.StatusBadRequest, "invalid form") + return + } + folderID := queryInt64(r, "folder_id") + if folderID == 0 { + writeErr(w, http.StatusBadRequest, "folder_id is required") + return + } + if err := a.persistCollapse(r.Context(), u.ID, folderID); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + w.WriteHeader(http.StatusNoContent) +} + +func (a *App) handleTreeCollapseAll(w http.ResponseWriter, r *http.Request) { + u := currentUser(r) + if err := a.persistCollapseAll(r.Context(), u.ID); err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// RootPanelData backs the main panel when nothing is selected: the virtual +// "Inventory" root, showing top-level folders, unfiled assets, and the +// small dashboard. +type RootPanelData struct { + Folders []models.Folder + UnfiledAssets []models.Asset + UnfiledTotal int + Stats *assets.DashboardStats +} + +func (a *App) rootPanelData(ctx context.Context) (*RootPanelData, error) { + folderList, err := a.Folders.Children(ctx, nil) + if err != nil { + return nil, err + } + unfiled, total, err := a.Assets.List(ctx, assets.ListFilter{FolderSet: true, FolderID: nil, Page: 1, PageSize: 50}) + if err != nil { + return nil, err + } + stats, err := a.Assets.Stats(ctx) + if err != nil { + return nil, err + } + return &RootPanelData{Folders: folderList, UnfiledAssets: unfiled, UnfiledTotal: total, Stats: stats}, nil +} + +func (a *App) handlePanelRoot(w http.ResponseWriter, r *http.Request) { + data, err := a.rootPanelData(r.Context()) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + a.RenderFragment(w, http.StatusOK, "panel_root", data) +} + +// FolderPanelData backs the main panel when a folder is selected. +type FolderPanelData struct { + Folder *models.Folder + Breadcrumb []models.BreadcrumbItem + Subfolders []models.Folder + Assets []models.Asset + Total int + Page int + PageSize int + ItemTypes []models.ItemType + Statuses []models.Status + FilterType int64 + FilterStat int64 + Query string +} + +func (a *App) handlePanelFolder(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, "invalid folder id") + return + } + folder, err := a.Folders.Get(ctx, id) + if err != nil { + writeErr(w, http.StatusNotFound, err.Error()) + return + } + breadcrumb, err := a.Folders.Path(ctx, id) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + subfolders, err := a.Folders.Children(ctx, &id) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + + page := queryInt(r, "page", 1) + filterType := queryInt64(r, "item_type_id") + filterStatus := queryInt64(r, "status_id") + query := r.URL.Query().Get("q") + + assetList, total, err := a.Assets.List(ctx, assets.ListFilter{ + FolderID: &id, FolderSet: true, ItemTypeID: filterType, StatusID: filterStatus, + Query: query, Page: page, PageSize: 50, + }) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + + itemTypes, err := a.ItemTypes.List(ctx, false) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + statusList, err := a.Statuses.List(ctx) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + + a.RenderFragment(w, http.StatusOK, "panel_folder", FolderPanelData{ + Folder: folder, Breadcrumb: breadcrumb, Subfolders: subfolders, + Assets: assetList, Total: total, Page: page, PageSize: 50, + ItemTypes: itemTypes, Statuses: statusList, + FilterType: filterType, FilterStat: filterStatus, Query: query, + }) +} + +// AssetPanelData backs the main panel when an asset is selected (view mode). +type AssetPanelData struct { + Asset *models.Asset + CanEdit bool + FolderOptions []FolderOption +} + +func (a *App) handlePanelAsset(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, "invalid asset id") + return + } + asset, err := a.Assets.Get(ctx, id) + if err != nil { + writeErr(w, http.StatusNotFound, err.Error()) + return + } + opts, err := a.folderOptions(ctx) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + a.RenderFragment(w, http.StatusOK, "panel_asset", AssetPanelData{Asset: asset, CanEdit: currentUser(r).IsAdmin(), FolderOptions: opts}) +} + +// AssetFormData backs both the "new asset" and "edit asset" forms. +type AssetFormData struct { + Asset *models.Asset // nil when creating + FolderID *int64 + ItemTypes []models.ItemType + Statuses []models.Status + Users []models.User + FolderOptions []FolderOption + SelectedIT int64 + Fields []models.AssetFieldValue +} + +func (a *App) handlePanelAssetNew(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + folderID, err := queryOptionalID(r, "folder_id") + if err != nil { + writeErr(w, http.StatusBadRequest, "invalid folder_id") + return + } + itemTypes, err := a.ItemTypes.List(ctx, false) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + statusList, err := a.Statuses.List(ctx) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + userList, err := a.Users.List(ctx) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + folderOpts, err := a.folderOptions(ctx) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + + var selectedIT int64 + var fields []models.AssetFieldValue + if len(itemTypes) > 0 { + selectedIT = itemTypes[0].ID + fields = fieldValuesForCreate(itemTypes[0].Fields) + } + + a.RenderFragment(w, http.StatusOK, "asset_form", AssetFormData{ + FolderID: folderID, ItemTypes: itemTypes, Statuses: statusList, Users: userList, FolderOptions: folderOpts, + SelectedIT: selectedIT, Fields: fields, + }) +} + +func (a *App) handlePanelAssetEdit(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, "invalid asset id") + return + } + asset, err := a.Assets.Get(ctx, id) + if err != nil { + writeErr(w, http.StatusNotFound, err.Error()) + return + } + itemTypes, err := a.ItemTypes.List(ctx, true) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + statusList, err := a.Statuses.List(ctx) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + userList, err := a.Users.List(ctx) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + folderOpts, err := a.folderOptions(ctx) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + + a.RenderFragment(w, http.StatusOK, "asset_form", AssetFormData{ + Asset: asset, ItemTypes: itemTypes, Statuses: statusList, Users: userList, FolderOptions: folderOpts, + SelectedIT: asset.ItemTypeID, Fields: asset.CustomFields, + }) +} + +// handleAssetFieldsFragment renders just the dynamic custom-field inputs for +// a chosen item type, used when the item type dropdown changes on the +// "new asset" form. +func (a *App) handleAssetFieldsFragment(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + itemTypeID := queryInt64(r, "item_type_id") + it, err := a.ItemTypes.Get(ctx, itemTypeID) + if err != nil { + writeErr(w, http.StatusNotFound, err.Error()) + return + } + a.RenderFragment(w, http.StatusOK, "asset_fields_only", fieldValuesForCreate(it.Fields)) +} diff --git a/internal/web/handlers_itemtypes.go b/internal/web/handlers_itemtypes.go new file mode 100644 index 0000000..95218c3 --- /dev/null +++ b/internal/web/handlers_itemtypes.go @@ -0,0 +1,202 @@ +package web + +import ( + "net/http" + "strconv" + "strings" + + "inventree/internal/models" +) + +type itemTypesListData struct { + User *models.User + ItemTypes []models.ItemType +} + +func (a *App) handleItemTypesList(w http.ResponseWriter, r *http.Request) { + types, err := a.ItemTypes.List(r.Context(), true) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + a.Render(w, http.StatusOK, "itemtypes_list", itemTypesListData{User: currentUser(r), ItemTypes: types}) +} + +type itemTypeFormData struct { + User *models.User + ItemType *models.ItemType + FieldTypes []models.FieldType + Error string +} + +func (a *App) handleItemTypeNewForm(w http.ResponseWriter, r *http.Request) { + a.Render(w, http.StatusOK, "itemtypes_form", itemTypeFormData{User: currentUser(r), FieldTypes: models.AllFieldTypes}) +} + +func (a *App) handleItemTypeCreate(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + it := &models.ItemType{ + Name: r.FormValue("name"), Description: r.FormValue("description"), + Icon: r.FormValue("icon"), Enabled: r.FormValue("enabled") == "on", SortOrder: atoiDefault(r.FormValue("sort_order"), 0), + } + if err := a.ItemTypes.Create(r.Context(), it); err != nil { + a.Render(w, http.StatusBadRequest, "itemtypes_form", itemTypeFormData{User: currentUser(r), ItemType: it, FieldTypes: models.AllFieldTypes, Error: err.Error()}) + return + } + http.Redirect(w, r, "/item-types/"+strconv.FormatInt(it.ID, 10)+"/edit", http.StatusSeeOther) +} + +func (a *App) handleItemTypeEditForm(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + it, err := a.ItemTypes.Get(r.Context(), id) + if err != nil { + writeErr(w, http.StatusNotFound, err.Error()) + return + } + a.Render(w, http.StatusOK, "itemtypes_form", itemTypeFormData{User: currentUser(r), ItemType: it, FieldTypes: models.AllFieldTypes}) +} + +func (a *App) handleItemTypeUpdate(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + if err := r.ParseForm(); err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + it, err := a.ItemTypes.Get(r.Context(), id) + if err != nil { + writeErr(w, http.StatusNotFound, err.Error()) + return + } + it.Name = r.FormValue("name") + it.Description = r.FormValue("description") + it.Icon = r.FormValue("icon") + it.Enabled = r.FormValue("enabled") == "on" + it.SortOrder = atoiDefault(r.FormValue("sort_order"), it.SortOrder) + if err := a.ItemTypes.Update(r.Context(), it); err != nil { + a.Render(w, http.StatusBadRequest, "itemtypes_form", itemTypeFormData{User: currentUser(r), ItemType: it, FieldTypes: models.AllFieldTypes, Error: err.Error()}) + return + } + http.Redirect(w, r, "/item-types/"+strconv.FormatInt(id, 10)+"/edit", http.StatusSeeOther) +} + +func (a *App) handleItemTypeDelete(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + if err := a.ItemTypes.Delete(r.Context(), id); err != nil { + http.Redirect(w, r, "/item-types/"+strconv.FormatInt(id, 10)+"/edit?error="+err.Error(), http.StatusSeeOther) + return + } + http.Redirect(w, r, "/item-types", http.StatusSeeOther) +} + +func (a *App) handleFieldCreate(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + if err := r.ParseForm(); err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + f := &models.CustomFieldDef{ + ItemTypeID: id, Name: r.FormValue("name"), FieldType: models.FieldType(r.FormValue("field_type")), + Required: r.FormValue("required") == "on", DefaultValue: r.FormValue("default_value"), + SortOrder: atoiDefault(r.FormValue("sort_order"), 0), + } + if opts := r.FormValue("options"); opts != "" { + f.Options = splitLines(opts) + } + if err := a.ItemTypes.CreateField(r.Context(), f); err != nil { + a.Log.Warn("create field failed", "error", err) + } + http.Redirect(w, r, "/item-types/"+strconv.FormatInt(id, 10)+"/edit", http.StatusSeeOther) +} + +func (a *App) handleFieldUpdate(w http.ResponseWriter, r *http.Request) { + itemTypeID, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + fieldID, err := pathID(r, "fieldID") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + if err := r.ParseForm(); err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + f, err := a.ItemTypes.GetField(r.Context(), fieldID) + if err != nil { + writeErr(w, http.StatusNotFound, err.Error()) + return + } + f.Name = r.FormValue("name") + f.FieldType = models.FieldType(r.FormValue("field_type")) + f.Required = r.FormValue("required") == "on" + f.DefaultValue = r.FormValue("default_value") + f.SortOrder = atoiDefault(r.FormValue("sort_order"), f.SortOrder) + f.Options = nil + if opts := r.FormValue("options"); opts != "" { + f.Options = splitLines(opts) + } + if err := a.ItemTypes.UpdateField(r.Context(), f); err != nil { + a.Log.Warn("update field failed", "error", err) + } + http.Redirect(w, r, "/item-types/"+strconv.FormatInt(itemTypeID, 10)+"/edit", http.StatusSeeOther) +} + +func (a *App) handleFieldDelete(w http.ResponseWriter, r *http.Request) { + itemTypeID, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + fieldID, err := pathID(r, "fieldID") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + if err := a.ItemTypes.DeleteField(r.Context(), fieldID); err != nil { + a.Log.Warn("delete field failed", "error", err) + } + http.Redirect(w, r, "/item-types/"+strconv.FormatInt(itemTypeID, 10)+"/edit", http.StatusSeeOther) +} + +func atoiDefault(s string, def int) int { + if s == "" { + return def + } + n, err := strconv.Atoi(s) + if err != nil { + return def + } + return n +} + +func splitLines(s string) []string { + var out []string + for _, line := range strings.Split(strings.ReplaceAll(s, "\r\n", "\n"), "\n") { + line = strings.TrimSpace(line) + if line != "" { + out = append(out, line) + } + } + return out +} diff --git a/internal/web/handlers_search.go b/internal/web/handlers_search.go new file mode 100644 index 0000000..97c1176 --- /dev/null +++ b/internal/web/handlers_search.go @@ -0,0 +1,22 @@ +package web + +import ( + "net/http" + + "inventree/internal/models" +) + +type SearchPanelData struct { + Query string + Results []models.Asset +} + +func (a *App) handleSearch(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query().Get("q") + results, err := a.Assets.Search(r.Context(), q, 50) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + a.RenderFragment(w, http.StatusOK, "panel_search", SearchPanelData{Query: q, Results: results}) +} diff --git a/internal/web/handlers_settings.go b/internal/web/handlers_settings.go new file mode 100644 index 0000000..73c8828 --- /dev/null +++ b/internal/web/handlers_settings.go @@ -0,0 +1,156 @@ +package web + +import ( + "net/http" + "strconv" + + "inventree/internal/models" +) + +type settingsPageData struct { + User *models.User + Statuses []models.Status + SiteName string + AssetPrefix string + AssetPadding string + Error string +} + +func (a *App) settingsData(r *http.Request) (settingsPageData, error) { + ctx := r.Context() + statusList, err := a.Statuses.List(ctx) + if err != nil { + return settingsPageData{}, err + } + siteName, err := a.Settings.Get(ctx, "site_name", "inventree") + if err != nil { + return settingsPageData{}, err + } + prefix, err := a.Settings.Get(ctx, "asset_id_prefix", "IT-") + if err != nil { + return settingsPageData{}, err + } + padding, err := a.Settings.Get(ctx, "asset_id_padding", "6") + if err != nil { + return settingsPageData{}, err + } + return settingsPageData{ + User: currentUser(r), Statuses: statusList, SiteName: siteName, AssetPrefix: prefix, AssetPadding: padding, + }, nil +} + +func (a *App) handleSettingsPage(w http.ResponseWriter, r *http.Request) { + data, err := a.settingsData(r) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + a.Render(w, http.StatusOK, "settings", data) +} + +func (a *App) handleSettingsGeneral(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + ctx := r.Context() + _ = a.Settings.Set(ctx, "site_name", r.FormValue("site_name")) + _ = a.Settings.Set(ctx, "asset_id_prefix", r.FormValue("asset_id_prefix")) + if _, err := strconv.Atoi(r.FormValue("asset_id_padding")); err == nil { + _ = a.Settings.Set(ctx, "asset_id_padding", r.FormValue("asset_id_padding")) + } + http.Redirect(w, r, "/settings", http.StatusSeeOther) +} + +func (a *App) handleStatusCreate(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + st := &models.Status{Name: r.FormValue("name"), Color: r.FormValue("color")} + if err := a.Statuses.Create(r.Context(), st); err != nil { + a.Log.Warn("create status failed", "error", err) + } + http.Redirect(w, r, "/settings", http.StatusSeeOther) +} + +func (a *App) handleStatusUpdate(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + if err := r.ParseForm(); err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + st, err := a.Statuses.Get(r.Context(), id) + if err != nil { + writeErr(w, http.StatusNotFound, err.Error()) + return + } + st.Name = r.FormValue("name") + st.Color = r.FormValue("color") + if err := a.Statuses.Update(r.Context(), st); err != nil { + a.Log.Warn("update status failed", "error", err) + } + http.Redirect(w, r, "/settings", http.StatusSeeOther) +} + +func (a *App) handleStatusDelete(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + if err := a.Statuses.Delete(r.Context(), id); err != nil { + a.Log.Warn("delete status failed", "error", err) + } + http.Redirect(w, r, "/settings", http.StatusSeeOther) +} + +func (a *App) handleStatusReorder(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + if err := r.ParseForm(); err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + dir := r.FormValue("dir") + + list, err := a.Statuses.List(ctx) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + idx := -1 + for i, st := range list { + if st.ID == id { + idx = i + break + } + } + if idx >= 0 { + swapWith := -1 + if dir == "up" && idx > 0 { + swapWith = idx - 1 + } else if dir == "down" && idx < len(list)-1 { + swapWith = idx + 1 + } + if swapWith >= 0 { + ids := make([]int64, len(list)) + for i, st := range list { + ids[i] = st.ID + } + ids[idx], ids[swapWith] = ids[swapWith], ids[idx] + if err := a.Statuses.Reorder(ctx, ids); err != nil { + a.Log.Warn("reorder statuses failed", "error", err) + } + } + } + http.Redirect(w, r, "/settings", http.StatusSeeOther) +} diff --git a/internal/web/handlers_users.go b/internal/web/handlers_users.go new file mode 100644 index 0000000..3017780 --- /dev/null +++ b/internal/web/handlers_users.go @@ -0,0 +1,127 @@ +package web + +import ( + "net/http" + "strconv" + + "inventree/internal/models" +) + +type usersListData struct { + User *models.User + Users []models.User +} + +func (a *App) handleUsersList(w http.ResponseWriter, r *http.Request) { + list, err := a.Users.List(r.Context()) + if err != nil { + writeErr(w, http.StatusInternalServerError, err.Error()) + return + } + a.Render(w, http.StatusOK, "users_list", usersListData{User: currentUser(r), Users: list}) +} + +type userFormData struct { + User *models.User + EditUser *models.User + Error string +} + +func (a *App) handleUserNewForm(w http.ResponseWriter, r *http.Request) { + a.Render(w, http.StatusOK, "users_form", userFormData{User: currentUser(r)}) +} + +func (a *App) handleUserCreate(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + role := models.RoleUser + if r.FormValue("role") == "admin" { + role = models.RoleAdmin + } + u := &models.User{Username: r.FormValue("username"), Role: role, Enabled: r.FormValue("enabled") == "on"} + password := r.FormValue("password") + if password == "" { + a.Render(w, http.StatusBadRequest, "users_form", userFormData{User: currentUser(r), EditUser: u, Error: "Password is required."}) + return + } + if err := a.Users.Create(r.Context(), u, password); err != nil { + a.Render(w, http.StatusBadRequest, "users_form", userFormData{User: currentUser(r), EditUser: u, Error: err.Error()}) + return + } + http.Redirect(w, r, "/users", http.StatusSeeOther) +} + +func (a *App) handleUserEditForm(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + u, err := a.Users.Get(r.Context(), id) + if err != nil { + writeErr(w, http.StatusNotFound, err.Error()) + return + } + a.Render(w, http.StatusOK, "users_form", userFormData{User: currentUser(r), EditUser: u}) +} + +func (a *App) handleUserUpdate(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + if err := r.ParseForm(); err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + u, err := a.Users.Get(r.Context(), id) + if err != nil { + writeErr(w, http.StatusNotFound, err.Error()) + return + } + u.Username = r.FormValue("username") + u.Role = models.RoleUser + if r.FormValue("role") == "admin" { + u.Role = models.RoleAdmin + } + u.Enabled = r.FormValue("enabled") == "on" + if err := a.Users.Update(r.Context(), u); err != nil { + a.Render(w, http.StatusBadRequest, "users_form", userFormData{User: currentUser(r), EditUser: u, Error: err.Error()}) + return + } + http.Redirect(w, r, "/users", http.StatusSeeOther) +} + +func (a *App) handleUserPassword(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + if err := r.ParseForm(); err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + password := r.FormValue("password") + if password != "" { + if err := a.Users.SetPassword(r.Context(), id, password); err != nil { + a.Log.Warn("set password failed", "error", err) + } + } + http.Redirect(w, r, "/users/"+strconv.FormatInt(id, 10)+"/edit", http.StatusSeeOther) +} + +func (a *App) handleUserDelete(w http.ResponseWriter, r *http.Request) { + id, err := pathID(r, "id") + if err != nil { + writeErr(w, http.StatusBadRequest, err.Error()) + return + } + if err := a.Users.Delete(r.Context(), id); err != nil { + a.Log.Warn("delete user failed", "error", err) + } + http.Redirect(w, r, "/users", http.StatusSeeOther) +} diff --git a/internal/web/helpers.go b/internal/web/helpers.go new file mode 100644 index 0000000..67dba2a --- /dev/null +++ b/internal/web/helpers.go @@ -0,0 +1,74 @@ +package web + +import ( + "encoding/json" + "net/http" + "strconv" +) + +func pathID(r *http.Request, name string) (int64, error) { + return strconv.ParseInt(r.PathValue(name), 10, 64) +} + +// queryOptionalID parses a query parameter as *int64; an empty or missing +// value returns nil (meaning "top level" / "unfiled" in folder/asset +// contexts). +func queryOptionalID(r *http.Request, name string) (*int64, error) { + v := r.URL.Query().Get(name) + if v == "" { + return nil, nil + } + id, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return nil, err + } + return &id, nil +} + +func queryInt64(r *http.Request, name string) int64 { + v := r.URL.Query().Get(name) + if v == "" { + return 0 + } + id, _ := strconv.ParseInt(v, 10, 64) + return id +} + +func queryInt(r *http.Request, name string, def int) int { + v := r.URL.Query().Get(name) + if v == "" { + return def + } + n, err := strconv.Atoi(v) + if err != nil { + return def + } + return n +} + +type jsonEnvelope struct { + OK bool `json:"ok"` + Error string `json:"error,omitempty"` + Message string `json:"message,omitempty"` + Data any `json:"data,omitempty"` +} + +func writeJSON(w http.ResponseWriter, status int, v jsonEnvelope) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(v) +} + +func jsonOK(w http.ResponseWriter, data any) { + writeJSON(w, http.StatusOK, jsonEnvelope{OK: true, Data: data}) +} + +func jsonErr(w http.ResponseWriter, status int, err error) { + writeJSON(w, status, jsonEnvelope{OK: false, Error: err.Error()}) +} + +func readJSON(r *http.Request, v any) error { + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + return dec.Decode(v) +} diff --git a/internal/web/middleware.go b/internal/web/middleware.go new file mode 100644 index 0000000..2c9bb6f --- /dev/null +++ b/internal/web/middleware.go @@ -0,0 +1,65 @@ +package web + +import ( + "net/http" + + "inventree/internal/auth" + "inventree/internal/models" +) + +// loadSession attaches the current user (if any) to the request context +// based on the session cookie. It never blocks the request. +func (a *App) loadSession(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie(auth.CookieName) + if err != nil || cookie.Value == "" { + next.ServeHTTP(w, r) + return + } + u, err := a.Auth.UserForToken(r.Context(), cookie.Value) + if err != nil { + next.ServeHTTP(w, r) + return + } + r = r.WithContext(auth.WithUser(r.Context(), u)) + next.ServeHTTP(w, r) + }) +} + +// requireLogin redirects unauthenticated page requests to /login, and +// returns 401 for AJAX/API requests instead. +func (a *App) requireLogin(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if _, ok := auth.UserFromContext(r.Context()); !ok { + if isAjax(r) { + writeErr(w, http.StatusUnauthorized, "not authenticated") + return + } + http.Redirect(w, r, "/login?next="+r.URL.Path, http.StatusSeeOther) + return + } + next(w, r) + } +} + +// requireAdmin additionally requires the current user to hold the admin +// role. +func (a *App) requireAdmin(next http.HandlerFunc) http.HandlerFunc { + return a.requireLogin(func(w http.ResponseWriter, r *http.Request) { + u, _ := auth.UserFromContext(r.Context()) + if !u.IsAdmin() { + writeErr(w, http.StatusForbidden, "admin access required") + return + } + next(w, r) + }) +} + +func isAjax(r *http.Request) bool { + return r.Header.Get("X-Requested-With") == "XMLHttpRequest" || r.Header.Get("HX-Request") != "" +} + +func currentUser(r *http.Request) *models.User { + u, _ := auth.UserFromContext(r.Context()) + return u +} diff --git a/internal/web/render.go b/internal/web/render.go new file mode 100644 index 0000000..a6729c1 --- /dev/null +++ b/internal/web/render.go @@ -0,0 +1,180 @@ +package web + +import ( + "fmt" + "html/template" + "io/fs" + "net/http" + "path" + "strings" + "time" + + content "inventree/web" +) + +// templateFuncs are available to every template (pages and fragments). +var templateFuncs = template.FuncMap{ + "dict": func(pairs ...any) (map[string]any, error) { + if len(pairs)%2 != 0 { + return nil, fmt.Errorf("dict: odd number of arguments") + } + m := make(map[string]any, len(pairs)/2) + for i := 0; i < len(pairs); i += 2 { + key, ok := pairs[i].(string) + if !ok { + return nil, fmt.Errorf("dict: key %v is not a string", pairs[i]) + } + m[key] = pairs[i+1] + } + return m, nil + }, + "add": func(a, b int) int { return a + b }, + "sub": func(a, b int) int { return a - b }, + "mul": func(a, b int) int { return a * b }, + "ceilDiv": func(a, b int) int { + if b <= 0 { + return 0 + } + return (a + b - 1) / b + }, + "seq": func(n int) []int { + out := make([]int, n) + for i := range out { + out[i] = i + } + return out + }, + "seqFrom1": func(n int) []int { + out := make([]int, n) + for i := range out { + out[i] = i + 1 + } + return out + }, + "fmtDate": func(t time.Time) string { + if t.IsZero() { + return "" + } + return t.Format("2006-01-02 15:04") + }, + "fmtDateShort": func(t time.Time) string { + if t.IsZero() { + return "" + } + return t.Format("Jan 2, 2006") + }, + "iconOr": func(icon, fallback string) string { + if icon == "" { + return fallback + } + return icon + }, + "hasPrefix": strings.HasPrefix, + "lower": strings.ToLower, + "list": func(items ...string) []string { return items }, + "deref": func(p *int64) int64 { + if p == nil { + return 0 + } + return *p + }, +} + +// templateSet bundles the shared fragment/partial template collection with +// one isolated template set per full page (see loadTemplates for why pages +// must be isolated from one another). +type templateSet struct { + shared *template.Template // layout.html + partials/*.html; used to render AJAX fragments directly + pages map[string]*template.Template // page name -> shared clone + that page's templates/pages/.html + login *template.Template // standalone minimal layout for the login screen +} + +// loadTemplates parses layout.html and every partial once into a shared +// template set, then clones that set once per page in templates/pages/ so +// each page can define a block named "content" without colliding with the +// others (html/template templates sharing one *template.Template must have +// unique names, but isolated clones may reuse the same name freely). +func loadTemplates() (*templateSet, error) { + shared := template.New("root").Funcs(templateFuncs) + + shared, err := shared.ParseFS(content.TemplatesFS, "templates/layout.html") + if err != nil { + return nil, fmt.Errorf("parse layout: %w", err) + } + shared, err = parseGlob(shared, "templates/partials") + if err != nil { + return nil, fmt.Errorf("parse partials: %w", err) + } + + pageFiles, err := fs.Glob(content.TemplatesFS, "templates/pages/*.html") + if err != nil { + return nil, err + } + + pages := map[string]*template.Template{} + for _, pf := range pageFiles { + name := strings.TrimSuffix(path.Base(pf), ".html") + clone, err := shared.Clone() + if err != nil { + return nil, fmt.Errorf("clone template set for %s: %w", name, err) + } + if _, err := clone.ParseFS(content.TemplatesFS, pf); err != nil { + return nil, fmt.Errorf("parse page %s: %w", pf, err) + } + pages[name] = clone + } + + login := template.New("login-root").Funcs(templateFuncs) + login, err = login.ParseFS(content.TemplatesFS, "templates/login.html") + if err != nil { + return nil, fmt.Errorf("parse login: %w", err) + } + + return &templateSet{shared: shared, pages: pages, login: login}, nil +} + +func parseGlob(t *template.Template, dir string) (*template.Template, error) { + files, err := fs.Glob(content.TemplatesFS, dir+"/*.html") + if err != nil { + return nil, err + } + if len(files) == 0 { + return t, nil + } + return t.ParseFS(content.TemplatesFS, files...) +} + +// Render executes the named page's "layout" template (which pulls in that +// page's "content" block) into the response. Page "login" uses its own +// minimal "login" template instead. +func (a *App) Render(w http.ResponseWriter, status int, page string, data any) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + if page == "login" { + w.WriteHeader(status) + if err := a.templates.login.ExecuteTemplate(w, "login", data); err != nil { + a.Log.Error("template render failed", "page", page, "error", err) + } + return + } + + ts, ok := a.templates.pages[page] + if !ok { + writeErr(w, http.StatusInternalServerError, "template not found: "+page) + return + } + w.WriteHeader(status) + if err := ts.ExecuteTemplate(w, "layout", data); err != nil { + a.Log.Error("template render failed", "page", page, "error", err) + } +} + +// RenderFragment executes a single named partial (defined under +// templates/partials/) with no layout wrapper, for AJAX responses. +func (a *App) RenderFragment(w http.ResponseWriter, status int, name string, data any) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(status) + if err := a.templates.shared.ExecuteTemplate(w, name, data); err != nil { + a.Log.Error("fragment render failed", "name", name, "error", err) + } +} diff --git a/internal/web/router.go b/internal/web/router.go new file mode 100644 index 0000000..7ebef1f --- /dev/null +++ b/internal/web/router.go @@ -0,0 +1,104 @@ +package web + +import ( + "net/http" + + content "inventree/web" +) + +// Router builds the complete HTTP handler for inventree: static assets, +// authentication, the AJAX-driven inventory tree, and the traditional +// full-page admin sections (item types, users, settings). +func (a *App) Router() http.Handler { + mux := http.NewServeMux() + + // Static assets (Bootstrap, icons, vanilla JS) and health check are + // always public. + mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServerFS(mustSub(content.StaticFS, "static")))) + mux.HandleFunc("GET /healthz", a.handleHealthz) + + // Auth + mux.HandleFunc("GET /login", a.handleLoginForm) + mux.HandleFunc("POST /login", a.handleLoginSubmit) + mux.HandleFunc("POST /logout", a.handleLogout) + + mux.HandleFunc("GET /{$}", a.requireLogin(a.handleRoot)) + + // Inventory tree (AJAX fragments) + mux.HandleFunc("GET /inventory", a.requireLogin(a.handleInventoryPage)) + mux.HandleFunc("GET /app/tree/children", a.requireLogin(a.handleTreeChildren)) + mux.HandleFunc("POST /app/tree/collapse", a.requireLogin(a.handleTreeCollapse)) + mux.HandleFunc("POST /app/tree/collapse-all", a.requireLogin(a.handleTreeCollapseAll)) + + mux.HandleFunc("GET /app/panel/root", a.requireLogin(a.handlePanelRoot)) + mux.HandleFunc("GET /app/panel/folders/{id}", a.requireLogin(a.handlePanelFolder)) + mux.HandleFunc("GET /app/panel/assets/{id}", a.requireLogin(a.handlePanelAsset)) + mux.HandleFunc("GET /app/panel/assets/new", a.requireAdmin(a.handlePanelAssetNew)) + mux.HandleFunc("GET /app/panel/assets/{id}/edit", a.requireAdmin(a.handlePanelAssetEdit)) + mux.HandleFunc("GET /app/forms/asset-fields", a.requireLogin(a.handleAssetFieldsFragment)) + + mux.HandleFunc("GET /app/search", a.requireLogin(a.handleSearch)) + + // Folder mutations (JSON in/out) + mux.HandleFunc("GET /app/folders/{id}", a.requireAdmin(a.handleFolderGet)) + mux.HandleFunc("POST /app/folders", a.requireAdmin(a.handleFolderCreate)) + mux.HandleFunc("PATCH /app/folders/{id}", a.requireAdmin(a.handleFolderUpdate)) + mux.HandleFunc("POST /app/folders/{id}/move", a.requireAdmin(a.handleFolderMove)) + mux.HandleFunc("GET /app/folders/{id}/delete-info", a.requireAdmin(a.handleFolderDeleteInfo)) + mux.HandleFunc("DELETE /app/folders/{id}", a.requireAdmin(a.handleFolderDelete)) + + // Asset mutations (JSON in/out) + mux.HandleFunc("POST /app/assets", a.requireAdmin(a.handleAssetCreate)) + mux.HandleFunc("PATCH /app/assets/{id}", a.requireAdmin(a.handleAssetUpdate)) + mux.HandleFunc("DELETE /app/assets/{id}", a.requireAdmin(a.handleAssetDelete)) + mux.HandleFunc("POST /app/assets/{id}/duplicate", a.requireAdmin(a.handleAssetDuplicate)) + mux.HandleFunc("POST /app/assets/{id}/move", a.requireAdmin(a.handleAssetMove)) + + // CSV import/export + mux.HandleFunc("GET /export/assets.csv", a.requireLogin(a.handleExportCSV)) + mux.HandleFunc("POST /import/assets.csv", a.requireAdmin(a.handleImportCSV)) + + // Item types (admin, traditional pages) + mux.HandleFunc("GET /item-types", a.requireAdmin(a.handleItemTypesList)) + mux.HandleFunc("GET /item-types/new", a.requireAdmin(a.handleItemTypeNewForm)) + mux.HandleFunc("POST /item-types", a.requireAdmin(a.handleItemTypeCreate)) + mux.HandleFunc("GET /item-types/{id}/edit", a.requireAdmin(a.handleItemTypeEditForm)) + mux.HandleFunc("POST /item-types/{id}", a.requireAdmin(a.handleItemTypeUpdate)) + mux.HandleFunc("POST /item-types/{id}/delete", a.requireAdmin(a.handleItemTypeDelete)) + mux.HandleFunc("POST /item-types/{id}/fields", a.requireAdmin(a.handleFieldCreate)) + mux.HandleFunc("POST /item-types/{id}/fields/{fieldID}", a.requireAdmin(a.handleFieldUpdate)) + mux.HandleFunc("POST /item-types/{id}/fields/{fieldID}/delete", a.requireAdmin(a.handleFieldDelete)) + + // Users (admin, traditional pages) + mux.HandleFunc("GET /users", a.requireAdmin(a.handleUsersList)) + mux.HandleFunc("GET /users/new", a.requireAdmin(a.handleUserNewForm)) + mux.HandleFunc("POST /users", a.requireAdmin(a.handleUserCreate)) + mux.HandleFunc("GET /users/{id}/edit", a.requireAdmin(a.handleUserEditForm)) + mux.HandleFunc("POST /users/{id}", a.requireAdmin(a.handleUserUpdate)) + mux.HandleFunc("POST /users/{id}/password", a.requireAdmin(a.handleUserPassword)) + mux.HandleFunc("POST /users/{id}/delete", a.requireAdmin(a.handleUserDelete)) + + // Settings (admin, traditional page) + mux.HandleFunc("GET /settings", a.requireAdmin(a.handleSettingsPage)) + mux.HandleFunc("POST /settings/general", a.requireAdmin(a.handleSettingsGeneral)) + mux.HandleFunc("POST /settings/statuses", a.requireAdmin(a.handleStatusCreate)) + mux.HandleFunc("POST /settings/statuses/{id}", a.requireAdmin(a.handleStatusUpdate)) + mux.HandleFunc("POST /settings/statuses/{id}/delete", a.requireAdmin(a.handleStatusDelete)) + mux.HandleFunc("POST /settings/statuses/{id}/reorder", a.requireAdmin(a.handleStatusReorder)) + + return a.loadSession(mux) +} + +func (a *App) handleHealthz(w http.ResponseWriter, r *http.Request) { + if err := a.DB.PingContext(r.Context()); err != nil { + writeErr(w, http.StatusServiceUnavailable, "database unavailable") + return + } + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) +} + +func (a *App) handleRoot(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/inventory", http.StatusSeeOther) +} diff --git a/internal/web/static.go b/internal/web/static.go new file mode 100644 index 0000000..e5f1c89 --- /dev/null +++ b/internal/web/static.go @@ -0,0 +1,14 @@ +package web + +import "io/fs" + +// mustSub returns an FS rooted at dir within root, panicking on error (dir +// is always a compile-time-known embedded path, so this can only fail if the +// embed itself is misconfigured). +func mustSub(root fs.FS, dir string) fs.FS { + sub, err := fs.Sub(root, dir) + if err != nil { + panic(err) + } + return sub +} diff --git a/internal/web/tree.go b/internal/web/tree.go new file mode 100644 index 0000000..180dc83 --- /dev/null +++ b/internal/web/tree.go @@ -0,0 +1,116 @@ +package web + +import ( + "context" + "fmt" + + "inventree/internal/assets" + "inventree/internal/models" +) + +// treeAssetLimit bounds how many assets are rendered directly inside a +// single tree node before showing a "N more" placeholder, so a folder with +// thousands of assets doesn't blow up the tree fragment. +const treeAssetLimit = 300 + +// expandedFolderIDs returns the set of folder IDs the given user has +// persisted as expanded. +func (a *App) expandedFolderIDs(ctx context.Context, userID int64) (map[int64]bool, error) { + rows, err := a.DB.QueryContext(ctx, `SELECT folder_id FROM user_tree_state WHERE user_id = ?`, userID) + if err != nil { + return nil, err + } + defer rows.Close() + out := map[int64]bool{} + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + out[id] = true + } + return out, rows.Err() +} + +func (a *App) persistExpand(ctx context.Context, userID, folderID int64) error { + _, err := a.DB.ExecContext(ctx, + `INSERT INTO user_tree_state (user_id, folder_id) VALUES (?, ?) ON CONFLICT(user_id, folder_id) DO NOTHING`, + userID, folderID) + return err +} + +func (a *App) persistCollapse(ctx context.Context, userID, folderID int64) error { + _, err := a.DB.ExecContext(ctx, `DELETE FROM user_tree_state WHERE user_id = ? AND folder_id = ?`, userID, folderID) + return err +} + +func (a *App) persistCollapseAll(ctx context.Context, userID int64) error { + _, err := a.DB.ExecContext(ctx, `DELETE FROM user_tree_state WHERE user_id = ?`, userID) + return err +} + +// buildTree returns the ordered list of tree nodes (subfolders then assets) +// directly under parentID. Folders present in the expanded set are +// recursively expanded inline; others are returned collapsed for the client +// to lazy-load on demand. +func (a *App) buildTree(ctx context.Context, parentID *int64, expanded map[int64]bool) ([]models.TreeNode, error) { + childFolders, err := a.Folders.Children(ctx, parentID) + if err != nil { + return nil, err + } + + var out []models.TreeNode + for _, f := range childFolders { + node := models.TreeNode{ + Type: "folder", + ID: f.ID, + Name: f.Name, + Icon: f.Icon, + ParentID: f.ParentID, + HasChildren: f.FolderCount > 0 || f.AssetCount > 0, + } + if expanded[f.ID] { + node.Expanded = true + children, err := a.buildTree(ctx, &f.ID, expanded) + if err != nil { + return nil, err + } + node.Children = children + } + out = append(out, node) + } + + assetList, total, err := a.Assets.List(ctx, assets.ListFilter{ + FolderID: parentID, FolderSet: true, Page: 1, PageSize: treeAssetLimit, + }) + if err != nil { + return nil, err + } + for _, as := range assetList { + out = append(out, models.TreeNode{ + Type: "asset", + ID: as.ID, + Name: as.Name, + Icon: as.ItemTypeIcon, + AssetTag: as.AssetTag, + StatusName: as.StatusName, + StatusColor: as.StatusColor, + }) + } + if total > len(assetList) { + out = append(out, models.TreeNode{ + Type: "more", + Name: fmt.Sprintf("%d more asset(s) — open this folder to view all", total-len(assetList)), + }) + } + + return out, nil +} + +func fieldValuesForCreate(fields []models.CustomFieldDef) []models.AssetFieldValue { + out := make([]models.AssetFieldValue, len(fields)) + for i, f := range fields { + out[i] = models.AssetFieldValue{Def: f, Value: f.DefaultValue} + } + return out +} diff --git a/internal/web/web_test.go b/internal/web/web_test.go new file mode 100644 index 0000000..1cdf375 --- /dev/null +++ b/internal/web/web_test.go @@ -0,0 +1,216 @@ +package web_test + +import ( + "bytes" + "encoding/json" + "io" + "log/slog" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + + "inventree/internal/database" + "inventree/internal/models" + "inventree/internal/web" +) + +func testApp(t *testing.T) *web.App { + t.Helper() + dir := t.TempDir() + db, err := database.Open(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("open db: %v", err) + } + t.Cleanup(func() { db.Close() }) + + log := slog.New(slog.NewTextHandler(io.Discard, nil)) + app, err := web.NewApp(db, log, false) + if err != nil { + t.Fatalf("new app: %v", err) + } + return app +} + +// loginAs signs in and returns the session cookie for subsequent requests. +func loginAs(t *testing.T, srv *httptest.Server, username, password string) *http.Cookie { + t.Helper() + form := "username=" + username + "&password=" + password + req, _ := http.NewRequest(http.MethodPost, srv.URL+"/login", strings.NewReader(form)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }} + res, err := client.Do(req) + if err != nil { + t.Fatalf("login request: %v", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusSeeOther { + t.Fatalf("expected redirect on login, got %d", res.StatusCode) + } + for _, c := range res.Cookies() { + if c.Name == "inventree_session" { + return c + } + } + t.Fatal("no session cookie set") + return nil +} + +func TestHealthz(t *testing.T) { + app := testApp(t) + srv := httptest.NewServer(app.Router()) + defer srv.Close() + + res, err := http.Get(srv.URL + "/healthz") + if err != nil { + t.Fatalf("get: %v", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + t.Fatalf("expected 200, got %d", res.StatusCode) + } +} + +func TestLoginAndInventoryPage(t *testing.T) { + app := testApp(t) + srv := httptest.NewServer(app.Router()) + defer srv.Close() + + cookie := loginAs(t, srv, "admin", "admin") + + req, _ := http.NewRequest(http.MethodGet, srv.URL+"/inventory", nil) + req.AddCookie(cookie) + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("get inventory: %v", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + t.Fatalf("expected 200, got %d", res.StatusCode) + } + body, _ := io.ReadAll(res.Body) + if !bytes.Contains(body, []byte("Inventory")) { + t.Fatalf("expected inventory page content") + } +} + +func TestUnauthenticatedRedirectsToLogin(t *testing.T) { + app := testApp(t) + srv := httptest.NewServer(app.Router()) + defer srv.Close() + + client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }} + res, err := client.Get(srv.URL + "/inventory") + if err != nil { + t.Fatalf("get: %v", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusSeeOther { + t.Fatalf("expected redirect to login, got %d", res.StatusCode) + } +} + +func TestFolderCreateRequiresAdmin(t *testing.T) { + app := testApp(t) + srv := httptest.NewServer(app.Router()) + defer srv.Close() + + // Create a normal (non-admin) user directly via the store. + normalUser := &models.User{Username: "normie", Role: models.RoleUser, Enabled: true} + if err := app.Users.Create(t.Context(), normalUser, "normiepass"); err != nil { + t.Fatalf("create user: %v", err) + } + + cookie := loginAs(t, srv, "normie", "normiepass") + + payload := `{"name":"Should Fail"}` + req, _ := http.NewRequest(http.MethodPost, srv.URL+"/app/folders", strings.NewReader(payload)) + req.Header.Set("Content-Type", "application/json") + req.AddCookie(cookie) + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("post folder: %v", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusForbidden { + t.Fatalf("expected 403 for non-admin folder create, got %d", res.StatusCode) + } +} + +func TestFolderAndAssetLifecycleAsAdmin(t *testing.T) { + app := testApp(t) + srv := httptest.NewServer(app.Router()) + defer srv.Close() + + cookie := loginAs(t, srv, "admin", "admin") + + folderID := postJSON(t, srv, cookie, "/app/folders", `{"name":"Main Office"}`)["ID"].(float64) + + assetPayload := `{"name":"Core Switch","item_type_id":4,"folder_id":` + itoa(int64(folderID)) + `,"status_id":1,"fields":{}}` + req, _ := http.NewRequest(http.MethodPost, srv.URL+"/app/assets", strings.NewReader(assetPayload)) + req.Header.Set("Content-Type", "application/json") + req.AddCookie(cookie) + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("post asset: %v", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + body, _ := io.ReadAll(res.Body) + t.Fatalf("expected 200 creating asset, got %d: %s", res.StatusCode, body) + } + var envelope struct { + Data struct { + AssetID string `json:"asset_id"` + } `json:"data"` + } + if err := json.NewDecoder(res.Body).Decode(&envelope); err != nil { + t.Fatalf("decode: %v", err) + } + if envelope.Data.AssetID != "IT-000001" { + t.Fatalf("expected auto-generated IT-000001, got %s", envelope.Data.AssetID) + } +} + +func postJSON(t *testing.T, srv *httptest.Server, cookie *http.Cookie, path, payload string) map[string]any { + t.Helper() + req, _ := http.NewRequest(http.MethodPost, srv.URL+path, strings.NewReader(payload)) + req.Header.Set("Content-Type", "application/json") + req.AddCookie(cookie) + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("post %s: %v", path, err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + body, _ := io.ReadAll(res.Body) + t.Fatalf("expected 200 for %s, got %d: %s", path, res.StatusCode, body) + } + var envelope struct { + Data map[string]any `json:"data"` + } + if err := json.NewDecoder(res.Body).Decode(&envelope); err != nil { + t.Fatalf("decode: %v", err) + } + return envelope.Data +} + +func itoa(n int64) string { + if n == 0 { + return "0" + } + neg := n < 0 + if neg { + n = -n + } + var b []byte + for n > 0 { + b = append([]byte{byte('0' + n%10)}, b...) + n /= 10 + } + if neg { + b = append([]byte{'-'}, b...) + } + return string(b) +} diff --git a/web/content.go b/web/content.go new file mode 100644 index 0000000..b491497 --- /dev/null +++ b/web/content.go @@ -0,0 +1,11 @@ +// Package content embeds inventree's HTML templates and static assets +// (Bootstrap CSS/JS, icons, vanilla JS) into the compiled binary. +package content + +import "embed" + +//go:embed templates +var TemplatesFS embed.FS + +//go:embed static +var StaticFS embed.FS diff --git a/web/static/css/app.css b/web/static/css/app.css new file mode 100644 index 0000000..cc4fe57 --- /dev/null +++ b/web/static/css/app.css @@ -0,0 +1,184 @@ +:root { + --tree-indent: 1.15rem; +} + +body { + background-color: #f7f8fa; +} + +.app-navbar { + background-color: #fff; +} + +.login-body { + min-height: 100vh; + background: linear-gradient(180deg, #eef1f6 0%, #f7f8fa 100%); +} + +/* Admin-only UI: shown only when carries the role-admin class. */ +.admin-only { display: none !important; } +body.role-admin .admin-only { display: inherit !important; } +body.role-admin button.admin-only, +body.role-admin a.admin-only { display: inline-block !important; } + +.inventory-shell { + height: calc(100vh - 56px); + min-height: 480px; +} + +.tree-pane { + width: 340px; + min-width: 260px; + max-width: 45vw; + background-color: #fff; + flex-shrink: 0; +} + +.tree-scroll { + overflow-y: auto; +} + +.main-panel { + background-color: #fff; +} + +ul.tree, ul.tree-children { + list-style: none; + margin: 0; + padding-left: 0; +} + +ul.tree-children { + padding-left: var(--tree-indent); +} + +.tree-node-root > .tree-row { + font-weight: 600; +} + +.tree-row { + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.2rem 0.4rem; + border-radius: 0.25rem; + cursor: pointer; + white-space: nowrap; + user-select: none; +} + +.tree-row:hover { + background-color: #f1f3f5; +} + +.tree-row.selected { + background-color: #e7f1ff; + color: #0d6efd; + font-weight: 500; +} + +.tree-row.drop-target { + outline: 2px dashed #0d6efd; + outline-offset: -2px; + background-color: #e7f1ff; +} + +.tree-row-more { + cursor: default; +} + +.tree-toggle { + border: none; + background: none; + padding: 0; + width: 1.1rem; + height: 1.1rem; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: #6c757d; +} + +.tree-toggle i { + transition: transform 0.12s ease; +} + +.tree-toggle.expanded i { + transform: rotate(90deg); +} + +.tree-toggle-spacer { + width: 1.1rem; + flex-shrink: 0; + display: inline-block; +} + +.tree-icon { + flex-shrink: 0; +} + +.tree-label { + overflow: hidden; + text-overflow: ellipsis; + flex: 1 1 auto; +} + +.tree-status-badge { + font-size: 0.65rem; + flex-shrink: 0; +} + +.tree-actions { + flex-shrink: 0; +} + +.btn-tree-action { + border: none; + background: none; + color: #6c757d; + padding: 0 0.25rem; +} + +.btn-tree-action:hover { + color: #212529; +} + +.tree-node.dragging { + opacity: 0.4; +} + +.stat-card .card-body { + text-align: center; +} + +.stat-value { + font-size: 1.4rem; + font-weight: 600; + line-height: 1.2; +} + +.stat-label { + font-size: 0.75rem; + color: #6c757d; +} + +.panel-content { + max-width: 1100px; +} + +.asset-row, .folder-link, .asset-link { + cursor: pointer; +} + +@media (max-width: 767.98px) { + .inventory-shell { + flex-direction: column; + height: auto; + } + .tree-pane { + width: 100%; + max-width: 100%; + max-height: 40vh; + } +} diff --git a/web/static/css/bootstrap.min.css b/web/static/css/bootstrap.min.css new file mode 100644 index 0000000..3993414 --- /dev/null +++ b/web/static/css/bootstrap.min.css @@ -0,0 +1,6 @@ +@charset "UTF-8";/*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 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-check:checked:focus-visible+.btn{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='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%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='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--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;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-header .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-header .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-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-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}.accordion-flush>.accordion-item>.accordion-collapse{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;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;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:calc(-.5 * var(--bs-offcanvas-padding-y)) calc(-.5 * var(--bs-offcanvas-padding-x)) calc(-.5 * var(--bs-offcanvas-padding-y)) auto}.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/web/static/icons/bootstrap-icons.css b/web/static/icons/bootstrap-icons.css new file mode 100644 index 0000000..dadd6dc --- /dev/null +++ b/web/static/icons/bootstrap-icons.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap Icons v1.11.3 (https://icons.getbootstrap.com/) + * Copyright 2019-2024 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?dd67030699838ea613ee6dbda90effa6") format("woff2"),url("fonts/bootstrap-icons.woff?dd67030699838ea613ee6dbda90effa6") 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/web/static/icons/fonts/bootstrap-icons.woff b/web/static/icons/fonts/bootstrap-icons.woff new file mode 100644 index 0000000..51204d2 Binary files /dev/null and b/web/static/icons/fonts/bootstrap-icons.woff differ diff --git a/web/static/icons/fonts/bootstrap-icons.woff2 b/web/static/icons/fonts/bootstrap-icons.woff2 new file mode 100644 index 0000000..92c4830 Binary files /dev/null and b/web/static/icons/fonts/bootstrap-icons.woff2 differ diff --git a/web/static/js/app.js b/web/static/js/app.js new file mode 100644 index 0000000..b7186a5 --- /dev/null +++ b/web/static/js/app.js @@ -0,0 +1,722 @@ +// inventree - vanilla JS driving the inventory tree and AJAX panels. +// No build step, no frameworks: plain DOM APIs + fetch. +(function () { + 'use strict'; + + var treePane = document.getElementById('tree-pane'); + var mainPanel = document.getElementById('main-panel'); + var isAdmin = document.body.classList.contains('role-admin'); + var draggedNode = null; + var formReturnContext = { type: 'root', id: '' }; + + // --------------------------------------------------------------------- + // Generic helpers + // --------------------------------------------------------------------- + + function escapeHtml(s) { + return String(s).replace(/[&<>"']/g, function (c) { + return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; + }); + } + + async function fetchText(url) { + var res = await fetch(url); + if (!res.ok) throw new Error('Request failed (' + res.status + ')'); + return res.text(); + } + + async function apiFetch(url, opts) { + opts = opts || {}; + var fetchOpts = { method: opts.method || 'GET', headers: {} }; + if (opts.json !== undefined) { + fetchOpts.headers['Content-Type'] = 'application/json'; + fetchOpts.body = JSON.stringify(opts.json); + } + var res = await fetch(url, fetchOpts); + var body = null; + var ct = res.headers.get('content-type') || ''; + if (ct.indexOf('application/json') !== -1) { + body = await res.json(); + } + if (!res.ok) { + var msg = (body && body.error) ? body.error : ('Request failed (' + res.status + ')'); + throw new Error(msg); + } + return body ? body.data : null; + } + + // --------------------------------------------------------------------- + // Main panel navigation + // --------------------------------------------------------------------- + + async function loadPanel(type, id) { + var url; + if (type === 'root') url = '/app/panel/root'; + else if (type === 'folder') url = '/app/panel/folders/' + id; + else if (type === 'asset') url = '/app/panel/assets/' + id; + else return; + var html = await fetchText(url); + mainPanel.innerHTML = html; + mainPanel.dataset.contextType = type; + mainPanel.dataset.contextId = id || ''; + } + + async function reloadCurrentPanel() { + var type = mainPanel.dataset.contextType || 'root'; + var id = mainPanel.dataset.contextId || ''; + if (type !== 'root' && type !== 'folder' && type !== 'asset') return; + await loadPanel(type, id); + } + + function selectTreeRowVisually(type, id) { + document.querySelectorAll('.tree-row.selected').forEach(function (r) { r.classList.remove('selected'); }); + var row; + if (type === 'root') { + row = document.querySelector('.tree-row[data-type="root"]'); + } else { + var node = document.querySelector('.tree-node[data-type="' + type + '"][data-id="' + id + '"]'); + row = node ? node.querySelector(':scope > .tree-row') : null; + } + if (row) row.classList.add('selected'); + } + + async function navigateAndSelect(type, id) { + await loadPanel(type, id); + selectTreeRowVisually(type, id); + } + + async function selectNode(row) { + document.querySelectorAll('.tree-row.selected').forEach(function (r) { r.classList.remove('selected'); }); + row.classList.add('selected'); + var type = row.dataset.type; + var id = row.dataset.id || ''; + await loadPanel(type, id); + } + + // --------------------------------------------------------------------- + // Tree: refresh / expand / collapse + // --------------------------------------------------------------------- + + async function refreshWholeTree() { + var ul = document.getElementById('top-level-children'); + if (!ul) return; + var html = await fetchText('/app/tree/children'); + ul.innerHTML = html; + } + + async function toggleFolder(toggleBtn) { + var li = toggleBtn.closest('.tree-node'); + var childUl = li.querySelector(':scope > ul.tree-children'); + var id = toggleBtn.dataset.id; + var expanded = toggleBtn.classList.contains('expanded'); + if (expanded) { + toggleBtn.classList.remove('expanded'); + if (childUl) childUl.classList.add('d-none'); + fetch('/app/tree/collapse', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'folder_id=' + encodeURIComponent(id), + }); + } else { + toggleBtn.classList.add('expanded'); + if (childUl) { + childUl.classList.remove('d-none'); + var html = await fetchText('/app/tree/children?parent_id=' + encodeURIComponent(id) + '&persist=1'); + childUl.innerHTML = html; + } + } + } + + async function expandAllUnder(ul) { + var folderLis = Array.prototype.filter.call(ul.children, function (li) { + return li.dataset && li.dataset.type === 'folder'; + }); + for (var i = 0; i < folderLis.length; i++) { + var li = folderLis[i]; + var toggle = li.querySelector(':scope > .tree-row > .tree-toggle'); + if (toggle && !toggle.classList.contains('expanded')) { + await toggleFolder(toggle); + } + var childUl = li.querySelector(':scope > ul.tree-children'); + if (childUl) await expandAllUnder(childUl); + } + } + + async function expandAllHandler() { + var top = document.getElementById('top-level-children'); + if (top) await expandAllUnder(top); + } + + async function collapseAllHandler() { + document.querySelectorAll('.tree-toggle.expanded').forEach(function (t) { + t.classList.remove('expanded'); + var li = t.closest('.tree-node'); + var childUl = li ? li.querySelector(':scope > ul.tree-children') : null; + if (childUl) childUl.classList.add('d-none'); + }); + try { await fetch('/app/tree/collapse-all', { method: 'POST' }); } catch (e) { /* ignore */ } + } + + // --------------------------------------------------------------------- + // Tree context menu ("...") + // --------------------------------------------------------------------- + + var ctxMenu = null; + + function ensureCtxMenu() { + if (ctxMenu) return ctxMenu; + ctxMenu = document.createElement('div'); + ctxMenu.className = 'dropdown-menu tree-ctx-menu'; + ctxMenu.style.position = 'fixed'; + ctxMenu.style.display = 'none'; + ctxMenu.innerHTML = + '' + + '' + + '' + + ''; + document.body.appendChild(ctxMenu); + return ctxMenu; + } + + function closeCtxMenu() { + if (ctxMenu) ctxMenu.style.display = 'none'; + } + + function openTreeContextMenu(btn) { + var menu = ensureCtxMenu(); + var rect = btn.getBoundingClientRect(); + menu.style.left = Math.min(rect.left, window.innerWidth - 200) + 'px'; + menu.style.top = rect.bottom + 'px'; + menu.style.display = 'block'; + menu.dataset.folderId = btn.dataset.id; + } + + document.addEventListener('click', function (e) { + if (ctxMenu && ctxMenu.style.display === 'block' && !ctxMenu.contains(e.target) && !e.target.closest('.btn-tree-action')) { + closeCtxMenu(); + } + var menuAction = e.target.closest('[data-menu-action]'); + if (menuAction && ctxMenu) { + var id = ctxMenu.dataset.folderId; + var action = menuAction.dataset.menuAction; + closeCtxMenu(); + if (action === 'new-subfolder') openFolderModal({ parentId: id }); + else if (action === 'new-asset') loadAssetForm({ folderId: id }); + else if (action === 'rename') openRenameFolder(id); + else if (action === 'delete') startDeleteFolder(id); + } + }); + + // --------------------------------------------------------------------- + // Folder create / rename modal + // --------------------------------------------------------------------- + + var folderModalEl = document.getElementById('folderModal'); + var folderModal = (folderModalEl && window.bootstrap) ? new bootstrap.Modal(folderModalEl) : null; + + function openFolderModal(opts) { + if (!folderModal) return; + document.getElementById('folderFormId').value = ''; + document.getElementById('folderFormParentId').value = (opts && opts.parentId) || ''; + document.getElementById('folderFormName').value = ''; + document.getElementById('folderFormDescription').value = ''; + document.getElementById('folderFormIcon').value = ''; + document.getElementById('folderFormSortOrder').value = '0'; + document.getElementById('folderModalTitle').textContent = 'New Folder'; + document.getElementById('folderFormError').classList.add('d-none'); + folderModal.show(); + } + + async function openRenameFolder(id) { + if (!folderModal) return; + try { + var f = await apiFetch('/app/folders/' + id); + document.getElementById('folderFormId').value = f.ID; + document.getElementById('folderFormParentId').value = ''; + document.getElementById('folderFormName').value = f.Name; + document.getElementById('folderFormDescription').value = f.Description; + document.getElementById('folderFormIcon').value = f.Icon; + document.getElementById('folderFormSortOrder').value = f.SortOrder; + document.getElementById('folderModalTitle').textContent = 'Rename Folder'; + document.getElementById('folderFormError').classList.add('d-none'); + folderModal.show(); + } catch (err) { + alert('Could not load folder: ' + err.message); + } + } + + var folderFormEl = document.getElementById('folderForm'); + if (folderFormEl) { + folderFormEl.addEventListener('submit', async function (e) { + e.preventDefault(); + var id = document.getElementById('folderFormId').value; + var payload = { + name: document.getElementById('folderFormName').value, + description: document.getElementById('folderFormDescription').value, + icon: document.getElementById('folderFormIcon').value, + sort_order: Number(document.getElementById('folderFormSortOrder').value || 0), + }; + var errorBox = document.getElementById('folderFormError'); + errorBox.classList.add('d-none'); + try { + if (id) { + await apiFetch('/app/folders/' + id, { method: 'PATCH', json: payload }); + } else { + var parentId = document.getElementById('folderFormParentId').value; + payload.parent_id = parentId ? Number(parentId) : null; + await apiFetch('/app/folders', { method: 'POST', json: payload }); + } + folderModal.hide(); + await refreshWholeTree(); + await reloadCurrentPanel(); + } catch (err) { + errorBox.textContent = err.message; + errorBox.classList.remove('d-none'); + } + }); + } + + // --------------------------------------------------------------------- + // Folder delete modal + // --------------------------------------------------------------------- + + var deleteFolderModalEl = document.getElementById('deleteFolderModal'); + var deleteFolderModal = (deleteFolderModalEl && window.bootstrap) ? new bootstrap.Modal(deleteFolderModalEl) : null; + + async function startDeleteFolder(id) { + if (!deleteFolderModal) return; + try { + var info = await apiFetch('/app/folders/' + id + '/delete-info'); + deleteFolderModalEl.dataset.folderId = id; + document.getElementById('deleteFolderName').textContent = info.folder_name; + var empty = info.folder_count === 0 && info.asset_count === 0; + document.getElementById('deleteFolderEmptyNotice').classList.toggle('d-none', !empty); + document.getElementById('deleteFolderNotEmptyNotice').classList.toggle('d-none', empty); + if (!empty) { + document.getElementById('deleteFolderCounts').textContent = + info.folder_count + ' folder(s) and ' + info.asset_count + ' asset(s)'; + var sel = document.getElementById('deleteFolderDestination'); + sel.innerHTML = info.destinations.map(function (d) { + return ''; + }).join(''); + document.getElementById('modeRelocate').checked = true; + } + deleteFolderModal.show(); + } catch (err) { + alert('Could not load folder info: ' + err.message); + } + } + + var confirmDeleteFolderBtn = document.getElementById('confirmDeleteFolderBtn'); + if (confirmDeleteFolderBtn) { + confirmDeleteFolderBtn.addEventListener('click', async function () { + var id = deleteFolderModalEl.dataset.folderId; + var notEmpty = !document.getElementById('deleteFolderNotEmptyNotice').classList.contains('d-none'); + var url = '/app/folders/' + id; + if (notEmpty) { + var mode = document.querySelector('input[name="delete_mode"]:checked').value; + if (mode === 'cascade') { + url += '?mode=cascade'; + } else { + var dest = document.getElementById('deleteFolderDestination').value; + url += '?mode=relocate&dest=' + encodeURIComponent(dest); + } + } + try { + await apiFetch(url, { method: 'DELETE' }); + deleteFolderModal.hide(); + await refreshWholeTree(); + await navigateAndSelect('root', ''); + } catch (err) { + alert('Delete failed: ' + err.message); + } + }); + } + + // --------------------------------------------------------------------- + // Asset form (create / edit) + // --------------------------------------------------------------------- + + async function loadAssetForm(opts) { + formReturnContext = { type: mainPanel.dataset.contextType || 'root', id: mainPanel.dataset.contextId || '' }; + var params = new URLSearchParams(); + if (opts && opts.folderId) params.set('folder_id', opts.folderId); + var qs = params.toString(); + var html = await fetchText('/app/panel/assets/new' + (qs ? '?' + qs : '')); + mainPanel.innerHTML = html; + mainPanel.dataset.contextType = 'asset-form'; + mainPanel.dataset.contextId = ''; + } + + async function loadAssetEditForm(id) { + var html = await fetchText('/app/panel/assets/' + id + '/edit'); + mainPanel.innerHTML = html; + mainPanel.dataset.contextType = 'asset-form'; + mainPanel.dataset.contextId = id; + } + + function cancelAssetForm(btn) { + if (btn.dataset.id) { + navigateAndSelect('asset', btn.dataset.id); + } else { + navigateAndSelect(formReturnContext.type, formReturnContext.id); + } + } + + async function submitAssetForm(form) { + var errorBox = document.getElementById('assetFormError'); + errorBox.classList.add('d-none'); + var fd = new FormData(form); + var payload = { fields: {} }; + for (var pair of fd.entries()) { + var key = pair[0], value = pair[1]; + if (key.indexOf('field_') === 0) { + payload.fields[key.slice(6)] = value; + } else if (key === 'item_type_id') { + payload.item_type_id = Number(value); + } else if (key === 'folder_id') { + payload.folder_id = value ? Number(value) : null; + } else if (key === 'status_id') { + payload.status_id = Number(value); + } else if (key === 'assigned_user_id') { + payload.assigned_user_id = value ? Number(value) : null; + } else { + payload[key] = value; + } + } + var mode = form.dataset.mode; + var url = mode === 'edit' ? '/app/assets/' + form.dataset.assetId : '/app/assets'; + var method = mode === 'edit' ? 'PATCH' : 'POST'; + try { + var res = await apiFetch(url, { method: method, json: payload }); + await refreshWholeTree(); + await navigateAndSelect('asset', res.id); + } catch (err) { + errorBox.textContent = err.message; + errorBox.classList.remove('d-none'); + } + } + + async function duplicateAsset(id) { + try { + var res = await apiFetch('/app/assets/' + id + '/duplicate', { method: 'POST' }); + await refreshWholeTree(); + await navigateAndSelect('asset', res.id); + } catch (err) { + alert('Duplicate failed: ' + err.message); + } + } + + async function deleteAsset(id, name) { + if (!confirm('Delete asset "' + name + '"? This cannot be undone.')) return; + try { + var res = await apiFetch('/app/assets/' + id, { method: 'DELETE' }); + await refreshWholeTree(); + if (res.folder_id) await navigateAndSelect('folder', res.folder_id); + else await navigateAndSelect('root', ''); + } catch (err) { + alert('Delete failed: ' + err.message); + } + } + + async function confirmMoveAsset(id, selectId) { + var select = document.getElementById(selectId); + var val = select.value; + try { + await apiFetch('/app/assets/' + id + '/move', { method: 'POST', json: { folder_id: val ? Number(val) : null } }); + var modalEl = select.closest('.modal'); + if (modalEl && window.bootstrap) { + var inst = bootstrap.Modal.getInstance(modalEl); + if (inst) inst.hide(); + } + await refreshWholeTree(); + await navigateAndSelect('asset', id); + } catch (err) { + alert('Move failed: ' + err.message); + } + } + + // --------------------------------------------------------------------- + // Folder listing filters / pagination + // --------------------------------------------------------------------- + + async function loadFolderPanelWithFilters(folderId, overrides) { + var form = mainPanel.querySelector('.panel-filter-form'); + var params = new URLSearchParams(); + if (form) { + var fd = new FormData(form); + for (var pair of fd.entries()) { + if (pair[1]) params.set(pair[0], pair[1]); + } + } + if (overrides) { + for (var k in overrides) params.set(k, overrides[k]); + } + var qs = params.toString(); + var html = await fetchText('/app/panel/folders/' + folderId + (qs ? '?' + qs : '')); + mainPanel.innerHTML = html; + mainPanel.dataset.contextType = 'folder'; + mainPanel.dataset.contextId = folderId; + } + + // --------------------------------------------------------------------- + // Search + // --------------------------------------------------------------------- + + async function runSearch(q) { + if (!q || !q.trim()) return; + var html = await fetchText('/app/search?q=' + encodeURIComponent(q)); + mainPanel.innerHTML = html; + mainPanel.dataset.contextType = 'search'; + mainPanel.dataset.contextId = ''; + document.querySelectorAll('.tree-row.selected').forEach(function (r) { r.classList.remove('selected'); }); + } + + var treeSearchEl = document.getElementById('tree-search'); + if (treeSearchEl) { + var searchDebounce; + treeSearchEl.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + runSearch(treeSearchEl.value); + } + }); + treeSearchEl.addEventListener('input', function () { + clearTimeout(searchDebounce); + var q = treeSearchEl.value; + if (!q) return; + searchDebounce = setTimeout(function () { runSearch(q); }, 400); + }); + } + + // --------------------------------------------------------------------- + // CSV import + // --------------------------------------------------------------------- + + var importFormEl = document.getElementById('importForm'); + if (importFormEl) { + importFormEl.addEventListener('submit', async function (e) { + e.preventDefault(); + var resultBox = document.getElementById('importResult'); + resultBox.innerHTML = '
Importing…
'; + var fd = new FormData(importFormEl); + try { + var res = await fetch('/import/assets.csv', { method: 'POST', body: fd }); + var body = await res.json(); + if (!res.ok || !body.ok) throw new Error(body.error || 'Import failed'); + var data = body.data; + var html = '
Created ' + data.Created + ', updated ' + data.Updated + '.
'; + if (data.Errors && data.Errors.length) { + html += '
' + data.Errors.length + ' note(s):
    ' + + data.Errors.map(function (m) { return '
  • ' + escapeHtml(m) + '
  • '; }).join('') + '
'; + } + resultBox.innerHTML = html; + await refreshWholeTree(); + await reloadCurrentPanel(); + } catch (err) { + resultBox.innerHTML = '
' + escapeHtml(err.message) + '
'; + } + }); + } + + // --------------------------------------------------------------------- + // Tree pane: click delegation + // --------------------------------------------------------------------- + + if (treePane) { + treePane.addEventListener('click', function (e) { + if (e.target.closest('#btn-expand-all')) { expandAllHandler(); return; } + if (e.target.closest('#btn-collapse-all')) { collapseAllHandler(); return; } + if (e.target.closest('#btn-new-root-folder')) { openFolderModal({ parentId: null }); return; } + + var toggle = e.target.closest('.tree-toggle'); + if (toggle) { e.stopPropagation(); toggleFolder(toggle); return; } + + var actionBtn = e.target.closest('.btn-tree-action'); + if (actionBtn) { e.stopPropagation(); openTreeContextMenu(actionBtn); return; } + + var row = e.target.closest('.tree-row'); + if (row && row.dataset.type && !row.classList.contains('tree-row-more')) { + selectNode(row); + } + }); + + // Drag and drop (admin only) + treePane.addEventListener('dragstart', function (e) { + if (!isAdmin) { e.preventDefault(); return; } + var li = e.target.closest('.tree-node[draggable="true"]'); + if (!li) return; + draggedNode = { type: li.dataset.type, id: li.dataset.id }; + li.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', draggedNode.type + ':' + draggedNode.id); + }); + + treePane.addEventListener('dragend', function () { + document.querySelectorAll('.tree-node.dragging').forEach(function (n) { n.classList.remove('dragging'); }); + document.querySelectorAll('.tree-row.drop-target').forEach(function (n) { n.classList.remove('drop-target'); }); + draggedNode = null; + }); + + treePane.addEventListener('dragover', function (e) { + if (!draggedNode) return; + var row = e.target.closest('.tree-row'); + if (!row) return; + var type = row.dataset.type; + if (type !== 'folder' && type !== 'root') return; + if (type === 'folder' && draggedNode.type === 'folder' && draggedNode.id === row.dataset.id) return; + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + row.classList.add('drop-target'); + }); + + treePane.addEventListener('dragleave', function (e) { + var row = e.target.closest('.tree-row'); + if (row) row.classList.remove('drop-target'); + }); + + treePane.addEventListener('drop', async function (e) { + if (!draggedNode) return; + var row = e.target.closest('.tree-row'); + if (!row) return; + var type = row.dataset.type; + if (type !== 'folder' && type !== 'root') return; + e.preventDefault(); + row.classList.remove('drop-target'); + var destId = type === 'root' ? null : Number(row.dataset.id); + var dragged = draggedNode; + draggedNode = null; + try { + if (dragged.type === 'folder') { + await apiFetch('/app/folders/' + dragged.id + '/move', { method: 'POST', json: { parent_id: destId } }); + } else if (dragged.type === 'asset') { + await apiFetch('/app/assets/' + dragged.id + '/move', { method: 'POST', json: { folder_id: destId } }); + } + await refreshWholeTree(); + await reloadCurrentPanel(); + } catch (err) { + alert('Move failed: ' + err.message); + } + }); + } + + // --------------------------------------------------------------------- + // Main panel: click / change / submit delegation + // --------------------------------------------------------------------- + + if (mainPanel) { + mainPanel.addEventListener('click', function (e) { + var folderLink = e.target.closest('.folder-link'); + if (folderLink) { e.preventDefault(); navigateAndSelect('folder', folderLink.dataset.id); return; } + + var assetLink = e.target.closest('.asset-link, .asset-row'); + if (assetLink) { e.preventDefault(); navigateAndSelect('asset', assetLink.dataset.id); return; } + + var crumb = e.target.closest('.breadcrumb-link'); + if (crumb) { e.preventDefault(); navigateAndSelect(crumb.dataset.type, crumb.dataset.id || ''); return; } + + var pageLink = e.target.closest('.panel-page-link'); + if (pageLink) { e.preventDefault(); loadFolderPanelWithFilters(pageLink.dataset.folderId, { page: pageLink.dataset.page }); return; } + + var newSub = e.target.closest('[data-action="new-subfolder"]'); + if (newSub) { openFolderModal({ parentId: newSub.dataset.id }); return; } + + var renameBtn = e.target.closest('[data-action="rename-folder"]'); + if (renameBtn) { openRenameFolder(renameBtn.dataset.id); return; } + + var deleteBtn = e.target.closest('[data-action="delete-folder"]'); + if (deleteBtn) { startDeleteFolder(deleteBtn.dataset.id); return; } + + var newAssetBtn = e.target.closest('[data-action="new-asset"]'); + if (newAssetBtn) { loadAssetForm({ folderId: newAssetBtn.dataset.folderId }); return; } + + var editAssetBtn = e.target.closest('[data-action="edit-asset"]'); + if (editAssetBtn) { loadAssetEditForm(editAssetBtn.dataset.id); return; } + + var dupBtn = e.target.closest('[data-action="duplicate-asset"]'); + if (dupBtn) { duplicateAsset(dupBtn.dataset.id); return; } + + var delAssetBtn = e.target.closest('[data-action="delete-asset"]'); + if (delAssetBtn) { deleteAsset(delAssetBtn.dataset.id, delAssetBtn.dataset.name); return; } + + var confirmMoveBtn = e.target.closest('[data-action="confirm-move-asset"]'); + if (confirmMoveBtn) { confirmMoveAsset(confirmMoveBtn.dataset.id, confirmMoveBtn.dataset.select); return; } + + var cancelFormBtn = e.target.closest('[data-action="cancel-asset-form"]'); + if (cancelFormBtn) { cancelAssetForm(cancelFormBtn); return; } + }); + + mainPanel.addEventListener('change', async function (e) { + if (e.target.id === 'assetFormItemType') { + var html = await fetchText('/app/forms/asset-fields?item_type_id=' + e.target.value); + var container = document.getElementById('asset-fields-container'); + if (container) container.innerHTML = html; + return; + } + var filterForm = e.target.closest('.panel-filter-form'); + if (filterForm) { + loadFolderPanelWithFilters(filterForm.dataset.folderId, { page: 1 }); + } + }); + + var filterDebounce; + mainPanel.addEventListener('input', function (e) { + if (e.target.matches && e.target.matches('.panel-filter-form input[name="q"]')) { + clearTimeout(filterDebounce); + var form = e.target.closest('.panel-filter-form'); + filterDebounce = setTimeout(function () { loadFolderPanelWithFilters(form.dataset.folderId, { page: 1 }); }, 350); + } + }); + + mainPanel.addEventListener('submit', function (e) { + if (e.target.matches && e.target.matches('.panel-filter-form')) { + e.preventDefault(); + loadFolderPanelWithFilters(e.target.dataset.folderId, { page: 1 }); + return; + } + if (e.target.id === 'assetForm') { + e.preventDefault(); + submitAssetForm(e.target); + } + }); + + // Assets/folders are also draggable as drop-into-root targets from the + // panel view when browsing (drag onto the tree's root row still works + // via the tree pane handler above); dragstart on panel rows is not + // implemented since the tree is the primary drag surface. + } + + // --------------------------------------------------------------------- + // Item type custom field admin form (item-types/{id}/edit page) + // --------------------------------------------------------------------- + + var fieldForm = document.getElementById('fieldForm'); + if (fieldForm) { + document.addEventListener('click', function (e) { + var editBtn = e.target.closest('.field-edit-btn'); + if (editBtn) { + document.getElementById('fieldFormTitle').textContent = 'Edit Field'; + document.getElementById('fieldFormName').value = editBtn.dataset.name; + document.getElementById('fieldFormType').value = editBtn.dataset.type; + document.getElementById('fieldFormDefault').value = editBtn.dataset.default; + document.getElementById('fieldFormOptions').value = editBtn.dataset.options || ''; + document.getElementById('fieldFormRequired').checked = editBtn.dataset.required === 'true'; + document.getElementById('fieldFormSortOrder').value = editBtn.dataset.sortOrder; + document.getElementById('fieldFormSubmit').textContent = 'Save'; + fieldForm.action = window.itemTypeFieldsBaseAction + '/' + editBtn.dataset.id; + var cancelBtn = document.getElementById('fieldFormCancel'); + cancelBtn.classList.remove('d-none'); + fieldForm.scrollIntoView({ behavior: 'smooth', block: 'center' }); + return; + } + var cancelBtn2 = e.target.closest('#fieldFormCancel'); + if (cancelBtn2) { + document.getElementById('fieldFormTitle').textContent = 'Add Field'; + fieldForm.reset(); + document.getElementById('fieldFormSubmit').textContent = 'Add'; + fieldForm.action = window.itemTypeFieldsBaseAction; + cancelBtn2.classList.add('d-none'); + } + }); + } +})(); diff --git a/web/static/js/bootstrap.bundle.min.js b/web/static/js/bootstrap.bundle.min.js new file mode 100644 index 0000000..04e9185 --- /dev/null +++ b/web/static/js/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 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():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(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},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=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?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=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 O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){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=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function j(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 M(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${M(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${M(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=j(t.dataset[n])}return e},getDataAttribute:(t,e)=>j(t.getAttribute(`data-bs-${M(e)}`))};class H{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=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(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.3"}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 B=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?i.trim():null}return e?e.split(",").map((t=>n(t))).join(","):null},z={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 n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.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=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).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(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.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)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y 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=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_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(),g(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&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(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 ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt 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=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[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=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),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(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.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)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",jt="collapsing",Mt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.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 Ht}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=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(jt),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(jt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(jt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.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(jt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(Mt,this._config.parent);return z.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=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function je(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const Me={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:je(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:je(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==P(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],j=f?-T[$]/2:0,M=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-j-q-z-O.mainAxis:M-q-z-O.mainAxis,K=v?-E[$]/2+j+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.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)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.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=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)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,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),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=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{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,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({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=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(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(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),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=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,jn=`hide${xn}`,Mn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn 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 zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,jn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,Mn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,Mn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.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)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],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:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}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},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.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 n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,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"},ls={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 cs extends W{constructor(t,e){if(void 0===vi)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 as}static get DefaultType(){return ls}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),N.off(this._element.closest(is),ns,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=N.trigger(this._element,this.constructor.eventName("show")),e=(c(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:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.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(ts,es),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(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...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(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_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 g(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,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,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=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.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:r(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=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ds={...cs.DefaultType,content:"(null|string|element|function)"};class us extends cs{static get Default(){return hs}static get DefaultType(){return ds}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=us.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(us);const fs=".bs.scrollspy",ps=`activate${fs}`,ms=`click${fs}`,gs=`load${fs}.data-api`,_s="active",bs="[href]",vs=".nav-link",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Es 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 ws}static get DefaultType(){return As}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=r(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&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_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))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;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(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(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(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(_s);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s);const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.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]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Es.getOrCreateInstance(t)})),m(Es);const Ts=".bs.tab",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}`,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s="ArrowLeft",Is="ArrowRight",Ns="ArrowUp",Ps="ArrowDown",js="Home",Ms="End",Fs="active",Hs="fade",Ws="show",Bs=".dropdown-toggle",zs=`:not(${Bs})`,Rs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',qs=`.nav-link${zs}, .list-group-item${zs}, [role="tab"]${zs}, ${Rs}`,Vs=`.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;class Ks extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(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?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,Os,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,js,Ms].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([js,Ms].includes(t.key))i=e[t.key===js?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Ks.getOrCreateInstance(i).show())}_getChildren(){return z.find(qs,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=z.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 n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(Bs,Fs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return t.matches(qs)?t:z.findOne(qs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Ks.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ls,Rs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Ks.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t of z.find(Vs))Ks.getOrCreateInstance(t)})),m(Ks);const Qs=".bs.toast",Xs=`mouseover${Qs}`,Ys=`mouseout${Qs}`,Us=`focusin${Qs}`,Gs=`focusout${Qs}`,Js=`hide${Qs}`,Zs=`hidden${Qs}`,to=`show${Qs}`,eo=`shown${Qs}`,io="hide",no="show",so="showing",oo={animation:"boolean",autohide:"boolean",delay:"number"},ro={animation:!0,autohide:!0,delay:5e3};class ao extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return ro}static get DefaultType(){return oo}static get NAME(){return"toast"}show(){N.trigger(this._element,to).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(io),d(this._element),this._element.classList.add(no,so),this._queueCallback((()=>{this._element.classList.remove(so),N.trigger(this._element,eo),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,Js).defaultPrevented||(this._element.classList.add(so),this._queueCallback((()=>{this._element.classList.add(io),this._element.classList.remove(so,no),N.trigger(this._element,Zs)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(no),super.dispose()}isShown(){return this._element.classList.contains(no)}_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(){N.on(this._element,Xs,(t=>this._onInteraction(t,!0))),N.on(this._element,Ys,(t=>this._onInteraction(t,!1))),N.on(this._element,Us,(t=>this._onInteraction(t,!0))),N.on(this._element,Gs,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ao.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(ao),m(ao),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Ks,Toast:ao,Tooltip:cs}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/web/templates/layout.html b/web/templates/layout.html new file mode 100644 index 0000000..b70d491 --- /dev/null +++ b/web/templates/layout.html @@ -0,0 +1,54 @@ +{{define "layout"}} + + + + + inventree + + + + + + + +
+ {{template "content" .}} +
+ + + + + +{{end}} diff --git a/web/templates/login.html b/web/templates/login.html new file mode 100644 index 0000000..48f49c9 --- /dev/null +++ b/web/templates/login.html @@ -0,0 +1,43 @@ +{{define "login"}} + + + + + Sign in · inventree + + + + + +
+
+
+
+
+

inventree

+
IT asset inventory
+
+
+
+ {{if .Error}}
{{.Error}}
{{end}} +
+ +
+ + +
+
+ + +
+ +
+
+
+

Default login: admin / admin

+
+
+
+ + +{{end}} diff --git a/web/templates/pages/inventory.html b/web/templates/pages/inventory.html new file mode 100644 index 0000000..8112563 --- /dev/null +++ b/web/templates/pages/inventory.html @@ -0,0 +1,43 @@ +{{define "content"}} +
+ + +
+ {{template "panel_root" .Root}} +
+
+ +{{template "modals" .}} +{{end}} diff --git a/web/templates/pages/itemtypes_form.html b/web/templates/pages/itemtypes_form.html new file mode 100644 index 0000000..c5a02c2 --- /dev/null +++ b/web/templates/pages/itemtypes_form.html @@ -0,0 +1,120 @@ +{{define "content"}} +
+
+

{{if .ItemType}}Edit Item Type{{else}}New Item Type{{end}}

+ Back to Item Types +
+ + {{if .Error}}
{{.Error}}
{{end}} + +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + {{if .ItemType}} + + {{end}} +
+
+
+ + {{if .ItemType}} +
Custom Fields
+
+
+ + + + {{range .ItemType.Fields}} + + + + + + + + + + + {{end}} + {{if not .ItemType.Fields}} + + {{end}} + +
NameKeyTypeRequiredDefaultOptionsOrder
{{.Name}}{{.FieldKey}}{{.FieldType.Label}}{{if .Required}}{{end}}{{.DefaultValue}}{{range .Options}}{{.}}
{{end}}
{{.SortOrder}} + +
+ +
+
No custom fields yet.
+
+
+
Add Field
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ +
+ +
+ +
+
+ + {{else}} +

Save the item type first to add custom fields.

+ {{end}} +
+{{end}} diff --git a/web/templates/pages/itemtypes_list.html b/web/templates/pages/itemtypes_list.html new file mode 100644 index 0000000..dd9f21e --- /dev/null +++ b/web/templates/pages/itemtypes_list.html @@ -0,0 +1,27 @@ +{{define "content"}} +
+
+

Item Types

+ New Item Type +
+
+ + + + {{range .ItemTypes}} + + + + + + + + + {{else}} + + {{end}} + +
IconNameDescriptionCustom FieldsStatus
{{.Name}}{{.Description}}{{len .Fields}}{{if .Enabled}}Enabled{{else}}Disabled{{end}}Edit
No item types defined yet.
+
+
+{{end}} diff --git a/web/templates/pages/settings.html b/web/templates/pages/settings.html new file mode 100644 index 0000000..c02b5eb --- /dev/null +++ b/web/templates/pages/settings.html @@ -0,0 +1,86 @@ +{{define "content"}} +
+

Settings

+ +
+
General
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ +
+
Statuses
+
+ + + + {{range $i, $st := .Statuses}} + + + + + + + {{end}} + +
OrderPreviewName & Color
+
+ + +
+
+ + +
+
{{$st.Name}} +
+ + + +
+
+
+ +
+
+
+
+
Add Status
+
+
+ + +
+
+ + +
+ +
+
+
+
+{{end}} diff --git a/web/templates/pages/users_form.html b/web/templates/pages/users_form.html new file mode 100644 index 0000000..cd7916a --- /dev/null +++ b/web/templates/pages/users_form.html @@ -0,0 +1,61 @@ +{{define "content"}} +
+
+

{{if .EditUser}}Edit User{{else}}New User{{end}}

+ Back to Users +
+ + {{if .Error}}
{{.Error}}
{{end}} + +
+
+
+
+ + +
+ {{if not .EditUser}} +
+ + +
+ {{end}} +
+ + +
+
+ + +
+ +
+ {{if .EditUser}} +
+ +
+ {{end}} +
+
+ + {{if .EditUser}} +
+
Change Password
+
+
+
+ + +
+
+ +
+
+
+
+ {{end}} +
+{{end}} diff --git a/web/templates/pages/users_list.html b/web/templates/pages/users_list.html new file mode 100644 index 0000000..b40b2bd --- /dev/null +++ b/web/templates/pages/users_list.html @@ -0,0 +1,24 @@ +{{define "content"}} +
+
+

Users

+ New User +
+
+ + + + {{range .Users}} + + + + + + + + {{end}} + +
UsernameRoleStatusCreated
{{.Username}}{{if .IsAdmin}}Admin{{else}}User{{end}}{{if .Enabled}}Enabled{{else}}Disabled{{end}}{{fmtDateShort .CreatedAt}}Edit
+
+
+{{end}} diff --git a/web/templates/partials/asset_fields.html b/web/templates/partials/asset_fields.html new file mode 100644 index 0000000..f4824c3 --- /dev/null +++ b/web/templates/partials/asset_fields.html @@ -0,0 +1,34 @@ +{{define "asset_fields_only"}} +{{if .}} +
Type-Specific Fields
+{{range .}} +{{$fv := .}} +
+ + {{if eq .Def.FieldType "long_text"}} + + {{else if eq .Def.FieldType "boolean"}} + + {{else if eq .Def.FieldType "dropdown"}} + + {{else if eq .Def.FieldType "date"}} + + {{else if eq .Def.FieldType "number"}} + + {{else if eq .Def.FieldType "url"}} + + {{else if eq .Def.FieldType "ip_address"}} + + {{else}} + + {{end}} +
+{{end}} +{{end}} +{{end}} diff --git a/web/templates/partials/asset_form.html b/web/templates/partials/asset_form.html new file mode 100644 index 0000000..59a1966 --- /dev/null +++ b/web/templates/partials/asset_form.html @@ -0,0 +1,81 @@ +{{define "asset_form"}} +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + {{if .Asset}} + +
Item type cannot be changed after creation.
+ {{else}} + + {{end}} +
+
+ + {{$selectedFolder := deref .FolderID}} + {{if .Asset}}{{$selectedFolder = deref .Asset.FolderID}}{{end}} + +
+ +
+ + +
+
+ + {{$selectedUser := 0}} + {{if .Asset}}{{$selectedUser = deref .Asset.AssignedUserID}}{{end}} + +
+ +
+ + +
+ +
+ {{template "asset_fields_only" .Fields}} +
+
+ +
+ + +
+
+
+
+{{end}} diff --git a/web/templates/partials/breadcrumb.html b/web/templates/partials/breadcrumb.html new file mode 100644 index 0000000..ddf4ad2 --- /dev/null +++ b/web/templates/partials/breadcrumb.html @@ -0,0 +1,15 @@ +{{define "breadcrumb"}} + +{{end}} diff --git a/web/templates/partials/modals.html b/web/templates/partials/modals.html new file mode 100644 index 0000000..b9ff23d --- /dev/null +++ b/web/templates/partials/modals.html @@ -0,0 +1,112 @@ +{{define "modals"}} +{{if .User.IsAdmin}} + + + + + + + + +{{end}} +{{end}} diff --git a/web/templates/partials/panel_asset.html b/web/templates/partials/panel_asset.html new file mode 100644 index 0000000..18dadfd --- /dev/null +++ b/web/templates/partials/panel_asset.html @@ -0,0 +1,104 @@ +{{define "panel_asset"}} +
+
+ + {{if .CanEdit}} +
+ + + + +
+ {{end}} +
+ +
+ +
+

{{.Asset.Name}}

+
{{.Asset.AssetTag}} · {{.Asset.ItemTypeName}}
+
+ {{.Asset.StatusName}} +
+ +
+
+
+
Details
+ + + + + + +
Assigned User{{if .Asset.AssignedUsername}}{{.Asset.AssignedUsername}}{{else}}Unassigned{{end}}
Created{{fmtDate .Asset.CreatedAt}}
Updated{{fmtDate .Asset.UpdatedAt}}
+
+ {{if .Asset.Notes}} +
+
Notes
+
{{.Asset.Notes}}
+
+ {{end}} +
+
+ {{if .Asset.CustomFields}} +
+
{{.Asset.ItemTypeName}} Fields
+ + + {{range .Asset.CustomFields}} + + + + + {{end}} + +
{{.Def.Name}} + {{if eq .Def.FieldType "boolean"}} + {{if eq .Value "true"}}{{else}}{{end}} + {{else if eq .Def.FieldType "url"}} + {{if .Value}}{{.Value}}{{end}} + {{else}} + {{if .Value}}{{.Value}}{{else}}—{{end}} + {{end}} +
+
+ {{end}} +
+
+
+ +{{if .CanEdit}} + +{{end}} +{{end}} diff --git a/web/templates/partials/panel_folder.html b/web/templates/partials/panel_folder.html new file mode 100644 index 0000000..bfa5bc1 --- /dev/null +++ b/web/templates/partials/panel_folder.html @@ -0,0 +1,80 @@ +{{define "panel_folder"}} +
+
+ {{template "breadcrumb" .Breadcrumb}} +
+ + + +
+
+ +

{{.Folder.Name}}

+ {{if .Folder.Description}}

{{.Folder.Description}}

{{end}} + + {{if .Subfolders}} +
Subfolders
+ + {{end}} + +
+
Assets
+
+ + + +
+
+ Export + +
+
+ + {{if .Assets}} +
+ + + + {{range .Assets}} + + + + + + + + {{end}} + +
Asset IDNameTypeStatusAssigned
{{.AssetTag}}{{.Name}}{{.ItemTypeName}}{{.StatusName}}{{if .AssignedUsername}}{{.AssignedUsername}}{{else}}—{{end}}
+
+ {{$pages := ceilDiv .Total .PageSize}} + {{if gt $pages 1}} + + {{end}} + {{else}} +

No assets in this folder yet.

+ {{end}} +
+{{end}} diff --git a/web/templates/partials/panel_root.html b/web/templates/partials/panel_root.html new file mode 100644 index 0000000..50a25d9 --- /dev/null +++ b/web/templates/partials/panel_root.html @@ -0,0 +1,109 @@ +{{define "panel_root"}} +
+ + +
+
+
+
+
{{.Stats.Total}}
+
Total Assets
+
+
+
+ {{range .Stats.ByStatus}} +
+
+
+
{{.Count}}
+
{{.Name}}
+
+
+
+ {{end}} +
+ +
+
+
+
Assets by Type
+
    + {{range .Stats.ByType}} +
  • + {{.Name}} + {{.Count}} +
  • + {{else}} +
  • No assets yet.
  • + {{end}} +
+
+
+
+
+
Recently Added
+ +
+
+
+ +
+
Top-Level Folders
+
+ Export CSV + + +
+
+ + {{if .Folders}} + + {{else}} +

No folders yet. {{if true}}Create one to start organizing your inventory.{{end}}

+ {{end}} + + {{if .UnfiledAssets}} +
Unfiled Assets
+
+ + + + {{range .UnfiledAssets}} + + + + + + + + {{end}} + +
Asset IDNameTypeStatusAssigned
{{.AssetTag}}{{.Name}}{{.ItemTypeName}}{{.StatusName}}{{if .AssignedUsername}}{{.AssignedUsername}}{{else}}—{{end}}
+
+ {{end}} +
+{{end}} diff --git a/web/templates/partials/panel_search.html b/web/templates/partials/panel_search.html new file mode 100644 index 0000000..a8628fd --- /dev/null +++ b/web/templates/partials/panel_search.html @@ -0,0 +1,33 @@ +{{define "panel_search"}} +
+ + +
{{len .Results}} result(s) for "{{.Query}}"
+ + {{if .Results}} + + {{else}} +

No matches found.

+ {{end}} +
+{{end}} diff --git a/web/templates/partials/tree_children.html b/web/templates/partials/tree_children.html new file mode 100644 index 0000000..3a205a0 --- /dev/null +++ b/web/templates/partials/tree_children.html @@ -0,0 +1,44 @@ +{{define "tree_children"}} +{{range .}} + {{if eq .Type "folder"}} +
  • +
    + {{if .HasChildren}} + + {{else}} + + {{end}} + + {{.Name}} + + + +
    +
      + {{if .Expanded}}{{template "tree_children" .Children}}{{end}} +
    +
  • + {{else if eq .Type "asset"}} +
  • +
    + + + {{.Name}} + {{.StatusName}} +
    +
  • + {{else}} +
  • +
    + + + {{.Name}} +
    +
  • + {{end}} +{{end}} +{{end}}