A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.

Compare changes

Choose any two refs to compare.

Changed files
+8516 -1972
cmd
appview
credential-helper
hold
usage-report
deploy
docs
examples
plugins
gatekeeper-provider
ratify-verifier
lexicons
io
atcr
pkg
+25
.air.hold.toml
··· 1 + root = "." 2 + tmp_dir = "tmp" 3 + 4 + [build] 5 + cmd = "go build -buildvcs=false -o ./tmp/atcr-hold ./cmd/hold" 6 + entrypoint = ["./tmp/atcr-hold"] 7 + include_ext = ["go"] 8 + exclude_dir = ["bin", "tmp", "vendor", "deploy", "docs", ".git", "dist", "pkg/appview"] 9 + exclude_regex = ["_test\\.go$"] 10 + delay = 1000 11 + stop_on_error = true 12 + send_interrupt = true 13 + kill_delay = 500 14 + 15 + [log] 16 + time = false 17 + 18 + [color] 19 + main = "blue" 20 + watcher = "magenta" 21 + build = "yellow" 22 + runner = "green" 23 + 24 + [misc] 25 + clean_on_exit = true
+1
.gitignore
··· 18 18 pkg/appview/static/js/lucide.min.js 19 19 20 20 # IDE 21 + .zed/ 21 22 .claude/ 22 23 .vscode/ 23 24 .idea/
+7 -2
.golangci.yml
··· 1 1 # golangci-lint configuration for ATCR 2 2 # See: https://golangci-lint.run/usage/configuration/ 3 3 version: "2" 4 - linters: 4 + linters: 5 5 settings: 6 6 staticcheck: 7 7 checks: ··· 20 20 exclusions: 21 21 presets: 22 22 - std-error-handling 23 + rules: 24 + - path: _test\.go 25 + linters: 26 + - errcheck 27 + 23 28 formatters: 24 29 enable: 25 30 - gofmt 26 - - goimports 31 + - goimports
+6 -4
Dockerfile.dev
··· 1 1 # Development image with Air hot reload 2 - # Build: docker build -f Dockerfile.dev -t atcr-appview-dev . 3 - # Run: docker run -v $(pwd):/app -p 5000:5000 atcr-appview-dev 2 + # Build: docker build -f Dockerfile.dev -t atcr-dev . 3 + # Run: docker run -v $(pwd):/app -p 5000:5000 atcr-dev 4 4 FROM docker.io/golang:1.25.4-trixie 5 5 6 + ARG AIR_CONFIG=.air.toml 7 + 6 8 ENV DEBIAN_FRONTEND=noninteractive 9 + ENV AIR_CONFIG=${AIR_CONFIG} 7 10 8 11 RUN apt-get update && \ 9 12 apt-get install -y --no-install-recommends sqlite3 libsqlite3-dev curl && \ ··· 17 20 RUN go mod download 18 21 19 22 # For development: source mounted as volume, Air handles builds 20 - EXPOSE 5000 21 - CMD ["air", "-c", ".air.toml"] 23 + CMD ["sh", "-c", "air -c ${AIR_CONFIG}"]
+11 -25
cmd/appview/serve.go
··· 114 114 115 115 slog.Debug("Base URL for OAuth", "base_url", baseURL) 116 116 if testMode { 117 - slog.Info("TEST_MODE enabled - will use HTTP for local DID resolution and transition:generic scope") 117 + slog.Info("TEST_MODE enabled - will use HTTP for local DID resolution") 118 118 } 119 119 120 120 // Create OAuth client app (automatically configures confidential client for production) ··· 122 122 oauthClientApp, err := oauth.NewClientApp(baseURL, oauthStore, desiredScopes, cfg.Server.OAuthKeyPath, cfg.Server.ClientName) 123 123 if err != nil { 124 124 return fmt.Errorf("failed to create OAuth client app: %w", err) 125 - } 126 - if testMode { 127 - slog.Info("Using OAuth scopes with transition:generic (test mode)") 128 - } else { 129 - slog.Info("Using OAuth scopes with RPC scope (production mode)") 130 125 } 131 126 132 127 // Invalidate sessions with mismatched scopes on startup ··· 380 375 // OAuth client metadata endpoint 381 376 mainRouter.Get("/oauth-client-metadata.json", func(w http.ResponseWriter, r *http.Request) { 382 377 config := oauthClientApp.Config 378 + logoURI := cfg.Server.BaseURL + "/web-app-manifest-192x192.png" 379 + policyURI := cfg.Server.BaseURL + "/privacy" 380 + tosURI := cfg.Server.BaseURL + "/terms" 381 + 383 382 metadata := config.ClientMetadata() 383 + metadata.ClientName = &cfg.Server.ClientName 384 + metadata.ClientURI = &cfg.Server.BaseURL 385 + metadata.LogoURI = &logoURI 386 + metadata.PolicyURI = &policyURI 387 + metadata.TosURI = &tosURI 384 388 385 389 // For confidential clients, ensure JWKS is included 386 390 // The indigo library should populate this automatically, but we explicitly set it here ··· 390 394 metadata.JWKS = &jwks 391 395 } 392 396 393 - // Convert indigo's metadata to map so we can add custom fields 394 - metadataBytes, err := json.Marshal(metadata) 395 - if err != nil { 396 - http.Error(w, "Failed to marshal metadata", http.StatusInternalServerError) 397 - return 398 - } 399 - 400 - var metadataMap map[string]interface{} 401 - if err := json.Unmarshal(metadataBytes, &metadataMap); err != nil { 402 - http.Error(w, "Failed to unmarshal metadata", http.StatusInternalServerError) 403 - return 404 - } 405 - 406 - // Add custom fields 407 - metadataMap["client_name"] = cfg.Server.ClientName 408 - metadataMap["client_uri"] = cfg.Server.BaseURL 409 - metadataMap["logo_uri"] = cfg.Server.BaseURL + "/web-app-manifest-192x192.png" 410 - 411 397 w.Header().Set("Content-Type", "application/json") 412 398 w.Header().Set("Access-Control-Allow-Origin", "*") 413 399 // Limit caching to allow scope changes to propagate quickly 414 400 // PDS servers cache client metadata, so short max-age helps with updates 415 401 w.Header().Set("Cache-Control", "public, max-age=300") 416 - if err := json.NewEncoder(w).Encode(metadataMap); err != nil { 402 + if err := json.NewEncoder(w).Encode(metadata); err != nil { 417 403 http.Error(w, "Failed to encode metadata", http.StatusInternalServerError) 418 404 } 419 405 })
+2 -2
cmd/credential-helper/main.go
··· 180 180 181 181 // Wait for user to complete OAuth flow, then retry 182 182 fmt.Fprintf(os.Stderr, "Waiting for authentication") 183 - for i := 0; i < 60; i++ { // Wait up to 2 minutes 183 + for range 60 { // Wait up to 2 minutes 184 184 time.Sleep(2 * time.Second) 185 185 fmt.Fprintf(os.Stderr, ".") 186 186 ··· 765 765 curParts := strings.Split(curV, ".") 766 766 767 767 // Compare each part 768 - for i := 0; i < len(newParts) && i < len(curParts); i++ { 768 + for i := range min(len(newParts), len(curParts)) { 769 769 newNum := 0 770 770 curNum := 0 771 771 fmt.Sscanf(newParts[i], "%d", &newNum)
+45 -2
cmd/hold/main.go
··· 11 11 "time" 12 12 13 13 "atcr.io/pkg/hold" 14 + "atcr.io/pkg/hold/admin" 14 15 "atcr.io/pkg/hold/oci" 15 16 "atcr.io/pkg/hold/pds" 17 + "atcr.io/pkg/hold/quota" 16 18 "atcr.io/pkg/logging" 17 19 "atcr.io/pkg/s3" 18 20 ··· 98 100 os.Exit(1) 99 101 } 100 102 103 + // Initialize quota manager from quotas.yaml 104 + quotaMgr, err := quota.NewManager("./quotas.yaml") 105 + if err != nil { 106 + slog.Error("Failed to load quota config", "error", err) 107 + os.Exit(1) 108 + } 109 + if quotaMgr.IsEnabled() { 110 + slog.Info("Quota enforcement enabled", "tiers", quotaMgr.TierCount(), "defaultTier", quotaMgr.GetDefaultTier()) 111 + } else { 112 + slog.Info("Quota enforcement disabled (no quotas.yaml found)") 113 + } 114 + 101 115 // Create blob store adapter and XRPC handlers 102 116 var ociHandler *oci.XRPCHandler 103 117 if holdPDS != nil { ··· 116 130 } 117 131 118 132 // Create PDS XRPC handler (ATProto endpoints) 119 - xrpcHandler = pds.NewXRPCHandler(holdPDS, *s3Service, driver, broadcaster, nil) 133 + xrpcHandler = pds.NewXRPCHandler(holdPDS, *s3Service, driver, broadcaster, nil, quotaMgr) 120 134 121 135 // Create OCI XRPC handler (multipart upload endpoints) 122 - ociHandler = oci.NewXRPCHandler(holdPDS, *s3Service, driver, cfg.Server.DisablePresignedURLs, cfg.Registration.EnableBlueskyPosts, nil) 136 + ociHandler = oci.NewXRPCHandler(holdPDS, *s3Service, driver, cfg.Server.DisablePresignedURLs, cfg.Registration.EnableBlueskyPosts, nil, quotaMgr) 123 137 } 124 138 125 139 // Setup HTTP routes with chi router ··· 154 168 ociHandler.RegisterHandlers(r) 155 169 } 156 170 171 + // Initialize and register admin panel if enabled 172 + var adminUI *admin.AdminUI 173 + if cfg.Admin.Enabled && holdPDS != nil { 174 + adminCfg := admin.AdminConfig{ 175 + Enabled: true, 176 + PublicURL: cfg.Server.PublicURL, 177 + } 178 + 179 + adminUI, err = admin.NewAdminUI(context.Background(), holdPDS, quotaMgr, adminCfg) 180 + if err != nil { 181 + slog.Error("Failed to initialize admin panel", "error", err) 182 + os.Exit(1) 183 + } 184 + 185 + if adminUI != nil { 186 + slog.Info("Registering admin panel routes") 187 + adminUI.RegisterRoutes(r) 188 + } 189 + } 190 + 157 191 // Create server 158 192 server := &http.Server{ 159 193 Addr: cfg.Server.Addr, ··· 220 254 slog.Warn("Failed to close broadcaster database", "error", err) 221 255 } else { 222 256 slog.Info("Broadcaster database closed") 257 + } 258 + } 259 + 260 + // Close admin panel 261 + if adminUI != nil { 262 + if err := adminUI.Close(); err != nil { 263 + slog.Warn("Failed to close admin panel", "error", err) 264 + } else { 265 + slog.Info("Admin panel closed") 223 266 } 224 267 } 225 268
+624
cmd/usage-report/main.go
··· 1 + // usage-report queries a hold service and generates a storage usage report 2 + // grouped by user, with unique layers and totals. 3 + // 4 + // Usage: 5 + // 6 + // go run ./cmd/usage-report --hold https://hold01.atcr.io 7 + // go run ./cmd/usage-report --hold https://hold01.atcr.io --from-manifests 8 + package main 9 + 10 + import ( 11 + "encoding/json" 12 + "flag" 13 + "fmt" 14 + "io" 15 + "net/http" 16 + "net/url" 17 + "os" 18 + "sort" 19 + "strings" 20 + "time" 21 + ) 22 + 23 + // LayerRecord matches the io.atcr.hold.layer record structure 24 + type LayerRecord struct { 25 + Type string `json:"$type"` 26 + Digest string `json:"digest"` 27 + Size int64 `json:"size"` 28 + MediaType string `json:"mediaType"` 29 + Manifest string `json:"manifest"` 30 + UserDID string `json:"userDid"` 31 + CreatedAt string `json:"createdAt"` 32 + } 33 + 34 + // ManifestRecord matches the io.atcr.manifest record structure 35 + type ManifestRecord struct { 36 + Type string `json:"$type"` 37 + Repository string `json:"repository"` 38 + Digest string `json:"digest"` 39 + HoldDID string `json:"holdDid"` 40 + Config *struct { 41 + Digest string `json:"digest"` 42 + Size int64 `json:"size"` 43 + } `json:"config"` 44 + Layers []struct { 45 + Digest string `json:"digest"` 46 + Size int64 `json:"size"` 47 + MediaType string `json:"mediaType"` 48 + } `json:"layers"` 49 + Manifests []struct { 50 + Digest string `json:"digest"` 51 + Size int64 `json:"size"` 52 + } `json:"manifests"` 53 + CreatedAt string `json:"createdAt"` 54 + } 55 + 56 + // CrewRecord matches the io.atcr.hold.crew record structure 57 + type CrewRecord struct { 58 + Member string `json:"member"` 59 + Role string `json:"role"` 60 + Permissions []string `json:"permissions"` 61 + AddedAt string `json:"addedAt"` 62 + } 63 + 64 + // ListRecordsResponse is the response from com.atproto.repo.listRecords 65 + type ListRecordsResponse struct { 66 + Records []struct { 67 + URI string `json:"uri"` 68 + CID string `json:"cid"` 69 + Value json.RawMessage `json:"value"` 70 + } `json:"records"` 71 + Cursor string `json:"cursor,omitempty"` 72 + } 73 + 74 + // UserUsage tracks storage for a single user 75 + type UserUsage struct { 76 + DID string 77 + Handle string 78 + UniqueLayers map[string]int64 // digest -> size 79 + TotalSize int64 80 + LayerCount int 81 + Repositories map[string]bool // unique repos 82 + } 83 + 84 + var client = &http.Client{Timeout: 30 * time.Second} 85 + 86 + func main() { 87 + holdURL := flag.String("hold", "https://hold01.atcr.io", "Hold service URL") 88 + fromManifests := flag.Bool("from-manifests", false, "Calculate usage from user manifests instead of hold layer records (more accurate but slower)") 89 + flag.Parse() 90 + 91 + // Normalize URL 92 + baseURL := strings.TrimSuffix(*holdURL, "/") 93 + 94 + fmt.Printf("Querying %s...\n\n", baseURL) 95 + 96 + // First, get the hold's DID 97 + holdDID, err := getHoldDID(baseURL) 98 + if err != nil { 99 + fmt.Fprintf(os.Stderr, "Failed to get hold DID: %v\n", err) 100 + os.Exit(1) 101 + } 102 + fmt.Printf("Hold DID: %s\n\n", holdDID) 103 + 104 + var userUsage map[string]*UserUsage 105 + 106 + if *fromManifests { 107 + fmt.Println("=== Calculating from user manifests (bypasses layer record bug) ===") 108 + userUsage, err = calculateFromManifests(baseURL, holdDID) 109 + } else { 110 + fmt.Println("=== Calculating from hold layer records ===") 111 + fmt.Println("NOTE: May undercount app-password users due to layer record bug") 112 + fmt.Println(" Use --from-manifests for more accurate results") 113 + 114 + userUsage, err = calculateFromLayerRecords(baseURL, holdDID) 115 + } 116 + 117 + if err != nil { 118 + fmt.Fprintf(os.Stderr, "Failed to calculate usage: %v\n", err) 119 + os.Exit(1) 120 + } 121 + 122 + // Resolve DIDs to handles 123 + fmt.Println("\n\nResolving DIDs to handles...") 124 + for _, usage := range userUsage { 125 + handle, err := resolveDIDToHandle(usage.DID) 126 + if err != nil { 127 + usage.Handle = usage.DID 128 + } else { 129 + usage.Handle = handle 130 + } 131 + } 132 + 133 + // Convert to slice and sort by total size (descending) 134 + var sorted []*UserUsage 135 + for _, u := range userUsage { 136 + sorted = append(sorted, u) 137 + } 138 + sort.Slice(sorted, func(i, j int) bool { 139 + return sorted[i].TotalSize > sorted[j].TotalSize 140 + }) 141 + 142 + // Print report 143 + fmt.Println("\n========================================") 144 + fmt.Println("STORAGE USAGE REPORT") 145 + fmt.Println("========================================") 146 + 147 + var grandTotal int64 148 + var grandLayers int 149 + for _, u := range sorted { 150 + grandTotal += u.TotalSize 151 + grandLayers += u.LayerCount 152 + } 153 + 154 + fmt.Printf("\nTotal Users: %d\n", len(sorted)) 155 + fmt.Printf("Total Unique Layers: %d\n", grandLayers) 156 + fmt.Printf("Total Storage: %s\n\n", humanSize(grandTotal)) 157 + 158 + fmt.Println("BY USER (sorted by storage):") 159 + fmt.Println("----------------------------------------") 160 + for i, u := range sorted { 161 + fmt.Printf("%3d. %s\n", i+1, u.Handle) 162 + fmt.Printf(" DID: %s\n", u.DID) 163 + fmt.Printf(" Unique Layers: %d\n", u.LayerCount) 164 + fmt.Printf(" Total Size: %s\n", humanSize(u.TotalSize)) 165 + if len(u.Repositories) > 0 { 166 + var repos []string 167 + for r := range u.Repositories { 168 + repos = append(repos, r) 169 + } 170 + sort.Strings(repos) 171 + fmt.Printf(" Repositories: %s\n", strings.Join(repos, ", ")) 172 + } 173 + pct := float64(0) 174 + if grandTotal > 0 { 175 + pct = float64(u.TotalSize) / float64(grandTotal) * 100 176 + } 177 + fmt.Printf(" Share: %.1f%%\n\n", pct) 178 + } 179 + 180 + // Output CSV format for easy analysis 181 + fmt.Println("\n========================================") 182 + fmt.Println("CSV FORMAT") 183 + fmt.Println("========================================") 184 + fmt.Println("handle,did,unique_layers,total_bytes,total_human,repositories") 185 + for _, u := range sorted { 186 + var repos []string 187 + for r := range u.Repositories { 188 + repos = append(repos, r) 189 + } 190 + sort.Strings(repos) 191 + fmt.Printf("%s,%s,%d,%d,%s,\"%s\"\n", u.Handle, u.DID, u.LayerCount, u.TotalSize, humanSize(u.TotalSize), strings.Join(repos, ";")) 192 + } 193 + } 194 + 195 + // calculateFromLayerRecords uses the hold's layer records (original method) 196 + func calculateFromLayerRecords(baseURL, holdDID string) (map[string]*UserUsage, error) { 197 + layers, err := fetchAllLayerRecords(baseURL, holdDID) 198 + if err != nil { 199 + return nil, err 200 + } 201 + 202 + fmt.Printf("Fetched %d layer records\n", len(layers)) 203 + 204 + userUsage := make(map[string]*UserUsage) 205 + for _, layer := range layers { 206 + if layer.UserDID == "" { 207 + continue 208 + } 209 + 210 + usage, exists := userUsage[layer.UserDID] 211 + if !exists { 212 + usage = &UserUsage{ 213 + DID: layer.UserDID, 214 + UniqueLayers: make(map[string]int64), 215 + Repositories: make(map[string]bool), 216 + } 217 + userUsage[layer.UserDID] = usage 218 + } 219 + 220 + if _, seen := usage.UniqueLayers[layer.Digest]; !seen { 221 + usage.UniqueLayers[layer.Digest] = layer.Size 222 + usage.TotalSize += layer.Size 223 + usage.LayerCount++ 224 + } 225 + } 226 + 227 + return userUsage, nil 228 + } 229 + 230 + // calculateFromManifests queries crew members and fetches their manifests from their PDSes 231 + func calculateFromManifests(baseURL, holdDID string) (map[string]*UserUsage, error) { 232 + // Get all crew members 233 + crewDIDs, err := fetchCrewMembers(baseURL, holdDID) 234 + if err != nil { 235 + return nil, fmt.Errorf("failed to fetch crew: %w", err) 236 + } 237 + 238 + // Also get captain 239 + captainDID, err := fetchCaptain(baseURL, holdDID) 240 + if err == nil && captainDID != "" { 241 + // Add captain to list if not already there 242 + found := false 243 + for _, d := range crewDIDs { 244 + if d == captainDID { 245 + found = true 246 + break 247 + } 248 + } 249 + if !found { 250 + crewDIDs = append(crewDIDs, captainDID) 251 + } 252 + } 253 + 254 + fmt.Printf("Found %d users (crew + captain)\n", len(crewDIDs)) 255 + 256 + userUsage := make(map[string]*UserUsage) 257 + 258 + for _, did := range crewDIDs { 259 + fmt.Printf(" Checking manifests for %s...", did) 260 + 261 + // Resolve DID to PDS 262 + pdsEndpoint, err := resolveDIDToPDS(did) 263 + if err != nil { 264 + fmt.Printf(" (failed to resolve PDS: %v)\n", err) 265 + continue 266 + } 267 + 268 + // Fetch manifests that use this hold 269 + manifests, err := fetchUserManifestsForHold(pdsEndpoint, did, holdDID) 270 + if err != nil { 271 + fmt.Printf(" (failed to fetch manifests: %v)\n", err) 272 + continue 273 + } 274 + 275 + if len(manifests) == 0 { 276 + fmt.Printf(" 0 manifests\n") 277 + continue 278 + } 279 + 280 + // Calculate unique layers across all manifests 281 + usage := &UserUsage{ 282 + DID: did, 283 + UniqueLayers: make(map[string]int64), 284 + Repositories: make(map[string]bool), 285 + } 286 + 287 + for _, m := range manifests { 288 + usage.Repositories[m.Repository] = true 289 + 290 + // Add config blob 291 + if m.Config != nil { 292 + if _, seen := usage.UniqueLayers[m.Config.Digest]; !seen { 293 + usage.UniqueLayers[m.Config.Digest] = m.Config.Size 294 + usage.TotalSize += m.Config.Size 295 + usage.LayerCount++ 296 + } 297 + } 298 + 299 + // Add layers 300 + for _, layer := range m.Layers { 301 + if _, seen := usage.UniqueLayers[layer.Digest]; !seen { 302 + usage.UniqueLayers[layer.Digest] = layer.Size 303 + usage.TotalSize += layer.Size 304 + usage.LayerCount++ 305 + } 306 + } 307 + } 308 + 309 + fmt.Printf(" %d manifests, %d unique layers, %s\n", len(manifests), usage.LayerCount, humanSize(usage.TotalSize)) 310 + 311 + if usage.LayerCount > 0 { 312 + userUsage[did] = usage 313 + } 314 + } 315 + 316 + return userUsage, nil 317 + } 318 + 319 + // fetchCrewMembers gets all crew member DIDs from the hold 320 + func fetchCrewMembers(baseURL, holdDID string) ([]string, error) { 321 + var dids []string 322 + seen := make(map[string]bool) 323 + 324 + cursor := "" 325 + for { 326 + u := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords", baseURL) 327 + params := url.Values{} 328 + params.Set("repo", holdDID) 329 + params.Set("collection", "io.atcr.hold.crew") 330 + params.Set("limit", "100") 331 + if cursor != "" { 332 + params.Set("cursor", cursor) 333 + } 334 + 335 + resp, err := client.Get(u + "?" + params.Encode()) 336 + if err != nil { 337 + return nil, err 338 + } 339 + 340 + var listResp ListRecordsResponse 341 + if err := json.NewDecoder(resp.Body).Decode(&listResp); err != nil { 342 + resp.Body.Close() 343 + return nil, err 344 + } 345 + resp.Body.Close() 346 + 347 + for _, rec := range listResp.Records { 348 + var crew CrewRecord 349 + if err := json.Unmarshal(rec.Value, &crew); err != nil { 350 + continue 351 + } 352 + if crew.Member != "" && !seen[crew.Member] { 353 + seen[crew.Member] = true 354 + dids = append(dids, crew.Member) 355 + } 356 + } 357 + 358 + if listResp.Cursor == "" || len(listResp.Records) < 100 { 359 + break 360 + } 361 + cursor = listResp.Cursor 362 + } 363 + 364 + return dids, nil 365 + } 366 + 367 + // fetchCaptain gets the captain DID from the hold 368 + func fetchCaptain(baseURL, holdDID string) (string, error) { 369 + u := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=io.atcr.hold.captain&rkey=self", 370 + baseURL, url.QueryEscape(holdDID)) 371 + 372 + resp, err := client.Get(u) 373 + if err != nil { 374 + return "", err 375 + } 376 + defer resp.Body.Close() 377 + 378 + if resp.StatusCode != http.StatusOK { 379 + return "", fmt.Errorf("status %d", resp.StatusCode) 380 + } 381 + 382 + var result struct { 383 + Value struct { 384 + Owner string `json:"owner"` 385 + } `json:"value"` 386 + } 387 + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 388 + return "", err 389 + } 390 + 391 + return result.Value.Owner, nil 392 + } 393 + 394 + // fetchUserManifestsForHold fetches all manifests from a user's PDS that use the specified hold 395 + func fetchUserManifestsForHold(pdsEndpoint, userDID, holdDID string) ([]ManifestRecord, error) { 396 + var manifests []ManifestRecord 397 + cursor := "" 398 + 399 + for { 400 + u := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords", pdsEndpoint) 401 + params := url.Values{} 402 + params.Set("repo", userDID) 403 + params.Set("collection", "io.atcr.manifest") 404 + params.Set("limit", "100") 405 + if cursor != "" { 406 + params.Set("cursor", cursor) 407 + } 408 + 409 + resp, err := client.Get(u + "?" + params.Encode()) 410 + if err != nil { 411 + return nil, err 412 + } 413 + 414 + if resp.StatusCode != http.StatusOK { 415 + resp.Body.Close() 416 + return nil, fmt.Errorf("status %d", resp.StatusCode) 417 + } 418 + 419 + var listResp ListRecordsResponse 420 + if err := json.NewDecoder(resp.Body).Decode(&listResp); err != nil { 421 + resp.Body.Close() 422 + return nil, err 423 + } 424 + resp.Body.Close() 425 + 426 + for _, rec := range listResp.Records { 427 + var m ManifestRecord 428 + if err := json.Unmarshal(rec.Value, &m); err != nil { 429 + continue 430 + } 431 + // Only include manifests for this hold 432 + if m.HoldDID == holdDID { 433 + manifests = append(manifests, m) 434 + } 435 + } 436 + 437 + if listResp.Cursor == "" || len(listResp.Records) < 100 { 438 + break 439 + } 440 + cursor = listResp.Cursor 441 + } 442 + 443 + return manifests, nil 444 + } 445 + 446 + // getHoldDID fetches the hold's DID from /.well-known/atproto-did 447 + func getHoldDID(baseURL string) (string, error) { 448 + resp, err := http.Get(baseURL + "/.well-known/atproto-did") 449 + if err != nil { 450 + return "", err 451 + } 452 + defer resp.Body.Close() 453 + 454 + if resp.StatusCode != http.StatusOK { 455 + return "", fmt.Errorf("unexpected status: %d", resp.StatusCode) 456 + } 457 + 458 + body, err := io.ReadAll(resp.Body) 459 + if err != nil { 460 + return "", err 461 + } 462 + 463 + return strings.TrimSpace(string(body)), nil 464 + } 465 + 466 + // fetchAllLayerRecords fetches all layer records with pagination 467 + func fetchAllLayerRecords(baseURL, holdDID string) ([]LayerRecord, error) { 468 + var allLayers []LayerRecord 469 + cursor := "" 470 + limit := 100 471 + 472 + for { 473 + u := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords", baseURL) 474 + params := url.Values{} 475 + params.Set("repo", holdDID) 476 + params.Set("collection", "io.atcr.hold.layer") 477 + params.Set("limit", fmt.Sprintf("%d", limit)) 478 + if cursor != "" { 479 + params.Set("cursor", cursor) 480 + } 481 + 482 + fullURL := u + "?" + params.Encode() 483 + fmt.Printf(" Fetching: %s\n", fullURL) 484 + 485 + resp, err := client.Get(fullURL) 486 + if err != nil { 487 + return nil, fmt.Errorf("request failed: %w", err) 488 + } 489 + 490 + if resp.StatusCode != http.StatusOK { 491 + body, _ := io.ReadAll(resp.Body) 492 + resp.Body.Close() 493 + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) 494 + } 495 + 496 + var listResp ListRecordsResponse 497 + if err := json.NewDecoder(resp.Body).Decode(&listResp); err != nil { 498 + resp.Body.Close() 499 + return nil, fmt.Errorf("decode failed: %w", err) 500 + } 501 + resp.Body.Close() 502 + 503 + for _, rec := range listResp.Records { 504 + var layer LayerRecord 505 + if err := json.Unmarshal(rec.Value, &layer); err != nil { 506 + fmt.Fprintf(os.Stderr, "Warning: failed to parse layer record: %v\n", err) 507 + continue 508 + } 509 + allLayers = append(allLayers, layer) 510 + } 511 + 512 + fmt.Printf(" Got %d records (total: %d)\n", len(listResp.Records), len(allLayers)) 513 + 514 + if listResp.Cursor == "" || len(listResp.Records) < limit { 515 + break 516 + } 517 + cursor = listResp.Cursor 518 + } 519 + 520 + return allLayers, nil 521 + } 522 + 523 + // resolveDIDToHandle resolves a DID to a handle using the PLC directory or did:web 524 + func resolveDIDToHandle(did string) (string, error) { 525 + if strings.HasPrefix(did, "did:web:") { 526 + return strings.TrimPrefix(did, "did:web:"), nil 527 + } 528 + 529 + if strings.HasPrefix(did, "did:plc:") { 530 + plcURL := "https://plc.directory/" + did 531 + resp, err := client.Get(plcURL) 532 + if err != nil { 533 + return "", fmt.Errorf("PLC query failed: %w", err) 534 + } 535 + defer resp.Body.Close() 536 + 537 + if resp.StatusCode != http.StatusOK { 538 + return "", fmt.Errorf("PLC returned status %d", resp.StatusCode) 539 + } 540 + 541 + var plcDoc struct { 542 + AlsoKnownAs []string `json:"alsoKnownAs"` 543 + } 544 + if err := json.NewDecoder(resp.Body).Decode(&plcDoc); err != nil { 545 + return "", fmt.Errorf("failed to parse PLC response: %w", err) 546 + } 547 + 548 + for _, aka := range plcDoc.AlsoKnownAs { 549 + if strings.HasPrefix(aka, "at://") { 550 + return strings.TrimPrefix(aka, "at://"), nil 551 + } 552 + } 553 + 554 + return did, nil 555 + } 556 + 557 + return did, nil 558 + } 559 + 560 + // resolveDIDToPDS resolves a DID to its PDS endpoint 561 + func resolveDIDToPDS(did string) (string, error) { 562 + if strings.HasPrefix(did, "did:web:") { 563 + // did:web:example.com -> https://example.com 564 + domain := strings.TrimPrefix(did, "did:web:") 565 + return "https://" + domain, nil 566 + } 567 + 568 + if strings.HasPrefix(did, "did:plc:") { 569 + plcURL := "https://plc.directory/" + did 570 + resp, err := client.Get(plcURL) 571 + if err != nil { 572 + return "", fmt.Errorf("PLC query failed: %w", err) 573 + } 574 + defer resp.Body.Close() 575 + 576 + if resp.StatusCode != http.StatusOK { 577 + return "", fmt.Errorf("PLC returned status %d", resp.StatusCode) 578 + } 579 + 580 + var plcDoc struct { 581 + Service []struct { 582 + ID string `json:"id"` 583 + Type string `json:"type"` 584 + ServiceEndpoint string `json:"serviceEndpoint"` 585 + } `json:"service"` 586 + } 587 + if err := json.NewDecoder(resp.Body).Decode(&plcDoc); err != nil { 588 + return "", fmt.Errorf("failed to parse PLC response: %w", err) 589 + } 590 + 591 + for _, svc := range plcDoc.Service { 592 + if svc.Type == "AtprotoPersonalDataServer" { 593 + return svc.ServiceEndpoint, nil 594 + } 595 + } 596 + 597 + return "", fmt.Errorf("no PDS found in DID document") 598 + } 599 + 600 + return "", fmt.Errorf("unsupported DID method") 601 + } 602 + 603 + // humanSize converts bytes to human-readable format 604 + func humanSize(bytes int64) string { 605 + const ( 606 + KB = 1024 607 + MB = 1024 * KB 608 + GB = 1024 * MB 609 + TB = 1024 * GB 610 + ) 611 + 612 + switch { 613 + case bytes >= TB: 614 + return fmt.Sprintf("%.2f TB", float64(bytes)/TB) 615 + case bytes >= GB: 616 + return fmt.Sprintf("%.2f GB", float64(bytes)/GB) 617 + case bytes >= MB: 618 + return fmt.Sprintf("%.2f MB", float64(bytes)/MB) 619 + case bytes >= KB: 620 + return fmt.Sprintf("%.2f KB", float64(bytes)/KB) 621 + default: 622 + return fmt.Sprintf("%d B", bytes) 623 + } 624 + }
+2
deploy/docker-compose.prod.yml
··· 91 91 container_name: atcr-hold 92 92 restart: unless-stopped 93 93 environment: 94 + HOLD_ADMIN_ENABLED: true 94 95 # Hold service configuration (derived from HOLD_DOMAIN) 95 96 HOLD_PUBLIC_URL: ${HOLD_PUBLIC_URL:-https://${HOLD_DOMAIN:-hold01.atcr.io}} 96 97 HOLD_SERVER_ADDR: :8080 ··· 123 124 volumes: 124 125 # PDS data (carstore SQLite + signing keys) 125 126 - atcr-hold-data:/var/lib/atcr-hold 127 + - ./quotas.yaml:/quotas.yaml:ro 126 128 networks: 127 129 - atcr-network 128 130 healthcheck:
+35
deploy/quotas.yaml
··· 1 + # ATCR Hold Service Quota Configuration 2 + # Copy this file to quotas.yaml to enable quota enforcement. 3 + # If quotas.yaml doesn't exist, quotas are disabled (unlimited for all users). 4 + 5 + # Tiers define quota levels using nautical crew ranks. 6 + # Each tier has a quota limit specified in human-readable format. 7 + # Supported units: B, KB, MB, GB, TB, PB (case-insensitive) 8 + tiers: 9 + # Entry-level crew - suitable for new or casual users 10 + deckhand: 11 + quota: 5GB 12 + 13 + # Mid-level crew - for regular contributors 14 + bosun: 15 + quota: 10GB 16 + 17 + # Senior crew - for power users or trusted contributors 18 + quartermaster: 19 + quota: 50GB 20 + 21 + # You can add custom tiers with any name: 22 + # unlimited_crew: 23 + # quota: 1TB 24 + 25 + defaults: 26 + # Default tier assigned to new crew members who don't have an explicit tier. 27 + # This tier must exist in the tiers section above. 28 + new_crew_tier: deckhand 29 + 30 + # Notes: 31 + # - The hold captain (owner) always has unlimited quota regardless of tiers. 32 + # - Crew members can be assigned a specific tier in their crew record. 33 + # - If a crew member's tier doesn't exist in config, they fall back to the default. 34 + # - Quota is calculated per-user by summing unique blob sizes (deduplicated). 35 + # - Quota is checked when pushing manifests (after blobs are already uploaded).
+11 -2
docker-compose.yml
··· 45 45 env_file: 46 46 - ../atcr-secrets.env # Load S3/Storj credentials from external file 47 47 environment: 48 + HOLD_ADMIN_ENABLED: true 48 49 HOLD_PUBLIC_URL: http://172.28.0.3:8080 49 50 HOLD_OWNER: did:plc:pddp4xt5lgnv2qsegbzzs4xg 50 51 HOLD_PUBLIC: false 52 + HOLD_ALLOW_ALL_CREW: true 51 53 # STORAGE_DRIVER: filesystem 52 54 # STORAGE_ROOT_DIR: /var/lib/atcr/hold 53 55 TEST_MODE: true ··· 57 59 # Storage config comes from env_file (STORAGE_DRIVER, AWS_*, S3_*) 58 60 build: 59 61 context: . 60 - dockerfile: Dockerfile.hold 61 - image: atcr-hold:latest 62 + dockerfile: Dockerfile.dev 63 + args: 64 + AIR_CONFIG: .air.hold.toml 65 + image: atcr-hold-dev:latest 62 66 container_name: atcr-hold 63 67 ports: 64 68 - "8080:8080" 65 69 volumes: 70 + # Mount source code for Air hot reload 71 + - .:/app 72 + # Cache go modules between rebuilds 73 + - go-mod-cache:/go/pkg/mod 66 74 # PDS data (carstore SQLite + signing keys) 67 75 - atcr-hold:/var/lib/atcr-hold 76 + - ./deploy/quotas.yaml:/app/quotas.yaml:ro 68 77 restart: unless-stopped 69 78 dns: 70 79 - 8.8.8.8
+1399
docs/ADMIN_PANEL.md
··· 1 + # Hold Admin Panel Implementation Plan 2 + 3 + This document describes the implementation plan for adding an owner-only admin web UI to the ATCR hold service. The admin panel will be embedded directly in the hold service binary for simplified deployment. 4 + 5 + ## Table of Contents 6 + 7 + 1. [Overview](#overview) 8 + 2. [Requirements](#requirements) 9 + 3. [Architecture](#architecture) 10 + 4. [File Structure](#file-structure) 11 + 5. [Authentication](#authentication) 12 + 6. [Session Management](#session-management) 13 + 7. [Route Structure](#route-structure) 14 + 8. [Feature Implementations](#feature-implementations) 15 + 9. [Templates](#templates) 16 + 10. [Environment Variables](#environment-variables) 17 + 11. [Security Considerations](#security-considerations) 18 + 12. [Implementation Phases](#implementation-phases) 19 + 13. [Testing Strategy](#testing-strategy) 20 + 21 + --- 22 + 23 + ## Overview 24 + 25 + The hold admin panel provides a web-based interface for hold owners to: 26 + 27 + - **Manage crew members**: Add, remove, edit permissions and quota tiers 28 + - **Configure hold settings**: Toggle public access, open registration, Bluesky posting 29 + - **View usage metrics**: Storage usage per user, top users, repository statistics 30 + - **Monitor quota utilization**: Track tier distribution and usage percentages 31 + 32 + The admin panel is owner-only - only the DID that matches `captain.Owner` can access it. 33 + 34 + --- 35 + 36 + ## Requirements 37 + 38 + ### Functional Requirements 39 + 40 + 1. **Crew Management** 41 + - List all crew members with their DID, role, permissions, tier, and storage usage 42 + - Add new crew members with specified permissions and tier 43 + - Edit existing crew member permissions and tier 44 + - Remove crew members (with confirmation) 45 + - Display each crew member's quota utilization percentage 46 + 47 + 2. **Quota/Tier Management** 48 + - Display available tiers from `quotas.yaml` 49 + - Show tier limits and descriptions 50 + - Allow changing crew member tiers 51 + - Display current vs limit usage for each user 52 + 53 + 3. **Usage Metrics** 54 + - Total storage used across all users 55 + - Total unique blobs (deduplicated) 56 + - Number of crew members 57 + - Top 10/50/100 users by storage consumption 58 + - Per-repository statistics (pulls, pushes) 59 + 60 + 4. **Hold Settings** 61 + - Toggle `public` (allow anonymous blob reads) 62 + - Toggle `allowAllCrew` (allow any authenticated user to join) 63 + - Toggle `enableBlueskyPosts` (post to Bluesky on image push) 64 + 65 + ### Non-Functional Requirements 66 + 67 + - **Single binary**: Embedded in hold service, no separate deployment 68 + - **Responsive UI**: Works on desktop and mobile browsers 69 + - **Low latency**: Dashboard loads in <500ms for typical data volumes 70 + - **Minimal dependencies**: Uses Go templates, HTMX for interactivity 71 + 72 + --- 73 + 74 + ## Architecture 75 + 76 + ### High-Level Design 77 + 78 + ``` 79 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 80 + โ”‚ Hold Service โ”‚ 81 + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค 82 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 83 + โ”‚ โ”‚ XRPC/PDS โ”‚ โ”‚ OCI XRPC โ”‚ โ”‚ Admin Panel โ”‚ โ”‚ 84 + โ”‚ โ”‚ Handlers โ”‚ โ”‚ Handlers โ”‚ โ”‚ Handlers โ”‚ โ”‚ 85 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 86 + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 87 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 88 + โ”‚ โ”‚ Chi Router โ”‚ โ”‚ 89 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 90 + โ”‚ โ”‚ โ”‚ 91 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 92 + โ”‚ โ”‚ Embedded PDS โ”‚ โ”‚ 93 + โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ 94 + โ”‚ โ”‚ โ”‚ Captain โ”‚ โ”‚ Crew โ”‚ โ”‚ Layer โ”‚ โ”‚ โ”‚ 95 + โ”‚ โ”‚ โ”‚ Records โ”‚ โ”‚ Records โ”‚ โ”‚ Records โ”‚ โ”‚ โ”‚ 96 + โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ 97 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 98 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 99 + ``` 100 + 101 + ### Components 102 + 103 + 1. **AdminUI** - Main struct containing all admin dependencies 104 + 2. **Session Store** - SQLite-backed session management (separate from carstore) 105 + 3. **OAuth Client** - Reuses `pkg/auth/oauth/` for browser-based login 106 + 4. **Auth Middleware** - Validates owner-only access 107 + 5. **Handlers** - HTTP handlers for each admin page 108 + 6. **Templates** - Go html/template with embed.FS 109 + 110 + --- 111 + 112 + ## File Structure 113 + 114 + ``` 115 + pkg/hold/admin/ 116 + โ”œโ”€โ”€ admin.go # Main struct, initialization, route registration 117 + โ”œโ”€โ”€ auth.go # requireOwner middleware, session validation 118 + โ”œโ”€โ”€ handlers.go # HTTP handlers for all admin pages 119 + โ”œโ”€โ”€ session.go # SQLite session store implementation 120 + โ”œโ”€โ”€ metrics.go # Metrics collection and aggregation 121 + โ”œโ”€โ”€ templates/ 122 + โ”‚ โ”œโ”€โ”€ base.html # Base layout (html, head, body wrapper) 123 + โ”‚ โ”œโ”€โ”€ components/ 124 + โ”‚ โ”‚ โ”œโ”€โ”€ head.html # CSS/JS includes (HTMX, Lucide icons) 125 + โ”‚ โ”‚ โ”œโ”€โ”€ nav.html # Admin navigation bar 126 + โ”‚ โ”‚ โ””โ”€โ”€ flash.html # Flash message component 127 + โ”‚ โ”œโ”€โ”€ pages/ 128 + โ”‚ โ”‚ โ”œโ”€โ”€ login.html # OAuth login page 129 + โ”‚ โ”‚ โ”œโ”€โ”€ dashboard.html # Metrics overview 130 + โ”‚ โ”‚ โ”œโ”€โ”€ crew.html # Crew list with management actions 131 + โ”‚ โ”‚ โ”œโ”€โ”€ crew_add.html # Add crew member form 132 + โ”‚ โ”‚ โ”œโ”€โ”€ crew_edit.html # Edit crew member form 133 + โ”‚ โ”‚ โ””โ”€โ”€ settings.html # Hold settings page 134 + โ”‚ โ””โ”€โ”€ partials/ 135 + โ”‚ โ”œโ”€โ”€ crew_row.html # Single crew row (for HTMX updates) 136 + โ”‚ โ”œโ”€โ”€ usage_stats.html # Usage stats partial 137 + โ”‚ โ””โ”€โ”€ top_users.html # Top users table partial 138 + โ””โ”€โ”€ static/ 139 + โ”œโ”€โ”€ css/ 140 + โ”‚ โ””โ”€โ”€ admin.css # Admin-specific styles 141 + โ””โ”€โ”€ js/ 142 + โ””โ”€โ”€ admin.js # Admin-specific JavaScript (if needed) 143 + ``` 144 + 145 + ### Files to Modify 146 + 147 + | File | Changes | 148 + |------|---------| 149 + | `cmd/hold/main.go` | Add admin UI initialization and route registration | 150 + | `pkg/hold/config.go` | Add `Admin.Enabled` and `Admin.SessionDuration` fields | 151 + | `.env.hold.example` | Document `HOLD_ADMIN_ENABLED`, `HOLD_ADMIN_SESSION_DURATION` | 152 + 153 + --- 154 + 155 + ## Authentication 156 + 157 + ### OAuth Flow for Admin Login 158 + 159 + The admin panel uses ATProto OAuth with DPoP for browser-based authentication: 160 + 161 + ``` 162 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 163 + โ”‚ Browser โ”‚ โ”‚ Hold โ”‚ โ”‚ PDS โ”‚ โ”‚ Owner โ”‚ 164 + โ”‚ โ”‚ โ”‚ Admin โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 165 + โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ 166 + โ”‚ โ”‚ โ”‚ โ”‚ 167 + โ”‚ GET /admin โ”‚ โ”‚ โ”‚ 168 + โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 169 + โ”‚ โ”‚ โ”‚ โ”‚ 170 + โ”‚ 302 /admin/auth/login โ”‚ โ”‚ 171 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ 172 + โ”‚ โ”‚ โ”‚ โ”‚ 173 + โ”‚ GET /admin/auth/login โ”‚ โ”‚ 174 + โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 175 + โ”‚ โ”‚ โ”‚ โ”‚ 176 + โ”‚ Login page (enter handle) โ”‚ โ”‚ 177 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ 178 + โ”‚ โ”‚ โ”‚ โ”‚ 179 + โ”‚ POST handle โ”‚ โ”‚ โ”‚ 180 + โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 181 + โ”‚ โ”‚ โ”‚ โ”‚ 182 + โ”‚ โ”‚ StartAuthFlow โ”‚ โ”‚ 183 + โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 184 + โ”‚ โ”‚ โ”‚ โ”‚ 185 + โ”‚ 302 to PDS auth URL โ”‚ โ”‚ 186 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ 187 + โ”‚ โ”‚ โ”‚ โ”‚ 188 + โ”‚ Authorize in browser โ”‚ โ”‚ 189 + โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 190 + โ”‚ โ”‚ โ”‚ Approve? โ”‚ 191 + โ”‚ โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 192 + โ”‚ โ”‚ โ”‚ โ”‚ 193 + โ”‚ โ”‚ โ”‚ Yes โ”‚ 194 + โ”‚ โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ 195 + โ”‚ โ”‚ โ”‚ โ”‚ 196 + โ”‚ 302 callback with code โ”‚ โ”‚ 197 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ 198 + โ”‚ โ”‚ โ”‚ โ”‚ 199 + โ”‚ GET /admin/auth/oauth/callback โ”‚ โ”‚ 200 + โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 201 + โ”‚ โ”‚ โ”‚ โ”‚ 202 + โ”‚ โ”‚ ProcessCallbackโ”‚ โ”‚ 203 + โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 204 + โ”‚ โ”‚ โ”‚ โ”‚ 205 + โ”‚ โ”‚ OAuth tokens โ”‚ โ”‚ 206 + โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ 207 + โ”‚ โ”‚ โ”‚ โ”‚ 208 + โ”‚ โ”‚ Check: DID == captain.Owner? โ”‚ 209 + โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ 210 + โ”‚ โ”‚ โ”‚ โ”‚ 211 + โ”‚ โ”‚ YES: Create session โ”‚ 212 + โ”‚ โ”‚ โ”‚ โ”‚ 213 + โ”‚ 302 /admin + session cookie โ”‚ โ”‚ 214 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ 215 + โ”‚ โ”‚ โ”‚ โ”‚ 216 + โ”‚ GET /admin (with cookie) โ”‚ โ”‚ 217 + โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 218 + โ”‚ โ”‚ โ”‚ โ”‚ 219 + โ”‚ Dashboard โ”‚ โ”‚ โ”‚ 220 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ 221 + ``` 222 + 223 + ### Owner Validation 224 + 225 + The callback handler performs owner validation: 226 + 227 + ```go 228 + func (ui *AdminUI) handleCallback(w http.ResponseWriter, r *http.Request) { 229 + // Process OAuth callback 230 + sessionData, err := ui.clientApp.ProcessCallback(r.Context(), r.URL.Query()) 231 + if err != nil { 232 + ui.renderError(w, "OAuth failed: " + err.Error()) 233 + return 234 + } 235 + 236 + did := sessionData.AccountDID.String() 237 + 238 + // Get captain record to check owner 239 + _, captain, err := ui.pds.GetCaptainRecord(r.Context()) 240 + if err != nil { 241 + ui.renderError(w, "Failed to verify ownership") 242 + return 243 + } 244 + 245 + // CRITICAL: Only allow the hold owner 246 + if did != captain.Owner { 247 + slog.Warn("Non-owner attempted admin access", "did", did, "owner", captain.Owner) 248 + ui.renderError(w, "Access denied: Only the hold owner can access the admin panel") 249 + return 250 + } 251 + 252 + // Create admin session 253 + sessionID, err := ui.sessionStore.Create(did, sessionData.Handle, 24*time.Hour) 254 + if err != nil { 255 + ui.renderError(w, "Failed to create session") 256 + return 257 + } 258 + 259 + // Set session cookie 260 + http.SetCookie(w, &http.Cookie{ 261 + Name: "hold_admin_session", 262 + Value: sessionID, 263 + Path: "/admin", 264 + MaxAge: 86400, // 24 hours 265 + HttpOnly: true, 266 + Secure: r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https", 267 + SameSite: http.SameSiteLaxMode, 268 + }) 269 + 270 + http.Redirect(w, r, "/admin", http.StatusFound) 271 + } 272 + ``` 273 + 274 + ### Auth Middleware 275 + 276 + ```go 277 + // requireOwner ensures the request is from the hold owner 278 + func (ui *AdminUI) requireOwner(next http.Handler) http.Handler { 279 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 280 + // Get session cookie 281 + cookie, err := r.Cookie("hold_admin_session") 282 + if err != nil { 283 + http.Redirect(w, r, "/admin/auth/login?return_to="+r.URL.Path, http.StatusFound) 284 + return 285 + } 286 + 287 + // Validate session 288 + session, err := ui.sessionStore.Get(cookie.Value) 289 + if err != nil || session == nil || session.ExpiresAt.Before(time.Now()) { 290 + // Clear invalid cookie 291 + http.SetCookie(w, &http.Cookie{ 292 + Name: "hold_admin_session", 293 + Value: "", 294 + Path: "/admin", 295 + MaxAge: -1, 296 + }) 297 + http.Redirect(w, r, "/admin/auth/login", http.StatusFound) 298 + return 299 + } 300 + 301 + // Double-check DID still matches captain.Owner 302 + // (in case ownership transferred while session active) 303 + _, captain, err := ui.pds.GetCaptainRecord(r.Context()) 304 + if err != nil || session.DID != captain.Owner { 305 + ui.sessionStore.Delete(cookie.Value) 306 + http.Error(w, "Access denied: ownership verification failed", http.StatusForbidden) 307 + return 308 + } 309 + 310 + // Add session to context for handlers 311 + ctx := context.WithValue(r.Context(), adminSessionKey, session) 312 + next.ServeHTTP(w, r.WithContext(ctx)) 313 + }) 314 + } 315 + ``` 316 + 317 + --- 318 + 319 + ## Session Management 320 + 321 + ### Session Store Schema 322 + 323 + ```sql 324 + -- Admin sessions (browser login state) 325 + CREATE TABLE IF NOT EXISTS admin_sessions ( 326 + id TEXT PRIMARY KEY, 327 + did TEXT NOT NULL, 328 + handle TEXT, 329 + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 330 + expires_at DATETIME NOT NULL, 331 + last_accessed DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP 332 + ); 333 + 334 + -- Index for cleanup queries 335 + CREATE INDEX IF NOT EXISTS idx_admin_sessions_expires ON admin_sessions(expires_at); 336 + CREATE INDEX IF NOT EXISTS idx_admin_sessions_did ON admin_sessions(did); 337 + 338 + -- OAuth sessions (indigo library storage) 339 + CREATE TABLE IF NOT EXISTS admin_oauth_sessions ( 340 + session_id TEXT PRIMARY KEY, 341 + did TEXT NOT NULL, 342 + data BLOB NOT NULL, 343 + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 344 + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP 345 + ); 346 + ``` 347 + 348 + ### Session Store Interface 349 + 350 + ```go 351 + // AdminSession represents an authenticated admin session 352 + type AdminSession struct { 353 + ID string 354 + DID string 355 + Handle string 356 + CreatedAt time.Time 357 + ExpiresAt time.Time 358 + LastAccessed time.Time 359 + } 360 + 361 + // AdminSessionStore manages admin sessions 362 + type AdminSessionStore struct { 363 + db *sql.DB 364 + } 365 + 366 + func NewAdminSessionStore(dbPath string) (*AdminSessionStore, error) 367 + 368 + func (s *AdminSessionStore) Create(did, handle string, duration time.Duration) (string, error) 369 + func (s *AdminSessionStore) Get(sessionID string) (*AdminSession, error) 370 + func (s *AdminSessionStore) Delete(sessionID string) error 371 + func (s *AdminSessionStore) DeleteForDID(did string) error 372 + func (s *AdminSessionStore) Cleanup() error // Remove expired sessions 373 + func (s *AdminSessionStore) Touch(sessionID string) error // Update last_accessed 374 + ``` 375 + 376 + ### Database Location 377 + 378 + The admin database should be in the same directory as the carstore database: 379 + 380 + ```go 381 + adminDBPath := filepath.Join(cfg.Database.Path, "admin.db") 382 + ``` 383 + 384 + This keeps all hold data together while maintaining separation between the carstore (ATProto records) and admin sessions. 385 + 386 + --- 387 + 388 + ## Route Structure 389 + 390 + ### Complete Route Table 391 + 392 + | Route | Method | Auth | Handler | Description | 393 + |-------|--------|------|---------|-------------| 394 + | `/admin` | GET | Owner | `DashboardHandler` | Main dashboard with metrics | 395 + | `/admin/crew` | GET | Owner | `CrewListHandler` | List all crew members | 396 + | `/admin/crew/add` | GET | Owner | `CrewAddFormHandler` | Add crew form | 397 + | `/admin/crew/add` | POST | Owner | `CrewAddHandler` | Process add crew | 398 + | `/admin/crew/{rkey}` | GET | Owner | `CrewEditFormHandler` | Edit crew form | 399 + | `/admin/crew/{rkey}/update` | POST | Owner | `CrewUpdateHandler` | Process crew update | 400 + | `/admin/crew/{rkey}/delete` | POST | Owner | `CrewDeleteHandler` | Delete crew member | 401 + | `/admin/settings` | GET | Owner | `SettingsHandler` | Hold settings page | 402 + | `/admin/settings/update` | POST | Owner | `SettingsUpdateHandler` | Update settings | 403 + | `/admin/api/stats` | GET | Owner | `StatsAPIHandler` | JSON stats endpoint | 404 + | `/admin/api/top-users` | GET | Owner | `TopUsersAPIHandler` | JSON top users | 405 + | `/admin/auth/login` | GET | Public | `LoginHandler` | Login page | 406 + | `/admin/auth/oauth/authorize` | GET | Public | OAuth authorize | Start OAuth flow | 407 + | `/admin/auth/oauth/callback` | GET | Public | `CallbackHandler` | OAuth callback | 408 + | `/admin/auth/logout` | GET | Owner | `LogoutHandler` | Logout and clear session | 409 + | `/admin/static/*` | GET | Public | Static files | CSS, JS assets | 410 + 411 + ### Route Registration 412 + 413 + ```go 414 + func (ui *AdminUI) RegisterRoutes(r chi.Router) { 415 + // Public routes (login flow) 416 + r.Get("/admin/auth/login", ui.handleLogin) 417 + r.Get("/admin/auth/oauth/authorize", ui.handleAuthorize) 418 + r.Get("/admin/auth/oauth/callback", ui.handleCallback) 419 + 420 + // Static files (public) 421 + r.Handle("/admin/static/*", http.StripPrefix("/admin/static/", ui.staticHandler())) 422 + 423 + // Protected routes (require owner) 424 + r.Group(func(r chi.Router) { 425 + r.Use(ui.requireOwner) 426 + 427 + // Dashboard 428 + r.Get("/admin", ui.handleDashboard) 429 + 430 + // Crew management 431 + r.Get("/admin/crew", ui.handleCrewList) 432 + r.Get("/admin/crew/add", ui.handleCrewAddForm) 433 + r.Post("/admin/crew/add", ui.handleCrewAdd) 434 + r.Get("/admin/crew/{rkey}", ui.handleCrewEditForm) 435 + r.Post("/admin/crew/{rkey}/update", ui.handleCrewUpdate) 436 + r.Post("/admin/crew/{rkey}/delete", ui.handleCrewDelete) 437 + 438 + // Settings 439 + r.Get("/admin/settings", ui.handleSettings) 440 + r.Post("/admin/settings/update", ui.handleSettingsUpdate) 441 + 442 + // API endpoints (for HTMX) 443 + r.Get("/admin/api/stats", ui.handleStatsAPI) 444 + r.Get("/admin/api/top-users", ui.handleTopUsersAPI) 445 + 446 + // Logout 447 + r.Get("/admin/auth/logout", ui.handleLogout) 448 + }) 449 + } 450 + ``` 451 + 452 + --- 453 + 454 + ## Feature Implementations 455 + 456 + ### Dashboard Handler 457 + 458 + ```go 459 + type DashboardStats struct { 460 + TotalCrewMembers int 461 + TotalBlobs int64 462 + TotalStorageBytes int64 463 + TotalStorageHuman string 464 + TierDistribution map[string]int // tier -> count 465 + RecentActivity []ActivityEntry 466 + } 467 + 468 + func (ui *AdminUI) handleDashboard(w http.ResponseWriter, r *http.Request) { 469 + ctx := r.Context() 470 + 471 + // Collect basic stats 472 + crew, _ := ui.pds.ListCrewMembers(ctx) 473 + 474 + stats := DashboardStats{ 475 + TotalCrewMembers: len(crew), 476 + TierDistribution: make(map[string]int), 477 + } 478 + 479 + // Count tier distribution 480 + for _, member := range crew { 481 + tier := member.Tier 482 + if tier == "" { 483 + tier = ui.quotaMgr.GetDefaultTier() 484 + } 485 + stats.TierDistribution[tier]++ 486 + } 487 + 488 + // Storage stats (loaded via HTMX to avoid slow initial load) 489 + // The actual calculation happens in handleStatsAPI 490 + 491 + data := struct { 492 + AdminPageData 493 + Stats DashboardStats 494 + }{ 495 + AdminPageData: ui.newPageData(r), 496 + Stats: stats, 497 + } 498 + 499 + ui.templates.ExecuteTemplate(w, "dashboard", data) 500 + } 501 + ``` 502 + 503 + ### Crew List Handler 504 + 505 + ```go 506 + type CrewMemberView struct { 507 + RKey string 508 + DID string 509 + Handle string // Resolved from DID 510 + Role string 511 + Permissions []string 512 + Tier string 513 + TierLimit string // Human-readable 514 + CurrentUsage int64 515 + UsageHuman string 516 + UsagePercent int 517 + Plankowner bool 518 + AddedAt time.Time 519 + } 520 + 521 + func (ui *AdminUI) handleCrewList(w http.ResponseWriter, r *http.Request) { 522 + ctx := r.Context() 523 + 524 + crew, err := ui.pds.ListCrewMembers(ctx) 525 + if err != nil { 526 + ui.renderError(w, "Failed to list crew: "+err.Error()) 527 + return 528 + } 529 + 530 + // Enrich with usage data 531 + var crewViews []CrewMemberView 532 + for _, member := range crew { 533 + view := CrewMemberView{ 534 + RKey: member.RKey, 535 + DID: member.Member, 536 + Role: member.Role, 537 + Permissions: member.Permissions, 538 + Tier: member.Tier, 539 + Plankowner: member.Plankowner, 540 + AddedAt: member.AddedAt, 541 + } 542 + 543 + // Get tier limit 544 + if limit := ui.quotaMgr.GetTierLimit(member.Tier); limit != nil { 545 + view.TierLimit = quota.FormatHumanBytes(*limit) 546 + } else { 547 + view.TierLimit = "Unlimited" 548 + } 549 + 550 + // Get usage (expensive - consider caching) 551 + usage, _, tier, limit, _ := ui.pds.GetQuotaForUserWithTier(ctx, member.Member, ui.quotaMgr) 552 + view.CurrentUsage = usage 553 + view.UsageHuman = quota.FormatHumanBytes(usage) 554 + if limit != nil && *limit > 0 { 555 + view.UsagePercent = int(float64(usage) / float64(*limit) * 100) 556 + } 557 + 558 + crewViews = append(crewViews, view) 559 + } 560 + 561 + // Sort by usage (highest first) 562 + sort.Slice(crewViews, func(i, j int) bool { 563 + return crewViews[i].CurrentUsage > crewViews[j].CurrentUsage 564 + }) 565 + 566 + data := struct { 567 + AdminPageData 568 + Crew []CrewMemberView 569 + Tiers []TierOption 570 + }{ 571 + AdminPageData: ui.newPageData(r), 572 + Crew: crewViews, 573 + Tiers: ui.getTierOptions(), 574 + } 575 + 576 + ui.templates.ExecuteTemplate(w, "crew", data) 577 + } 578 + ``` 579 + 580 + ### Add Crew Handler 581 + 582 + ```go 583 + func (ui *AdminUI) handleCrewAdd(w http.ResponseWriter, r *http.Request) { 584 + ctx := r.Context() 585 + 586 + if err := r.ParseForm(); err != nil { 587 + ui.setFlash(w, "error", "Invalid form data") 588 + http.Redirect(w, r, "/admin/crew/add", http.StatusFound) 589 + return 590 + } 591 + 592 + did := strings.TrimSpace(r.FormValue("did")) 593 + role := r.FormValue("role") 594 + tier := r.FormValue("tier") 595 + 596 + // Parse permissions checkboxes 597 + var permissions []string 598 + if r.FormValue("perm_read") == "on" { 599 + permissions = append(permissions, "blob:read") 600 + } 601 + if r.FormValue("perm_write") == "on" { 602 + permissions = append(permissions, "blob:write") 603 + } 604 + if r.FormValue("perm_admin") == "on" { 605 + permissions = append(permissions, "crew:admin") 606 + } 607 + 608 + // Validate DID format 609 + if !strings.HasPrefix(did, "did:") { 610 + ui.setFlash(w, "error", "Invalid DID format") 611 + http.Redirect(w, r, "/admin/crew/add", http.StatusFound) 612 + return 613 + } 614 + 615 + // Add crew member 616 + _, err := ui.pds.AddCrewMember(ctx, did, role, permissions) 617 + if err != nil { 618 + ui.setFlash(w, "error", "Failed to add crew member: "+err.Error()) 619 + http.Redirect(w, r, "/admin/crew/add", http.StatusFound) 620 + return 621 + } 622 + 623 + // Set tier if specified 624 + if tier != "" && tier != ui.quotaMgr.GetDefaultTier() { 625 + if err := ui.pds.UpdateCrewMemberTier(ctx, did, tier); err != nil { 626 + slog.Warn("Failed to set tier for new crew member", "did", did, "tier", tier, "error", err) 627 + } 628 + } 629 + 630 + ui.setFlash(w, "success", "Crew member added successfully") 631 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 632 + } 633 + ``` 634 + 635 + ### Update Crew Handler 636 + 637 + ```go 638 + func (ui *AdminUI) handleCrewUpdate(w http.ResponseWriter, r *http.Request) { 639 + ctx := r.Context() 640 + rkey := chi.URLParam(r, "rkey") 641 + 642 + if err := r.ParseForm(); err != nil { 643 + ui.setFlash(w, "error", "Invalid form data") 644 + http.Redirect(w, r, "/admin/crew/"+rkey, http.StatusFound) 645 + return 646 + } 647 + 648 + // Get current crew member 649 + current, err := ui.pds.GetCrewMemberByRKey(ctx, rkey) 650 + if err != nil { 651 + ui.setFlash(w, "error", "Crew member not found") 652 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 653 + return 654 + } 655 + 656 + // Parse new values 657 + role := r.FormValue("role") 658 + tier := r.FormValue("tier") 659 + 660 + var permissions []string 661 + if r.FormValue("perm_read") == "on" { 662 + permissions = append(permissions, "blob:read") 663 + } 664 + if r.FormValue("perm_write") == "on" { 665 + permissions = append(permissions, "blob:write") 666 + } 667 + if r.FormValue("perm_admin") == "on" { 668 + permissions = append(permissions, "crew:admin") 669 + } 670 + 671 + // Update tier if changed 672 + if tier != current.Tier { 673 + if err := ui.pds.UpdateCrewMemberTier(ctx, current.Member, tier); err != nil { 674 + ui.setFlash(w, "error", "Failed to update tier: "+err.Error()) 675 + http.Redirect(w, r, "/admin/crew/"+rkey, http.StatusFound) 676 + return 677 + } 678 + } 679 + 680 + // For role/permissions changes, need to delete and recreate 681 + // (ATProto records are immutable, updates require delete+create) 682 + if role != current.Role || !slicesEqual(permissions, current.Permissions) { 683 + // Delete old record 684 + if err := ui.pds.RemoveCrewMember(ctx, rkey); err != nil { 685 + ui.setFlash(w, "error", "Failed to update: "+err.Error()) 686 + http.Redirect(w, r, "/admin/crew/"+rkey, http.StatusFound) 687 + return 688 + } 689 + 690 + // Create new record with updated values 691 + if _, err := ui.pds.AddCrewMember(ctx, current.Member, role, permissions); err != nil { 692 + ui.setFlash(w, "error", "Failed to recreate crew record: "+err.Error()) 693 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 694 + return 695 + } 696 + 697 + // Re-apply tier to new record 698 + if tier != "" { 699 + ui.pds.UpdateCrewMemberTier(ctx, current.Member, tier) 700 + } 701 + } 702 + 703 + ui.setFlash(w, "success", "Crew member updated successfully") 704 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 705 + } 706 + ``` 707 + 708 + ### Delete Crew Handler 709 + 710 + ```go 711 + func (ui *AdminUI) handleCrewDelete(w http.ResponseWriter, r *http.Request) { 712 + ctx := r.Context() 713 + rkey := chi.URLParam(r, "rkey") 714 + 715 + // Get crew member to log who was deleted 716 + member, err := ui.pds.GetCrewMemberByRKey(ctx, rkey) 717 + if err != nil { 718 + ui.setFlash(w, "error", "Crew member not found") 719 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 720 + return 721 + } 722 + 723 + // Prevent deleting self (captain) 724 + session := getAdminSession(ctx) 725 + if member.Member == session.DID { 726 + ui.setFlash(w, "error", "Cannot remove yourself from crew") 727 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 728 + return 729 + } 730 + 731 + // Delete 732 + if err := ui.pds.RemoveCrewMember(ctx, rkey); err != nil { 733 + ui.setFlash(w, "error", "Failed to remove crew member: "+err.Error()) 734 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 735 + return 736 + } 737 + 738 + slog.Info("Crew member removed via admin panel", "did", member.Member, "by", session.DID) 739 + 740 + // For HTMX requests, return empty response (row will be removed) 741 + if r.Header.Get("HX-Request") == "true" { 742 + w.WriteHeader(http.StatusOK) 743 + return 744 + } 745 + 746 + ui.setFlash(w, "success", "Crew member removed") 747 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 748 + } 749 + ``` 750 + 751 + ### Settings Handler 752 + 753 + ```go 754 + func (ui *AdminUI) handleSettings(w http.ResponseWriter, r *http.Request) { 755 + ctx := r.Context() 756 + 757 + _, captain, err := ui.pds.GetCaptainRecord(ctx) 758 + if err != nil { 759 + ui.renderError(w, "Failed to load settings: "+err.Error()) 760 + return 761 + } 762 + 763 + data := struct { 764 + AdminPageData 765 + Settings struct { 766 + Public bool 767 + AllowAllCrew bool 768 + EnableBlueskyPosts bool 769 + OwnerDID string 770 + HoldDID string 771 + } 772 + }{ 773 + AdminPageData: ui.newPageData(r), 774 + } 775 + data.Settings.Public = captain.Public 776 + data.Settings.AllowAllCrew = captain.AllowAllCrew 777 + data.Settings.EnableBlueskyPosts = captain.EnableBlueskyPosts 778 + data.Settings.OwnerDID = captain.Owner 779 + data.Settings.HoldDID = ui.pds.DID() 780 + 781 + ui.templates.ExecuteTemplate(w, "settings", data) 782 + } 783 + 784 + func (ui *AdminUI) handleSettingsUpdate(w http.ResponseWriter, r *http.Request) { 785 + ctx := r.Context() 786 + 787 + if err := r.ParseForm(); err != nil { 788 + ui.setFlash(w, "error", "Invalid form data") 789 + http.Redirect(w, r, "/admin/settings", http.StatusFound) 790 + return 791 + } 792 + 793 + public := r.FormValue("public") == "on" 794 + allowAllCrew := r.FormValue("allow_all_crew") == "on" 795 + enablePosts := r.FormValue("enable_bluesky_posts") == "on" 796 + 797 + _, err := ui.pds.UpdateCaptainRecord(ctx, public, allowAllCrew, enablePosts) 798 + if err != nil { 799 + ui.setFlash(w, "error", "Failed to update settings: "+err.Error()) 800 + http.Redirect(w, r, "/admin/settings", http.StatusFound) 801 + return 802 + } 803 + 804 + ui.setFlash(w, "success", "Settings updated successfully") 805 + http.Redirect(w, r, "/admin/settings", http.StatusFound) 806 + } 807 + ``` 808 + 809 + ### Metrics Handler (for HTMX lazy loading) 810 + 811 + ```go 812 + func (ui *AdminUI) handleStatsAPI(w http.ResponseWriter, r *http.Request) { 813 + ctx := r.Context() 814 + 815 + // Calculate total storage (expensive operation) 816 + // Iterate through all layer records 817 + records, _, err := ui.pds.RecordsIndex().ListRecords(atproto.LayerCollection, 100000, "", true) 818 + if err != nil { 819 + http.Error(w, "Failed to load stats", http.StatusInternalServerError) 820 + return 821 + } 822 + 823 + var totalSize int64 824 + uniqueDigests := make(map[string]bool) 825 + userUsage := make(map[string]int64) 826 + 827 + for _, record := range records { 828 + var layer atproto.LayerRecord 829 + if err := json.Unmarshal(record.Value, &layer); err != nil { 830 + continue 831 + } 832 + 833 + if !uniqueDigests[layer.Digest] { 834 + uniqueDigests[layer.Digest] = true 835 + totalSize += layer.Size 836 + } 837 + 838 + userUsage[layer.UserDID] += layer.Size 839 + } 840 + 841 + stats := struct { 842 + TotalBlobs int `json:"totalBlobs"` 843 + TotalSize int64 `json:"totalSize"` 844 + TotalHuman string `json:"totalHuman"` 845 + }{ 846 + TotalBlobs: len(uniqueDigests), 847 + TotalSize: totalSize, 848 + TotalHuman: quota.FormatHumanBytes(totalSize), 849 + } 850 + 851 + // If HTMX request, return HTML partial 852 + if r.Header.Get("HX-Request") == "true" { 853 + data := struct { 854 + Stats interface{} 855 + }{Stats: stats} 856 + ui.templates.ExecuteTemplate(w, "usage_stats", data) 857 + return 858 + } 859 + 860 + // Otherwise return JSON 861 + w.Header().Set("Content-Type", "application/json") 862 + json.NewEncoder(w).Encode(stats) 863 + } 864 + ``` 865 + 866 + --- 867 + 868 + ## Templates 869 + 870 + ### Base Layout (templates/base.html) 871 + 872 + ```html 873 + {{ define "base" }} 874 + <!DOCTYPE html> 875 + <html lang="en"> 876 + <head> 877 + <meta charset="UTF-8"> 878 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 879 + <title>{{ .Title }} - Hold Admin</title> 880 + {{ template "head" . }} 881 + </head> 882 + <body> 883 + {{ template "nav" . }} 884 + 885 + <main class="admin-container"> 886 + {{ template "flash" . }} 887 + {{ template "content" . }} 888 + </main> 889 + 890 + <footer class="admin-footer"> 891 + <p>Hold: {{ .HoldDID }}</p> 892 + </footer> 893 + </body> 894 + </html> 895 + {{ end }} 896 + ``` 897 + 898 + ### Head Component (templates/components/head.html) 899 + 900 + ```html 901 + {{ define "head" }} 902 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 903 + <script src="https://unpkg.com/htmx.org@1.9.10"></script> 904 + <script src="https://unpkg.com/lucide@latest"></script> 905 + {{ end }} 906 + ``` 907 + 908 + ### Navigation (templates/components/nav.html) 909 + 910 + ```html 911 + {{ define "nav" }} 912 + <nav class="admin-nav"> 913 + <div class="nav-brand"> 914 + <a href="/admin">Hold Admin</a> 915 + </div> 916 + <ul class="nav-links"> 917 + <li><a href="/admin" class="{{ if eq .ActivePage "dashboard" }}active{{ end }}">Dashboard</a></li> 918 + <li><a href="/admin/crew" class="{{ if eq .ActivePage "crew" }}active{{ end }}">Crew</a></li> 919 + <li><a href="/admin/settings" class="{{ if eq .ActivePage "settings" }}active{{ end }}">Settings</a></li> 920 + </ul> 921 + <div class="nav-user"> 922 + <span>{{ .User.Handle }}</span> 923 + <a href="/admin/auth/logout">Logout</a> 924 + </div> 925 + </nav> 926 + {{ end }} 927 + ``` 928 + 929 + ### Dashboard Page (templates/pages/dashboard.html) 930 + 931 + ```html 932 + {{ define "dashboard" }} 933 + {{ template "base" . }} 934 + {{ define "content" }} 935 + <h1>Dashboard</h1> 936 + 937 + <div class="stats-grid"> 938 + <div class="stat-card"> 939 + <h3>Crew Members</h3> 940 + <p class="stat-value">{{ .Stats.TotalCrewMembers }}</p> 941 + </div> 942 + 943 + <div class="stat-card" hx-get="/admin/api/stats" hx-trigger="load" hx-swap="innerHTML"> 944 + <p>Loading storage stats...</p> 945 + </div> 946 + </div> 947 + 948 + <section class="dashboard-section"> 949 + <h2>Tier Distribution</h2> 950 + <div class="tier-chart"> 951 + {{ range $tier, $count := .Stats.TierDistribution }} 952 + <div class="tier-bar"> 953 + <span class="tier-name">{{ $tier }}</span> 954 + <span class="tier-count">{{ $count }}</span> 955 + </div> 956 + {{ end }} 957 + </div> 958 + </section> 959 + 960 + <section class="dashboard-section"> 961 + <h2>Top Users by Storage</h2> 962 + <div hx-get="/admin/api/top-users?limit=10" hx-trigger="load" hx-swap="innerHTML"> 963 + <p>Loading top users...</p> 964 + </div> 965 + </section> 966 + {{ end }} 967 + {{ end }} 968 + ``` 969 + 970 + ### Crew List Page (templates/pages/crew.html) 971 + 972 + ```html 973 + {{ define "crew" }} 974 + {{ template "base" . }} 975 + {{ define "content" }} 976 + <div class="page-header"> 977 + <h1>Crew Members</h1> 978 + <a href="/admin/crew/add" class="btn btn-primary">Add Crew Member</a> 979 + </div> 980 + 981 + <table class="data-table"> 982 + <thead> 983 + <tr> 984 + <th>DID</th> 985 + <th>Role</th> 986 + <th>Permissions</th> 987 + <th>Tier</th> 988 + <th>Usage</th> 989 + <th>Actions</th> 990 + </tr> 991 + </thead> 992 + <tbody id="crew-list"> 993 + {{ range .Crew }} 994 + {{ template "crew_row" . }} 995 + {{ end }} 996 + </tbody> 997 + </table> 998 + {{ end }} 999 + {{ end }} 1000 + ``` 1001 + 1002 + ### Crew Row Partial (templates/partials/crew_row.html) 1003 + 1004 + ```html 1005 + {{ define "crew_row" }} 1006 + <tr id="crew-{{ .RKey }}"> 1007 + <td> 1008 + <code title="{{ .DID }}">{{ .DID | truncate 20 }}</code> 1009 + {{ if .Plankowner }}<span class="badge badge-gold">Plankowner</span>{{ end }} 1010 + </td> 1011 + <td>{{ .Role }}</td> 1012 + <td> 1013 + {{ range .Permissions }} 1014 + <span class="badge badge-perm">{{ . }}</span> 1015 + {{ end }} 1016 + </td> 1017 + <td> 1018 + <span class="badge badge-tier tier-{{ .Tier }}">{{ .Tier }}</span> 1019 + <small>({{ .TierLimit }})</small> 1020 + </td> 1021 + <td> 1022 + <div class="usage-cell"> 1023 + <span>{{ .UsageHuman }}</span> 1024 + <div class="progress-bar"> 1025 + <div class="progress-fill {{ if gt .UsagePercent 90 }}danger{{ else if gt .UsagePercent 75 }}warning{{ end }}" 1026 + style="width: {{ .UsagePercent }}%"></div> 1027 + </div> 1028 + <small>{{ .UsagePercent }}%</small> 1029 + </div> 1030 + </td> 1031 + <td> 1032 + <a href="/admin/crew/{{ .RKey }}" class="btn btn-sm">Edit</a> 1033 + <button class="btn btn-sm btn-danger" 1034 + hx-post="/admin/crew/{{ .RKey }}/delete" 1035 + hx-confirm="Are you sure you want to remove this crew member?" 1036 + hx-target="#crew-{{ .RKey }}" 1037 + hx-swap="outerHTML"> 1038 + Delete 1039 + </button> 1040 + </td> 1041 + </tr> 1042 + {{ end }} 1043 + ``` 1044 + 1045 + ### Settings Page (templates/pages/settings.html) 1046 + 1047 + ```html 1048 + {{ define "settings" }} 1049 + {{ template "base" . }} 1050 + {{ define "content" }} 1051 + <h1>Hold Settings</h1> 1052 + 1053 + <form action="/admin/settings/update" method="POST" class="settings-form"> 1054 + <div class="setting-group"> 1055 + <h2>Access Control</h2> 1056 + 1057 + <label class="toggle-setting"> 1058 + <input type="checkbox" name="public" {{ if .Settings.Public }}checked{{ end }}> 1059 + <span class="toggle-label"> 1060 + <strong>Public Hold</strong> 1061 + <small>Allow anonymous users to read blobs (no auth required for pulls)</small> 1062 + </span> 1063 + </label> 1064 + 1065 + <label class="toggle-setting"> 1066 + <input type="checkbox" name="allow_all_crew" {{ if .Settings.AllowAllCrew }}checked{{ end }}> 1067 + <span class="toggle-label"> 1068 + <strong>Open Registration</strong> 1069 + <small>Allow any authenticated user to join as crew via requestCrew</small> 1070 + </span> 1071 + </label> 1072 + </div> 1073 + 1074 + <div class="setting-group"> 1075 + <h2>Integrations</h2> 1076 + 1077 + <label class="toggle-setting"> 1078 + <input type="checkbox" name="enable_bluesky_posts" {{ if .Settings.EnableBlueskyPosts }}checked{{ end }}> 1079 + <span class="toggle-label"> 1080 + <strong>Bluesky Posts</strong> 1081 + <small>Post to Bluesky when images are pushed to this hold</small> 1082 + </span> 1083 + </label> 1084 + </div> 1085 + 1086 + <div class="setting-group"> 1087 + <h2>Hold Information</h2> 1088 + <dl> 1089 + <dt>Hold DID</dt> 1090 + <dd><code>{{ .Settings.HoldDID }}</code></dd> 1091 + <dt>Owner DID</dt> 1092 + <dd><code>{{ .Settings.OwnerDID }}</code></dd> 1093 + </dl> 1094 + </div> 1095 + 1096 + <button type="submit" class="btn btn-primary">Save Settings</button> 1097 + </form> 1098 + {{ end }} 1099 + {{ end }} 1100 + ``` 1101 + 1102 + --- 1103 + 1104 + ## Environment Variables 1105 + 1106 + Add to `.env.hold.example`: 1107 + 1108 + ```bash 1109 + # ============================================================================= 1110 + # ADMIN PANEL CONFIGURATION 1111 + # ============================================================================= 1112 + 1113 + # Enable the admin web UI (default: false) 1114 + # When enabled, accessible at /admin 1115 + HOLD_ADMIN_ENABLED=false 1116 + 1117 + # Admin session duration (default: 24h) 1118 + # How long admin sessions remain valid before requiring re-authentication 1119 + # Format: Go duration string (e.g., 24h, 168h for 1 week) 1120 + HOLD_ADMIN_SESSION_DURATION=24h 1121 + ``` 1122 + 1123 + ### Config Struct Updates 1124 + 1125 + ```go 1126 + // In pkg/hold/config.go 1127 + 1128 + type Config struct { 1129 + // ... existing fields ... 1130 + 1131 + Admin AdminConfig 1132 + } 1133 + 1134 + type AdminConfig struct { 1135 + Enabled bool `env:"HOLD_ADMIN_ENABLED" envDefault:"false"` 1136 + SessionDuration time.Duration `env:"HOLD_ADMIN_SESSION_DURATION" envDefault:"24h"` 1137 + } 1138 + ``` 1139 + 1140 + --- 1141 + 1142 + ## Security Considerations 1143 + 1144 + ### 1. Owner-Only Access 1145 + 1146 + All admin routes validate that the authenticated user's DID matches `captain.Owner`. This check happens: 1147 + - In the OAuth callback (primary gate) 1148 + - In the `requireOwner` middleware (defense in depth) 1149 + - Before destructive operations (extra validation) 1150 + 1151 + ### 2. Cookie Security 1152 + 1153 + ```go 1154 + http.SetCookie(w, &http.Cookie{ 1155 + Name: "hold_admin_session", 1156 + Value: sessionID, 1157 + Path: "/admin", // Scoped to admin paths only 1158 + MaxAge: 86400, // 24 hours 1159 + HttpOnly: true, // No JavaScript access 1160 + Secure: isHTTPS(r), // HTTPS only in production 1161 + SameSite: http.SameSiteLaxMode, // CSRF protection 1162 + }) 1163 + ``` 1164 + 1165 + ### 3. CSRF Protection 1166 + 1167 + For state-changing operations: 1168 + - Forms include hidden CSRF token 1169 + - HTMX requests include token in header 1170 + - Server validates token before processing 1171 + 1172 + ```html 1173 + <form action="/admin/crew/add" method="POST"> 1174 + <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> 1175 + ... 1176 + </form> 1177 + ``` 1178 + 1179 + ### 4. Input Validation 1180 + 1181 + - DID format validation before database operations 1182 + - Tier names validated against `quotas.yaml` 1183 + - Permission values validated against known set 1184 + - All user input sanitized before display 1185 + 1186 + ### 5. Rate Limiting 1187 + 1188 + Consider adding rate limiting for: 1189 + - Login attempts (prevent brute force) 1190 + - OAuth flow starts (prevent abuse) 1191 + - API endpoints (prevent DoS) 1192 + 1193 + ### 6. Audit Logging 1194 + 1195 + Log all administrative actions: 1196 + ```go 1197 + slog.Info("Admin action", 1198 + "action", "crew_add", 1199 + "admin_did", session.DID, 1200 + "target_did", newMemberDID, 1201 + "permissions", permissions) 1202 + ``` 1203 + 1204 + --- 1205 + 1206 + ## Implementation Phases 1207 + 1208 + ### Phase 1: Foundation (Est. 4-6 hours) 1209 + 1210 + 1. Create `pkg/hold/admin/` package structure 1211 + 2. Implement `AdminSessionStore` with SQLite 1212 + 3. Implement OAuth client setup (reuse `pkg/auth/oauth/`) 1213 + 4. Implement `requireOwner` middleware 1214 + 5. Create basic template loading with embed.FS 1215 + 6. Add env var configuration to `pkg/hold/config.go` 1216 + 1217 + **Deliverables:** 1218 + - Admin package compiles 1219 + - Can start OAuth flow 1220 + - Session store creates/validates sessions 1221 + 1222 + ### Phase 2: Authentication (Est. 3-4 hours) 1223 + 1224 + 1. Implement login page handler 1225 + 2. Implement OAuth authorize redirect 1226 + 3. Implement callback with owner validation 1227 + 4. Implement logout handler 1228 + 5. Wire up routes in `cmd/hold/main.go` 1229 + 1230 + **Deliverables:** 1231 + - Can login as hold owner 1232 + - Non-owners rejected at callback 1233 + - Sessions persist across requests 1234 + 1235 + ### Phase 3: Dashboard (Est. 3-4 hours) 1236 + 1237 + 1. Create base template and navigation 1238 + 2. Implement dashboard handler with basic stats 1239 + 3. Implement stats API for HTMX lazy loading 1240 + 4. Implement top users API 1241 + 5. Create dashboard template 1242 + 1243 + **Deliverables:** 1244 + - Dashboard shows crew count, tier distribution 1245 + - Storage stats load asynchronously 1246 + - Top users table displays 1247 + 1248 + ### Phase 4: Crew Management (Est. 4-6 hours) 1249 + 1250 + 1. Implement crew list handler 1251 + 2. Create crew list template with HTMX delete 1252 + 3. Implement add crew form and handler 1253 + 4. Implement edit crew form and handler 1254 + 5. Implement delete crew handler 1255 + 1256 + **Deliverables:** 1257 + - Full CRUD for crew members 1258 + - Tier and permission editing works 1259 + - HTMX updates without page reload 1260 + 1261 + ### Phase 5: Settings (Est. 2-3 hours) 1262 + 1263 + 1. Implement settings handler 1264 + 2. Create settings template 1265 + 3. Implement settings update handler 1266 + 1267 + **Deliverables:** 1268 + - Can toggle public/allowAllCrew/enableBlueskyPosts 1269 + - Settings persist correctly 1270 + 1271 + ### Phase 6: Polish (Est. 2-4 hours) 1272 + 1273 + 1. Add CSS styling 1274 + 2. Add flash messages 1275 + 3. Add CSRF protection 1276 + 4. Add input validation 1277 + 5. Add audit logging 1278 + 6. Update documentation 1279 + 1280 + **Deliverables:** 1281 + - Professional-looking UI 1282 + - Security hardening complete 1283 + - Documentation updated 1284 + 1285 + **Total Estimated Time: 18-27 hours** 1286 + 1287 + --- 1288 + 1289 + ## Testing Strategy 1290 + 1291 + ### Unit Tests 1292 + 1293 + ```go 1294 + // pkg/hold/admin/session_test.go 1295 + func TestSessionStore_Create(t *testing.T) { 1296 + store := newTestSessionStore(t) 1297 + 1298 + sessionID, err := store.Create("did:plc:test", "test.handle", 24*time.Hour) 1299 + require.NoError(t, err) 1300 + require.NotEmpty(t, sessionID) 1301 + 1302 + session, err := store.Get(sessionID) 1303 + require.NoError(t, err) 1304 + assert.Equal(t, "did:plc:test", session.DID) 1305 + } 1306 + 1307 + // pkg/hold/admin/auth_test.go 1308 + func TestRequireOwner_RejectsNonOwner(t *testing.T) { 1309 + pds := setupTestPDSWithOwner(t, "did:plc:owner") 1310 + store := newTestSessionStore(t) 1311 + 1312 + // Create session for non-owner 1313 + sessionID, _ := store.Create("did:plc:notowner", "notowner", 24*time.Hour) 1314 + 1315 + middleware := requireOwner(pds, store) 1316 + handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1317 + w.WriteHeader(http.StatusOK) 1318 + })) 1319 + 1320 + req := httptest.NewRequest("GET", "/admin", nil) 1321 + req.AddCookie(&http.Cookie{Name: "hold_admin_session", Value: sessionID}) 1322 + w := httptest.NewRecorder() 1323 + 1324 + handler.ServeHTTP(w, req) 1325 + 1326 + assert.Equal(t, http.StatusForbidden, w.Code) 1327 + } 1328 + ``` 1329 + 1330 + ### Integration Tests 1331 + 1332 + ```go 1333 + // pkg/hold/admin/integration_test.go 1334 + func TestAdminLoginFlow(t *testing.T) { 1335 + // Start test hold server 1336 + server := startTestHoldWithAdmin(t) 1337 + defer server.Close() 1338 + 1339 + // Verify login page accessible 1340 + resp, _ := http.Get(server.URL + "/admin/auth/login") 1341 + assert.Equal(t, http.StatusOK, resp.StatusCode) 1342 + 1343 + // Verify dashboard redirects to login 1344 + client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { 1345 + return http.ErrUseLastResponse 1346 + }} 1347 + resp, _ = client.Get(server.URL + "/admin") 1348 + assert.Equal(t, http.StatusFound, resp.StatusCode) 1349 + assert.Contains(t, resp.Header.Get("Location"), "/admin/auth/login") 1350 + } 1351 + ``` 1352 + 1353 + ### Manual Testing Checklist 1354 + 1355 + - [ ] Login as owner succeeds 1356 + - [ ] Login as non-owner fails with clear error 1357 + - [ ] Dashboard loads with correct stats 1358 + - [ ] Add crew member with all permission combinations 1359 + - [ ] Edit crew member permissions 1360 + - [ ] Change crew member tier 1361 + - [ ] Delete crew member 1362 + - [ ] Toggle public setting 1363 + - [ ] Toggle allowAllCrew setting 1364 + - [ ] Toggle enableBlueskyPosts setting 1365 + - [ ] Logout clears session 1366 + - [ ] Session expires after configured duration 1367 + - [ ] Expired session redirects to login 1368 + 1369 + --- 1370 + 1371 + ## Future Enhancements 1372 + 1373 + ### Potential Future Features 1374 + 1375 + 1. **Crew Invite Links** - Generate one-time invite URLs for adding crew 1376 + 2. **Usage Alerts** - Email/webhook when users approach quota 1377 + 3. **Bulk Operations** - Add/remove multiple crew members at once 1378 + 4. **Export Data** - Download crew list, usage reports as CSV 1379 + 5. **Activity Log** - View recent admin actions 1380 + 6. **API Keys** - Generate programmatic access keys for admin API 1381 + 7. **Backup/Restore** - Backup crew records, restore from backup 1382 + 8. **Multi-Hold Management** - Manage multiple holds from one UI (separate feature) 1383 + 1384 + ### Performance Optimizations 1385 + 1386 + 1. **Cache usage stats** - Don't recalculate on every request 1387 + 2. **Paginate crew list** - Handle holds with 1000+ crew members 1388 + 3. **Background stat refresh** - Update stats periodically in background 1389 + 4. **Batch DID resolution** - Resolve multiple DIDs in parallel 1390 + 1391 + --- 1392 + 1393 + ## References 1394 + 1395 + - [ATProto OAuth Specification](https://atproto.com/specs/oauth) 1396 + - [DPoP RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449) 1397 + - [HTMX Documentation](https://htmx.org/docs/) 1398 + - [Chi Router](https://github.com/go-chi/chi) 1399 + - [Go html/template](https://pkg.go.dev/html/template)
+1 -1
docs/INTEGRATION_STRATEGY.md
··· 251 251 return 252 252 } 253 253 254 - json.NewEncoder(w).Encode(map[string]interface{}{ 254 + json.NewEncoder(w).Encode(map[string]any{ 255 255 "verified": result.Verified, 256 256 "did": result.Signature.DID, 257 257 "signedAt": result.Signature.SignedAt,
+354 -1068
docs/QUOTAS.md
··· 1 1 # ATCR Quota System 2 2 3 - This document describes ATCR's storage quota implementation, inspired by Harbor's proven approach to per-project blob tracking with deduplication. 3 + This document describes ATCR's storage quota implementation using ATProto records for per-user layer tracking. 4 4 5 5 ## Table of Contents 6 6 7 7 - [Overview](#overview) 8 - - [Harbor's Approach (Reference Implementation)](#harbors-approach-reference-implementation) 9 - - [Storage Options](#storage-options) 10 - - [Quota Data Model](#quota-data-model) 11 - - [Push Flow (Detailed)](#push-flow-detailed) 8 + - [Quota Model](#quota-model) 9 + - [Layer Record Schema](#layer-record-schema) 10 + - [Quota Calculation](#quota-calculation) 11 + - [Push Flow](#push-flow) 12 12 - [Delete Flow](#delete-flow) 13 13 - [Garbage Collection](#garbage-collection) 14 - - [Quota Reconciliation](#quota-reconciliation) 15 14 - [Configuration](#configuration) 16 - - [Trade-offs & Design Decisions](#trade-offs--design-decisions) 17 15 - [Future Enhancements](#future-enhancements) 18 16 19 17 ## Overview 20 18 21 19 ATCR implements per-user storage quotas to: 22 20 1. **Limit storage consumption** on shared hold services 23 - 2. **Track actual S3 costs** (what new data was added) 24 - 3. **Benefit from deduplication** (users only pay once per layer) 25 - 4. **Provide transparency** (show users their storage usage) 21 + 2. **Provide transparency** (show users their storage usage) 22 + 3. **Enable fair billing** (users pay for what they use) 26 23 27 - **Key principle:** Users pay for layers they've uploaded, but only ONCE per layer regardless of how many images reference it. 24 + **Key principle:** Users pay for layers they reference, deduplicated per-user. If you push the same layer in multiple images, you only pay once. 28 25 29 26 ### Example Scenario 30 27 31 28 ``` 32 29 Alice pushes myapp:v1 (layers A, B, C - each 100MB) 33 - โ†’ Alice's quota: +300MB (all new layers) 30 + โ†’ Creates 3 layer records in hold's PDS 31 + โ†’ Alice's quota: 300MB (3 unique layers) 34 32 35 33 Alice pushes myapp:v2 (layers A, B, D) 36 - โ†’ Layers A, B already claimed by Alice 37 - โ†’ Layer D is new (100MB) 38 - โ†’ Alice's quota: +100MB (only D is new) 39 - โ†’ Total: 400MB 34 + โ†’ Creates 3 more layer records (A, B again, plus D) 35 + โ†’ Alice's quota: 400MB (4 unique layers: A, B, C, D) 36 + โ†’ Layers A, B appear twice in records but deduplicated in quota calc 40 37 41 38 Bob pushes his-app:latest (layers A, E) 42 - โ†’ Layer A already exists in S3 (uploaded by Alice) 43 - โ†’ Bob claims it for first time โ†’ +100MB to Bob's quota 44 - โ†’ Layer E is new โ†’ +100MB to Bob's quota 45 - โ†’ Bob's quota: 200MB 39 + โ†’ Creates 2 layer records for Bob 40 + โ†’ Bob's quota: 200MB (2 unique layers: A, E) 41 + โ†’ Layer A shared with Alice in S3, but Bob pays for his own usage 46 42 47 - Physical S3 storage: 500MB (A, B, C, D, E) 48 - Claimed storage: 600MB (Alice: 400MB, Bob: 200MB) 49 - Deduplication savings: 100MB (layer A shared) 43 + Physical S3 storage: 500MB (A, B, C, D, E - deduplicated globally) 44 + Alice's quota: 400MB 45 + Bob's quota: 200MB 50 46 ``` 51 47 52 - ## Harbor's Approach (Reference Implementation) 48 + ## Quota Model 53 49 54 - Harbor is built on distribution/distribution (same as ATCR) and implements quotas as middleware. Their approach: 50 + ### Everyone Pays for What They Upload 55 51 56 - ### Key Insights from Harbor 57 - 58 - 1. **"Shared blobs are only computed once per project"** 59 - - Each project tracks which blobs it has uploaded 60 - - Same blob used in multiple images counts only once per project 61 - - Different projects claiming the same blob each pay for it 62 - 63 - 2. **Quota checked when manifest is pushed** 64 - - Blobs upload first (presigned URLs, can't intercept) 65 - - Manifest pushed last โ†’ quota check happens here 66 - - Can reject manifest if quota exceeded (orphaned blobs cleaned by GC) 52 + Each user is charged for all unique layers they reference, regardless of whether those layers exist in S3 from other users' uploads. 67 53 68 - 3. **Middleware-based implementation** 69 - - distribution/distribution has NO built-in quota support 70 - - Harbor added it as request preprocessing middleware 71 - - Uses database (PostgreSQL) or Redis for quota storage 54 + **Why this model?** 55 + - **Simple mental model**: "I pushed 500MB of layers, I use 500MB of quota" 56 + - **Predictable**: Your quota doesn't change based on others' actions 57 + - **Clean deletion**: Delete manifest โ†’ layer records removed โ†’ quota freed 58 + - **No cross-user dependencies**: Users are isolated 72 59 73 - 4. **Per-project ownership model** 74 - - Blobs are physically deduplicated globally 75 - - Quota accounting is logical (per-project claims) 76 - - Total claimed storage can exceed physical storage 60 + **Trade-off:** 61 + - Total claimed storage can exceed physical S3 storage 62 + - This is acceptable - deduplication is an operational benefit for ATCR, not a billing feature 77 63 78 - ### References 64 + ### ATProto-Native Storage 79 65 80 - - Harbor Quota Documentation: https://goharbor.io/docs/1.10/administration/configure-project-quotas/ 81 - - Harbor Source: https://github.com/goharbor/harbor (see `src/controller/quota`) 66 + Layer tracking uses ATProto records stored in the hold's embedded PDS: 67 + - **Collection**: `io.atcr.hold.layer` 68 + - **Repository**: Hold's DID (e.g., `did:web:hold01.atcr.io`) 69 + - **Records**: One per manifest-layer relationship (TID-based keys) 82 70 83 - ## Storage Options 71 + This approach: 72 + - Keeps quota data in ATProto (no separate database) 73 + - Enables standard ATProto sync/query mechanisms 74 + - Provides full audit trail of layer usage 84 75 85 - The hold service needs to store quota data somewhere. Two options: 76 + ## Layer Record Schema 86 77 87 - ### Option 1: S3-Based Storage (Recommended for BYOS) 78 + ### LayerRecord 88 79 89 - Store quota metadata alongside blobs in the same S3 bucket: 80 + ```go 81 + // pkg/atproto/lexicon.go 90 82 91 - ``` 92 - Bucket structure: 93 - /docker/registry/v2/blobs/sha256/ab/abc123.../data โ† actual blobs 94 - /atcr/quota/did:plc:alice.json โ† quota tracking 95 - /atcr/quota/did:plc:bob.json 83 + type LayerRecord struct { 84 + Type string `json:"$type"` // "io.atcr.hold.layer" 85 + Digest string `json:"digest"` // Layer digest (sha256:abc123...) 86 + Size int64 `json:"size"` // Size in bytes 87 + MediaType string `json:"mediaType"` // e.g., "application/vnd.oci.image.layer.v1.tar+gzip" 88 + Manifest string `json:"manifest"` // at://did:plc:alice/io.atcr.manifest/abc123 89 + UserDID string `json:"userDid"` // User's DID for quota grouping 90 + CreatedAt string `json:"createdAt"` // ISO 8601 timestamp 91 + } 96 92 ``` 97 93 98 - **Pros:** 99 - - โœ… No separate database needed 100 - - โœ… Single S3 bucket (better UX - no second bucket to configure) 101 - - โœ… Quota data lives with the blobs 102 - - โœ… Hold service stays relatively stateless 103 - - โœ… Works with any S3-compatible service (Storj, Minio, Upcloud, Fly.io) 94 + ### Record Key 104 95 105 - **Cons:** 106 - - โŒ Slower than local database (network round-trip) 107 - - โŒ Eventual consistency issues 108 - - โŒ Race conditions on concurrent updates 109 - - โŒ Extra S3 API costs (GET/PUT per upload) 110 - 111 - **Performance:** 112 - - Each blob upload: 1 HEAD (blob exists?) + 1 GET (quota) + 1 PUT (update quota) 113 - - Typical latency: 100-200ms total overhead 114 - - For high-throughput registries, consider SQLite 96 + Records use TID (timestamp-based ID) as the rkey. This means: 97 + - Multiple records can exist for the same layer (from different manifests) 98 + - Deduplication happens at query time, not storage time 99 + - Simple append-only writes on manifest push 115 100 116 - ### Option 2: SQLite Database (Recommended for Shared Holds) 101 + ### Example Records 117 102 118 - Local database in hold service: 119 - 120 - ```bash 121 - /var/lib/atcr/hold-quota.db 122 103 ``` 123 - 124 - **Pros:** 125 - - โœ… Fast local queries (no network latency) 126 - - โœ… ACID transactions (no race conditions) 127 - - โœ… Efficient for high-throughput registries 128 - - โœ… Can use foreign keys and joins 129 - 130 - **Cons:** 131 - - โŒ Makes hold service stateful (persistent volume needed) 132 - - โŒ Not ideal for ephemeral BYOS deployments 133 - - โŒ Backup/restore complexity 134 - - โŒ Multi-instance scaling requires shared database 135 - 136 - **Schema:** 137 - ```sql 138 - CREATE TABLE user_quotas ( 139 - did TEXT PRIMARY KEY, 140 - quota_limit INTEGER NOT NULL DEFAULT 10737418240, -- 10GB 141 - quota_used INTEGER NOT NULL DEFAULT 0, 142 - updated_at TIMESTAMP 143 - ); 104 + Manifest A (layers X, Y, Z) โ†’ creates 3 records 105 + Manifest B (layers X, W) โ†’ creates 2 records 144 106 145 - CREATE TABLE claimed_layers ( 146 - did TEXT NOT NULL, 147 - digest TEXT NOT NULL, 148 - size INTEGER NOT NULL, 149 - claimed_at TIMESTAMP, 150 - PRIMARY KEY(did, digest) 151 - ); 107 + io.atcr.hold.layer collection: 108 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 109 + โ”‚ rkey (TID) โ”‚ digest โ”‚ size โ”‚ manifest โ”‚ userDid โ”‚ 110 + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค 111 + โ”‚ 3jui7...001 โ”‚ X โ”‚ 100 โ”‚ at://did:plc:alice/.../manifestA โ”‚ did:plc:alice โ”‚ 112 + โ”‚ 3jui7...002 โ”‚ Y โ”‚ 200 โ”‚ at://did:plc:alice/.../manifestA โ”‚ did:plc:alice โ”‚ 113 + โ”‚ 3jui7...003 โ”‚ Z โ”‚ 150 โ”‚ at://did:plc:alice/.../manifestA โ”‚ did:plc:alice โ”‚ 114 + โ”‚ 3jui7...004 โ”‚ X โ”‚ 100 โ”‚ at://did:plc:alice/.../manifestB โ”‚ did:plc:alice โ”‚ โ† duplicate digest 115 + โ”‚ 3jui7...005 โ”‚ W โ”‚ 300 โ”‚ at://did:plc:alice/.../manifestB โ”‚ did:plc:alice โ”‚ 116 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 152 117 ``` 153 118 154 - ### Recommendation 155 - 156 - - **BYOS (user-owned holds):** S3-based (keeps hold service ephemeral) 157 - - **Shared holds (multi-user):** SQLite (better performance and consistency) 158 - - **High-traffic production:** SQLite or PostgreSQL (Harbor uses this) 119 + ## Quota Calculation 159 120 160 - ## Quota Data Model 121 + ### Query: User's Unique Storage 161 122 162 - ### Quota File Format (S3-based) 163 - 164 - ```json 165 - { 166 - "did": "did:plc:alice123", 167 - "limit": 10737418240, 168 - "used": 5368709120, 169 - "claimed_layers": { 170 - "sha256:abc123...": 104857600, 171 - "sha256:def456...": 52428800, 172 - "sha256:789ghi...": 209715200 173 - }, 174 - "last_updated": "2025-10-09T12:34:56Z", 175 - "version": 1 176 - } 123 + ```sql 124 + -- Calculate quota by deduplicating layers 125 + SELECT SUM(size) FROM ( 126 + SELECT DISTINCT digest, size 127 + FROM io.atcr.hold.layer 128 + WHERE userDid = ? 129 + ) 177 130 ``` 178 131 179 - **Fields:** 180 - - `did`: User's ATProto DID 181 - - `limit`: Maximum storage in bytes (default: 10GB) 182 - - `used`: Current storage usage in bytes (sum of claimed_layers) 183 - - `claimed_layers`: Map of digest โ†’ size for all layers user has uploaded 184 - - `last_updated`: Timestamp of last quota update 185 - - `version`: Schema version for future migrations 186 - 187 - ### Why Track Individual Layers? 188 - 189 - **Q: Can't we just track a counter?** 190 - 191 - **A: We need layer tracking for:** 192 - 193 - 1. **Deduplication detection** 194 - - Check if user already claimed a layer โ†’ free upload 195 - - Example: Updating an image reuses most layers 196 - 197 - 2. **Accurate deletes** 198 - - When manifest deleted, only decrement unclaimed layers 199 - - User may have 5 images sharing layer A - deleting 1 image doesn't free layer A 200 - 201 - 3. **Quota reconciliation** 202 - - Verify quota matches reality by listing user's manifests 203 - - Recalculate from layers in manifests vs claimed_layers map 204 - 205 - 4. **Auditing** 206 - - "Show me what I'm storing" 207 - - Users can see which layers consume their quota 208 - 209 - ## Push Flow (Detailed) 210 - 211 - ### Step-by-Step: User Pushes Image 212 - 213 - ``` 214 - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 215 - โ”‚ Client โ”‚ โ”‚ Hold โ”‚ โ”‚ S3 โ”‚ 216 - โ”‚ (Docker) โ”‚ โ”‚ Service โ”‚ โ”‚ Bucket โ”‚ 217 - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 218 - โ”‚ โ”‚ โ”‚ 219 - โ”‚ 1. PUT /v2/.../blobs/ โ”‚ โ”‚ 220 - โ”‚ upload?digest=sha256:abcโ”‚ โ”‚ 221 - โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 222 - โ”‚ โ”‚ โ”‚ 223 - โ”‚ โ”‚ 2. Check if blob exists โ”‚ 224 - โ”‚ โ”‚ (Stat/HEAD request) โ”‚ 225 - โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 226 - โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค 227 - โ”‚ โ”‚ 200 OK (exists) or โ”‚ 228 - โ”‚ โ”‚ 404 Not Found โ”‚ 229 - โ”‚ โ”‚ โ”‚ 230 - โ”‚ โ”‚ 3. Read user quota โ”‚ 231 - โ”‚ โ”‚ GET /atcr/quota/{did} โ”‚ 232 - โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 233 - โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค 234 - โ”‚ โ”‚ quota.json โ”‚ 235 - โ”‚ โ”‚ โ”‚ 236 - โ”‚ โ”‚ 4. Calculate quota impact โ”‚ 237 - โ”‚ โ”‚ - If digest in โ”‚ 238 - โ”‚ โ”‚ claimed_layers: 0 โ”‚ 239 - โ”‚ โ”‚ - Else: size โ”‚ 240 - โ”‚ โ”‚ โ”‚ 241 - โ”‚ โ”‚ 5. Check quota limit โ”‚ 242 - โ”‚ โ”‚ used + impact <= limit? โ”‚ 243 - โ”‚ โ”‚ โ”‚ 244 - โ”‚ โ”‚ 6. Update quota โ”‚ 245 - โ”‚ โ”‚ PUT /atcr/quota/{did} โ”‚ 246 - โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 247 - โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค 248 - โ”‚ โ”‚ 200 OK โ”‚ 249 - โ”‚ โ”‚ โ”‚ 250 - โ”‚ 7. Presigned URL โ”‚ โ”‚ 251 - โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ 252 - โ”‚ {url: "https://s3..."} โ”‚ โ”‚ 253 - โ”‚ โ”‚ โ”‚ 254 - โ”‚ 8. Upload blob to S3 โ”‚ โ”‚ 255 - โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 256 - โ”‚ โ”‚ โ”‚ 257 - โ”‚ 9. 200 OK โ”‚ โ”‚ 258 - โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค 259 - โ”‚ โ”‚ โ”‚ 260 - ``` 132 + Using the example above: 133 + - Layer X appears twice but counted once: 100 134 + - Layers Y, Z, W counted once each: 200 + 150 + 300 135 + - **Total: 750 bytes** 261 136 262 - ### Implementation (Pseudocode) 137 + ### Implementation 263 138 264 139 ```go 265 - // cmd/hold/main.go - HandlePutPresignedURL 266 - 267 - func (s *HoldService) HandlePutPresignedURL(w http.ResponseWriter, r *http.Request) { 268 - var req PutPresignedURLRequest 269 - json.NewDecoder(r.Body).Decode(&req) 270 - 271 - // Step 1: Check if blob already exists in S3 272 - blobPath := fmt.Sprintf("/docker/registry/v2/blobs/%s/%s/%s/data", 273 - algorithm, digest[:2], digest) 140 + // pkg/hold/quota/quota.go 274 141 275 - _, err := s.driver.Stat(ctx, blobPath) 276 - blobExists := (err == nil) 142 + type QuotaManager struct { 143 + pds *pds.Server // Hold's embedded PDS 144 + } 277 145 278 - // Step 2: Read quota from S3 (or SQLite) 279 - quota, err := s.quotaManager.GetQuota(req.DID) 146 + // GetUsage calculates a user's current quota usage 147 + func (q *QuotaManager) GetUsage(ctx context.Context, userDID string) (int64, error) { 148 + // List all layer records for this user 149 + records, err := q.pds.ListRecords(ctx, LayerCollection, userDID) 280 150 if err != nil { 281 - // First upload - create quota with defaults 282 - quota = &Quota{ 283 - DID: req.DID, 284 - Limit: s.config.QuotaDefaultLimit, 285 - Used: 0, 286 - ClaimedLayers: make(map[string]int64), 287 - } 151 + return 0, err 288 152 } 289 153 290 - // Step 3: Calculate quota impact 291 - quotaImpact := req.Size // Default: assume new layer 292 - 293 - if _, alreadyClaimed := quota.ClaimedLayers[req.Digest]; alreadyClaimed { 294 - // User already uploaded this layer before 295 - quotaImpact = 0 296 - log.Printf("Layer %s already claimed by %s, no quota impact", 297 - req.Digest, req.DID) 298 - } else if blobExists { 299 - // Blob exists in S3 (uploaded by another user) 300 - // But this user is claiming it for first time 301 - // Still counts against their quota 302 - log.Printf("Layer %s exists globally but new to %s, quota impact: %d", 303 - req.Digest, req.DID, quotaImpact) 304 - } else { 305 - // Brand new blob - will be uploaded to S3 306 - log.Printf("New layer %s for %s, quota impact: %d", 307 - req.Digest, req.DID, quotaImpact) 154 + // Deduplicate by digest 155 + uniqueLayers := make(map[string]int64) // digest -> size 156 + for _, record := range records { 157 + var layer LayerRecord 158 + if err := json.Unmarshal(record.Value, &layer); err != nil { 159 + continue 160 + } 161 + if layer.UserDID == userDID { 162 + uniqueLayers[layer.Digest] = layer.Size 163 + } 308 164 } 309 165 310 - // Step 4: Check quota limit 311 - if quota.Used + quotaImpact > quota.Limit { 312 - http.Error(w, fmt.Sprintf( 313 - "quota exceeded: used=%d, impact=%d, limit=%d", 314 - quota.Used, quotaImpact, quota.Limit, 315 - ), http.StatusPaymentRequired) // 402 316 - return 166 + // Sum unique layer sizes 167 + var total int64 168 + for _, size := range uniqueLayers { 169 + total += size 317 170 } 318 171 319 - // Step 5: Update quota (optimistic - before upload completes) 320 - quota.Used += quotaImpact 321 - if quotaImpact > 0 { 322 - quota.ClaimedLayers[req.Digest] = req.Size 323 - } 324 - quota.LastUpdated = time.Now() 172 + return total, nil 173 + } 325 174 326 - if err := s.quotaManager.SaveQuota(quota); err != nil { 327 - http.Error(w, "failed to update quota", http.StatusInternalServerError) 328 - return 329 - } 330 - 331 - // Step 6: Generate presigned URL 332 - presignedURL, err := s.getUploadURL(ctx, req.Digest, req.Size, req.DID) 175 + // CheckQuota returns true if user has space for additional bytes 176 + func (q *QuotaManager) CheckQuota(ctx context.Context, userDID string, additional int64, limit int64) (bool, int64, error) { 177 + current, err := q.GetUsage(ctx, userDID) 333 178 if err != nil { 334 - // Rollback quota update on error 335 - quota.Used -= quotaImpact 336 - delete(quota.ClaimedLayers, req.Digest) 337 - s.quotaManager.SaveQuota(quota) 338 - 339 - http.Error(w, "failed to generate presigned URL", http.StatusInternalServerError) 340 - return 341 - } 342 - 343 - // Step 7: Return presigned URL + quota info 344 - resp := PutPresignedURLResponse{ 345 - URL: presignedURL, 346 - ExpiresAt: time.Now().Add(15 * time.Minute), 347 - QuotaInfo: QuotaInfo{ 348 - Used: quota.Used, 349 - Limit: quota.Limit, 350 - Available: quota.Limit - quota.Used, 351 - Impact: quotaImpact, 352 - AlreadyClaimed: quotaImpact == 0, 353 - }, 179 + return false, 0, err 354 180 } 355 181 356 - w.Header().Set("Content-Type", "application/json") 357 - json.NewEncoder(w).Encode(resp) 182 + return current+additional <= limit, current, nil 358 183 } 359 184 ``` 360 185 361 - ### Race Condition Handling 362 - 363 - **Problem:** Two concurrent uploads of the same blob 186 + ### Quota Response 364 187 365 - ``` 366 - Time User A User B 367 - 0ms Upload layer X (100MB) 368 - 10ms Upload layer X (100MB) 369 - 20ms Check exists: NO Check exists: NO 370 - 30ms Quota impact: 100MB Quota impact: 100MB 371 - 40ms Update quota A: +100MB Update quota B: +100MB 372 - 50ms Generate presigned URL Generate presigned URL 373 - 100ms Upload to S3 completes Upload to S3 (overwrites A's) 188 + ```go 189 + type QuotaInfo struct { 190 + Used int64 `json:"used"` // Current usage (deduplicated) 191 + Limit int64 `json:"limit"` // User's quota limit 192 + Available int64 `json:"available"` // Remaining space 193 + } 374 194 ``` 375 195 376 - **Result:** Both users charged 100MB, but only 100MB stored in S3. 377 - 378 - **Mitigation strategies:** 379 - 380 - 1. **Accept eventual consistency** (recommended for S3-based) 381 - - Run periodic reconciliation to fix discrepancies 382 - - Small inconsistency window (minutes) is acceptable 383 - - Reconciliation uses PDS as source of truth 384 - 385 - 2. **Optimistic locking** (S3 ETags) 386 - ```go 387 - // Use S3 ETags for conditional writes 388 - oldETag := getQuotaFileETag(did) 389 - err := putQuotaFileWithCondition(quota, oldETag) 390 - if err == PreconditionFailed { 391 - // Retry with fresh read 392 - } 393 - ``` 394 - 395 - 3. **Database transactions** (SQLite-based) 396 - ```sql 397 - BEGIN TRANSACTION; 398 - SELECT * FROM user_quotas WHERE did = ? FOR UPDATE; 399 - UPDATE user_quotas SET used = used + ? WHERE did = ?; 400 - COMMIT; 401 - ``` 402 - 403 - ## Delete Flow 404 - 405 - ### Manifest Deletion via AppView UI 196 + ## Push Flow 406 197 407 - When a user deletes a manifest through the AppView web interface: 198 + ### Step-by-Step: User Pushes Image 408 199 409 200 ``` 410 201 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 411 - โ”‚ User โ”‚ โ”‚ AppView โ”‚ โ”‚ Hold โ”‚ โ”‚ PDS โ”‚ 412 - โ”‚ UI โ”‚ โ”‚ Database โ”‚ โ”‚ Service โ”‚ โ”‚ โ”‚ 202 + โ”‚ Client โ”‚ โ”‚ AppView โ”‚ โ”‚ Hold โ”‚ โ”‚ User PDS โ”‚ 203 + โ”‚ (Docker) โ”‚ โ”‚ โ”‚ โ”‚ Service โ”‚ โ”‚ โ”‚ 413 204 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 414 205 โ”‚ โ”‚ โ”‚ โ”‚ 415 - โ”‚ DELETE manifest โ”‚ โ”‚ โ”‚ 206 + โ”‚ 1. Upload blobs โ”‚ โ”‚ โ”‚ 207 + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 208 + โ”‚ โ”‚ 2. Route to hold โ”‚ โ”‚ 209 + โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 210 + โ”‚ โ”‚ โ”‚ 3. Store in S3 โ”‚ 211 + โ”‚ โ”‚ โ”‚ โ”‚ 212 + โ”‚ 4. PUT manifest โ”‚ โ”‚ โ”‚ 416 213 โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 417 214 โ”‚ โ”‚ โ”‚ โ”‚ 418 - โ”‚ โ”‚ 1. Get manifest โ”‚ โ”‚ 419 - โ”‚ โ”‚ and layers โ”‚ โ”‚ 215 + โ”‚ โ”‚ 5. Calculate quota โ”‚ โ”‚ 216 + โ”‚ โ”‚ impact for new โ”‚ โ”‚ 217 + โ”‚ โ”‚ layers โ”‚ โ”‚ 420 218 โ”‚ โ”‚ โ”‚ โ”‚ 421 - โ”‚ โ”‚ 2. Check which โ”‚ โ”‚ 422 - โ”‚ โ”‚ layers still โ”‚ โ”‚ 423 - โ”‚ โ”‚ referenced by โ”‚ โ”‚ 424 - โ”‚ โ”‚ user's other โ”‚ โ”‚ 425 - โ”‚ โ”‚ manifests โ”‚ โ”‚ 219 + โ”‚ โ”‚ 6. Check quota limit โ”‚ โ”‚ 220 + โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 221 + โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ 426 222 โ”‚ โ”‚ โ”‚ โ”‚ 427 - โ”‚ โ”‚ 3. DELETE manifest โ”‚ โ”‚ 428 - โ”‚ โ”‚ from PDS โ”‚ โ”‚ 223 + โ”‚ โ”‚ 7. Store manifest โ”‚ โ”‚ 429 224 โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 430 225 โ”‚ โ”‚ โ”‚ โ”‚ 431 - โ”‚ โ”‚ 4. POST /quota/decrement โ”‚ 226 + โ”‚ โ”‚ 8. Create layer โ”‚ โ”‚ 227 + โ”‚ โ”‚ records โ”‚ โ”‚ 432 228 โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 433 - โ”‚ โ”‚ {layers: [...]} โ”‚ โ”‚ 229 + โ”‚ โ”‚ โ”‚ 9. Write to โ”‚ 230 + โ”‚ โ”‚ โ”‚ hold's PDS โ”‚ 434 231 โ”‚ โ”‚ โ”‚ โ”‚ 435 - โ”‚ โ”‚ โ”‚ 5. Update quota โ”‚ 436 - โ”‚ โ”‚ โ”‚ Remove unclaimed โ”‚ 437 - โ”‚ โ”‚ โ”‚ layers โ”‚ 438 - โ”‚ โ”‚ โ”‚ โ”‚ 439 - โ”‚ โ”‚ 6. 200 OK โ”‚ โ”‚ 440 - โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ 441 - โ”‚ โ”‚ โ”‚ โ”‚ 442 - โ”‚ โ”‚ 7. Delete from DB โ”‚ โ”‚ 443 - โ”‚ โ”‚ โ”‚ โ”‚ 444 - โ”‚ 8. Success โ”‚ โ”‚ โ”‚ 232 + โ”‚ 10. 201 Created โ”‚ โ”‚ โ”‚ 445 233 โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ 446 - โ”‚ โ”‚ โ”‚ โ”‚ 447 234 ``` 448 235 449 - ### AppView Implementation 236 + ### Implementation 450 237 451 238 ```go 452 - // pkg/appview/handlers/manifest.go 239 + // pkg/appview/storage/routing_repository.go 453 240 454 - func (h *ManifestHandler) DeleteManifest(w http.ResponseWriter, r *http.Request) { 455 - did := r.Context().Value("auth.did").(string) 456 - repository := chi.URLParam(r, "repository") 457 - digest := chi.URLParam(r, "digest") 458 - 459 - // Step 1: Get manifest and its layers from database 460 - manifest, err := db.GetManifest(h.db, digest) 461 - if err != nil { 462 - http.Error(w, "manifest not found", 404) 463 - return 464 - } 241 + func (r *RoutingRepository) PutManifest(ctx context.Context, manifest distribution.Manifest) error { 242 + // Parse manifest to get layers 243 + layers := extractLayers(manifest) 465 244 466 - layers, err := db.GetLayersForManifest(h.db, manifest.ID) 245 + // Get user's current unique layers from hold 246 + existingLayers, err := r.holdClient.GetUserLayers(ctx, r.userDID) 467 247 if err != nil { 468 - http.Error(w, "failed to get layers", 500) 469 - return 248 + return err 470 249 } 250 + existingSet := makeDigestSet(existingLayers) 471 251 472 - // Step 2: For each layer, check if user still references it 473 - // in other manifests 474 - layersToDecrement := []LayerInfo{} 475 - 252 + // Calculate quota impact (only new unique layers) 253 + var quotaImpact int64 476 254 for _, layer := range layers { 477 - // Query: does this user have other manifests using this layer? 478 - stillReferenced, err := db.CheckLayerReferencedByUser( 479 - h.db, did, repository, layer.Digest, manifest.ID, 480 - ) 481 - 482 - if err != nil { 483 - http.Error(w, "failed to check layer references", 500) 484 - return 485 - } 486 - 487 - if !stillReferenced { 488 - // This layer is no longer used by user 489 - layersToDecrement = append(layersToDecrement, LayerInfo{ 490 - Digest: layer.Digest, 491 - Size: layer.Size, 492 - }) 255 + if !existingSet[layer.Digest] { 256 + quotaImpact += layer.Size 493 257 } 494 258 } 495 259 496 - // Step 3: Delete manifest from user's PDS 497 - atprotoClient := atproto.NewClient(manifest.PDSEndpoint, did, accessToken) 498 - err = atprotoClient.DeleteRecord(ctx, atproto.ManifestCollection, manifestRKey) 260 + // Check quota 261 + ok, current, err := r.quotaManager.CheckQuota(ctx, r.userDID, quotaImpact, r.quotaLimit) 499 262 if err != nil { 500 - http.Error(w, "failed to delete from PDS", 500) 501 - return 263 + return err 502 264 } 503 - 504 - // Step 4: Notify hold service to decrement quota 505 - if len(layersToDecrement) > 0 { 506 - holdClient := &http.Client{} 507 - 508 - decrementReq := QuotaDecrementRequest{ 509 - DID: did, 510 - Layers: layersToDecrement, 511 - } 512 - 513 - body, _ := json.Marshal(decrementReq) 514 - resp, err := holdClient.Post( 515 - manifest.HoldEndpoint + "/quota/decrement", 516 - "application/json", 517 - bytes.NewReader(body), 518 - ) 519 - 520 - if err != nil || resp.StatusCode != 200 { 521 - log.Printf("Warning: failed to update quota on hold service: %v", err) 522 - // Continue anyway - GC reconciliation will fix it 523 - } 265 + if !ok { 266 + return fmt.Errorf("quota exceeded: used=%d, impact=%d, limit=%d", 267 + current, quotaImpact, r.quotaLimit) 524 268 } 525 269 526 - // Step 5: Delete from AppView database 527 - err = db.DeleteManifest(h.db, did, repository, digest) 270 + // Store manifest in user's PDS 271 + manifestURI, err := r.atprotoClient.PutManifest(ctx, manifest) 528 272 if err != nil { 529 - http.Error(w, "failed to delete from database", 500) 530 - return 273 + return err 531 274 } 532 275 533 - w.WriteHeader(http.StatusNoContent) 534 - } 535 - ``` 536 - 537 - ### Hold Service Decrement Endpoint 538 - 539 - ```go 540 - // cmd/hold/main.go 541 - 542 - type QuotaDecrementRequest struct { 543 - DID string `json:"did"` 544 - Layers []LayerInfo `json:"layers"` 545 - } 546 - 547 - type LayerInfo struct { 548 - Digest string `json:"digest"` 549 - Size int64 `json:"size"` 550 - } 551 - 552 - func (s *HoldService) HandleQuotaDecrement(w http.ResponseWriter, r *http.Request) { 553 - var req QuotaDecrementRequest 554 - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 555 - http.Error(w, "invalid request", 400) 556 - return 557 - } 558 - 559 - // Read current quota 560 - quota, err := s.quotaManager.GetQuota(req.DID) 561 - if err != nil { 562 - http.Error(w, "quota not found", 404) 563 - return 564 - } 565 - 566 - // Decrement quota for each layer 567 - for _, layer := range req.Layers { 568 - if size, claimed := quota.ClaimedLayers[layer.Digest]; claimed { 569 - // Remove from claimed layers 570 - delete(quota.ClaimedLayers, layer.Digest) 571 - quota.Used -= size 572 - 573 - log.Printf("Decremented quota for %s: layer %s (%d bytes)", 574 - req.DID, layer.Digest, size) 575 - } else { 576 - log.Printf("Warning: layer %s not in claimed_layers for %s", 577 - layer.Digest, req.DID) 276 + // Create layer records in hold's PDS 277 + for _, layer := range layers { 278 + record := LayerRecord{ 279 + Type: "io.atcr.hold.layer", 280 + Digest: layer.Digest, 281 + Size: layer.Size, 282 + MediaType: layer.MediaType, 283 + Manifest: manifestURI, 284 + UserDID: r.userDID, 285 + CreatedAt: time.Now().Format(time.RFC3339), 286 + } 287 + if err := r.holdClient.CreateLayerRecord(ctx, record); err != nil { 288 + log.Printf("Warning: failed to create layer record: %v", err) 289 + // Continue - reconciliation will fix 578 290 } 579 291 } 580 292 581 - // Ensure quota.Used doesn't go negative (defensive) 582 - if quota.Used < 0 { 583 - log.Printf("Warning: quota.Used went negative for %s, resetting to 0", req.DID) 584 - quota.Used = 0 585 - } 586 - 587 - // Save updated quota 588 - quota.LastUpdated = time.Now() 589 - if err := s.quotaManager.SaveQuota(quota); err != nil { 590 - http.Error(w, "failed to save quota", 500) 591 - return 592 - } 593 - 594 - // Return updated quota info 595 - json.NewEncoder(w).Encode(map[string]any{ 596 - "used": quota.Used, 597 - "limit": quota.Limit, 598 - }) 293 + return nil 599 294 } 600 295 ``` 601 296 602 - ### SQL Query: Check Layer References 603 - 604 - ```sql 605 - -- pkg/appview/db/queries.go 297 + ### Quota Check Timing 606 298 607 - -- Check if user still references this layer in other manifests 608 - SELECT COUNT(*) 609 - FROM layers l 610 - JOIN manifests m ON l.manifest_id = m.id 611 - WHERE m.did = ? -- User's DID 612 - AND l.digest = ? -- Layer digest 613 - AND m.id != ? -- Exclude the manifest being deleted 614 - ``` 299 + Quota is checked when the **manifest is pushed** (after blobs are uploaded): 300 + - Blobs upload first via presigned URLs 301 + - Manifest pushed last triggers quota check 302 + - If quota exceeded, manifest is rejected (orphaned blobs cleaned by GC) 615 303 616 - ## Garbage Collection 304 + This matches Harbor's approach and is the industry standard. 617 305 618 - ### Background: Orphaned Blobs 306 + ## Delete Flow 619 307 620 - Orphaned blobs accumulate when: 621 - 1. Manifest push fails after blobs uploaded (presigned URLs bypass hold) 622 - 2. Quota exceeded - manifest rejected, blobs already in S3 623 - 3. User deletes manifest - blobs no longer referenced 308 + ### Manifest Deletion 624 309 625 - **GC periodically cleans these up.** 310 + When a user deletes a manifest: 626 311 627 - ### GC Cron Implementation 312 + ``` 313 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 314 + โ”‚ User โ”‚ โ”‚ AppView โ”‚ โ”‚ Hold โ”‚ โ”‚ User PDS โ”‚ 315 + โ”‚ UI โ”‚ โ”‚ โ”‚ โ”‚ Service โ”‚ โ”‚ โ”‚ 316 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 317 + โ”‚ โ”‚ โ”‚ โ”‚ 318 + โ”‚ DELETE manifest โ”‚ โ”‚ โ”‚ 319 + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 320 + โ”‚ โ”‚ โ”‚ โ”‚ 321 + โ”‚ โ”‚ 1. Delete manifest โ”‚ โ”‚ 322 + โ”‚ โ”‚ from user's PDS โ”‚ โ”‚ 323 + โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 324 + โ”‚ โ”‚ โ”‚ โ”‚ 325 + โ”‚ โ”‚ 2. Delete layer โ”‚ โ”‚ 326 + โ”‚ โ”‚ records for this โ”‚ โ”‚ 327 + โ”‚ โ”‚ manifest โ”‚ โ”‚ 328 + โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 329 + โ”‚ โ”‚ โ”‚ 3. Remove records โ”‚ 330 + โ”‚ โ”‚ โ”‚ where manifest โ”‚ 331 + โ”‚ โ”‚ โ”‚ == deleted URI โ”‚ 332 + โ”‚ โ”‚ โ”‚ โ”‚ 333 + โ”‚ 4. 204 No Content โ”‚ โ”‚ โ”‚ 334 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ 335 + ``` 628 336 629 - Similar to AppView's backfill worker, the hold service can run periodic GC: 337 + ### Implementation 630 338 631 339 ```go 632 - // cmd/hold/gc/gc.go 340 + // pkg/appview/handlers/manifest.go 633 341 634 - type GarbageCollector struct { 635 - driver storagedriver.StorageDriver 636 - appviewURL string 637 - holdURL string 638 - quotaManager *quota.Manager 639 - } 342 + func (h *ManifestHandler) DeleteManifest(w http.ResponseWriter, r *http.Request) { 343 + userDID := auth.GetDID(r.Context()) 344 + repository := chi.URLParam(r, "repository") 345 + digest := chi.URLParam(r, "digest") 640 346 641 - // Run garbage collection 642 - func (gc *GarbageCollector) Run(ctx context.Context) error { 643 - log.Println("Starting garbage collection...") 644 - 645 - // Step 1: Get list of referenced blobs from AppView 646 - referenced, err := gc.getReferencedBlobs() 647 - if err != nil { 648 - return fmt.Errorf("failed to get referenced blobs: %w", err) 649 - } 650 - 651 - referencedSet := make(map[string]bool) 652 - for _, digest := range referenced { 653 - referencedSet[digest] = true 654 - } 655 - 656 - log.Printf("AppView reports %d referenced blobs", len(referenced)) 657 - 658 - // Step 2: Walk S3 blobs 659 - deletedCount := 0 660 - reclaimedBytes := int64(0) 661 - 662 - err = gc.driver.Walk(ctx, "/docker/registry/v2/blobs", func(fileInfo storagedriver.FileInfo) error { 663 - if fileInfo.IsDir() { 664 - return nil // Skip directories 665 - } 666 - 667 - // Extract digest from path 668 - // Path: /docker/registry/v2/blobs/sha256/ab/abc123.../data 669 - digest := extractDigestFromPath(fileInfo.Path()) 670 - 671 - if !referencedSet[digest] { 672 - // Unreferenced blob - delete it 673 - size := fileInfo.Size() 674 - 675 - if err := gc.driver.Delete(ctx, fileInfo.Path()); err != nil { 676 - log.Printf("Failed to delete blob %s: %v", digest, err) 677 - return nil // Continue anyway 678 - } 679 - 680 - deletedCount++ 681 - reclaimedBytes += size 682 - 683 - log.Printf("GC: Deleted unreferenced blob %s (%d bytes)", digest, size) 684 - } 685 - 686 - return nil 687 - }) 688 - 689 - if err != nil { 690 - return fmt.Errorf("failed to walk blobs: %w", err) 691 - } 692 - 693 - log.Printf("GC complete: deleted %d blobs, reclaimed %d bytes", 694 - deletedCount, reclaimedBytes) 695 - 696 - return nil 697 - } 698 - 699 - // Get referenced blobs from AppView 700 - func (gc *GarbageCollector) getReferencedBlobs() ([]string, error) { 701 - // Query AppView for all blobs referenced by manifests 702 - // stored in THIS hold service 703 - url := fmt.Sprintf("%s/internal/blobs/referenced?hold=%s", 704 - gc.appviewURL, url.QueryEscape(gc.holdURL)) 705 - 706 - resp, err := http.Get(url) 707 - if err != nil { 708 - return nil, err 709 - } 710 - defer resp.Body.Close() 347 + // Get manifest URI before deletion 348 + manifestURI := fmt.Sprintf("at://%s/%s/%s", userDID, ManifestCollection, digest) 711 349 712 - var result struct { 713 - Blobs []string `json:"blobs"` 350 + // Delete manifest from user's PDS 351 + if err := h.atprotoClient.DeleteRecord(ctx, ManifestCollection, digest); err != nil { 352 + http.Error(w, "failed to delete manifest", 500) 353 + return 714 354 } 715 355 716 - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 717 - return nil, err 356 + // Delete associated layer records from hold's PDS 357 + if err := h.holdClient.DeleteLayerRecords(ctx, manifestURI); err != nil { 358 + log.Printf("Warning: failed to delete layer records: %v", err) 359 + // Continue - reconciliation will clean up 718 360 } 719 361 720 - return result.Blobs, nil 362 + w.WriteHeader(http.StatusNoContent) 721 363 } 722 364 ``` 723 365 724 - ### AppView Internal API 366 + ### Hold Service: Delete Layer Records 725 367 726 368 ```go 727 - // pkg/appview/handlers/internal.go 369 + // pkg/hold/pds/xrpc.go 728 370 729 - // Get all referenced blobs for a specific hold 730 - func (h *InternalHandler) GetReferencedBlobs(w http.ResponseWriter, r *http.Request) { 731 - holdEndpoint := r.URL.Query().Get("hold") 732 - if holdEndpoint == "" { 733 - http.Error(w, "missing hold parameter", 400) 734 - return 735 - } 736 - 737 - // Query database for all layers in manifests stored in this hold 738 - query := ` 739 - SELECT DISTINCT l.digest 740 - FROM layers l 741 - JOIN manifests m ON l.manifest_id = m.id 742 - WHERE m.hold_endpoint = ? 743 - ` 744 - 745 - rows, err := h.db.Query(query, holdEndpoint) 371 + func (s *Server) DeleteLayerRecords(ctx context.Context, manifestURI string) error { 372 + // List all layer records 373 + records, err := s.ListRecords(ctx, LayerCollection, "") 746 374 if err != nil { 747 - http.Error(w, "database error", 500) 748 - return 375 + return err 749 376 } 750 - defer rows.Close() 751 377 752 - blobs := []string{} 753 - for rows.Next() { 754 - var digest string 755 - if err := rows.Scan(&digest); err != nil { 378 + // Delete records matching this manifest 379 + for _, record := range records { 380 + var layer LayerRecord 381 + if err := json.Unmarshal(record.Value, &layer); err != nil { 756 382 continue 757 383 } 758 - blobs = append(blobs, digest) 759 - } 760 - 761 - json.NewEncoder(w).Encode(map[string]any{ 762 - "blobs": blobs, 763 - "count": len(blobs), 764 - "hold": holdEndpoint, 765 - }) 766 - } 767 - ``` 768 - 769 - ### GC Cron Schedule 770 - 771 - ```go 772 - // cmd/hold/main.go 773 - 774 - func main() { 775 - // ... service setup ... 776 - 777 - // Start GC cron if enabled 778 - if os.Getenv("GC_ENABLED") == "true" { 779 - gcInterval := 24 * time.Hour // Daily by default 780 - 781 - go func() { 782 - ticker := time.NewTicker(gcInterval) 783 - defer ticker.Stop() 784 - 785 - for range ticker.C { 786 - if err := garbageCollector.Run(context.Background()); err != nil { 787 - log.Printf("GC error: %v", err) 788 - } 384 + if layer.Manifest == manifestURI { 385 + if err := s.DeleteRecord(ctx, LayerCollection, record.RKey); err != nil { 386 + log.Printf("Failed to delete layer record %s: %v", record.RKey, err) 789 387 } 790 - }() 791 - 792 - log.Printf("GC cron started: runs every %v", gcInterval) 388 + } 793 389 } 794 390 795 - // Start server... 391 + return nil 796 392 } 797 393 ``` 798 394 799 - ## Quota Reconciliation 395 + ### Quota After Deletion 800 396 801 - ### PDS as Source of Truth 397 + After deleting a manifest: 398 + - Layer records for that manifest are removed 399 + - Quota recalculated with `SELECT DISTINCT` query 400 + - If layer was only in deleted manifest โ†’ quota decreases 401 + - If layer exists in other manifests โ†’ quota unchanged (still deduplicated) 802 402 803 - **Key insight:** Manifest records in PDS are publicly readable (no OAuth needed for reads). 403 + ## Garbage Collection 804 404 805 - Each manifest contains: 806 - - Repository name 807 - - Digest 808 - - Layers array with digest + size 809 - - Hold endpoint 405 + ### Orphaned Blobs 810 406 811 - The hold service can query the PDS to calculate the user's true quota: 812 - 813 - ``` 814 - 1. List all io.atcr.manifest records for user 815 - 2. Filter manifests where holdEndpoint == this hold service 816 - 3. Extract unique layers (deduplicate by digest) 817 - 4. Sum layer sizes = true quota usage 818 - 5. Compare to quota file 819 - 6. Fix discrepancies 820 - ``` 407 + Orphaned blobs accumulate when: 408 + 1. Manifest push fails after blobs uploaded 409 + 2. Quota exceeded - manifest rejected 410 + 3. User deletes manifest - blobs may no longer be referenced 821 411 822 - ### Implementation 412 + ### GC Process 823 413 824 414 ```go 825 - // cmd/hold/quota/reconcile.go 415 + // pkg/hold/gc/gc.go 826 416 827 - type Reconciler struct { 828 - quotaManager *Manager 829 - atprotoResolver *atproto.Resolver 830 - holdURL string 831 - } 832 - 833 - // ReconcileUser recalculates quota from PDS manifests 834 - func (r *Reconciler) ReconcileUser(ctx context.Context, did string) error { 835 - log.Printf("Reconciling quota for %s", did) 836 - 837 - // Step 1: Resolve user's PDS endpoint 838 - identity, err := r.atprotoResolver.ResolveIdentity(ctx, did) 417 + func (gc *GarbageCollector) Run(ctx context.Context) error { 418 + // Step 1: Get all referenced digests from layer records 419 + records, err := gc.pds.ListRecords(ctx, LayerCollection, "") 839 420 if err != nil { 840 - return fmt.Errorf("failed to resolve DID: %w", err) 841 - } 842 - 843 - // Step 2: Create unauthenticated ATProto client 844 - // (manifest records are public - no OAuth needed) 845 - client := atproto.NewClient(identity.PDSEndpoint, did, "") 846 - 847 - // Step 3: List all manifest records for this user 848 - manifests, err := client.ListRecords(ctx, atproto.ManifestCollection, 1000) 849 - if err != nil { 850 - return fmt.Errorf("failed to list manifests: %w", err) 421 + return err 851 422 } 852 423 853 - // Step 4: Filter manifests stored in THIS hold service 854 - // and extract unique layers 855 - uniqueLayers := make(map[string]int64) // digest -> size 856 - 857 - for _, record := range manifests { 858 - var manifest atproto.ManifestRecord 859 - if err := json.Unmarshal(record.Value, &manifest); err != nil { 860 - log.Printf("Warning: failed to parse manifest: %v", err) 861 - continue 862 - } 863 - 864 - // Only count manifests stored in this hold 865 - if manifest.HoldEndpoint != r.holdURL { 424 + referenced := make(map[string]bool) 425 + for _, record := range records { 426 + var layer LayerRecord 427 + if err := json.Unmarshal(record.Value, &layer); err != nil { 866 428 continue 867 429 } 868 - 869 - // Add config blob 870 - if manifest.Config.Digest != "" { 871 - uniqueLayers[manifest.Config.Digest] = manifest.Config.Size 872 - } 873 - 874 - // Add layer blobs 875 - for _, layer := range manifest.Layers { 876 - uniqueLayers[layer.Digest] = layer.Size 877 - } 430 + referenced[layer.Digest] = true 878 431 } 879 432 880 - // Step 5: Calculate true quota usage 881 - trueUsage := int64(0) 882 - for _, size := range uniqueLayers { 883 - trueUsage += size 884 - } 433 + log.Printf("Found %d referenced blobs", len(referenced)) 885 434 886 - log.Printf("User %s true usage from PDS: %d bytes (%d unique layers)", 887 - did, trueUsage, len(uniqueLayers)) 888 - 889 - // Step 6: Compare with current quota file 890 - quota, err := r.quotaManager.GetQuota(did) 891 - if err != nil { 892 - log.Printf("No existing quota for %s, creating new", did) 893 - quota = &Quota{ 894 - DID: did, 895 - Limit: r.quotaManager.DefaultLimit, 896 - ClaimedLayers: make(map[string]int64), 435 + // Step 2: Walk S3 blobs and delete unreferenced 436 + var deleted, reclaimed int64 437 + err = gc.driver.Walk(ctx, "/docker/registry/v2/blobs", func(fi storagedriver.FileInfo) error { 438 + if fi.IsDir() { 439 + return nil 897 440 } 898 - } 899 441 900 - // Step 7: Fix discrepancies 901 - if quota.Used != trueUsage || len(quota.ClaimedLayers) != len(uniqueLayers) { 902 - log.Printf("Quota mismatch for %s: recorded=%d, actual=%d (diff=%d)", 903 - did, quota.Used, trueUsage, trueUsage - quota.Used) 904 - 905 - // Update quota to match PDS truth 906 - quota.Used = trueUsage 907 - quota.ClaimedLayers = uniqueLayers 908 - quota.LastUpdated = time.Now() 909 - 910 - if err := r.quotaManager.SaveQuota(quota); err != nil { 911 - return fmt.Errorf("failed to save reconciled quota: %w", err) 442 + digest := extractDigestFromPath(fi.Path()) 443 + if !referenced[digest] { 444 + size := fi.Size() 445 + if err := gc.driver.Delete(ctx, fi.Path()); err != nil { 446 + log.Printf("Failed to delete %s: %v", digest, err) 447 + return nil 448 + } 449 + deleted++ 450 + reclaimed += size 451 + log.Printf("GC: deleted %s (%d bytes)", digest, size) 912 452 } 913 - 914 - log.Printf("Reconciled quota for %s: %d bytes", did, trueUsage) 915 - } else { 916 - log.Printf("Quota for %s is accurate", did) 917 - } 918 - 919 - return nil 920 - } 921 - 922 - // ReconcileAll reconciles all users (run periodically) 923 - func (r *Reconciler) ReconcileAll(ctx context.Context) error { 924 - // Get list of all users with quota files 925 - users, err := r.quotaManager.ListUsers() 926 - if err != nil { 927 - return err 928 - } 929 - 930 - log.Printf("Starting reconciliation for %d users", len(users)) 931 - 932 - for _, did := range users { 933 - if err := r.ReconcileUser(ctx, did); err != nil { 934 - log.Printf("Failed to reconcile %s: %v", did, err) 935 - // Continue with other users 936 - } 937 - } 453 + return nil 454 + }) 938 455 939 - log.Println("Reconciliation complete") 940 - return nil 456 + log.Printf("GC complete: deleted %d blobs, reclaimed %d bytes", deleted, reclaimed) 457 + return err 941 458 } 942 459 ``` 943 460 944 - ### Reconciliation Cron 461 + ### GC Schedule 945 462 946 - ```go 947 - // cmd/hold/main.go 948 - 949 - func main() { 950 - // ... setup ... 951 - 952 - // Start reconciliation cron 953 - if os.Getenv("QUOTA_RECONCILE_ENABLED") == "true" { 954 - reconcileInterval := 24 * time.Hour // Daily 955 - 956 - go func() { 957 - ticker := time.NewTicker(reconcileInterval) 958 - defer ticker.Stop() 959 - 960 - for range ticker.C { 961 - if err := reconciler.ReconcileAll(context.Background()); err != nil { 962 - log.Printf("Reconciliation error: %v", err) 963 - } 964 - } 965 - }() 966 - 967 - log.Printf("Quota reconciliation cron started: runs every %v", reconcileInterval) 968 - } 969 - 970 - // ... start server ... 971 - } 463 + ```bash 464 + # Environment variable 465 + GC_ENABLED=true 466 + GC_INTERVAL=24h # Daily by default 972 467 ``` 973 - 974 - ### Why PDS as Source of Truth Works 975 - 976 - 1. **Manifests are canonical** - If manifest exists in PDS, user owns those layers 977 - 2. **Public reads** - No OAuth needed, just resolve DID โ†’ PDS endpoint 978 - 3. **ATProto durability** - PDS is user's authoritative data store 979 - 4. **AppView is cache** - AppView database might lag or have inconsistencies 980 - 5. **Reconciliation fixes drift** - Periodic sync from PDS ensures accuracy 981 - 982 - **Example reconciliation scenarios:** 983 - 984 - - **Orphaned quota entries:** User deleted manifest from PDS, but hold quota still has it 985 - โ†’ Reconciliation removes from claimed_layers 986 - 987 - - **Missing quota entries:** User pushed manifest, but quota update failed 988 - โ†’ Reconciliation adds to claimed_layers 989 - 990 - - **Race condition duplicates:** Two concurrent pushes double-counted a layer 991 - โ†’ Reconciliation fixes to actual usage 992 468 993 469 ## Configuration 994 470 ··· 997 473 ```bash 998 474 # .env.hold 999 475 1000 - # ============================================================================ 1001 476 # Quota Configuration 1002 - # ============================================================================ 1003 - 1004 - # Enable quota enforcement 1005 477 QUOTA_ENABLED=true 1006 - 1007 - # Default quota limit per user (bytes) 1008 - # 10GB = 10737418240 1009 - # 50GB = 53687091200 1010 - # 100GB = 107374182400 1011 - QUOTA_DEFAULT_LIMIT=10737418240 1012 - 1013 - # Storage backend for quota data 1014 - # Options: s3, sqlite 1015 - QUOTA_STORAGE_BACKEND=s3 1016 - 1017 - # For S3-based storage: 1018 - # Quota files stored in same bucket as blobs 1019 - QUOTA_STORAGE_PREFIX=/atcr/quota/ 1020 - 1021 - # For SQLite-based storage: 1022 - QUOTA_DB_PATH=/var/lib/atcr/hold-quota.db 478 + QUOTA_DEFAULT_LIMIT=10737418240 # 10GB in bytes 1023 479 1024 - # ============================================================================ 1025 480 # Garbage Collection 1026 - # ============================================================================ 1027 - 1028 - # Enable periodic garbage collection 1029 481 GC_ENABLED=true 1030 - 1031 - # GC interval (default: 24h) 1032 482 GC_INTERVAL=24h 1033 - 1034 - # AppView URL for GC reference checking 1035 - APPVIEW_URL=https://atcr.io 1036 - 1037 - # ============================================================================ 1038 - # Quota Reconciliation 1039 - # ============================================================================ 1040 - 1041 - # Enable quota reconciliation from PDS 1042 - QUOTA_RECONCILE_ENABLED=true 1043 - 1044 - # Reconciliation interval (default: 24h) 1045 - QUOTA_RECONCILE_INTERVAL=24h 1046 - 1047 - # ============================================================================ 1048 - # Hold Service Identity (Required) 1049 - # ============================================================================ 1050 - 1051 - # Public URL of this hold service 1052 - HOLD_PUBLIC_URL=https://hold1.example.com 1053 - 1054 - # Owner DID (for auto-registration) 1055 - HOLD_OWNER=did:plc:xyz123 1056 483 ``` 1057 484 1058 - ### AppView Configuration 485 + ### Quota Limits by Bytes 1059 486 1060 - ```bash 1061 - # .env.appview 1062 - 1063 - # Internal API endpoint for hold services 1064 - # Used for GC reference checking 1065 - ATCR_INTERNAL_API_ENABLED=true 1066 - 1067 - # Optional: authentication token for internal APIs 1068 - ATCR_INTERNAL_API_TOKEN=secret123 1069 - ``` 1070 - 1071 - ## Trade-offs & Design Decisions 1072 - 1073 - ### 1. Claimed Storage vs Physical Storage 1074 - 1075 - **Decision:** Track claimed storage (logical accounting) 1076 - 1077 - **Why:** 1078 - - Predictable for users: "you pay for what you upload" 1079 - - No complex cross-user dependencies 1080 - - Delete always gives you quota back 1081 - - Matches Harbor's proven model 1082 - 1083 - **Trade-off:** 1084 - - Total claimed can exceed physical storage 1085 - - Users might complain "I uploaded 10GB but S3 only has 6GB" 1086 - 1087 - **Mitigation:** 1088 - - Show deduplication savings metric 1089 - - Educate users: "You claimed 10GB, but deduplication saved 4GB" 1090 - 1091 - ### 2. S3 vs SQLite for Quota Storage 1092 - 1093 - **Decision:** Support both, recommend based on use case 1094 - 1095 - **S3 Pros:** 1096 - - No database to manage 1097 - - Quota data lives with blobs 1098 - - Better for ephemeral BYOS 1099 - 1100 - **SQLite Pros:** 1101 - - Faster (no network) 1102 - - ACID transactions (no race conditions) 1103 - - Better for high-traffic shared holds 1104 - 1105 - **Trade-off:** 1106 - - S3: eventual consistency, race conditions 1107 - - SQLite: stateful service, scaling challenges 1108 - 1109 - **Mitigation:** 1110 - - Reconciliation fixes S3 inconsistencies 1111 - - SQLite can use shared DB for multi-instance 1112 - 1113 - ### 3. Optimistic Quota Update 1114 - 1115 - **Decision:** Update quota BEFORE upload completes 1116 - 1117 - **Why:** 1118 - - Prevent race conditions (two users uploading simultaneously) 1119 - - Can reject before presigned URL generated 1120 - - Simpler flow 1121 - 1122 - **Trade-off:** 1123 - - If upload fails, quota already incremented (user "paid" for nothing) 1124 - 1125 - **Mitigation:** 1126 - - Reconciliation from PDS fixes orphaned quota entries 1127 - - Acceptable for MVP (upload failures are rare) 1128 - 1129 - ### 4. AppView as Intermediary 1130 - 1131 - **Decision:** AppView notifies hold service on deletes 1132 - 1133 - **Why:** 1134 - - AppView already has manifest/layer database 1135 - - Can efficiently check if layer still referenced 1136 - - Hold service doesn't need to query PDS on every delete 1137 - 1138 - **Trade-off:** 1139 - - AppView โ†’ Hold dependency 1140 - - Network hop on delete 1141 - 1142 - **Mitigation:** 1143 - - If notification fails, reconciliation fixes quota 1144 - - Eventually consistent is acceptable 1145 - 1146 - ### 5. PDS as Source of Truth 1147 - 1148 - **Decision:** Use PDS manifests for reconciliation 1149 - 1150 - **Why:** 1151 - - Manifests in PDS are canonical user data 1152 - - Public reads (no OAuth for reconciliation) 1153 - - AppView database might lag or be inconsistent 1154 - 1155 - **Trade-off:** 1156 - - Reconciliation requires PDS queries (slower) 1157 - - Limited to 1000 manifests per query 1158 - 1159 - **Mitigation:** 1160 - - Run reconciliation daily (not real-time) 1161 - - Paginate if user has >1000 manifests 487 + | Size | Bytes | 488 + |------|-------| 489 + | 1 GB | 1073741824 | 490 + | 5 GB | 5368709120 | 491 + | 10 GB | 10737418240 | 492 + | 50 GB | 53687091200 | 493 + | 100 GB | 107374182400 | 1162 494 1163 495 ## Future Enhancements 1164 496 1165 497 ### 1. Quota API Endpoints 1166 498 1167 499 ``` 1168 - GET /quota/usage - Get current user's quota 1169 - GET /quota/breakdown - Get storage by repository 1170 - POST /quota/limit - Update user's quota limit (admin) 1171 - GET /quota/stats - Get hold-wide statistics 500 + GET /xrpc/io.atcr.hold.getQuota?did={userDID} - Get user's quota usage 501 + GET /xrpc/io.atcr.hold.getQuotaBreakdown - Storage by repository 1172 502 ``` 1173 503 1174 504 ### 2. Quota Alerts 1175 505 1176 - Notify users when approaching limit: 1177 - - Email/webhook at 80%, 90%, 95% 1178 - - Reject uploads at 100% (currently implemented) 1179 - - Grace period: allow 105% temporarily 506 + - Warning thresholds at 80%, 90%, 95% 507 + - Email/webhook notifications 508 + - Grace period before hard enforcement 1180 509 1181 - ### 3. Tiered Quotas 510 + ### 3. Tier-Based Quotas (Implemented) 1182 511 1183 - Different limits based on user tier: 1184 - - Free: 10GB 1185 - - Pro: 100GB 1186 - - Enterprise: unlimited 512 + ATCR uses quota tiers to limit storage per crew member, configured via `quotas.yaml`: 1187 513 1188 - ### 4. Quota Purchasing 514 + ```yaml 515 + # quotas.yaml 516 + tiers: 517 + deckhand: # Entry-level crew 518 + quota: 5GB 519 + bosun: # Mid-level crew 520 + quota: 50GB 521 + quartermaster: # High-level crew 522 + quota: 100GB 1189 523 1190 - Allow users to buy additional storage: 1191 - - Stripe integration 1192 - - $0.10/GB/month pricing 1193 - - Dynamic limit updates 524 + defaults: 525 + new_crew_tier: deckhand # Default tier for new crew members 526 + ``` 1194 527 1195 - ### 5. Cross-Hold Deduplication 528 + | Tier | Limit | Description | 529 + |------|-------|-------------| 530 + | deckhand | 5 GB | Entry-level crew member | 531 + | bosun | 50 GB | Mid-level crew member | 532 + | quartermaster | 100 GB | Senior crew member | 533 + | owner (captain) | Unlimited | Hold owner always has unlimited | 1196 534 1197 - If multiple holds share same S3 bucket: 1198 - - Track blob ownership globally 1199 - - Split costs proportionally 1200 - - More complex, but maximizes deduplication 535 + **Tier Resolution:** 536 + 1. If user is captain (owner) โ†’ unlimited 537 + 2. If crew member has explicit tier โ†’ use that tier's limit 538 + 3. If crew member has no tier โ†’ use `defaults.new_crew_tier` 539 + 4. If default tier not found โ†’ unlimited 1201 540 1202 - ### 6. Manifest-Based Quota (Alternative Model) 1203 - 1204 - Instead of tracking layers, track manifests: 1205 - - Simpler: just count manifest sizes 1206 - - No deduplication benefits for users 1207 - - Might be acceptable for some use cases 1208 - 1209 - ### 7. Redis-Based Quota (High Performance) 1210 - 1211 - For high-traffic registries: 1212 - - Use Redis instead of S3/SQLite 1213 - - Sub-millisecond quota checks 1214 - - Harbor-proven approach 1215 - 1216 - ### 8. Quota Visualizations 1217 - 1218 - Web UI showing: 1219 - - Storage usage over time 1220 - - Top consumers by repository 1221 - - Deduplication savings graph 1222 - - Layer size distribution 1223 - 1224 - ## Appendix: SQL Queries 1225 - 1226 - ### Check if User Still References Layer 1227 - 1228 - ```sql 1229 - -- After deleting manifest, check if user has other manifests using this layer 1230 - SELECT COUNT(*) 1231 - FROM layers l 1232 - JOIN manifests m ON l.manifest_id = m.id 1233 - WHERE m.did = ? -- User's DID 1234 - AND l.digest = ? -- Layer digest to check 1235 - AND m.id != ? -- Exclude the manifest being deleted 1236 - ``` 1237 - 1238 - ### Get All Unique Layers for User 1239 - 1240 - ```sql 1241 - -- Calculate true quota usage for a user 1242 - SELECT DISTINCT l.digest, l.size 1243 - FROM layers l 1244 - JOIN manifests m ON l.manifest_id = m.id 1245 - WHERE m.did = ? 1246 - AND m.hold_endpoint = ? 541 + **Crew Record Example:** 542 + ```json 543 + { 544 + "$type": "io.atcr.hold.crew", 545 + "member": "did:plc:alice123", 546 + "role": "writer", 547 + "permissions": ["blob:write"], 548 + "tier": "bosun", 549 + "addedAt": "2026-01-04T12:00:00Z" 550 + } 1247 551 ``` 1248 552 1249 - ### Get Referenced Blobs for Hold 553 + ### 4. Rate Limiting 1250 554 1251 - ```sql 1252 - -- For GC: get all blobs still referenced by any user of this hold 1253 - SELECT DISTINCT l.digest 1254 - FROM layers l 1255 - JOIN manifests m ON l.manifest_id = m.id 1256 - WHERE m.hold_endpoint = ? 1257 - ``` 555 + Pull rate limits (Docker Hub style): 556 + - Anonymous: 100 pulls per 6 hours per IP 557 + - Authenticated: 200 pulls per 6 hours 558 + - Paid: Unlimited 1258 559 1259 - ### Get Storage Stats by Repository 560 + ### 5. Quota Purchasing 1260 561 1261 - ```sql 1262 - -- User's storage broken down by repository 1263 - SELECT 1264 - m.repository, 1265 - COUNT(DISTINCT m.id) as manifest_count, 1266 - COUNT(DISTINCT l.digest) as unique_layers, 1267 - SUM(l.size) as total_size 1268 - FROM manifests m 1269 - JOIN layers l ON l.manifest_id = m.id 1270 - WHERE m.did = ? 1271 - AND m.hold_endpoint = ? 1272 - GROUP BY m.repository 1273 - ORDER BY total_size DESC 1274 - ``` 562 + - Stripe integration for additional storage 563 + - $0.10/GB/month pricing (industry standard) 1275 564 1276 565 ## References 1277 566 1278 567 - **Harbor Quotas:** https://goharbor.io/docs/1.10/administration/configure-project-quotas/ 1279 - - **Harbor Source:** https://github.com/goharbor/harbor 1280 568 - **ATProto Spec:** https://atproto.com/specs/record 1281 569 - **OCI Distribution Spec:** https://github.com/opencontainers/distribution-spec 1282 - - **S3 API Reference:** https://docs.aws.amazon.com/AmazonS3/latest/API/ 1283 - - **Distribution GC:** https://github.com/distribution/distribution/blob/main/registry/storage/garbagecollect.go 1284 570 1285 571 --- 1286 572 1287 - **Document Version:** 1.0 1288 - **Last Updated:** 2025-10-09 1289 - **Author:** Generated from implementation research and Harbor analysis 573 + **Document Version:** 2.0 574 + **Last Updated:** 2026-01-04 575 + **Model:** Per-user layer tracking with ATProto records
+4 -4
docs/SIGNATURE_INTEGRATION.md
··· 545 545 Name: v.name, 546 546 Type: v.Type(), 547 547 Message: fmt.Sprintf("Verified for DID %s", sigData.ATProto.DID), 548 - Extensions: map[string]interface{}{ 548 + Extensions: map[string]any{ 549 549 "did": sigData.ATProto.DID, 550 550 "handle": sigData.ATProto.Handle, 551 551 "signedAt": sigData.ATProto.SignedAt, ··· 673 673 674 674 type ProviderResponse struct { 675 675 SystemError string `json:"system_error,omitempty"` 676 - Responses []map[string]interface{} `json:"responses"` 676 + Responses []map[string]any `json:"responses"` 677 677 } 678 678 679 679 func handleProvide(w http.ResponseWriter, r *http.Request) { ··· 684 684 } 685 685 686 686 // Verify each image 687 - responses := make([]map[string]interface{}, 0, len(req.Values)) 687 + responses := make([]map[string]any, 0, len(req.Values)) 688 688 for _, image := range req.Values { 689 689 result, err := verifier.Verify(context.Background(), image) 690 690 691 - response := map[string]interface{}{ 691 + response := map[string]any{ 692 692 "image": image, 693 693 "verified": false, 694 694 }
+4 -4
examples/plugins/gatekeeper-provider/main.go.temp
··· 35 35 // ProviderResponse is the response format to Gatekeeper. 36 36 type ProviderResponse struct { 37 37 SystemError string `json:"system_error,omitempty"` 38 - Responses []map[string]interface{} `json:"responses"` 38 + Responses []map[string]any `json:"responses"` 39 39 } 40 40 41 41 // VerificationResult holds the result of verifying a single image. ··· 110 110 log.Printf("INFO: received verification request for %d images", len(req.Values)) 111 111 112 112 // Verify each image 113 - responses := make([]map[string]interface{}, 0, len(req.Values)) 113 + responses := make([]map[string]any, 0, len(req.Values)) 114 114 for _, image := range req.Values { 115 115 result := s.verifyImage(r.Context(), image) 116 116 responses = append(responses, structToMap(result)) ··· 186 186 } 187 187 188 188 // structToMap converts a struct to a map for JSON encoding. 189 - func structToMap(v interface{}) map[string]interface{} { 189 + func structToMap(v any) map[string]any { 190 190 data, _ := json.Marshal(v) 191 - var m map[string]interface{} 191 + var m map[string]any 192 192 json.Unmarshal(data, &m) 193 193 return m 194 194 }
+1 -1
examples/plugins/ratify-verifier/README.md
··· 196 196 Name string 197 197 Type string 198 198 Message string 199 - Extensions map[string]interface{} 199 + Extensions map[string]any 200 200 } 201 201 ``` 202 202
+2 -2
examples/plugins/ratify-verifier/verifier.go.temp
··· 166 166 Name: v.name, 167 167 Type: v.Type(), 168 168 Message: fmt.Sprintf("Successfully verified ATProto signature for DID %s", sigData.ATProto.DID), 169 - Extensions: map[string]interface{}{ 169 + Extensions: map[string]any{ 170 170 "did": sigData.ATProto.DID, 171 171 "handle": sigData.ATProto.Handle, 172 172 "signedAt": sigData.ATProto.SignedAt, ··· 203 203 Name: v.name, 204 204 Type: v.Type(), 205 205 Message: message, 206 - Extensions: map[string]interface{}{ 206 + Extensions: map[string]any{ 207 207 "error": message, 208 208 }, 209 209 }
+5
lexicons/io/atcr/hold/crew.json
··· 29 29 "maxLength": 64 30 30 } 31 31 }, 32 + "tier": { 33 + "type": "string", 34 + "description": "Optional tier for quota limits (e.g., 'deckhand', 'bosun', 'quartermaster'). If empty, uses defaults.new_crew_tier from quotas.yaml.", 35 + "maxLength": 32 36 + }, 32 37 "addedAt": { 33 38 "type": "string", 34 39 "format": "datetime",
+4 -9
lexicons/io/atcr/hold/layer.json
··· 8 8 "description": "Represents metadata about a container layer stored in the hold. Stored in the hold's embedded PDS for tracking and analytics.", 9 9 "record": { 10 10 "type": "object", 11 - "required": ["digest", "size", "mediaType", "repository", "userDid", "userHandle", "createdAt"], 11 + "required": ["digest", "size", "mediaType", "manifest", "userDid", "createdAt"], 12 12 "properties": { 13 13 "digest": { 14 14 "type": "string", ··· 24 24 "description": "Media type (e.g., application/vnd.oci.image.layer.v1.tar+gzip)", 25 25 "maxLength": 128 26 26 }, 27 - "repository": { 27 + "manifest": { 28 28 "type": "string", 29 - "description": "Repository this layer belongs to", 30 - "maxLength": 255 29 + "format": "at-uri", 30 + "description": "AT-URI of the manifest that included this layer (e.g., at://did:plc:xyz/io.atcr.manifest/abc123)" 31 31 }, 32 32 "userDid": { 33 33 "type": "string", 34 34 "format": "did", 35 35 "description": "DID of user who uploaded this layer" 36 - }, 37 - "userHandle": { 38 - "type": "string", 39 - "format": "handle", 40 - "description": "Handle of user (for display purposes)" 41 36 }, 42 37 "createdAt": { 43 38 "type": "string",
+1 -2
pkg/appview/config.go
··· 388 388 return checksums 389 389 } 390 390 391 - pairs := strings.Split(checksumsStr, ",") 392 - for _, pair := range pairs { 391 + for pair := range strings.SplitSeq(checksumsStr, ",") { 393 392 parts := strings.SplitN(strings.TrimSpace(pair), ":", 2) 394 393 if len(parts) == 2 { 395 394 platform := strings.TrimSpace(parts[0])
+4 -2
pkg/appview/db/device_store.go
··· 365 365 } 366 366 367 367 // UpdateLastUsed updates the last used timestamp 368 - func (s *DeviceStore) UpdateLastUsed(secretHash string) error { 368 + func (s *DeviceStore) UpdateLastUsed(secretHash string) { 369 369 _, err := s.db.Exec(` 370 370 UPDATE devices 371 371 SET last_used = ? 372 372 WHERE secret_hash = ? 373 373 `, time.Now(), secretHash) 374 374 375 - return err 375 + if err != nil { 376 + slog.Warn("Failed to update device last used timestamp", "component", "device_store", "error", err) 377 + } 376 378 } 377 379 378 380 // CleanupExpired removes expired pending authorizations
+4 -10
pkg/appview/db/device_store_test.go
··· 56 56 func TestGenerateUserCode(t *testing.T) { 57 57 // Generate multiple codes to test 58 58 codes := make(map[string]bool) 59 - for i := 0; i < 100; i++ { 59 + for range 100 { 60 60 code := generateUserCode() 61 61 62 62 // Test format: XXXX-XXXX ··· 372 372 return 373 373 } 374 374 if !tt.wantErr { 375 - if device == nil { 376 - t.Error("Expected device, got nil") 377 - } 378 375 if device.DID != "did:plc:alice123" { 379 376 t.Errorf("DID = %v, want did:plc:alice123", device.DID) 380 377 } ··· 399 396 } 400 397 401 398 // Create 3 devices 402 - for i := 0; i < 3; i++ { 399 + for i := range 3 { 403 400 pending, err := store.CreatePendingAuth("Device "+string(rune('A'+i)), "192.168.1.1", "Agent") 404 401 if err != nil { 405 402 t.Fatalf("CreatePendingAuth() error = %v", err) ··· 417 414 } 418 415 419 416 // Verify they're sorted by created_at DESC (newest first) 420 - for i := 0; i < len(devices)-1; i++ { 417 + for i := range len(devices) - 1 { 421 418 if devices[i].CreatedAt.Before(devices[i+1].CreatedAt) { 422 419 t.Error("Devices should be sorted by created_at DESC") 423 420 } ··· 521 518 time.Sleep(10 * time.Millisecond) 522 519 523 520 // Update last used 524 - err = store.UpdateLastUsed(device.SecretHash) 525 - if err != nil { 526 - t.Errorf("UpdateLastUsed() error = %v", err) 527 - } 521 + store.UpdateLastUsed(device.SecretHash) 528 522 529 523 // Verify it was updated 530 524 device2, err := store.ValidateDeviceSecret(secret)
+2 -1
pkg/appview/db/models.go
··· 154 154 Tag 155 155 Platforms []PlatformInfo 156 156 IsMultiArch bool 157 - HasAttestations bool // true if manifest list contains attestation references 157 + HasAttestations bool // true if manifest list contains attestation references 158 + ArtifactType string // container-image, helm-chart, unknown 158 159 } 159 160 160 161 // ManifestWithMetadata extends Manifest with tags and platform information
+21 -41
pkg/appview/db/oauth_store.go
··· 8 8 "log/slog" 9 9 "time" 10 10 11 + atoauth "atcr.io/pkg/auth/oauth" 11 12 "github.com/bluesky-social/indigo/atproto/auth/oauth" 12 13 "github.com/bluesky-social/indigo/atproto/syntax" 13 14 ) ··· 213 214 } 214 215 215 216 // CleanupOldSessions removes sessions older than the specified duration 216 - func (s *OAuthStore) CleanupOldSessions(ctx context.Context, olderThan time.Duration) error { 217 + func (s *OAuthStore) CleanupOldSessions(ctx context.Context, olderThan time.Duration) { 217 218 cutoff := time.Now().Add(-olderThan) 218 219 219 220 result, err := s.db.ExecContext(ctx, ` ··· 222 223 `, cutoff) 223 224 224 225 if err != nil { 225 - return fmt.Errorf("failed to cleanup old sessions: %w", err) 226 + slog.Warn("Failed to cleanup old OAuth sessions", "component", "oauth_store", "error", err) 227 + return 226 228 } 227 229 228 230 deleted, _ := result.RowsAffected() 229 231 if deleted > 0 { 230 232 slog.Info("Cleaned up old OAuth sessions", "count", deleted, "older_than", olderThan) 231 233 } 232 - 233 - return nil 234 234 } 235 235 236 236 // CleanupExpiredAuthRequests removes auth requests older than 10 minutes 237 - func (s *OAuthStore) CleanupExpiredAuthRequests(ctx context.Context) error { 237 + func (s *OAuthStore) CleanupExpiredAuthRequests(ctx context.Context) { 238 238 cutoff := time.Now().Add(-10 * time.Minute) 239 239 240 240 result, err := s.db.ExecContext(ctx, ` ··· 243 243 `, cutoff) 244 244 245 245 if err != nil { 246 - return fmt.Errorf("failed to cleanup auth requests: %w", err) 246 + slog.Warn("Failed to cleanup expired auth requests", "component", "oauth_store", "error", err) 247 + return 247 248 } 248 249 249 250 deleted, _ := result.RowsAffected() 250 251 if deleted > 0 { 251 252 slog.Info("Cleaned up expired auth requests", "count", deleted) 252 253 } 253 - 254 - return nil 255 254 } 256 255 257 256 // InvalidateSessionsWithMismatchedScopes removes all sessions whose scopes don't match the desired scopes ··· 285 284 continue 286 285 } 287 286 288 - // Check if scopes match (need to import oauth package for ScopesMatch) 289 - // Since we're in db package, we can't import oauth (circular dependency) 290 - // So we'll implement a simple scope comparison here 291 - if !scopesMatch(sessionData.Scopes, desiredScopes) { 287 + // Check if scopes match (expands include: scopes before comparing) 288 + if !atoauth.ScopesMatch(sessionData.Scopes, desiredScopes) { 289 + slog.Debug("Session has mismatched scopes", 290 + "component", "oauth/store", 291 + "session_key", sessionKey, 292 + "account_did", accountDID, 293 + "session_scopes", sessionData.Scopes, 294 + "desired_scopes", desiredScopes, 295 + ) 292 296 sessionsToDelete = append(sessionsToDelete, sessionKey) 293 297 } 294 298 } ··· 313 317 return len(sessionsToDelete), nil 314 318 } 315 319 316 - // scopesMatch checks if two scope lists are equivalent (order-independent) 317 - // Local implementation to avoid circular dependency with oauth package 318 - func scopesMatch(stored, desired []string) bool { 319 - if len(stored) == 0 && len(desired) == 0 { 320 - return true 321 - } 322 - if len(stored) != len(desired) { 323 - return false 324 - } 325 - 326 - desiredMap := make(map[string]bool, len(desired)) 327 - for _, scope := range desired { 328 - desiredMap[scope] = true 329 - } 330 - 331 - for _, scope := range stored { 332 - if !desiredMap[scope] { 333 - return false 334 - } 335 - } 336 - 337 - return true 338 - } 339 - 340 320 // GetSessionStats returns statistics about stored OAuth sessions 341 321 // Useful for monitoring and debugging session health 342 - func (s *OAuthStore) GetSessionStats(ctx context.Context) (map[string]interface{}, error) { 343 - stats := make(map[string]interface{}) 322 + func (s *OAuthStore) GetSessionStats(ctx context.Context) (map[string]any, error) { 323 + stats := make(map[string]any) 344 324 345 325 // Total sessions 346 326 var totalSessions int ··· 392 372 393 373 // ListSessionsForMonitoring returns a list of all sessions with basic info for monitoring 394 374 // Returns: DID, session age (minutes), last update time 395 - func (s *OAuthStore) ListSessionsForMonitoring(ctx context.Context) ([]map[string]interface{}, error) { 375 + func (s *OAuthStore) ListSessionsForMonitoring(ctx context.Context) ([]map[string]any, error) { 396 376 rows, err := s.db.QueryContext(ctx, ` 397 377 SELECT 398 378 account_did, ··· 408 388 } 409 389 defer rows.Close() 410 390 411 - var sessions []map[string]interface{} 391 + var sessions []map[string]any 412 392 for rows.Next() { 413 393 var did, sessionID, createdAt, updatedAt string 414 394 var idleMinutes int ··· 418 398 continue 419 399 } 420 400 421 - sessions = append(sessions, map[string]interface{}{ 401 + sessions = append(sessions, map[string]any{ 422 402 "did": did, 423 403 "session_id": sessionID, 424 404 "created_at": createdAt,
+17 -6
pkg/appview/db/oauth_store_test.go
··· 5 5 "testing" 6 6 "time" 7 7 8 + atcroauth "atcr.io/pkg/auth/oauth" 8 9 "github.com/bluesky-social/indigo/atproto/auth/oauth" 9 10 "github.com/bluesky-social/indigo/atproto/syntax" 10 11 ) ··· 161 162 } 162 163 163 164 func TestScopesMatch(t *testing.T) { 164 - // Test the local scopesMatch function to ensure it matches the oauth.ScopesMatch behavior 165 + // Test oauth.ScopesMatch function including include: scope expansion 165 166 tests := []struct { 166 167 name string 167 168 stored []string ··· 204 205 desired: []string{}, 205 206 expected: true, 206 207 }, 208 + { 209 + name: "include scope expansion", 210 + stored: []string{ 211 + "atproto", 212 + "repo?collection=io.atcr.manifest&collection=io.atcr.repo.page&collection=io.atcr.sailor.profile&collection=io.atcr.sailor.star&collection=io.atcr.tag", 213 + }, 214 + desired: []string{ 215 + "atproto", 216 + "include:io.atcr.authFullApp", 217 + }, 218 + expected: true, 219 + }, 207 220 } 208 221 209 222 for _, tt := range tests { 210 223 t.Run(tt.name, func(t *testing.T) { 211 - result := scopesMatch(tt.stored, tt.desired) 224 + result := atcroauth.ScopesMatch(tt.stored, tt.desired) 212 225 if result != tt.expected { 213 - t.Errorf("scopesMatch(%v, %v) = %v, want %v", 226 + t.Errorf("ScopesMatch(%v, %v) = %v, want %v", 214 227 tt.stored, tt.desired, result, tt.expected) 215 228 } 216 229 }) ··· 353 366 } 354 367 355 368 // Run cleanup (remove sessions older than 30 days) 356 - if err := store.CleanupOldSessions(ctx, 30*24*time.Hour); err != nil { 357 - t.Fatalf("Failed to cleanup old sessions: %v", err) 358 - } 369 + store.CleanupOldSessions(ctx, 30*24*time.Hour) 359 370 360 371 // Verify old session was deleted 361 372 _, err = store.GetSession(ctx, did1, "old_session")
+6 -4
pkg/appview/db/queries.go
··· 653 653 t.digest, 654 654 t.created_at, 655 655 m.media_type, 656 + m.artifact_type, 656 657 COALESCE(mr.platform_os, '') as platform_os, 657 658 COALESCE(mr.platform_architecture, '') as platform_architecture, 658 659 COALESCE(mr.platform_variant, '') as platform_variant, ··· 676 677 677 678 for rows.Next() { 678 679 var t Tag 679 - var mediaType, platformOS, platformArch, platformVariant, platformOSVersion string 680 + var mediaType, artifactType, platformOS, platformArch, platformVariant, platformOSVersion string 680 681 var isAttestation bool 681 682 682 683 if err := rows.Scan(&t.ID, &t.DID, &t.Repository, &t.Tag, &t.Digest, &t.CreatedAt, 683 - &mediaType, &platformOS, &platformArch, &platformVariant, &platformOSVersion, &isAttestation); err != nil { 684 + &mediaType, &artifactType, &platformOS, &platformArch, &platformVariant, &platformOSVersion, &isAttestation); err != nil { 684 685 return nil, err 685 686 } 686 687 ··· 688 689 tagKey := t.Tag 689 690 if _, exists := tagMap[tagKey]; !exists { 690 691 tagMap[tagKey] = &TagWithPlatforms{ 691 - Tag: t, 692 - Platforms: []PlatformInfo{}, 692 + Tag: t, 693 + Platforms: []PlatformInfo{}, 694 + ArtifactType: artifactType, 693 695 } 694 696 tagOrder = append(tagOrder, tagKey) 695 697 }
+64 -11
pkg/appview/db/schema.go
··· 37 37 return nil, err 38 38 } 39 39 40 - // Create schema from embedded SQL file 41 - if _, err := db.Exec(schemaSQL); err != nil { 42 - return nil, err 40 + // Check if this is an existing database with migrations applied 41 + isExisting, err := hasAppliedMigrations(db) 42 + if err != nil { 43 + return nil, fmt.Errorf("failed to check database state: %w", err) 44 + } 45 + 46 + if isExisting { 47 + // Existing database: skip schema.sql, only run pending migrations 48 + slog.Debug("Existing database detected, skipping schema.sql") 49 + } else { 50 + // Fresh database: apply schema.sql 51 + slog.Info("Fresh database detected, applying schema") 52 + if err := applySchema(db); err != nil { 53 + return nil, err 54 + } 43 55 } 44 56 45 57 // Run migrations unless skipped 58 + // For fresh databases, migrations are recorded but not executed (schema.sql is already complete) 46 59 if !skipMigrations { 47 - if err := runMigrations(db); err != nil { 60 + if err := runMigrations(db, !isExisting); err != nil { 48 61 return nil, err 49 62 } 50 63 } ··· 52 65 return db, nil 53 66 } 54 67 68 + // hasAppliedMigrations checks if this is an existing database with migrations applied 69 + func hasAppliedMigrations(db *sql.DB) (bool, error) { 70 + // Check if schema_migrations table exists 71 + var count int 72 + err := db.QueryRow(` 73 + SELECT COUNT(*) FROM sqlite_master 74 + WHERE type='table' AND name='schema_migrations' 75 + `).Scan(&count) 76 + if err != nil { 77 + return false, err 78 + } 79 + if count == 0 { 80 + return false, nil // No migrations table = fresh DB 81 + } 82 + 83 + // Table exists, check if it has entries 84 + err = db.QueryRow("SELECT COUNT(*) FROM schema_migrations").Scan(&count) 85 + if err != nil { 86 + return false, err 87 + } 88 + return count > 0, nil 89 + } 90 + 91 + // applySchema executes schema.sql for fresh databases 92 + func applySchema(db *sql.DB) error { 93 + for _, stmt := range splitSQLStatements(schemaSQL) { 94 + if _, err := db.Exec(stmt); err != nil { 95 + return fmt.Errorf("failed to apply schema: %w", err) 96 + } 97 + } 98 + return nil 99 + } 100 + 55 101 // Migration represents a database migration 56 102 type Migration struct { 57 103 Version int ··· 61 107 } 62 108 63 109 // runMigrations applies any pending database migrations 64 - func runMigrations(db *sql.DB) error { 110 + // If freshDB is true, migrations are recorded but not executed (schema.sql already includes their changes) 111 + func runMigrations(db *sql.DB, freshDB bool) error { 65 112 // Load migrations from files 66 113 migrations, err := loadMigrations() 67 114 if err != nil { ··· 86 133 continue 87 134 } 88 135 89 - // Apply migration in a transaction 136 + if freshDB { 137 + // Fresh database: schema.sql already has everything, just record the migration 138 + slog.Debug("Recording migration as applied (fresh DB)", "version", m.Version, "name", m.Name) 139 + if _, err := db.Exec("INSERT INTO schema_migrations (version) VALUES (?)", m.Version); err != nil { 140 + return fmt.Errorf("failed to record migration %d: %w", m.Version, err) 141 + } 142 + continue 143 + } 144 + 145 + // Existing database: apply migration in a transaction 90 146 slog.Info("Applying migration", "version", m.Version, "name", m.Name, "description", m.Description) 91 147 92 148 tx, err := db.Begin() ··· 169 225 var statements []string 170 226 171 227 // Split on semicolons 172 - parts := strings.Split(query, ";") 173 - 174 - for _, part := range parts { 228 + for part := range strings.SplitSeq(query, ";") { 175 229 // Trim whitespace 176 230 stmt := strings.TrimSpace(part) 177 231 ··· 181 235 } 182 236 183 237 // Skip comment-only statements 184 - lines := strings.Split(stmt, "\n") 185 238 hasCode := false 186 - for _, line := range lines { 239 + for line := range strings.SplitSeq(stmt, "\n") { 187 240 trimmed := strings.TrimSpace(line) 188 241 if trimmed != "" && !strings.HasPrefix(trimmed, "--") { 189 242 hasCode = true
+2 -2
pkg/appview/db/session_store_test.go
··· 252 252 253 253 // Create multiple sessions for alice 254 254 sessionIDs := make([]string, 3) 255 - for i := 0; i < 3; i++ { 255 + for i := range 3 { 256 256 id, err := store.Create(did, "alice.bsky.social", "https://pds.example.com", 1*time.Hour) 257 257 if err != nil { 258 258 t.Fatalf("Create() error = %v", err) ··· 516 516 517 517 // Generate multiple session IDs 518 518 ids := make(map[string]bool) 519 - for i := 0; i < 100; i++ { 519 + for range 100 { 520 520 id, err := store.Create("did:plc:alice123", "alice.bsky.social", "https://pds.example.com", 1*time.Hour) 521 521 if err != nil { 522 522 t.Fatalf("Create() error = %v", err)
+1 -1
pkg/appview/handlers/images.go
··· 95 95 96 96 w.Header().Set("Content-Type", "application/json") 97 97 w.WriteHeader(http.StatusConflict) 98 - json.NewEncoder(w).Encode(map[string]interface{}{ 98 + json.NewEncoder(w).Encode(map[string]any{ 99 99 "error": "confirmation_required", 100 100 "message": "This manifest has associated tags that will also be deleted", 101 101 "tags": tags,
+44
pkg/appview/handlers/legal.go
··· 1 + package handlers 2 + 3 + import ( 4 + "html/template" 5 + "net/http" 6 + ) 7 + 8 + // PrivacyPolicyHandler handles the /privacy page 9 + type PrivacyPolicyHandler struct { 10 + Templates *template.Template 11 + RegistryURL string 12 + } 13 + 14 + func (h *PrivacyPolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 15 + data := struct { 16 + PageData 17 + }{ 18 + PageData: NewPageData(r, h.RegistryURL), 19 + } 20 + 21 + if err := h.Templates.ExecuteTemplate(w, "privacy", data); err != nil { 22 + http.Error(w, err.Error(), http.StatusInternalServerError) 23 + return 24 + } 25 + } 26 + 27 + // TermsOfServiceHandler handles the /terms page 28 + type TermsOfServiceHandler struct { 29 + Templates *template.Template 30 + RegistryURL string 31 + } 32 + 33 + func (h *TermsOfServiceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 34 + data := struct { 35 + PageData 36 + }{ 37 + PageData: NewPageData(r, h.RegistryURL), 38 + } 39 + 40 + if err := h.Templates.ExecuteTemplate(w, "terms", data); err != nil { 41 + http.Error(w, err.Error(), http.StatusInternalServerError) 42 + return 43 + } 44 + }
+1 -1
pkg/appview/handlers/opengraph.go
··· 105 105 106 106 if licenses != "" { 107 107 // Show first license if multiple 108 - license := strings.Split(licenses, ",")[0] 108 + license, _, _ := strings.Cut(licenses, ",") 109 109 license = strings.TrimSpace(license) 110 110 card.DrawBadge(license, badgeX, badgeY, ogcard.FontBadge, ogcard.ColorBadgeBg, ogcard.ColorText) 111 111 }
+17 -17
pkg/appview/handlers/repository.go
··· 89 89 continue 90 90 } 91 91 92 - wg.Add(1) 93 - go func(idx int) { 94 - defer wg.Done() 95 - 96 - endpoint := manifests[idx].HoldEndpoint 92 + wg.Go(func() { 93 + endpoint := manifests[i].HoldEndpoint 97 94 98 95 // Try to get cached status first (instant) 99 96 if cached := h.HealthChecker.GetCachedStatus(endpoint); cached != nil { 100 97 mu.Lock() 101 - manifests[idx].Reachable = cached.Reachable 102 - manifests[idx].Pending = false 98 + manifests[i].Reachable = cached.Reachable 99 + manifests[i].Pending = false 103 100 mu.Unlock() 104 101 return 105 102 } ··· 110 107 mu.Lock() 111 108 if ctx.Err() == context.DeadlineExceeded { 112 109 // Timeout - mark as pending for HTMX polling 113 - manifests[idx].Reachable = false 114 - manifests[idx].Pending = true 110 + manifests[i].Reachable = false 111 + manifests[i].Pending = true 115 112 } else if err != nil { 116 113 // Error - mark as unreachable 117 - manifests[idx].Reachable = false 118 - manifests[idx].Pending = false 114 + manifests[i].Reachable = false 115 + manifests[i].Pending = false 119 116 } else { 120 117 // Success 121 - manifests[idx].Reachable = reachable 122 - manifests[idx].Pending = false 118 + manifests[i].Reachable = reachable 119 + manifests[i].Pending = false 123 120 } 124 121 mu.Unlock() 125 - }(i) 122 + }) 126 123 } 127 124 128 125 // Wait for all checks to complete or timeout ··· 231 228 } 232 229 } 233 230 234 - // Determine dominant artifact type from manifests 231 + // Determine artifact type for header section from first tag 232 + // This is used for the "Pull this image/chart" header command 235 233 artifactType := "container-image" 236 - if len(manifests) > 0 { 237 - // Use the most recent manifest's artifact type 234 + if len(tagsWithPlatforms) > 0 { 235 + artifactType = tagsWithPlatforms[0].ArtifactType 236 + } else if len(manifests) > 0 { 237 + // Fallback to manifests if no tags 238 238 artifactType = manifests[0].ArtifactType 239 239 } 240 240
+153
pkg/appview/handlers/storage.go
··· 1 + package handlers 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "html/template" 7 + "log/slog" 8 + "net/http" 9 + 10 + "atcr.io/pkg/appview/middleware" 11 + "atcr.io/pkg/appview/storage" 12 + "atcr.io/pkg/atproto" 13 + "atcr.io/pkg/auth/oauth" 14 + ) 15 + 16 + // StorageHandler handles the storage quota API endpoint 17 + // Returns an HTML partial for HTMX to swap into the settings page 18 + type StorageHandler struct { 19 + Templates *template.Template 20 + Refresher *oauth.Refresher 21 + } 22 + 23 + // QuotaStats mirrors the hold service response 24 + type QuotaStats struct { 25 + UserDID string `json:"userDid"` 26 + UniqueBlobs int `json:"uniqueBlobs"` 27 + TotalSize int64 `json:"totalSize"` 28 + Limit *int64 `json:"limit,omitempty"` // nil = unlimited 29 + Tier string `json:"tier,omitempty"` // e.g., "deckhand", "bosun", "owner" 30 + } 31 + 32 + func (h *StorageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 33 + user := middleware.GetUser(r) 34 + if user == nil { 35 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 36 + return 37 + } 38 + 39 + // Create ATProto client with session provider 40 + client := atproto.NewClientWithSessionProvider(user.PDSEndpoint, user.DID, h.Refresher) 41 + 42 + // Get user's sailor profile to find their default hold 43 + profile, err := storage.GetProfile(r.Context(), client) 44 + if err != nil { 45 + slog.Warn("Failed to get profile for storage quota", "did", user.DID, "error", err) 46 + h.renderError(w, "Failed to load profile") 47 + return 48 + } 49 + 50 + if profile == nil || profile.DefaultHold == "" { 51 + // No default hold configured - can't check quota 52 + h.renderNoHold(w) 53 + return 54 + } 55 + 56 + // Resolve hold URL from DID 57 + holdURL := atproto.ResolveHoldURL(profile.DefaultHold) 58 + if holdURL == "" { 59 + slog.Warn("Failed to resolve hold URL", "did", user.DID, "holdDid", profile.DefaultHold) 60 + h.renderError(w, "Failed to resolve hold service") 61 + return 62 + } 63 + 64 + // Call the hold's quota endpoint 65 + quotaURL := fmt.Sprintf("%s%s?userDid=%s", holdURL, atproto.HoldGetQuota, user.DID) 66 + resp, err := http.Get(quotaURL) 67 + if err != nil { 68 + slog.Warn("Failed to fetch quota from hold", "did", user.DID, "holdURL", holdURL, "error", err) 69 + h.renderError(w, "Failed to connect to hold service") 70 + return 71 + } 72 + defer resp.Body.Close() 73 + 74 + if resp.StatusCode != http.StatusOK { 75 + slog.Warn("Hold returned error for quota", "did", user.DID, "status", resp.StatusCode) 76 + h.renderError(w, "Hold service returned an error") 77 + return 78 + } 79 + 80 + var stats QuotaStats 81 + if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil { 82 + slog.Warn("Failed to decode quota response", "did", user.DID, "error", err) 83 + h.renderError(w, "Failed to parse quota data") 84 + return 85 + } 86 + 87 + // Render the stats partial 88 + h.renderStats(w, stats) 89 + } 90 + 91 + func (h *StorageHandler) renderStats(w http.ResponseWriter, stats QuotaStats) { 92 + // Calculate usage percentage if limit exists 93 + var usagePercent int 94 + var hasLimit bool 95 + var humanLimit string 96 + 97 + if stats.Limit != nil && *stats.Limit > 0 { 98 + hasLimit = true 99 + humanLimit = humanizeBytes(*stats.Limit) 100 + usagePercent = int(float64(stats.TotalSize) / float64(*stats.Limit) * 100) 101 + if usagePercent > 100 { 102 + usagePercent = 100 103 + } 104 + } 105 + 106 + data := struct { 107 + UniqueBlobs int 108 + TotalSize int64 109 + HumanSize string 110 + HasLimit bool 111 + HumanLimit string 112 + UsagePercent int 113 + Tier string 114 + }{ 115 + UniqueBlobs: stats.UniqueBlobs, 116 + TotalSize: stats.TotalSize, 117 + HumanSize: humanizeBytes(stats.TotalSize), 118 + HasLimit: hasLimit, 119 + HumanLimit: humanLimit, 120 + UsagePercent: usagePercent, 121 + Tier: stats.Tier, 122 + } 123 + 124 + w.Header().Set("Content-Type", "text/html") 125 + if err := h.Templates.ExecuteTemplate(w, "storage_stats", data); err != nil { 126 + slog.Error("Failed to render storage stats template", "error", err) 127 + http.Error(w, "Failed to render template", http.StatusInternalServerError) 128 + } 129 + } 130 + 131 + func (h *StorageHandler) renderError(w http.ResponseWriter, message string) { 132 + w.Header().Set("Content-Type", "text/html") 133 + fmt.Fprintf(w, `<div class="storage-error"><i data-lucide="alert-circle"></i> %s</div>`, message) 134 + } 135 + 136 + func (h *StorageHandler) renderNoHold(w http.ResponseWriter) { 137 + w.Header().Set("Content-Type", "text/html") 138 + fmt.Fprint(w, `<div class="storage-info"><i data-lucide="info"></i> No hold configured. Set a default hold above to see storage usage.</div>`) 139 + } 140 + 141 + // humanizeBytes converts bytes to human-readable format 142 + func humanizeBytes(bytes int64) string { 143 + const unit = 1024 144 + if bytes < unit { 145 + return fmt.Sprintf("%d B", bytes) 146 + } 147 + div, exp := int64(unit), 0 148 + for n := bytes / unit; n >= unit; n /= unit { 149 + div *= unit 150 + exp++ 151 + } 152 + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) 153 + }
+7 -14
pkg/appview/holdhealth/worker.go
··· 53 53 54 54 // Start begins the background worker 55 55 func (w *Worker) Start(ctx context.Context) { 56 - w.wg.Add(1) 57 - go func() { 58 - defer w.wg.Done() 59 - 56 + w.wg.Go(func() { 60 57 slog.Info("Hold health worker starting background health checks") 61 58 62 59 // Wait for services to be ready (Docker startup race condition) ··· 89 86 w.checker.Cleanup() 90 87 } 91 88 } 92 - }() 89 + }) 93 90 } 94 91 95 92 // Stop gracefully stops the worker ··· 154 151 var statsMu sync.Mutex 155 152 156 153 for _, endpoint := range uniqueEndpoints { 157 - wg.Add(1) 158 - 159 - go func(ep string) { 160 - defer wg.Done() 161 - 154 + wg.Go(func() { 162 155 // Acquire semaphore 163 156 sem <- struct{}{} 164 157 defer func() { <-sem }() 165 158 166 159 // Check health 167 - isReachable, err := w.checker.CheckHealth(ctx, ep) 160 + isReachable, err := w.checker.CheckHealth(ctx, endpoint) 168 161 169 162 // Update cache 170 - w.checker.SetStatus(ep, isReachable, err) 163 + w.checker.SetStatus(endpoint, isReachable, err) 171 164 172 165 // Update stats 173 166 statsMu.Lock() ··· 175 168 reachable++ 176 169 } else { 177 170 unreachable++ 178 - slog.Warn("Hold health worker hold unreachable", "endpoint", ep, "error", err) 171 + slog.Warn("Hold health worker hold unreachable", "endpoint", endpoint, "error", err) 179 172 } 180 173 statsMu.Unlock() 181 - }(endpoint) 174 + }) 182 175 } 183 176 184 177 // Wait for all checks to complete
-13
pkg/appview/holdhealth/worker_test.go
··· 1 - package holdhealth 2 - 3 - import "testing" 4 - 5 - func TestWorker_Struct(t *testing.T) { 6 - // Simple struct test 7 - worker := &Worker{} 8 - if worker == nil { 9 - t.Error("Expected non-nil worker") 10 - } 11 - } 12 - 13 - // TODO: Add background health check tests
+1 -1
pkg/appview/jetstream/processor_test.go
··· 675 675 } 676 676 677 677 // Test 5: Process multiple deactivation events (idempotent) 678 - for i := 0; i < 3; i++ { 678 + for i := range 3 { 679 679 err = processor.ProcessAccount(context.Background(), testDID, false, "deactivated") 680 680 if err != nil { 681 681 t.Logf("Expected cache invalidation error on iteration %d: %v", i, err)
+1 -2
pkg/appview/jetstream/worker.go
··· 128 128 129 129 // Reset read deadline - we know connection is alive 130 130 // Allow 90 seconds for next pong (3x ping interval) 131 - conn.SetReadDeadline(time.Now().Add(90 * time.Second)) 132 - return nil 131 + return conn.SetReadDeadline(time.Now().Add(90 * time.Second)) 133 132 }) 134 133 135 134 // Set initial read deadline
+1 -3
pkg/appview/licenses/licenses.go
··· 129 129 licensesStr = strings.ReplaceAll(licensesStr, " OR ", ",") 130 130 licensesStr = strings.ReplaceAll(licensesStr, ";", ",") 131 131 132 - parts := strings.Split(licensesStr, ",") 133 - 134 132 var result []LicenseInfo 135 133 seen := make(map[string]bool) // Deduplicate 136 134 137 - for _, part := range parts { 135 + for part := range strings.SplitSeq(licensesStr, ",") { 138 136 part = strings.TrimSpace(part) 139 137 if part == "" { 140 138 continue
+6 -9
pkg/appview/middleware/auth_test.go
··· 318 318 // Pre-create all users and sessions before concurrent access 319 319 // This ensures database is fully initialized before goroutines start 320 320 sessionIDs := make([]string, 10) 321 - for i := 0; i < 10; i++ { 321 + for i := range 10 { 322 322 did := fmt.Sprintf("did:plc:user%d", i) 323 323 handle := fmt.Sprintf("user%d.bsky.social", i) 324 324 ··· 358 358 var wg sync.WaitGroup 359 359 var mu sync.Mutex // Protect results map 360 360 361 - for i := 0; i < 10; i++ { 362 - wg.Add(1) 363 - go func(index int, sessionID string) { 364 - defer wg.Done() 365 - 361 + for i := range results { 362 + wg.Go(func() { 366 363 req := httptest.NewRequest("GET", "/test", nil) 367 364 req.AddCookie(&http.Cookie{ 368 365 Name: "atcr_session", 369 - Value: sessionID, 366 + Value: sessionIDs[i], 370 367 }) 371 368 w := httptest.NewRecorder() 372 369 373 370 wrappedHandler.ServeHTTP(w, req) 374 371 375 372 mu.Lock() 376 - results[index] = w.Code 373 + results[i] = w.Code 377 374 mu.Unlock() 378 - }(i, sessionIDs[i]) 375 + }) 379 376 } 380 377 381 378 wg.Wait()
+1 -1
pkg/appview/middleware/registry.go
··· 555 555 556 556 // Store HTTP method in context for routing decisions 557 557 // This is used by routing_repository.go to distinguish pull (GET/HEAD) from push (PUT/POST) 558 - ctx = context.WithValue(ctx, "http.request.method", r.Method) 558 + ctx = context.WithValue(ctx, storage.HTTPRequestMethod, r.Method) 559 559 560 560 // Extract Authorization header 561 561 authHeader := r.Header.Get("Authorization")
-6
pkg/appview/middleware/registry_test.go
··· 45 45 return nil 46 46 } 47 47 48 - // mockRepository is a minimal mock implementation 49 - type mockRepository struct { 50 - distribution.Repository 51 - name string 52 - } 53 - 54 48 func TestSetGlobalRefresher(t *testing.T) { 55 49 // Test that SetGlobalRefresher doesn't panic 56 50 SetGlobalRefresher(nil)
+14 -13
pkg/appview/ogcard/card.go
··· 143 143 defer face.Close() 144 144 145 145 textWidth := font.MeasureString(face, text).Round() 146 - if align == AlignCenter { 146 + switch align { 147 + case AlignCenter: 147 148 x -= float64(textWidth) / 2 148 - } else if align == AlignRight { 149 + case AlignRight: 149 150 x -= float64(textWidth) 150 151 } 151 152 } ··· 292 293 // DrawRoundedRect draws a filled rounded rectangle 293 294 func (c *Card) DrawRoundedRect(x, y, w, h, radius int, col color.Color) { 294 295 // Draw main rectangle (without corners) 295 - for dy := radius; dy < h-radius; dy++ { 296 - for dx := 0; dx < w; dx++ { 297 - c.img.Set(x+dx, y+dy, col) 296 + for dy := range h - 2*radius { 297 + for dx := range w { 298 + c.img.Set(x+dx, y+radius+dy, col) 298 299 } 299 300 } 300 301 // Draw top and bottom strips (without corners) 301 - for dy := 0; dy < radius; dy++ { 302 - for dx := radius; dx < w-radius; dx++ { 303 - c.img.Set(x+dx, y+dy, col) 304 - c.img.Set(x+dx, y+h-1-dy, col) 302 + for dy := range radius { 303 + for dx := range w - 2*radius { 304 + c.img.Set(x+radius+dx, y+dy, col) 305 + c.img.Set(x+radius+dx, y+h-1-dy, col) 305 306 } 306 307 } 307 308 // Draw rounded corners 308 - for dy := 0; dy < radius; dy++ { 309 - for dx := 0; dx < radius; dx++ { 309 + for dy := range radius { 310 + for dx := range radius { 310 311 // Check if point is within circle 311 312 cx := radius - dx - 1 312 313 cy := radius - dy - 1 ··· 388 389 centerX := radius 389 390 centerY := radius 390 391 391 - for y := 0; y < diameter; y++ { 392 - for x := 0; x < diameter; x++ { 392 + for y := range diameter { 393 + for x := range diameter { 393 394 dx := x - centerX 394 395 dy := y - centerY 395 396 if dx*dx+dy*dy <= radius*radius {
+62 -16
pkg/appview/readme/fetcher.go
··· 1 + // Package readme provides fetching and rendering of README files from Git hosting platforms. 1 2 package readme 2 3 3 4 import ( ··· 70 71 // FetchAndRender fetches a README from a URL and renders it as HTML 71 72 // Returns the rendered HTML and any error 72 73 func (f *Fetcher) FetchAndRender(ctx context.Context, readmeURL string) (string, error) { 73 - // Validate URL 74 - if readmeURL == "" { 75 - return "", fmt.Errorf("empty README URL") 76 - } 77 - 78 - parsedURL, err := url.Parse(readmeURL) 79 - if err != nil { 80 - return "", fmt.Errorf("invalid README URL: %w", err) 81 - } 82 - 83 - // Only allow HTTP/HTTPS 84 - if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { 85 - return "", fmt.Errorf("invalid URL scheme: %s", parsedURL.Scheme) 86 - } 87 - 88 - // Fetch content 74 + // Fetch content (includes URL validation, Content-Type check, and HTML detection) 89 75 content, baseURL, err := f.fetchContent(ctx, readmeURL) 90 76 if err != nil { 91 77 return "", err ··· 100 86 return html, nil 101 87 } 102 88 89 + // FetchRaw fetches raw README content from a URL without rendering 90 + // Returns raw bytes with Content-Type and HTML validation 91 + // Use this when you need to store the raw markdown (e.g., in PDS records) 92 + func (f *Fetcher) FetchRaw(ctx context.Context, readmeURL string) ([]byte, error) { 93 + // Fetch content (includes URL validation, Content-Type check, and HTML detection) 94 + content, _, err := f.fetchContent(ctx, readmeURL) 95 + if err != nil { 96 + return nil, err 97 + } 98 + 99 + return content, nil 100 + } 101 + 103 102 // fetchContent fetches the raw content from a URL 104 103 func (f *Fetcher) fetchContent(ctx context.Context, urlStr string) ([]byte, string, error) { 104 + // Validate URL 105 + if urlStr == "" { 106 + return nil, "", fmt.Errorf("empty README URL") 107 + } 108 + 109 + parsedURL, err := url.Parse(urlStr) 110 + if err != nil { 111 + return nil, "", fmt.Errorf("invalid README URL: %w", err) 112 + } 113 + 114 + // Only allow HTTP/HTTPS 115 + if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { 116 + return nil, "", fmt.Errorf("invalid URL scheme: %s", parsedURL.Scheme) 117 + } 118 + 105 119 req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil) 106 120 if err != nil { 107 121 return nil, "", fmt.Errorf("failed to create request: %w", err) ··· 120 134 return nil, "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) 121 135 } 122 136 137 + // Reject HTML content types (catches proper error pages) 138 + contentType := resp.Header.Get("Content-Type") 139 + if contentType != "" { 140 + ct := strings.ToLower(contentType) 141 + if strings.Contains(ct, "text/html") || strings.Contains(ct, "application/xhtml") { 142 + return nil, "", fmt.Errorf("unsupported content type: %s (expected markdown or plain text)", contentType) 143 + } 144 + } 145 + 123 146 // Limit content size to 1MB 124 147 limitedReader := io.LimitReader(resp.Body, 1*1024*1024) 125 148 content, err := io.ReadAll(limitedReader) ··· 127 150 return nil, "", fmt.Errorf("failed to read response body: %w", err) 128 151 } 129 152 153 + // Detect HTML content by checking for common markers (catches soft 404s) 154 + if LooksLikeHTML(content) { 155 + return nil, "", fmt.Errorf("detected HTML content instead of markdown") 156 + } 157 + 130 158 // Get base URL for relative link resolution 131 159 baseURL := getBaseURL(resp.Request.URL) 132 160 133 161 return content, baseURL, nil 162 + } 163 + 164 + // LooksLikeHTML checks if content appears to be HTML rather than markdown 165 + // Exported for use by other packages that fetch README content 166 + func LooksLikeHTML(content []byte) bool { 167 + if len(content) == 0 { 168 + return false 169 + } 170 + 171 + // Check first 512 bytes for HTML markers 172 + checkLen := min(len(content), 512) 173 + 174 + trimmed := strings.TrimSpace(string(content[:checkLen])) 175 + lower := strings.ToLower(trimmed) 176 + 177 + return strings.HasPrefix(lower, "<!doctype") || 178 + strings.HasPrefix(lower, "<html") || 179 + strings.HasPrefix(lower, "<?xml") 134 180 } 135 181 136 182 // renderMarkdown renders markdown content to sanitized HTML
+272 -2
pkg/appview/readme/fetcher_test.go
··· 1 1 package readme 2 2 3 3 import ( 4 + "context" 5 + "net/http" 6 + "net/http/httptest" 4 7 "net/url" 8 + "strings" 5 9 "testing" 6 10 ) 7 11 ··· 297 301 } 298 302 299 303 func containsSubstringHelper(s, substr string) bool { 300 - for i := 0; i <= len(s)-len(substr); i++ { 304 + for i := range len(s) - len(substr) + 1 { 301 305 if s[i:i+len(substr)] == substr { 302 306 return true 303 307 } ··· 305 309 return false 306 310 } 307 311 308 - // TODO: Add README fetching and caching tests 312 + func TestLooksLikeHTML(t *testing.T) { 313 + tests := []struct { 314 + name string 315 + content string 316 + expected bool 317 + }{ 318 + { 319 + name: "empty content", 320 + content: "", 321 + expected: false, 322 + }, 323 + { 324 + name: "markdown content", 325 + content: "# Hello World\n\nThis is a README.", 326 + expected: false, 327 + }, 328 + { 329 + name: "plain text", 330 + content: "Just some plain text without any HTML.", 331 + expected: false, 332 + }, 333 + { 334 + name: "doctype html", 335 + content: "<!DOCTYPE html>\n<html><body>Page</body></html>", 336 + expected: true, 337 + }, 338 + { 339 + name: "doctype html lowercase", 340 + content: "<!doctype html>\n<html><body>Page</body></html>", 341 + expected: true, 342 + }, 343 + { 344 + name: "html tag only", 345 + content: "<html><head></head><body>Page</body></html>", 346 + expected: true, 347 + }, 348 + { 349 + name: "html tag with whitespace", 350 + content: " \n <html>\n<body>Page</body></html>", 351 + expected: true, 352 + }, 353 + { 354 + name: "xml declaration", 355 + content: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<html>...</html>", 356 + expected: true, 357 + }, 358 + { 359 + name: "soft 404 page", 360 + content: "<!DOCTYPE html><html><head><title>Page Not Found</title></head><body><h1>404</h1></body></html>", 361 + expected: true, 362 + }, 363 + { 364 + name: "markdown with inline html", 365 + content: "# Title\n\nSome text with <strong>bold</strong> inline.", 366 + expected: false, 367 + }, 368 + { 369 + name: "markdown starting with hash", 370 + content: "## Section\n\nContent here.", 371 + expected: false, 372 + }, 373 + } 374 + 375 + for _, tt := range tests { 376 + t.Run(tt.name, func(t *testing.T) { 377 + result := LooksLikeHTML([]byte(tt.content)) 378 + if result != tt.expected { 379 + t.Errorf("looksLikeHTML(%q) = %v, want %v", tt.content, result, tt.expected) 380 + } 381 + }) 382 + } 383 + } 384 + 385 + func TestFetcher_FetchRaw(t *testing.T) { 386 + fetcher := NewFetcher() 387 + 388 + tests := []struct { 389 + name string 390 + handler http.HandlerFunc 391 + wantErr bool 392 + errContains string 393 + wantContent string 394 + }{ 395 + { 396 + name: "successful markdown fetch", 397 + handler: func(w http.ResponseWriter, r *http.Request) { 398 + w.Header().Set("Content-Type", "text/plain") 399 + w.Write([]byte("# Hello World\n\nThis is markdown.")) 400 + }, 401 + wantErr: false, 402 + wantContent: "# Hello World", 403 + }, 404 + { 405 + name: "rejects HTML content type", 406 + handler: func(w http.ResponseWriter, r *http.Request) { 407 + w.Header().Set("Content-Type", "text/html; charset=utf-8") 408 + w.Write([]byte("<html><body>Error</body></html>")) 409 + }, 410 + wantErr: true, 411 + errContains: "unsupported content type", 412 + }, 413 + { 414 + name: "rejects soft 404 HTML content", 415 + handler: func(w http.ResponseWriter, r *http.Request) { 416 + w.Header().Set("Content-Type", "text/plain") 417 + w.Write([]byte("<!DOCTYPE html><html><body>404 Not Found</body></html>")) 418 + }, 419 + wantErr: true, 420 + errContains: "detected HTML content", 421 + }, 422 + { 423 + name: "rejects 404 status", 424 + handler: func(w http.ResponseWriter, r *http.Request) { 425 + w.WriteHeader(http.StatusNotFound) 426 + w.Write([]byte("Not Found")) 427 + }, 428 + wantErr: true, 429 + errContains: "unexpected status code: 404", 430 + }, 431 + { 432 + name: "rejects 500 status", 433 + handler: func(w http.ResponseWriter, r *http.Request) { 434 + w.WriteHeader(http.StatusInternalServerError) 435 + w.Write([]byte("Internal Server Error")) 436 + }, 437 + wantErr: true, 438 + errContains: "unexpected status code: 500", 439 + }, 440 + } 441 + 442 + for _, tt := range tests { 443 + t.Run(tt.name, func(t *testing.T) { 444 + server := httptest.NewServer(tt.handler) 445 + defer server.Close() 446 + 447 + content, err := fetcher.FetchRaw(context.Background(), server.URL) 448 + 449 + if tt.wantErr { 450 + if err == nil { 451 + t.Errorf("FetchRaw() expected error containing %q, got nil", tt.errContains) 452 + return 453 + } 454 + if !strings.Contains(err.Error(), tt.errContains) { 455 + t.Errorf("FetchRaw() error = %q, want error containing %q", err.Error(), tt.errContains) 456 + } 457 + return 458 + } 459 + 460 + if err != nil { 461 + t.Errorf("FetchRaw() unexpected error: %v", err) 462 + return 463 + } 464 + 465 + if !strings.Contains(string(content), tt.wantContent) { 466 + t.Errorf("FetchRaw() content = %q, want content containing %q", string(content), tt.wantContent) 467 + } 468 + }) 469 + } 470 + } 471 + 472 + func TestFetcher_FetchRaw_URLValidation(t *testing.T) { 473 + fetcher := NewFetcher() 474 + 475 + tests := []struct { 476 + name string 477 + url string 478 + errContains string 479 + }{ 480 + { 481 + name: "empty URL", 482 + url: "", 483 + errContains: "empty README URL", 484 + }, 485 + { 486 + name: "invalid URL scheme", 487 + url: "ftp://example.com/README.md", 488 + errContains: "invalid URL scheme", 489 + }, 490 + { 491 + name: "file URL scheme", 492 + url: "file:///etc/passwd", 493 + errContains: "invalid URL scheme", 494 + }, 495 + } 496 + 497 + for _, tt := range tests { 498 + t.Run(tt.name, func(t *testing.T) { 499 + _, err := fetcher.FetchRaw(context.Background(), tt.url) 500 + if err == nil { 501 + t.Errorf("FetchRaw(%q) expected error, got nil", tt.url) 502 + return 503 + } 504 + if !strings.Contains(err.Error(), tt.errContains) { 505 + t.Errorf("FetchRaw(%q) error = %q, want error containing %q", tt.url, err.Error(), tt.errContains) 506 + } 507 + }) 508 + } 509 + } 510 + 511 + func TestFetcher_FetchAndRender(t *testing.T) { 512 + fetcher := NewFetcher() 513 + 514 + tests := []struct { 515 + name string 516 + handler http.HandlerFunc 517 + wantErr bool 518 + errContains string 519 + wantContain string 520 + }{ 521 + { 522 + name: "renders markdown to HTML", 523 + handler: func(w http.ResponseWriter, r *http.Request) { 524 + w.Header().Set("Content-Type", "text/plain") 525 + w.Write([]byte("# Hello World\n\nThis is **bold** text.")) 526 + }, 527 + wantErr: false, 528 + wantContain: "<strong>bold</strong>", 529 + }, 530 + { 531 + name: "rejects HTML content type", 532 + handler: func(w http.ResponseWriter, r *http.Request) { 533 + w.Header().Set("Content-Type", "text/html") 534 + w.Write([]byte("<html><body>Error</body></html>")) 535 + }, 536 + wantErr: true, 537 + errContains: "unsupported content type", 538 + }, 539 + { 540 + name: "rejects soft 404", 541 + handler: func(w http.ResponseWriter, r *http.Request) { 542 + w.Header().Set("Content-Type", "text/plain") 543 + w.Write([]byte("<!doctype html><html><body>Not Found</body></html>")) 544 + }, 545 + wantErr: true, 546 + errContains: "detected HTML content", 547 + }, 548 + } 549 + 550 + for _, tt := range tests { 551 + t.Run(tt.name, func(t *testing.T) { 552 + server := httptest.NewServer(tt.handler) 553 + defer server.Close() 554 + 555 + html, err := fetcher.FetchAndRender(context.Background(), server.URL) 556 + 557 + if tt.wantErr { 558 + if err == nil { 559 + t.Errorf("FetchAndRender() expected error containing %q, got nil", tt.errContains) 560 + return 561 + } 562 + if !strings.Contains(err.Error(), tt.errContains) { 563 + t.Errorf("FetchAndRender() error = %q, want error containing %q", err.Error(), tt.errContains) 564 + } 565 + return 566 + } 567 + 568 + if err != nil { 569 + t.Errorf("FetchAndRender() unexpected error: %v", err) 570 + return 571 + } 572 + 573 + if !strings.Contains(html, tt.wantContain) { 574 + t.Errorf("FetchAndRender() = %q, want HTML containing %q", html, tt.wantContain) 575 + } 576 + }) 577 + } 578 + }
+20
pkg/appview/routes/routes.go
··· 87 87 }, 88 88 ).ServeHTTP) 89 89 90 + // Legal pages (public) 91 + router.Get("/privacy", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 92 + &uihandlers.PrivacyPolicyHandler{ 93 + Templates: deps.Templates, 94 + RegistryURL: registryURL, 95 + }, 96 + ).ServeHTTP) 97 + 98 + router.Get("/terms", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 99 + &uihandlers.TermsOfServiceHandler{ 100 + Templates: deps.Templates, 101 + RegistryURL: registryURL, 102 + }, 103 + ).ServeHTTP) 104 + 90 105 // API route for repository stats (public, read-only) 91 106 router.Get("/api/stats/{handle}/{repository}", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 92 107 &uihandlers.GetStatsHandler{ ··· 172 187 Templates: deps.Templates, 173 188 Refresher: deps.Refresher, 174 189 RegistryURL: registryURL, 190 + }).ServeHTTP) 191 + 192 + r.Get("/api/storage", (&uihandlers.StorageHandler{ 193 + Templates: deps.Templates, 194 + Refresher: deps.Refresher, 175 195 }).ServeHTTP) 176 196 177 197 r.Post("/api/profile/default-hold", (&uihandlers.UpdateDefaultHoldHandler{
+151
pkg/appview/static/css/style.css
··· 1569 1569 margin-left: 0.5rem; 1570 1570 } 1571 1571 1572 + /* Helm chart badge */ 1573 + .badge-helm { 1574 + display: inline-flex; 1575 + align-items: center; 1576 + gap: 0.25rem; 1577 + padding: 0.25rem 0.6rem; 1578 + font-size: 0.75rem; 1579 + font-weight: 600; 1580 + border-radius: 12px; 1581 + background: #0f1689; 1582 + color: #fff; 1583 + white-space: nowrap; 1584 + margin-left: 0.5rem; 1585 + } 1586 + 1587 + .badge-helm svg { 1588 + width: 12px; 1589 + height: 12px; 1590 + } 1591 + 1572 1592 .platform-badge { 1573 1593 display: inline-flex; 1574 1594 align-items: center; ··· 2457 2477 background-color: rgba(13, 108, 191, 0.15); 2458 2478 color: #0d6cbf; 2459 2479 } 2480 + 2481 + /* Legal Pages (Privacy Policy, Terms of Service) */ 2482 + .legal-page { 2483 + max-width: 800px; 2484 + margin: 0 auto; 2485 + padding: 2rem 1rem; 2486 + } 2487 + 2488 + .legal-page h1 { 2489 + font-size: 2rem; 2490 + margin-bottom: 0.5rem; 2491 + color: var(--fg); 2492 + } 2493 + 2494 + .legal-updated { 2495 + color: var(--secondary); 2496 + margin-bottom: 2rem; 2497 + } 2498 + 2499 + .legal-section { 2500 + margin: 2rem 0; 2501 + padding-bottom: 1.5rem; 2502 + border-bottom: 1px solid var(--border); 2503 + } 2504 + 2505 + .legal-section:last-child { 2506 + border-bottom: none; 2507 + } 2508 + 2509 + .legal-section h2 { 2510 + font-size: 1.5rem; 2511 + margin-bottom: 1rem; 2512 + color: var(--fg); 2513 + } 2514 + 2515 + .legal-section h3 { 2516 + font-size: 1.15rem; 2517 + margin: 1.5rem 0 0.75rem; 2518 + color: var(--fg); 2519 + } 2520 + 2521 + .legal-section p { 2522 + margin-bottom: 1rem; 2523 + line-height: 1.7; 2524 + } 2525 + 2526 + .legal-section ul, 2527 + .legal-section ol { 2528 + margin-bottom: 1rem; 2529 + padding-left: 2rem; 2530 + } 2531 + 2532 + .legal-section li { 2533 + margin-bottom: 0.5rem; 2534 + line-height: 1.6; 2535 + } 2536 + 2537 + .legal-section ul ul { 2538 + margin-top: 0.5rem; 2539 + margin-bottom: 0.5rem; 2540 + } 2541 + 2542 + .legal-section code { 2543 + background: var(--code-bg); 2544 + padding: 0.2rem 0.4rem; 2545 + border-radius: 3px; 2546 + font-family: "Monaco", "Menlo", monospace; 2547 + font-size: 0.9em; 2548 + } 2549 + 2550 + .legal-section a { 2551 + color: var(--primary); 2552 + text-decoration: underline; 2553 + } 2554 + 2555 + .legal-section a:hover { 2556 + color: var(--primary-dark); 2557 + } 2558 + 2559 + .legal-section table { 2560 + width: 100%; 2561 + border-collapse: collapse; 2562 + margin: 1rem 0; 2563 + } 2564 + 2565 + .legal-section table th, 2566 + .legal-section table td { 2567 + padding: 0.75rem 1rem; 2568 + border: 1px solid var(--border); 2569 + text-align: left; 2570 + } 2571 + 2572 + .legal-section table th { 2573 + background: var(--code-bg); 2574 + font-weight: 600; 2575 + } 2576 + 2577 + .legal-section table tr:nth-child(even) { 2578 + background: var(--hover-bg); 2579 + } 2580 + 2581 + .legal-disclaimer { 2582 + background: var(--code-bg); 2583 + padding: 1rem; 2584 + border-radius: 4px; 2585 + font-size: 0.95rem; 2586 + margin: 1rem 0; 2587 + } 2588 + 2589 + @media (max-width: 768px) { 2590 + .legal-page { 2591 + padding: 1rem 0.5rem; 2592 + } 2593 + 2594 + .legal-page h1 { 2595 + font-size: 1.5rem; 2596 + } 2597 + 2598 + .legal-section h2 { 2599 + font-size: 1.25rem; 2600 + } 2601 + 2602 + .legal-section table { 2603 + font-size: 0.85rem; 2604 + } 2605 + 2606 + .legal-section table th, 2607 + .legal-section table td { 2608 + padding: 0.5rem; 2609 + } 2610 + }
-6
pkg/appview/storage/context_test.go
··· 17 17 return m.holdDID, nil 18 18 } 19 19 20 - type mockHoldAuthorizer struct{} 21 - 22 - func (m *mockHoldAuthorizer) Authorize(holdDID, userDID, permission string) (bool, error) { 23 - return true, nil 24 - } 25 - 26 20 func TestRegistryContext_Fields(t *testing.T) { 27 21 // Create a sample RegistryContext 28 22 ctx := &RegistryContext{
+62 -74
pkg/appview/storage/manifest_store.go
··· 76 76 // Notify hold about manifest pull (for stats tracking) 77 77 // Only count GET requests (actual downloads), not HEAD requests (existence checks) 78 78 // Check HTTP method from context (distribution library stores it as "http.request.method") 79 - if method, ok := ctx.Value("http.request.method").(string); ok && method == "GET" { 79 + if method, ok := ctx.Value(HTTPRequestMethod).(string); ok && method == "GET" { 80 80 // Do this asynchronously to avoid blocking the response 81 81 if s.ctx.ServiceToken != "" && s.ctx.Handle != "" { 82 82 go func() { ··· 90 90 } 91 91 }() 92 92 } 93 + } 94 + 95 + // Determine media type - prefer record field, fallback to blob mimeType for old records 96 + mediaType := manifestRecord.MediaType 97 + if mediaType == "" && manifestRecord.ManifestBlob != nil { 98 + mediaType = manifestRecord.ManifestBlob.MimeType 93 99 } 94 100 95 101 // Parse the manifest based on media type 96 102 // For now, we'll return the raw bytes wrapped in a manifest object 97 103 // In a full implementation, you'd use distribution's manifest parsing 98 104 return &rawManifest{ 99 - mediaType: manifestRecord.MediaType, 105 + mediaType: mediaType, 100 106 payload: ociManifest, 101 107 }, nil 102 108 } ··· 122 128 manifestRecord, err := atproto.NewManifestRecord(s.ctx.Repository, dgst.String(), payload) 123 129 if err != nil { 124 130 return "", fmt.Errorf("failed to create manifest record: %w", err) 131 + } 132 + 133 + // OCI spec allows omitting mediaType from the manifest body (inferred from Content-Type header) 134 + // Helm charts typically omit it, so use the media type from the request if body is empty 135 + if manifestRecord.MediaType == "" && mediaType != "" { 136 + manifestRecord.MediaType = mediaType 125 137 } 126 138 127 139 // Set the blob reference, hold DID, and hold endpoint ··· 212 224 213 225 // Notify hold about manifest push (for layer tracking, Bluesky posts, and stats) 214 226 // Do this asynchronously to avoid blocking the push 215 - if tag != "" && s.ctx.ServiceToken != "" && s.ctx.Handle != "" { 227 + // Note: We notify even for tagless pushes (e.g., buildx platform manifests) to create layer records 228 + // Bluesky posts are only created for tagged pushes (handled by hold service) 229 + if s.ctx.ServiceToken != "" && s.ctx.Handle != "" { 216 230 go func() { 217 231 defer func() { 218 232 if r := recover(); r != nil { ··· 313 327 serviceToken := s.ctx.ServiceToken 314 328 315 329 // Build notification request 330 + // Note: userHandle is resolved from userDid on the hold side (cached, 24-hour TTL) 316 331 notifyReq := map[string]any{ 317 - "repository": s.ctx.Repository, 318 - "userDid": s.ctx.DID, 319 - "userHandle": s.ctx.Handle, 320 - "operation": operation, 332 + "repository": s.ctx.Repository, 333 + "userDid": s.ctx.DID, 334 + "manifestDigest": manifestDigest, 335 + "operation": operation, 321 336 } 322 337 323 338 // For push operations, include full manifest data ··· 412 427 return nil 413 428 } 414 429 415 - // ensureRepoPage creates or updates a repo page record in the user's PDS if needed 430 + // ensureRepoPage creates or updates a repo page record in the user's PDS 416 431 // This syncs repository metadata from manifest annotations to the io.atcr.repo.page collection 417 - // Only creates a new record if one doesn't exist (doesn't overwrite user's custom content) 432 + // Always updates the description on push (since users can't edit it via appview yet) 433 + // Preserves user's avatar if they've set one via the appview 418 434 func (s *ManifestStore) ensureRepoPage(ctx context.Context, manifestRecord *atproto.ManifestRecord) { 419 - // Check if repo page already exists (don't overwrite user's custom content) 420 435 rkey := s.ctx.Repository 421 - _, err := s.ctx.ATProtoClient.GetRecord(ctx, atproto.RepoPageCollection, rkey) 422 - if err == nil { 423 - // Record already exists - don't overwrite 424 - slog.Debug("Repo page already exists, skipping creation", "did", s.ctx.DID, "repository", s.ctx.Repository) 425 - return 426 - } 427 436 428 - // Only continue if it's a "not found" error - other errors mean we should skip 429 - if !errors.Is(err, atproto.ErrRecordNotFound) { 437 + // Check for existing record to preserve user's avatar 438 + var existingAvatarRef *atproto.ATProtoBlobRef 439 + var existingRecord *atproto.RepoPageRecord 440 + record, err := s.ctx.ATProtoClient.GetRecord(ctx, atproto.RepoPageCollection, rkey) 441 + if err == nil && record != nil { 442 + // Unmarshal the Value to get the RepoPageRecord 443 + var repoPage atproto.RepoPageRecord 444 + if unmarshalErr := json.Unmarshal(record.Value, &repoPage); unmarshalErr == nil { 445 + existingRecord = &repoPage 446 + existingAvatarRef = repoPage.Avatar 447 + slog.Debug("Found existing repo page, will update", "did", s.ctx.DID, "repository", s.ctx.Repository, "hasExistingAvatar", existingAvatarRef != nil) 448 + } else { 449 + slog.Warn("Failed to unmarshal existing repo page", "did", s.ctx.DID, "repository", s.ctx.Repository, "error", unmarshalErr) 450 + } 451 + } else if err != nil && !errors.Is(err, atproto.ErrRecordNotFound) { 452 + // Unexpected error - log and continue (will create new record) 430 453 slog.Warn("Failed to check for existing repo page", "did", s.ctx.DID, "repository", s.ctx.Repository, "error", err) 431 - return 432 454 } 433 455 434 456 // Get annotations (may be nil if image has no OCI labels) ··· 446 468 description = annotations["org.opencontainers.image.description"] 447 469 } 448 470 449 - // Try to fetch and upload icon from io.atcr.icon annotation 450 - var avatarRef *atproto.ATProtoBlobRef 471 + // Determine avatar: prefer new icon from annotations, otherwise keep existing 472 + avatarRef := existingAvatarRef 451 473 if iconURL := annotations["io.atcr.icon"]; iconURL != "" { 452 - avatarRef = s.fetchAndUploadIcon(ctx, iconURL) 474 + if newAvatar := s.fetchAndUploadIcon(ctx, iconURL); newAvatar != nil { 475 + avatarRef = newAvatar 476 + } 453 477 } 454 478 455 - // Create new repo page record with description and optional avatar 479 + // Create/update repo page record with description and avatar 456 480 repoPage := atproto.NewRepoPageRecord(s.ctx.Repository, description, avatarRef) 457 481 458 - slog.Info("Creating repo page from manifest annotations", "did", s.ctx.DID, "repository", s.ctx.Repository, "descriptionLength", len(description), "hasAvatar", avatarRef != nil) 482 + isUpdate := existingRecord != nil 483 + action := "Creating" 484 + if isUpdate { 485 + action = "Updating" 486 + } 487 + slog.Info(action+" repo page from manifest annotations", "did", s.ctx.DID, "repository", s.ctx.Repository, "descriptionLength", len(description), "hasAvatar", avatarRef != nil) 459 488 460 489 _, err = s.ctx.ATProtoClient.PutRecord(ctx, atproto.RepoPageCollection, rkey, repoPage) 461 490 if err != nil { 462 - slog.Warn("Failed to create repo page", "did", s.ctx.DID, "repository", s.ctx.Repository, "error", err) 491 + slog.Warn("Failed to "+strings.ToLower(action)+" repo page", "did", s.ctx.DID, "repository", s.ctx.Repository, "error", err) 463 492 return 464 493 } 465 494 466 - slog.Info("Repo page created successfully", "did", s.ctx.DID, "repository", s.ctx.Repository) 495 + slog.Info("Repo page "+strings.ToLower(action)+"d successfully", "did", s.ctx.DID, "repository", s.ctx.Repository) 467 496 } 468 497 469 498 // fetchReadmeContent attempts to fetch README content from external sources 470 499 // Priority: io.atcr.readme annotation > derived from org.opencontainers.image.source 471 500 // Returns the raw markdown content, or empty string if not available 501 + // Uses the shared readme.Fetcher which validates Content-Type and rejects HTML content 472 502 func (s *ManifestStore) fetchReadmeContent(ctx context.Context, annotations map[string]string) string { 473 503 if s.ctx.ReadmeFetcher == nil { 474 504 return "" ··· 480 510 481 511 // Priority 1: Direct README URL from io.atcr.readme annotation 482 512 if readmeURL := annotations["io.atcr.readme"]; readmeURL != "" { 483 - content, err := s.fetchRawReadme(fetchCtx, readmeURL) 513 + content, err := s.ctx.ReadmeFetcher.FetchRaw(fetchCtx, readmeURL) 484 514 if err != nil { 485 515 slog.Debug("Failed to fetch README from io.atcr.readme annotation", "url", readmeURL, "error", err) 486 - } else if content != "" { 516 + } else if len(content) > 0 { 487 517 slog.Info("Fetched README from io.atcr.readme annotation", "url", readmeURL, "length", len(content)) 488 - return content 518 + return string(content) 489 519 } 490 520 } 491 521 ··· 498 528 continue 499 529 } 500 530 501 - content, err := s.fetchRawReadme(fetchCtx, readmeURL) 531 + content, err := s.ctx.ReadmeFetcher.FetchRaw(fetchCtx, readmeURL) 502 532 if err != nil { 503 533 // Only log non-404 errors (404 is expected when trying main vs master) 504 534 if !readme.Is404(err) { ··· 507 537 continue 508 538 } 509 539 510 - if content != "" { 540 + if len(content) > 0 { 511 541 slog.Info("Fetched README from source URL", "sourceURL", sourceURL, "branch", branch, "length", len(content)) 512 - return content 542 + return string(content) 513 543 } 514 544 } 515 545 } 516 546 517 547 return "" 518 - } 519 - 520 - // fetchRawReadme fetches raw markdown content from a URL 521 - // Returns the raw markdown (not rendered HTML) for storage in the repo page record 522 - func (s *ManifestStore) fetchRawReadme(ctx context.Context, readmeURL string) (string, error) { 523 - // Use a simple HTTP client to fetch raw content 524 - // We want raw markdown, not rendered HTML (the Fetcher renders to HTML) 525 - req, err := http.NewRequestWithContext(ctx, "GET", readmeURL, nil) 526 - if err != nil { 527 - return "", fmt.Errorf("failed to create request: %w", err) 528 - } 529 - 530 - req.Header.Set("User-Agent", "ATCR-README-Fetcher/1.0") 531 - 532 - client := &http.Client{ 533 - Timeout: 10 * time.Second, 534 - CheckRedirect: func(req *http.Request, via []*http.Request) error { 535 - if len(via) >= 5 { 536 - return fmt.Errorf("too many redirects") 537 - } 538 - return nil 539 - }, 540 - } 541 - 542 - resp, err := client.Do(req) 543 - if err != nil { 544 - return "", fmt.Errorf("failed to fetch URL: %w", err) 545 - } 546 - defer resp.Body.Close() 547 - 548 - if resp.StatusCode != http.StatusOK { 549 - return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) 550 - } 551 - 552 - // Limit content size to 100KB (repo page description has 100KB limit in lexicon) 553 - limitedReader := io.LimitReader(resp.Body, 100*1024) 554 - content, err := io.ReadAll(limitedReader) 555 - if err != nil { 556 - return "", fmt.Errorf("failed to read response body: %w", err) 557 - } 558 - 559 - return string(content), nil 560 548 } 561 549 562 550 // fetchAndUploadIcon fetches an image from a URL and uploads it as a blob to the user's PDS
+3 -5
pkg/appview/storage/profile_test.go
··· 340 340 341 341 // Make 5 concurrent GetProfile calls 342 342 var wg sync.WaitGroup 343 - for i := 0; i < 5; i++ { 344 - wg.Add(1) 345 - go func() { 346 - defer wg.Done() 343 + for range 5 { 344 + wg.Go(func() { 347 345 _, err := GetProfile(context.Background(), client) 348 346 if err != nil { 349 347 t.Errorf("GetProfile() error = %v", err) 350 348 } 351 - }() 349 + }) 352 350 } 353 351 354 352 wg.Wait()
+6 -5
pkg/appview/storage/proxy_blob_store.go
··· 552 552 } 553 553 554 554 // abortMultipartUpload aborts a multipart upload via XRPC abortUpload endpoint 555 - func (p *ProxyBlobStore) abortMultipartUpload(ctx context.Context, digest, uploadID string) error { 555 + func (p *ProxyBlobStore) abortMultipartUpload(ctx context.Context, uploadID string) error { 556 556 reqBody := map[string]any{ 557 557 "uploadId": uploadID, 558 558 } ··· 760 760 slog.Debug("Flushing final buffer", "component", "proxy_blob_store/Commit", "bytes", w.buffer.Len()) 761 761 if err := w.flushPart(); err != nil { 762 762 // Try to abort multipart on error 763 - tempDigest := fmt.Sprintf("uploads/temp-%s", w.id) 764 - w.store.abortMultipartUpload(ctx, tempDigest, w.uploadID) 763 + if err := w.store.abortMultipartUpload(ctx, w.uploadID); err != nil { 764 + slog.Warn("Failed to abort multipart upload", "component", "proxy_blob_store/Cancel", "error", err) 765 + // Continue anyway - we want to mark upload as cancelled 766 + } 765 767 return distribution.Descriptor{}, fmt.Errorf("failed to flush final part: %w", err) 766 768 } 767 769 } ··· 794 796 globalUploadsMu.Unlock() 795 797 796 798 // Abort multipart upload 797 - tempDigest := fmt.Sprintf("uploads/temp-%s", w.id) 798 - if err := w.store.abortMultipartUpload(ctx, tempDigest, w.uploadID); err != nil { 799 + if err := w.store.abortMultipartUpload(ctx, w.uploadID); err != nil { 799 800 slog.Warn("Failed to abort multipart upload", "component", "proxy_blob_store/Cancel", "error", err) 800 801 // Continue anyway - we want to mark upload as cancelled 801 802 }
+1 -1
pkg/appview/storage/proxy_blob_store_test.go
··· 563 563 { 564 564 name: "abortMultipartUpload", 565 565 testFunc: func(store *ProxyBlobStore) error { 566 - return store.abortMultipartUpload(context.Background(), "sha256:test", "upload-123") 566 + return store.abortMultipartUpload(context.Background(), "upload-123") 567 567 }, 568 568 expectedPath: atproto.HoldAbortUpload, 569 569 },
+46 -45
pkg/appview/storage/routing_repository.go
··· 7 7 import ( 8 8 "context" 9 9 "log/slog" 10 + "sync" 10 11 11 12 "github.com/distribution/distribution/v3" 12 13 ) 13 14 15 + type contextKey string 16 + 17 + const HTTPRequestMethod contextKey = "http.request.method" 18 + 14 19 // RoutingRepository routes manifests to ATProto and blobs to external hold service 15 20 // The registry (AppView) is stateless and NEVER stores blobs locally 16 21 // NOTE: A fresh instance is created per-request (see middleware/registry.go) 17 - // so no mutex is needed - each request has its own instance 18 22 type RoutingRepository struct { 19 23 distribution.Repository 20 - Ctx *RegistryContext // All context and services (exported for token updates) 21 - manifestStore *ManifestStore // Manifest store instance (lazy-initialized) 22 - blobStore *ProxyBlobStore // Blob store instance (lazy-initialized) 24 + Ctx *RegistryContext // All context and services (exported for token updates) 25 + manifestStore *ManifestStore // Manifest store instance (lazy-initialized) 26 + manifestStoreOnce sync.Once // Ensures thread-safe lazy initialization 27 + blobStore *ProxyBlobStore // Blob store instance (lazy-initialized) 28 + blobStoreOnce sync.Once // Ensures thread-safe lazy initialization 23 29 } 24 30 25 31 // NewRoutingRepository creates a new routing repository ··· 32 38 33 39 // Manifests returns the ATProto-backed manifest service 34 40 func (r *RoutingRepository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { 35 - // Lazy-initialize manifest store (no mutex needed - one instance per request) 36 - if r.manifestStore == nil { 41 + r.manifestStoreOnce.Do(func() { 37 42 // Ensure blob store is created first (needed for label extraction during push) 38 43 blobStore := r.Blobs(ctx) 39 44 r.manifestStore = NewManifestStore(r.Ctx, blobStore) 40 - } 45 + }) 41 46 return r.manifestStore, nil 42 47 } 43 48 44 49 // Blobs returns a proxy blob store that routes to external hold service 45 50 // The registry (AppView) NEVER stores blobs locally - all blobs go through hold service 46 51 func (r *RoutingRepository) Blobs(ctx context.Context) distribution.BlobStore { 47 - // Return cached blob store if available (no mutex needed - one instance per request) 48 - if r.blobStore != nil { 49 - slog.Debug("Returning cached blob store", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository) 50 - return r.blobStore 51 - } 52 + r.blobStoreOnce.Do(func() { 53 + // Determine if this is a pull (GET/HEAD) or push (PUT/POST/etc) operation 54 + // Pull operations use the historical hold DID from the database (blobs are where they were pushed) 55 + // Push operations use the discovery-based hold DID from user's profile/default 56 + // This allows users to change their default hold and have new pushes go there 57 + isPull := false 58 + if method, ok := ctx.Value(HTTPRequestMethod).(string); ok { 59 + isPull = method == "GET" || method == "HEAD" 60 + } 52 61 53 - // Determine if this is a pull (GET/HEAD) or push (PUT/POST/etc) operation 54 - // Pull operations use the historical hold DID from the database (blobs are where they were pushed) 55 - // Push operations use the discovery-based hold DID from user's profile/default 56 - // This allows users to change their default hold and have new pushes go there 57 - isPull := false 58 - if method, ok := ctx.Value("http.request.method").(string); ok { 59 - isPull = method == "GET" || method == "HEAD" 60 - } 62 + holdDID := r.Ctx.HoldDID // Default to discovery-based DID 63 + holdSource := "discovery" 61 64 62 - holdDID := r.Ctx.HoldDID // Default to discovery-based DID 63 - holdSource := "discovery" 65 + // Only query database for pull operations 66 + if isPull && r.Ctx.Database != nil { 67 + // Query database for the latest manifest's hold DID 68 + if dbHoldDID, err := r.Ctx.Database.GetLatestHoldDIDForRepo(r.Ctx.DID, r.Ctx.Repository); err == nil && dbHoldDID != "" { 69 + // Use hold DID from database (pull case - use historical reference) 70 + holdDID = dbHoldDID 71 + holdSource = "database" 72 + slog.Debug("Using hold from database manifest (pull)", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", dbHoldDID) 73 + } else if err != nil { 74 + // Log error but don't fail - fall back to discovery-based DID 75 + slog.Warn("Failed to query database for hold DID", "component", "storage/blobs", "error", err) 76 + } 77 + // If dbHoldDID is empty (no manifests yet), fall through to use discovery-based DID 78 + } 64 79 65 - // Only query database for pull operations 66 - if isPull && r.Ctx.Database != nil { 67 - // Query database for the latest manifest's hold DID 68 - if dbHoldDID, err := r.Ctx.Database.GetLatestHoldDIDForRepo(r.Ctx.DID, r.Ctx.Repository); err == nil && dbHoldDID != "" { 69 - // Use hold DID from database (pull case - use historical reference) 70 - holdDID = dbHoldDID 71 - holdSource = "database" 72 - slog.Debug("Using hold from database manifest (pull)", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", dbHoldDID) 73 - } else if err != nil { 74 - // Log error but don't fail - fall back to discovery-based DID 75 - slog.Warn("Failed to query database for hold DID", "component", "storage/blobs", "error", err) 80 + if holdDID == "" { 81 + // This should never happen if middleware is configured correctly 82 + panic("hold DID not set in RegistryContext - ensure default_hold_did is configured in middleware") 76 83 } 77 - // If dbHoldDID is empty (no manifests yet), fall through to use discovery-based DID 78 - } 79 - 80 - if holdDID == "" { 81 - // This should never happen if middleware is configured correctly 82 - panic("hold DID not set in RegistryContext - ensure default_hold_did is configured in middleware") 83 - } 84 84 85 - slog.Debug("Using hold DID for blobs", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", holdDID, "source", holdSource) 85 + slog.Debug("Using hold DID for blobs", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", holdDID, "source", holdSource) 86 86 87 - // Update context with the correct hold DID (may be from database or discovered) 88 - r.Ctx.HoldDID = holdDID 87 + // Update context with the correct hold DID (may be from database or discovered) 88 + r.Ctx.HoldDID = holdDID 89 89 90 - // Create and cache proxy blob store 91 - r.blobStore = NewProxyBlobStore(r.Ctx) 90 + // Create and cache proxy blob store 91 + r.blobStore = NewProxyBlobStore(r.Ctx) 92 + }) 92 93 return r.blobStore 93 94 } 94 95
+11 -15
pkg/appview/storage/routing_repository_test.go
··· 126 126 } 127 127 repo := NewRoutingRepository(nil, ctx) 128 128 129 - pullCtx := context.WithValue(context.Background(), "http.request.method", method) 129 + pullCtx := context.WithValue(context.Background(), HTTPRequestMethod, method) 130 130 blobStore := repo.Blobs(pullCtx) 131 131 132 132 assert.NotNil(t, blobStore) ··· 164 164 repo := NewRoutingRepository(nil, ctx) 165 165 166 166 // Create context with push method 167 - pushCtx := context.WithValue(context.Background(), "http.request.method", tc.method) 167 + pushCtx := context.WithValue(context.Background(), HTTPRequestMethod, tc.method) 168 168 blobStore := repo.Blobs(pushCtx) 169 169 170 170 assert.NotNil(t, blobStore) ··· 318 318 319 319 // Concurrent access to Manifests() 320 320 for i := 0; i < numGoroutines; i++ { 321 - wg.Add(1) 322 - go func(index int) { 323 - defer wg.Done() 321 + wg.Go(func() { 324 322 store, err := repo.Manifests(context.Background()) 325 323 require.NoError(t, err) 326 - manifestStores[index] = store 327 - }(i) 324 + manifestStores[i] = store 325 + }) 328 326 } 329 327 330 328 wg.Wait() 331 329 332 330 // Verify all stores are non-nil (due to race conditions, they may not all be the same instance) 333 - for i := 0; i < numGoroutines; i++ { 331 + for i := range numGoroutines { 334 332 assert.NotNil(t, manifestStores[i], "manifest store should not be nil") 335 333 } 336 334 ··· 341 339 342 340 // Concurrent access to Blobs() 343 341 for i := 0; i < numGoroutines; i++ { 344 - wg.Add(1) 345 - go func(index int) { 346 - defer wg.Done() 347 - blobStores[index] = repo.Blobs(context.Background()) 348 - }(i) 342 + wg.Go(func() { 343 + blobStores[i] = repo.Blobs(context.Background()) 344 + }) 349 345 } 350 346 351 347 wg.Wait() 352 348 353 349 // Verify all stores are non-nil (due to race conditions, they may not all be the same instance) 354 - for i := 0; i < numGoroutines; i++ { 350 + for i := range numGoroutines { 355 351 assert.NotNil(t, blobStores[i], "blob store should not be nil") 356 352 } 357 353 ··· 376 372 repo := NewRoutingRepository(nil, ctx) 377 373 378 374 // For pull (GET), database should take priority 379 - pullCtx := context.WithValue(context.Background(), "http.request.method", "GET") 375 + pullCtx := context.WithValue(context.Background(), HTTPRequestMethod, "GET") 380 376 blobStore := repo.Blobs(pullCtx) 381 377 382 378 assert.NotNil(t, blobStore)
+266
pkg/appview/templates/pages/privacy.html
··· 1 + {{ define "privacy" }} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <title>Privacy Policy - ATCR</title> 6 + {{ template "head" . }} 7 + </head> 8 + <body> 9 + {{ template "nav" . }} 10 + 11 + <main class="container"> 12 + <div class="legal-page"> 13 + <h1>Privacy Policy - AT Container Registry (atcr.io)</h1> 14 + <p class="legal-updated"><em>Last updated: January 2025</em></p> 15 + 16 + <div class="legal-section"> 17 + <h2>Data We Collect and Store</h2> 18 + 19 + <h3>Data Stored on Your PDS (Controlled by You)</h3> 20 + <p>When you use AT Container Registry, records are written to your Personal Data Server (PDS) under the <code>io.atcr.*</code> namespace. This data is stored on infrastructure you or your PDS hosting provider controls. We do not control this data, and its retention and deletion is governed by your PDS provider's policies.</p> 21 + 22 + <h3>Data Stored on Our Infrastructure</h3> 23 + 24 + <p><strong>Layer Records:</strong> We maintain records on our own PDS that reference container image layers you publish. These records are public and link your AT Protocol identity (DID) to content-addressed SHA identifiers.</p> 25 + 26 + <p><strong>OCI Blobs:</strong> Container image layers are stored in our object storage (S3). These blobs are content-addressed and deduplicatedโ€”meaning identical layers uploaded by different users are stored only once.</p> 27 + 28 + <p><strong>Authentication Data:</strong></p> 29 + <ul> 30 + <li>OAuth tokens obtained during sign-in</li> 31 + <li>Web UI session tokens</li> 32 + <li>Docker credential helper device tokens, including: 33 + <ul> 34 + <li>IP address</li> 35 + <li>Device name</li> 36 + <li>Token creation and last-used timestamps</li> 37 + </ul> 38 + </li> 39 + </ul> 40 + 41 + <p><strong>Cached PDS Data:</strong> We may cache data from your PDS in our database to improve performance and reduce load on your PDS. This cached data mirrors public information already stored on your PDS.</p> 42 + 43 + <p><strong>Server Logs:</strong> Our logs may include your handle, DID, IP address, timestamps, and actions performed. Logs are currently ephemeral but may be retained in the future for security and debugging purposes.</p> 44 + </div> 45 + 46 + <div class="legal-section"> 47 + <h2>Data Sharing and Deduplication</h2> 48 + 49 + <p>OCI container images use content-addressable storage. When you push an image layer, it is identified by its cryptographic hash (SHA256). If another user pushes an identical layer, both users reference the same underlying blob. This is standard practice for container registries and enables efficient storage and distribution.</p> 50 + 51 + <p><strong>What this means for your data:</strong></p> 52 + <ul> 53 + <li>Layer content is not uniquely "yours" if other users have pushed identical content</li> 54 + <li>Public SHA references may be associated with your AT Protocol identity</li> 55 + <li>Deleting your records does not delete blob data that other users also reference</li> 56 + </ul> 57 + </div> 58 + 59 + <div class="legal-section"> 60 + <h2>Your Rights Under GDPR</h2> 61 + 62 + <p>If you are located in the European Economic Area (EEA), you have the following rights:</p> 63 + 64 + <h3>Right to Access</h3> 65 + <p>You may request a copy of all personal data we store about you. This includes:</p> 66 + <ul> 67 + <li>Layer records associated with your DID on our PDS</li> 68 + <li>Server logs containing your handle, DID, IP address, or actions (if retained)</li> 69 + <li>OAuth tokens, web UI sessions, and device tokens</li> 70 + <li>Cached PDS data</li> 71 + <li>List of registered devices (credential helper)</li> 72 + </ul> 73 + <p class="note">Note: Data stored on your own PDS is already under your control and accessible to you directly.</p> 74 + 75 + <h3>Right to Erasure ("Right to be Forgotten")</h3> 76 + <p>You may request deletion of your data via the account settings page. Due to our technical architecture, deletion works as follows:</p> 77 + 78 + <p><strong>Immediately deleted:</strong></p> 79 + <ul> 80 + <li>Layer records on our PDS that reference your DID</li> 81 + <li>OAuth tokens, web UI sessions, and device tokens</li> 82 + <li>Cached PDS data</li> 83 + <li>Server logs containing your identifiers (deleted or anonymized, if retained)</li> 84 + </ul> 85 + 86 + <p><strong>Deleted within 30 days:</strong></p> 87 + <ul> 88 + <li>OCI blobs in our object storage that are no longer referenced by any user after your records are removed (via our orphan blob pruning process)</li> 89 + </ul> 90 + 91 + <p><strong>Cannot be deleted by us:</strong></p> 92 + <ul> 93 + <li>Records stored on your own PDS (you control these, or your PDS provider does)</li> 94 + <li>Blob data that is also referenced by other users (deduplicated content)</li> 95 + </ul> 96 + 97 + <p><strong>Optional: Delete AT Protocol Records</strong></p> 98 + <p>When deleting your account, you may optionally authorize us to delete <code>io.atcr.*</code> records from your PDS. This requires an active OAuth session and is optional because:</p> 99 + <ul> 100 + <li>Your PDS is controlled by you or your hosting provider, not us</li> 101 + <li>You may delete these records yourself at any time</li> 102 + <li>We have no ongoing obligation to manage data on infrastructure we do not control</li> 103 + </ul> 104 + 105 + <h3>Right to Rectification</h3> 106 + <p>You may update your data through normal use of the service. Data stored on your PDS is under your direct control.</p> 107 + 108 + <h3>Right to Data Portability</h3> 109 + <p>AT Protocol is designed for data portability. Your records are stored in an open, documented format on your PDS and can be exported or migrated at any time.</p> 110 + 111 + <h3>Right to Object / Restrict Processing</h3> 112 + <p>You may revoke our OAuth access at any time through your PDS provider's settings. This will prevent us from reading or writing records to your PDS.</p> 113 + </div> 114 + 115 + <div class="legal-section"> 116 + <h2>Your Rights Under CCPA</h2> 117 + 118 + <p>If you are a California resident, you have the following rights under the California Consumer Privacy Act:</p> 119 + 120 + <h3>Right to Know</h3> 121 + <p>You may request disclosure of:</p> 122 + <ul> 123 + <li>The categories of personal information we collect</li> 124 + <li>The purposes for which we use your personal information</li> 125 + <li>The categories of third parties with whom we share your personal information</li> 126 + </ul> 127 + 128 + <h3>Right to Delete</h3> 129 + <p>You may delete your personal information via the account settings page, subject to the same technical limitations described in the GDPR section above. For data not accessible through self-service, we will respond to requests within 45 days, except where retention is necessary for:</p> 130 + <ul> 131 + <li>Completing the transaction for which the data was collected</li> 132 + <li>Security and fraud prevention</li> 133 + <li>Legal compliance</li> 134 + </ul> 135 + 136 + <h3>Right to Non-Discrimination</h3> 137 + <p>We will not discriminate against you for exercising your CCPA rights.</p> 138 + 139 + <h3>Categories of Personal Information Collected</h3> 140 + <table> 141 + <thead> 142 + <tr> 143 + <th>Category</th> 144 + <th>Examples</th> 145 + <th>Collected</th> 146 + </tr> 147 + </thead> 148 + <tbody> 149 + <tr> 150 + <td>Identifiers</td> 151 + <td>DID, handle, IP address, device name</td> 152 + <td>Yes</td> 153 + </tr> 154 + <tr> 155 + <td>Internet activity</td> 156 + <td>Access logs, usage data, actions performed</td> 157 + <td>Yes</td> 158 + </tr> 159 + <tr> 160 + <td>Geolocation</td> 161 + <td>Approximate location via IP</td> 162 + <td>Yes</td> 163 + </tr> 164 + </tbody> 165 + </table> 166 + 167 + <p>We do not sell or share your personal information for cross-context behavioral advertising.</p> 168 + </div> 169 + 170 + <div class="legal-section"> 171 + <h2>Data Retention</h2> 172 + 173 + <table> 174 + <thead> 175 + <tr> 176 + <th>Data Type</th> 177 + <th>Retention Period</th> 178 + </tr> 179 + </thead> 180 + <tbody> 181 + <tr> 182 + <td>OAuth tokens</td> 183 + <td>Until revoked or logout</td> 184 + </tr> 185 + <tr> 186 + <td>Web UI session tokens</td> 187 + <td>Until logout or expiration</td> 188 + </tr> 189 + <tr> 190 + <td>Device tokens (credential helper)</td> 191 + <td>Until revoked by user</td> 192 + </tr> 193 + <tr> 194 + <td>Cached PDS data</td> 195 + <td>Refreshed periodically; deleted on account deletion</td> 196 + </tr> 197 + <tr> 198 + <td>Server logs</td> 199 + <td>Currently ephemeral; this policy will be updated if log retention is implemented</td> 200 + </tr> 201 + <tr> 202 + <td>Layer records (our PDS)</td> 203 + <td>Until you request deletion</td> 204 + </tr> 205 + <tr> 206 + <td>OCI blobs</td> 207 + <td>Until no longer referenced (pruned monthly)</td> 208 + </tr> 209 + </tbody> 210 + </table> 211 + </div> 212 + 213 + <div class="legal-section"> 214 + <h2>Important Notes on AT Protocol Architecture</h2> 215 + 216 + <p>AT Container Registry is built on the AT Protocol, which has a unique data architecture:</p> 217 + 218 + <ol> 219 + <li><strong>You control your data.</strong> Most data associated with your use of AT Container Registry is stored on your Personal Data Server (PDS), which you or your chosen provider controls.</li> 220 + <li><strong>Public by design.</strong> AT Protocol data is designed to be public and distributed. Records you create, including container image references, are publicly visible and may be replicated across the network.</li> 221 + <li><strong>Content-addressed storage.</strong> OCI blobs are identified by their cryptographic hash. This means blob data is inherently pseudonymousโ€”it cannot be attributed to you without the corresponding records that reference it.</li> 222 + <li><strong>Deletion limitations.</strong> Because AT Protocol is distributed, we cannot guarantee that copies of public records have not been made by other participants in the network. We can only delete data on infrastructure we control.</li> 223 + </ol> 224 + </div> 225 + 226 + <div class="legal-section"> 227 + <h2>How to Exercise Your Rights</h2> 228 + 229 + <h3>Self-Service (via Settings)</h3> 230 + <p>Most data management can be done directly through your account settings at atcr.io:</p> 231 + <ul> 232 + <li><strong>Delete your data:</strong> Use the "Delete Account" button in settings. This will remove your layer records, cached data, and authentication tokens. You may also choose to have us delete <code>io.atcr.*</code> records from your PDS (requires active OAuth session).</li> 233 + <li><strong>Revoke device tokens:</strong> Manage and revoke credential helper devices in settings.</li> 234 + <li><strong>Update your data:</strong> Corrections happen through normal use of the service.</li> 235 + </ul> 236 + 237 + <h3>Contact Us</h3> 238 + <p>For requests we cannot fulfill through self-service, such as:</p> 239 + <ul> 240 + <li>Copies of server logs containing your data</li> 241 + <li>Database records not exposed in the UI</li> 242 + <li>Questions about this policy</li> 243 + </ul> 244 + 245 + <p><strong>Email:</strong> <a href="mailto:privacy@atcr.io">privacy@atcr.io</a></p> 246 + 247 + <p>Please include your AT Protocol DID or handle so we can verify your identity.</p> 248 + 249 + <p>We will respond to requests within 30 days (GDPR) or 45 days (CCPA).</p> 250 + </div> 251 + 252 + <div class="legal-section"> 253 + <h2>Contact</h2> 254 + 255 + <p>For questions about this privacy policy or to exercise your data rights, contact:</p> 256 + 257 + <p><strong>Email:</strong> <a href="mailto:privacy@atcr.io">privacy@atcr.io</a></p> 258 + <p><strong>Website:</strong> <a href="https://atcr.io">https://atcr.io</a></p> 259 + </div> 260 + </div> 261 + </main> 262 + 263 + <div id="modal"></div> 264 + </body> 265 + </html> 266 + {{ end }}
+4 -2
pkg/appview/templates/pages/repository.html
··· 146 146 <div class="tag-item-header"> 147 147 <div> 148 148 <span class="tag-name-large">{{ .Tag.Tag }}</span> 149 - {{ if .IsMultiArch }} 149 + {{ if eq .ArtifactType "helm-chart" }} 150 + <span class="badge-helm"><i data-lucide="anchor"></i> Helm</span> 151 + {{ else if .IsMultiArch }} 150 152 <span class="badge-multi">Multi-arch</span> 151 153 {{ end }} 152 154 {{ if .HasAttestations }} ··· 183 185 {{ end }} 184 186 </div> 185 187 </div> 186 - {{ if eq $.ArtifactType "helm-chart" }} 188 + {{ if eq .ArtifactType "helm-chart" }} 187 189 {{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name " --version " .Tag.Tag) }} 188 190 {{ else }} 189 191 {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" .Tag.Tag) }}
+124
pkg/appview/templates/pages/settings.html
··· 29 29 </div> 30 30 </section> 31 31 32 + <!-- Storage Usage Section --> 33 + <section class="settings-section storage-section"> 34 + <h2>Stowage</h2> 35 + <p>Estimated storage usage on your default hold.</p> 36 + <div id="storage-stats" hx-get="/api/storage" hx-trigger="load" hx-swap="innerHTML"> 37 + <p><i data-lucide="loader-2" class="spin"></i> Loading...</p> 38 + </div> 39 + </section> 40 + 32 41 <!-- Default Hold Section --> 33 42 <section class="settings-section"> 34 43 <h2>Default Hold</h2> ··· 200 209 </script> 201 210 202 211 <style> 212 + /* Storage Section Styles */ 213 + .storage-section .storage-stats { 214 + background: var(--code-bg); 215 + padding: 1rem; 216 + border-radius: 4px; 217 + margin-top: 0.5rem; 218 + } 219 + .storage-section .stat-row { 220 + display: flex; 221 + justify-content: space-between; 222 + padding: 0.5rem 0; 223 + border-bottom: 1px solid var(--border); 224 + } 225 + .storage-section .stat-row:last-child { 226 + border-bottom: none; 227 + } 228 + .storage-section .stat-label { 229 + color: var(--fg-muted); 230 + } 231 + .storage-section .stat-value { 232 + font-weight: bold; 233 + font-family: monospace; 234 + } 235 + .storage-section .storage-error, 236 + .storage-section .storage-info { 237 + padding: 1rem; 238 + border-radius: 4px; 239 + margin-top: 0.5rem; 240 + display: flex; 241 + align-items: center; 242 + gap: 0.5rem; 243 + } 244 + .storage-section .storage-error { 245 + background: var(--error-bg, #fef2f2); 246 + color: var(--error, #dc2626); 247 + border: 1px solid var(--error, #dc2626); 248 + } 249 + .storage-section .storage-info { 250 + background: var(--info-bg, #eff6ff); 251 + color: var(--info, #2563eb); 252 + border: 1px solid var(--info, #2563eb); 253 + } 254 + .spin { 255 + animation: spin 1s linear infinite; 256 + } 257 + @keyframes spin { 258 + from { transform: rotate(0deg); } 259 + to { transform: rotate(360deg); } 260 + } 261 + 262 + /* Quota Progress Bar */ 263 + .storage-section .quota-progress { 264 + display: flex; 265 + align-items: center; 266 + gap: 0.75rem; 267 + padding: 0.75rem 0; 268 + } 269 + .storage-section .progress-bar { 270 + flex: 1; 271 + height: 8px; 272 + background: var(--border); 273 + border-radius: 4px; 274 + overflow: hidden; 275 + } 276 + .storage-section .progress-fill { 277 + height: 100%; 278 + border-radius: 4px; 279 + transition: width 0.3s ease; 280 + } 281 + .storage-section .progress-ok { 282 + background: #22c55e; 283 + } 284 + .storage-section .progress-warning { 285 + background: #eab308; 286 + } 287 + .storage-section .progress-danger { 288 + background: #ef4444; 289 + } 290 + .storage-section .progress-text { 291 + font-size: 0.85rem; 292 + color: var(--fg-muted); 293 + white-space: nowrap; 294 + } 295 + 296 + /* Tier Badge */ 297 + .storage-section .tier-badge { 298 + text-transform: capitalize; 299 + padding: 0.125rem 0.5rem; 300 + border-radius: 4px; 301 + font-size: 0.85rem; 302 + background: var(--accent-bg, #e0f2fe); 303 + color: var(--accent, #0369a1); 304 + } 305 + .storage-section .tier-owner { 306 + background: #fef3c7; 307 + color: #92400e; 308 + } 309 + .storage-section .tier-quartermaster { 310 + background: #dcfce7; 311 + color: #166534; 312 + } 313 + .storage-section .tier-bosun { 314 + background: #e0e7ff; 315 + color: #3730a3; 316 + } 317 + .storage-section .unlimited-badge { 318 + font-size: 0.75rem; 319 + padding: 0.125rem 0.375rem; 320 + background: #22c55e; 321 + color: #fff; 322 + border-radius: 3px; 323 + margin-left: 0.25rem; 324 + font-weight: 500; 325 + } 326 + 203 327 /* Devices Section Styles */ 204 328 .devices-section .setup-instructions { 205 329 margin: 1rem 0;
+204
pkg/appview/templates/pages/terms.html
··· 1 + {{ define "terms" }} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <title>Terms of Service - ATCR</title> 6 + {{ template "head" . }} 7 + </head> 8 + <body> 9 + {{ template "nav" . }} 10 + 11 + <main class="container"> 12 + <div class="legal-page"> 13 + <h1>Terms of Service - AT Container Registry (atcr.io)</h1> 14 + <p class="legal-updated"><em>Last updated: January 2025</em></p> 15 + 16 + <p>These Terms of Service ("Terms") govern your use of AT Container Registry ("atcr.io", "the Service", "we", "us", "our"). By using the Service, you agree to these Terms. If you do not agree, do not use the Service.</p> 17 + 18 + <div class="legal-section"> 19 + <h2>1. Description of Service</h2> 20 + <p>AT Container Registry is an OCI-compatible container registry built on the AT Protocol. The Service allows you to store, distribute, and manage container images using your AT Protocol identity.</p> 21 + </div> 22 + 23 + <div class="legal-section"> 24 + <h2>2. Accounts and Access</h2> 25 + 26 + <h3>2.1 AT Protocol Identity</h3> 27 + <p>Access to the Service requires an AT Protocol identity (DID). You are responsible for maintaining the security of your PDS credentials and any device tokens issued by the Service.</p> 28 + 29 + <h3>2.2 Account Responsibility</h3> 30 + <p>You are responsible for all activity under your account, whether authorized by you or not. Notify us immediately if you suspect unauthorized access.</p> 31 + 32 + <h3>2.3 Age Requirement</h3> 33 + <p>You must be at least 13 years old to use the Service. If you are under 18, you must have permission from a parent or legal guardian.</p> 34 + </div> 35 + 36 + <div class="legal-section"> 37 + <h2>3. Acceptable Use</h2> 38 + 39 + <p>You agree NOT to use the Service to:</p> 40 + <ul> 41 + <li>Store or distribute malware, viruses, or malicious code</li> 42 + <li>Store or distribute illegal content under applicable law</li> 43 + <li>Infringe on intellectual property rights of others</li> 44 + <li>Store content you do not have the right to distribute</li> 45 + <li>Circumvent or abuse usage quotas or rate limits</li> 46 + <li>Interfere with or disrupt the Service or its infrastructure</li> 47 + <li>Use the Service for cryptocurrency mining or similarly resource-intensive activities unrelated to container distribution</li> 48 + <li>Impersonate others or misrepresent your affiliation with any person or entity</li> 49 + </ul> 50 + 51 + <p>We reserve the right to determine what constitutes a violation of these terms.</p> 52 + </div> 53 + 54 + <div class="legal-section"> 55 + <h2>4. Content and Intellectual Property</h2> 56 + 57 + <h3>4.1 Your Content</h3> 58 + <p>You retain ownership of container images and other content you upload to the Service. By uploading content, you grant us a license to store, cache, and distribute that content as necessary to operate the Service.</p> 59 + 60 + <h3>4.2 Content-Addressed Storage</h3> 61 + <p>The Service uses content-addressed, deduplicated storage. Identical image layers uploaded by different users are stored once and shared. This means:</p> 62 + <ul> 63 + <li>You cannot delete blob data that is also referenced by other users</li> 64 + <li>Blob data alone cannot identify you; only the associated records link content to your identity</li> 65 + </ul> 66 + 67 + <h3>4.3 Public Data</h3> 68 + <p>AT Protocol records are public by design. Container image references, tags, and metadata associated with your identity are publicly visible. Do not store sensitive information in image tags, labels, or other public metadata.</p> 69 + 70 + <h3>4.4 Content Removal</h3> 71 + <p>We may remove content that violates these Terms or applicable law. We may also remove content in response to valid legal requests.</p> 72 + </div> 73 + 74 + <div class="legal-section"> 75 + <h2>5. Usage Quotas and Limits</h2> 76 + 77 + <h3>5.1 Free Tier</h3> 78 + <p>The Service offers a free tier subject to usage quotas. These quotas may include limits on storage, bandwidth, or number of repositories. Current quotas are published in your account settings.</p> 79 + 80 + <h3>5.2 Quota Changes</h3> 81 + <p>We may adjust free tier quotas at any time. We will make reasonable efforts to notify users of significant changes, but continued use after changes constitutes acceptance.</p> 82 + 83 + <h3>5.3 Paid Tiers</h3> 84 + <p>Paid tiers with higher quotas may be offered in the future. Paid tier terms will be provided at the time of purchase.</p> 85 + </div> 86 + 87 + <div class="legal-section"> 88 + <h2>6. Service Availability</h2> 89 + 90 + <h3>6.1 No SLA for Free Tier</h3> 91 + <p>The free tier is provided on a best-effort basis. We make no guarantees regarding uptime, availability, or performance. The Service may be unavailable due to maintenance, infrastructure issues, or resource constraints.</p> 92 + 93 + <h3>6.2 Service Changes</h3> 94 + <p>We may modify, suspend, or discontinue the Service (or any part of it) at any time, with or without notice. We are not liable to you or any third party for any modification, suspension, or discontinuation.</p> 95 + 96 + <h3>6.3 Data Durability</h3> 97 + <p>While we take reasonable measures to protect your data, we do not guarantee data durability. You are responsible for maintaining backups of your container images.</p> 98 + </div> 99 + 100 + <div class="legal-section"> 101 + <h2>7. AT Protocol Considerations</h2> 102 + 103 + <h3>7.1 Distributed Architecture</h3> 104 + <p>The Service operates on the AT Protocol, a distributed network. Data written to your PDS is controlled by you or your PDS hosting provider, not by us.</p> 105 + 106 + <h3>7.2 Records on Your PDS</h3> 107 + <p>When you use the Service, records are written to your PDS under the <code>io.atcr.*</code> namespace. We can create, update, and delete these records only while you have granted us OAuth access. Revoking access does not automatically delete existing records.</p> 108 + 109 + <h3>7.3 No Control Over Your PDS</h3> 110 + <p>We do not control your PDS. If your PDS is offline or your PDS provider terminates your account, we cannot restore your AT Protocol records.</p> 111 + </div> 112 + 113 + <div class="legal-section"> 114 + <h2>8. Termination</h2> 115 + 116 + <h3>8.1 By You</h3> 117 + <p>You may stop using the Service at any time. To delete your data, use the account deletion option in settings or contact us.</p> 118 + 119 + <h3>8.2 By Us</h3> 120 + <p>We may suspend or terminate your access to the Service at any time, for any reason, including but not limited to:</p> 121 + <ul> 122 + <li>Violation of these Terms</li> 123 + <li>Abuse of the Service or its infrastructure</li> 124 + <li>Extended inactivity</li> 125 + <li>Legal requirements</li> 126 + </ul> 127 + 128 + <h3>8.3 Effect of Termination</h3> 129 + <p>Upon termination, we will delete your data in accordance with our Privacy Policy. Deduplicated blob data referenced by other users will not be deleted.</p> 130 + </div> 131 + 132 + <div class="legal-section"> 133 + <h2>9. Disclaimer of Warranties</h2> 134 + 135 + <p class="legal-disclaimer">THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.</p> 136 + 137 + <p class="legal-disclaimer">WE DO NOT WARRANT THAT THE SERVICE WILL BE UNINTERRUPTED, ERROR-FREE, OR SECURE. WE DO NOT WARRANT THAT ANY DEFECTS WILL BE CORRECTED.</p> 138 + </div> 139 + 140 + <div class="legal-section"> 141 + <h2>10. Limitation of Liability</h2> 142 + 143 + <p class="legal-disclaimer">TO THE MAXIMUM EXTENT PERMITTED BY LAW, WE SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, DATA, OR GOODWILL, ARISING OUT OF OR RELATED TO YOUR USE OF THE SERVICE.</p> 144 + 145 + <p class="legal-disclaimer">OUR TOTAL LIABILITY FOR ANY CLAIM ARISING OUT OF OR RELATED TO THESE TERMS OR THE SERVICE SHALL NOT EXCEED THE AMOUNT YOU PAID US IN THE TWELVE (12) MONTHS PRECEDING THE CLAIM, OR $50 USD, WHICHEVER IS GREATER.</p> 146 + </div> 147 + 148 + <div class="legal-section"> 149 + <h2>11. Indemnification</h2> 150 + 151 + <p>You agree to indemnify and hold harmless AT Container Registry, its operators, and affiliates from any claims, damages, losses, or expenses (including reasonable legal fees) arising out of:</p> 152 + <ul> 153 + <li>Your use of the Service</li> 154 + <li>Your violation of these Terms</li> 155 + <li>Your violation of any third-party rights</li> 156 + <li>Content you upload to the Service</li> 157 + </ul> 158 + </div> 159 + 160 + <div class="legal-section"> 161 + <h2>12. Changes to Terms</h2> 162 + 163 + <p>We may update these Terms from time to time. If we make material changes, we will notify you by posting the updated Terms and updating the "Last updated" date. Your continued use of the Service after changes constitutes acceptance of the new Terms.</p> 164 + </div> 165 + 166 + <div class="legal-section"> 167 + <h2>13. Governing Law</h2> 168 + 169 + <p>These Terms shall be governed by and construed in accordance with the laws of the State of Texas, United States, without regard to conflict of law principles.</p> 170 + </div> 171 + 172 + <div class="legal-section"> 173 + <h2>14. Dispute Resolution</h2> 174 + 175 + <p>Any disputes arising out of or relating to these Terms or the Service shall first be attempted to be resolved through good-faith negotiation. If negotiation fails, disputes shall be resolved through binding arbitration or in the courts of Texas, at our discretion.</p> 176 + </div> 177 + 178 + <div class="legal-section"> 179 + <h2>15. Severability</h2> 180 + 181 + <p>If any provision of these Terms is found to be unenforceable, the remaining provisions shall remain in full force and effect.</p> 182 + </div> 183 + 184 + <div class="legal-section"> 185 + <h2>16. Entire Agreement</h2> 186 + 187 + <p>These Terms, together with our <a href="/privacy">Privacy Policy</a>, constitute the entire agreement between you and AT Container Registry regarding your use of the Service.</p> 188 + </div> 189 + 190 + <div class="legal-section"> 191 + <h2>Contact</h2> 192 + 193 + <p>For questions about these Terms, contact us at:</p> 194 + 195 + <p><strong>Email:</strong> <a href="mailto:legal@atcr.io">legal@atcr.io</a></p> 196 + <p><strong>Website:</strong> <a href="https://atcr.io">https://atcr.io</a></p> 197 + </div> 198 + </div> 199 + </main> 200 + 201 + <div id="modal"></div> 202 + </body> 203 + </html> 204 + {{ end }}
+32
pkg/appview/templates/partials/storage_stats.html
··· 1 + {{ define "storage_stats" }} 2 + <div class="storage-stats"> 3 + {{ if .Tier }} 4 + <div class="stat-row"> 5 + <span class="stat-label">Tier:</span> 6 + <span class="stat-value tier-badge tier-{{ .Tier }}">{{ .Tier }}</span> 7 + </div> 8 + {{ end }} 9 + <div class="stat-row"> 10 + <span class="stat-label">Storage:</span> 11 + <span class="stat-value"> 12 + {{ if .HasLimit }} 13 + {{ .HumanSize }} / {{ .HumanLimit }} 14 + {{ else }} 15 + {{ .HumanSize }} <span class="unlimited-badge">Unlimited</span> 16 + {{ end }} 17 + </span> 18 + </div> 19 + {{ if .HasLimit }} 20 + <div class="quota-progress"> 21 + <div class="progress-bar"> 22 + <div class="progress-fill {{ if ge .UsagePercent 95 }}progress-danger{{ else if ge .UsagePercent 80 }}progress-warning{{ else }}progress-ok{{ end }}" style="width: {{ .UsagePercent }}%"></div> 23 + </div> 24 + <span class="progress-text">{{ .UsagePercent }}% used</span> 25 + </div> 26 + {{ end }} 27 + <div class="stat-row"> 28 + <span class="stat-label">Unique Blobs:</span> 29 + <span class="stat-value">{{ .UniqueBlobs }}</span> 30 + </div> 31 + </div> 32 + {{ end }}
+77 -69
pkg/atproto/cbor_gen.go
··· 25 25 } 26 26 27 27 cw := cbg.NewCborWriter(w) 28 + fieldCount := 6 28 29 29 - if _, err := cw.Write([]byte{165}); err != nil { 30 + if t.Tier == "" { 31 + fieldCount-- 32 + } 33 + 34 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 30 35 return err 31 36 } 32 37 ··· 51 56 } 52 57 if _, err := cw.WriteString(string(t.Role)); err != nil { 53 58 return err 59 + } 60 + 61 + // t.Tier (string) (string) 62 + if t.Tier != "" { 63 + 64 + if len("tier") > 8192 { 65 + return xerrors.Errorf("Value in field \"tier\" was too long") 66 + } 67 + 68 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("tier"))); err != nil { 69 + return err 70 + } 71 + if _, err := cw.WriteString(string("tier")); err != nil { 72 + return err 73 + } 74 + 75 + if len(t.Tier) > 8192 { 76 + return xerrors.Errorf("Value in field t.Tier was too long") 77 + } 78 + 79 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Tier))); err != nil { 80 + return err 81 + } 82 + if _, err := cw.WriteString(string(t.Tier)); err != nil { 83 + return err 84 + } 54 85 } 55 86 56 87 // t.Type (string) (string) ··· 208 239 } 209 240 210 241 t.Role = string(sval) 242 + } 243 + // t.Tier (string) (string) 244 + case "tier": 245 + 246 + { 247 + sval, err := cbg.ReadStringWithMax(cr, 8192) 248 + if err != nil { 249 + return err 250 + } 251 + 252 + t.Tier = string(sval) 211 253 } 212 254 // t.Type (string) (string) 213 255 case "$type": ··· 654 696 655 697 cw := cbg.NewCborWriter(w) 656 698 657 - if _, err := cw.Write([]byte{168}); err != nil { 699 + if _, err := cw.Write([]byte{167}); err != nil { 658 700 return err 659 701 } 660 702 ··· 749 791 return err 750 792 } 751 793 794 + // t.Manifest (string) (string) 795 + if len("manifest") > 8192 { 796 + return xerrors.Errorf("Value in field \"manifest\" was too long") 797 + } 798 + 799 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("manifest"))); err != nil { 800 + return err 801 + } 802 + if _, err := cw.WriteString(string("manifest")); err != nil { 803 + return err 804 + } 805 + 806 + if len(t.Manifest) > 8192 { 807 + return xerrors.Errorf("Value in field t.Manifest was too long") 808 + } 809 + 810 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Manifest))); err != nil { 811 + return err 812 + } 813 + if _, err := cw.WriteString(string(t.Manifest)); err != nil { 814 + return err 815 + } 816 + 752 817 // t.CreatedAt (string) (string) 753 818 if len("createdAt") > 8192 { 754 819 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 794 859 if _, err := cw.WriteString(string(t.MediaType)); err != nil { 795 860 return err 796 861 } 797 - 798 - // t.Repository (string) (string) 799 - if len("repository") > 8192 { 800 - return xerrors.Errorf("Value in field \"repository\" was too long") 801 - } 802 - 803 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repository"))); err != nil { 804 - return err 805 - } 806 - if _, err := cw.WriteString(string("repository")); err != nil { 807 - return err 808 - } 809 - 810 - if len(t.Repository) > 8192 { 811 - return xerrors.Errorf("Value in field t.Repository was too long") 812 - } 813 - 814 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repository))); err != nil { 815 - return err 816 - } 817 - if _, err := cw.WriteString(string(t.Repository)); err != nil { 818 - return err 819 - } 820 - 821 - // t.UserHandle (string) (string) 822 - if len("userHandle") > 8192 { 823 - return xerrors.Errorf("Value in field \"userHandle\" was too long") 824 - } 825 - 826 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("userHandle"))); err != nil { 827 - return err 828 - } 829 - if _, err := cw.WriteString(string("userHandle")); err != nil { 830 - return err 831 - } 832 - 833 - if len(t.UserHandle) > 8192 { 834 - return xerrors.Errorf("Value in field t.UserHandle was too long") 835 - } 836 - 837 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.UserHandle))); err != nil { 838 - return err 839 - } 840 - if _, err := cw.WriteString(string(t.UserHandle)); err != nil { 841 - return err 842 - } 843 862 return nil 844 863 } 845 864 ··· 868 887 869 888 n := extra 870 889 871 - nameBuf := make([]byte, 10) 890 + nameBuf := make([]byte, 9) 872 891 for i := uint64(0); i < n; i++ { 873 892 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 874 893 if err != nil { ··· 943 962 944 963 t.UserDID = string(sval) 945 964 } 946 - // t.CreatedAt (string) (string) 947 - case "createdAt": 948 - 949 - { 950 - sval, err := cbg.ReadStringWithMax(cr, 8192) 951 - if err != nil { 952 - return err 953 - } 954 - 955 - t.CreatedAt = string(sval) 956 - } 957 - // t.MediaType (string) (string) 958 - case "mediaType": 965 + // t.Manifest (string) (string) 966 + case "manifest": 959 967 960 968 { 961 969 sval, err := cbg.ReadStringWithMax(cr, 8192) ··· 963 971 return err 964 972 } 965 973 966 - t.MediaType = string(sval) 974 + t.Manifest = string(sval) 967 975 } 968 - // t.Repository (string) (string) 969 - case "repository": 976 + // t.CreatedAt (string) (string) 977 + case "createdAt": 970 978 971 979 { 972 980 sval, err := cbg.ReadStringWithMax(cr, 8192) ··· 974 982 return err 975 983 } 976 984 977 - t.Repository = string(sval) 985 + t.CreatedAt = string(sval) 978 986 } 979 - // t.UserHandle (string) (string) 980 - case "userHandle": 987 + // t.MediaType (string) (string) 988 + case "mediaType": 981 989 982 990 { 983 991 sval, err := cbg.ReadStringWithMax(cr, 8192) ··· 985 993 return err 986 994 } 987 995 988 - t.UserHandle = string(sval) 996 + t.MediaType = string(sval) 989 997 } 990 998 991 999 default:
+11 -15
pkg/atproto/directory_test.go
··· 29 29 t.Run("concurrent access is thread-safe", func(t *testing.T) { 30 30 const numGoroutines = 100 31 31 var wg sync.WaitGroup 32 - wg.Add(numGoroutines) 33 32 34 33 // Channel to collect all directory instances 35 - instances := make(chan interface{}, numGoroutines) 34 + instances := make(chan any, numGoroutines) 36 35 37 36 // Launch many goroutines concurrently accessing GetDirectory 38 - for i := 0; i < numGoroutines; i++ { 39 - go func() { 40 - defer wg.Done() 37 + for range numGoroutines { 38 + wg.Go(func() { 41 39 dir := GetDirectory() 42 40 instances <- dir 43 - }() 41 + }) 44 42 } 45 43 46 44 // Wait for all goroutines to complete ··· 48 46 close(instances) 49 47 50 48 // Collect all instances 51 - var dirs []interface{} 49 + var dirs []any 52 50 for dir := range instances { 53 51 dirs = append(dirs, dir) 54 52 } ··· 72 70 func TestGetDirectorySequential(t *testing.T) { 73 71 t.Run("multiple calls in sequence", func(t *testing.T) { 74 72 // Get directory multiple times in sequence 75 - dirs := make([]interface{}, 10) 76 - for i := 0; i < 10; i++ { 73 + dirs := make([]any, 10) 74 + for i := range 10 { 77 75 dirs[i] = GetDirectory() 78 76 } 79 77 ··· 120 118 121 119 const numGoroutines = 50 122 120 var wg sync.WaitGroup 123 - wg.Add(numGoroutines) 124 121 125 - instances := make([]interface{}, numGoroutines) 122 + instances := make([]any, numGoroutines) 126 123 var mu sync.Mutex 127 124 128 125 // Simulate many goroutines trying to get the directory simultaneously 129 126 for i := 0; i < numGoroutines; i++ { 130 - go func(idx int) { 131 - defer wg.Done() 127 + wg.Go(func() { 132 128 dir := GetDirectory() 133 129 mu.Lock() 134 - instances[idx] = dir 130 + instances[i] = dir 135 131 mu.Unlock() 136 - }(i) 132 + }) 137 133 } 138 134 139 135 wg.Wait()
+6
pkg/atproto/endpoints.go
··· 51 51 // Request: {"ownerDid": "...", "repository": "...", "pullCount": 10, "pushCount": 5, "lastPull": "...", "lastPush": "..."} 52 52 // Response: {"success": true} 53 53 HoldSetStats = "/xrpc/io.atcr.hold.setStats" 54 + 55 + // HoldGetQuota returns storage quota information for a user. 56 + // Method: GET 57 + // Query: userDid={did} 58 + // Response: {"userDid": "...", "uniqueBlobs": 10, "totalSize": 1073741824} 59 + HoldGetQuota = "/xrpc/io.atcr.hold.getQuota" 54 60 ) 55 61 56 62 // Hold service crew management endpoints (io.atcr.hold.*)
+18 -18
pkg/atproto/lexicon.go
··· 594 594 Member string `json:"member" cborgen:"member"` 595 595 Role string `json:"role" cborgen:"role"` 596 596 Permissions []string `json:"permissions" cborgen:"permissions"` 597 - AddedAt string `json:"addedAt" cborgen:"addedAt"` // RFC3339 timestamp 597 + Tier string `json:"tier,omitempty" cborgen:"tier,omitempty"` // Optional tier for quota limits (e.g., 'deckhand', 'bosun', 'quartermaster') 598 + AddedAt string `json:"addedAt" cborgen:"addedAt"` // RFC3339 timestamp 598 599 } 599 600 600 601 // LayerRecord represents metadata about a container layer stored in the hold ··· 602 603 // Stored in the hold's embedded PDS for tracking and analytics 603 604 // Uses CBOR encoding for efficient storage in hold's carstore 604 605 type LayerRecord struct { 605 - Type string `json:"$type" cborgen:"$type"` 606 - Digest string `json:"digest" cborgen:"digest"` // Layer digest (e.g., "sha256:abc123...") 607 - Size int64 `json:"size" cborgen:"size"` // Size in bytes 608 - MediaType string `json:"mediaType" cborgen:"mediaType"` // Media type (e.g., "application/vnd.oci.image.layer.v1.tar+gzip") 609 - Repository string `json:"repository" cborgen:"repository"` // Repository this layer belongs to 610 - UserDID string `json:"userDid" cborgen:"userDid"` // DID of user who uploaded this layer 611 - UserHandle string `json:"userHandle" cborgen:"userHandle"` // Handle of user (for display purposes) 612 - CreatedAt string `json:"createdAt" cborgen:"createdAt"` // RFC3339 timestamp 606 + Type string `json:"$type" cborgen:"$type"` 607 + Digest string `json:"digest" cborgen:"digest"` // Layer digest (e.g., "sha256:abc123...") 608 + Size int64 `json:"size" cborgen:"size"` // Size in bytes 609 + MediaType string `json:"mediaType" cborgen:"mediaType"` // Media type (e.g., "application/vnd.oci.image.layer.v1.tar+gzip") 610 + Manifest string `json:"manifest" cborgen:"manifest"` // AT-URI of manifest that included this layer 611 + UserDID string `json:"userDid" cborgen:"userDid"` // DID of user who uploaded this layer 612 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` // RFC3339 timestamp 613 613 } 614 614 615 615 // NewLayerRecord creates a new layer record 616 - func NewLayerRecord(digest string, size int64, mediaType, repository, userDID, userHandle string) *LayerRecord { 616 + // manifestURI: AT-URI of the manifest (e.g., "at://did:plc:xyz/io.atcr.manifest/abc123") 617 + func NewLayerRecord(digest string, size int64, mediaType, userDID, manifestURI string) *LayerRecord { 617 618 return &LayerRecord{ 618 - Type: LayerCollection, 619 - Digest: digest, 620 - Size: size, 621 - MediaType: mediaType, 622 - Repository: repository, 623 - UserDID: userDID, 624 - UserHandle: userHandle, 625 - CreatedAt: time.Now().Format(time.RFC3339), 619 + Type: LayerCollection, 620 + Digest: digest, 621 + Size: size, 622 + MediaType: mediaType, 623 + Manifest: manifestURI, 624 + UserDID: userDID, 625 + CreatedAt: time.Now().Format(time.RFC3339), 626 626 } 627 627 } 628 628
+36 -49
pkg/atproto/lexicon_test.go
··· 1089 1089 1090 1090 func TestNewLayerRecord(t *testing.T) { 1091 1091 tests := []struct { 1092 - name string 1093 - digest string 1094 - size int64 1095 - mediaType string 1096 - repository string 1097 - userDID string 1098 - userHandle string 1092 + name string 1093 + digest string 1094 + size int64 1095 + mediaType string 1096 + userDID string 1097 + manifestURI string 1099 1098 }{ 1100 1099 { 1101 - name: "standard layer", 1102 - digest: "sha256:abc123", 1103 - size: 1024, 1104 - mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", 1105 - repository: "myapp", 1106 - userDID: "did:plc:user123", 1107 - userHandle: "alice.bsky.social", 1100 + name: "standard layer", 1101 + digest: "sha256:abc123", 1102 + size: 1024, 1103 + mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", 1104 + userDID: "did:plc:user123", 1105 + manifestURI: "at://did:plc:user123/io.atcr.manifest/abc123", 1108 1106 }, 1109 1107 { 1110 - name: "large layer", 1111 - digest: "sha256:def456", 1112 - size: 1073741824, // 1GB 1113 - mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", 1114 - repository: "largeapp", 1115 - userDID: "did:plc:user456", 1116 - userHandle: "bob.example.com", 1108 + name: "large layer", 1109 + digest: "sha256:def456", 1110 + size: 1073741824, // 1GB 1111 + mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", 1112 + userDID: "did:plc:user456", 1113 + manifestURI: "at://did:plc:user456/io.atcr.manifest/def456", 1117 1114 }, 1118 1115 { 1119 - name: "empty values", 1120 - digest: "", 1121 - size: 0, 1122 - mediaType: "", 1123 - repository: "", 1124 - userDID: "", 1125 - userHandle: "", 1116 + name: "empty values", 1117 + digest: "", 1118 + size: 0, 1119 + mediaType: "", 1120 + userDID: "", 1121 + manifestURI: "", 1126 1122 }, 1127 1123 { 1128 - name: "config layer", 1129 - digest: "sha256:config123", 1130 - size: 512, 1131 - mediaType: "application/vnd.oci.image.config.v1+json", 1132 - repository: "app/subapp", 1133 - userDID: "did:web:example.com", 1134 - userHandle: "charlie.tangled.io", 1124 + name: "config layer", 1125 + digest: "sha256:config123", 1126 + size: 512, 1127 + mediaType: "application/vnd.oci.image.config.v1+json", 1128 + userDID: "did:web:example.com", 1129 + manifestURI: "at://did:web:example.com/io.atcr.manifest/config123", 1135 1130 }, 1136 1131 } 1137 1132 1138 1133 for _, tt := range tests { 1139 1134 t.Run(tt.name, func(t *testing.T) { 1140 - record := NewLayerRecord(tt.digest, tt.size, tt.mediaType, tt.repository, tt.userDID, tt.userHandle) 1135 + record := NewLayerRecord(tt.digest, tt.size, tt.mediaType, tt.userDID, tt.manifestURI) 1141 1136 1142 1137 // Verify all fields 1143 1138 if record == nil { ··· 1160 1155 t.Errorf("MediaType = %q, want %q", record.MediaType, tt.mediaType) 1161 1156 } 1162 1157 1163 - if record.Repository != tt.repository { 1164 - t.Errorf("Repository = %q, want %q", record.Repository, tt.repository) 1158 + if record.Manifest != tt.manifestURI { 1159 + t.Errorf("Manifest = %q, want %q", record.Manifest, tt.manifestURI) 1165 1160 } 1166 1161 1167 1162 if record.UserDID != tt.userDID { 1168 1163 t.Errorf("UserDID = %q, want %q", record.UserDID, tt.userDID) 1169 - } 1170 - 1171 - if record.UserHandle != tt.userHandle { 1172 - t.Errorf("UserHandle = %q, want %q", record.UserHandle, tt.userHandle) 1173 1164 } 1174 1165 1175 1166 // Verify CreatedAt is set and is a valid RFC3339 timestamp ··· 1192 1183 "sha256:abc123", 1193 1184 1024, 1194 1185 "application/vnd.oci.image.layer.v1.tar+gzip", 1195 - "myapp", 1196 1186 "did:plc:user123", 1197 - "alice.bsky.social", 1187 + "at://did:plc:user123/io.atcr.manifest/abc123", 1198 1188 ) 1199 1189 1200 1190 // Marshal to JSON ··· 1222 1212 if decoded.MediaType != record.MediaType { 1223 1213 t.Errorf("MediaType = %q, want %q", decoded.MediaType, record.MediaType) 1224 1214 } 1225 - if decoded.Repository != record.Repository { 1226 - t.Errorf("Repository = %q, want %q", decoded.Repository, record.Repository) 1215 + if decoded.Manifest != record.Manifest { 1216 + t.Errorf("Manifest = %q, want %q", decoded.Manifest, record.Manifest) 1227 1217 } 1228 1218 if decoded.UserDID != record.UserDID { 1229 1219 t.Errorf("UserDID = %q, want %q", decoded.UserDID, record.UserDID) 1230 - } 1231 - if decoded.UserHandle != record.UserHandle { 1232 - t.Errorf("UserHandle = %q, want %q", decoded.UserHandle, record.UserHandle) 1233 1220 } 1234 1221 if decoded.CreatedAt != record.CreatedAt { 1235 1222 t.Errorf("CreatedAt = %q, want %q", decoded.CreatedAt, record.CreatedAt)
+1 -1
pkg/auth/cache.go
··· 1 - // Package token provides service token caching and management for AppView. 1 + // Package auth provides service token caching and management for AppView. 2 2 // Service tokens are JWTs issued by a user's PDS to authorize AppView to 3 3 // act on their behalf when communicating with hold services. Tokens are 4 4 // cached with automatic expiry parsing and 10-second safety margins.
+56 -39
pkg/auth/hold_remote.go
··· 324 324 } 325 325 326 326 // isCrewMemberNoCache queries XRPC without caching (internal helper) 327 + // Handles pagination to check all crew records, not just the first page 327 328 func (a *RemoteHoldAuthorizer) isCrewMemberNoCache(ctx context.Context, holdDID, userDID string) (bool, error) { 328 329 // Resolve DID to URL 329 330 holdURL := atproto.ResolveHoldURL(holdDID) 330 331 331 - // Build XRPC request URL 332 - // GET /xrpc/com.atproto.repo.listRecords?repo={did}&collection=io.atcr.hold.crew 333 - xrpcURL := fmt.Sprintf("%s%s?repo=%s&collection=%s", 334 - holdURL, atproto.RepoListRecords, url.QueryEscape(holdDID), url.QueryEscape(atproto.CrewCollection)) 332 + // Paginate through all crew records 333 + cursor := "" 334 + for { 335 + // Build XRPC request URL with pagination 336 + // GET /xrpc/com.atproto.repo.listRecords?repo={did}&collection=io.atcr.hold.crew&limit=100 337 + xrpcURL := fmt.Sprintf("%s%s?repo=%s&collection=%s&limit=100", 338 + holdURL, atproto.RepoListRecords, url.QueryEscape(holdDID), url.QueryEscape(atproto.CrewCollection)) 339 + if cursor != "" { 340 + xrpcURL += "&cursor=" + url.QueryEscape(cursor) 341 + } 335 342 336 - req, err := http.NewRequestWithContext(ctx, "GET", xrpcURL, nil) 337 - if err != nil { 338 - return false, err 339 - } 343 + req, err := http.NewRequestWithContext(ctx, "GET", xrpcURL, nil) 344 + if err != nil { 345 + return false, err 346 + } 340 347 341 - resp, err := a.httpClient.Do(req) 342 - if err != nil { 343 - return false, fmt.Errorf("XRPC request failed: %w", err) 344 - } 345 - defer resp.Body.Close() 348 + resp, err := a.httpClient.Do(req) 349 + if err != nil { 350 + return false, fmt.Errorf("XRPC request failed: %w", err) 351 + } 346 352 347 - if resp.StatusCode != http.StatusOK { 348 - body, _ := io.ReadAll(resp.Body) 349 - return false, fmt.Errorf("XRPC request failed: status %d: %s", resp.StatusCode, string(body)) 350 - } 353 + if resp.StatusCode != http.StatusOK { 354 + body, _ := io.ReadAll(resp.Body) 355 + resp.Body.Close() 356 + return false, fmt.Errorf("XRPC request failed: status %d: %s", resp.StatusCode, string(body)) 357 + } 358 + 359 + // Parse response 360 + var xrpcResp struct { 361 + Cursor string `json:"cursor"` 362 + Records []struct { 363 + URI string `json:"uri"` 364 + CID string `json:"cid"` 365 + Value struct { 366 + Type string `json:"$type"` 367 + Member string `json:"member"` 368 + Role string `json:"role"` 369 + Permissions []string `json:"permissions"` 370 + AddedAt string `json:"addedAt"` 371 + } `json:"value"` 372 + } `json:"records"` 373 + } 351 374 352 - // Parse response 353 - var xrpcResp struct { 354 - Records []struct { 355 - URI string `json:"uri"` 356 - CID string `json:"cid"` 357 - Value struct { 358 - Type string `json:"$type"` 359 - Member string `json:"member"` 360 - Role string `json:"role"` 361 - Permissions []string `json:"permissions"` 362 - AddedAt string `json:"addedAt"` 363 - } `json:"value"` 364 - } `json:"records"` 365 - } 375 + if err := json.NewDecoder(resp.Body).Decode(&xrpcResp); err != nil { 376 + resp.Body.Close() 377 + return false, fmt.Errorf("failed to decode XRPC response: %w", err) 378 + } 379 + resp.Body.Close() 366 380 367 - if err := json.NewDecoder(resp.Body).Decode(&xrpcResp); err != nil { 368 - return false, fmt.Errorf("failed to decode XRPC response: %w", err) 369 - } 381 + // Check if userDID is in this page of crew records 382 + for _, record := range xrpcResp.Records { 383 + if record.Value.Member == userDID { 384 + // TODO: Check expiration if set 385 + return true, nil 386 + } 387 + } 370 388 371 - // Check if userDID is in the crew list 372 - for _, record := range xrpcResp.Records { 373 - if record.Value.Member == userDID { 374 - // TODO: Check expiration if set 375 - return true, nil 389 + // Check if there are more pages 390 + if xrpcResp.Cursor == "" || len(xrpcResp.Records) == 0 { 391 + break 376 392 } 393 + cursor = xrpcResp.Cursor 377 394 } 378 395 379 396 return false, nil
+4 -15
pkg/auth/hold_remote_test.go
··· 14 14 "atcr.io/pkg/atproto" 15 15 ) 16 16 17 - func TestNewRemoteHoldAuthorizer(t *testing.T) { 18 - // Test with nil database (should still work) 19 - authorizer := NewRemoteHoldAuthorizer(nil, false) 20 - if authorizer == nil { 21 - t.Fatal("Expected non-nil authorizer") 22 - } 23 - 24 - // Verify it implements the HoldAuthorizer interface 25 - var _ HoldAuthorizer = authorizer 26 - } 27 - 28 17 func TestNewRemoteHoldAuthorizer_TestMode(t *testing.T) { 29 18 // Test with testMode enabled 30 19 authorizer := NewRemoteHoldAuthorizer(nil, true) ··· 78 67 } 79 68 80 69 // Return mock response 81 - response := map[string]interface{}{ 70 + response := map[string]any{ 82 71 "uri": "at://did:web:test-hold/io.atcr.hold.captain/self", 83 72 "cid": "bafytest123", 84 - "value": map[string]interface{}{ 73 + "value": map[string]any{ 85 74 "$type": atproto.CaptainCollection, 86 75 "owner": "did:plc:owner123", 87 76 "public": true, ··· 281 270 func TestCheckReadAccess_PublicHold(t *testing.T) { 282 271 // Create mock server that returns public captain record 283 272 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 284 - response := map[string]interface{}{ 273 + response := map[string]any{ 285 274 "uri": "at://did:web:test-hold/io.atcr.hold.captain/self", 286 275 "cid": "bafytest123", 287 - "value": map[string]interface{}{ 276 + "value": map[string]any{ 288 277 "$type": atproto.CaptainCollection, 289 278 "owner": "did:plc:owner123", 290 279 "public": true, // Public hold
+44 -12
pkg/auth/oauth/client.go
··· 17 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 18 ) 19 19 20 + // permissionSetExpansions maps lexicon IDs to their expanded scope format. 21 + // These must match the collections defined in lexicons/io/atcr/authFullApp.json 22 + // Collections are sorted alphabetically for consistent comparison with PDS-expanded scopes. 23 + var permissionSetExpansions = map[string]string{ 24 + "io.atcr.authFullApp": "repo?" + 25 + "collection=io.atcr.manifest&" + 26 + "collection=io.atcr.repo.page&" + 27 + "collection=io.atcr.sailor.profile&" + 28 + "collection=io.atcr.sailor.star&" + 29 + "collection=io.atcr.tag", 30 + } 31 + 32 + // ExpandIncludeScopes expands any "include:" prefixed scopes to their full form 33 + // by looking up the corresponding permission-set in the embedded lexicon files. 34 + // For example, "include:io.atcr.authFullApp" expands to "repo?collection=io.atcr.manifest&..." 35 + func ExpandIncludeScopes(scopes []string) []string { 36 + var expanded []string 37 + for _, scope := range scopes { 38 + if strings.HasPrefix(scope, "include:") { 39 + lexiconID := strings.TrimPrefix(scope, "include:") 40 + if exp, ok := permissionSetExpansions[lexiconID]; ok { 41 + expanded = append(expanded, exp) 42 + } else { 43 + expanded = append(expanded, scope) // Keep original if unknown 44 + } 45 + } else { 46 + expanded = append(expanded, scope) 47 + } 48 + } 49 + return expanded 50 + } 51 + 20 52 // NewClientApp creates an indigo OAuth ClientApp with ATCR-specific configuration 21 53 // Automatically configures confidential client for production deployments 22 54 // keyPath specifies where to store/load the OAuth client P-256 key (ignored for localhost) ··· 47 79 return nil, fmt.Errorf("failed to configure confidential client: %w", err) 48 80 } 49 81 50 - // Log clock information for debugging timestamp issues 51 - now := time.Now() 52 82 slog.Info("Configured confidential OAuth client", 53 83 "key_id", keyID, 54 84 "key_path", keyPath, 55 - "system_time_unix", now.Unix(), 56 - "system_time_rfc3339", now.Format(time.RFC3339), 57 - "timezone", now.Location().String()) 85 + ) 58 86 } else { 59 87 config = oauth.NewLocalhostConfig(redirectURI, scopes) 60 88 ··· 78 106 func GetDefaultScopes(did string) []string { 79 107 return []string{ 80 108 "atproto", 81 - // Permission-set (for future PDS support) 109 + // Permission-set 82 110 // See lexicons/io/atcr/authFullApp.json for definition 83 - // Uses "include:" prefix per ATProto permission spec 84 111 "include:io.atcr.authFullApp", 85 112 // com.atproto scopes must be separate (permission-sets are namespace-limited) 86 113 "rpc:com.atproto.repo.getRecord?aud=*", ··· 102 129 } 103 130 104 131 // ScopesMatch checks if two scope lists are equivalent (order-independent) 105 - // Returns true if both lists contain the same scopes, regardless of order 132 + // Returns true if both lists contain the same scopes, regardless of order. 133 + // Expands any "include:" prefixed scopes in the desired list before comparing, 134 + // since the PDS returns expanded scopes in the stored session. 106 135 func ScopesMatch(stored, desired []string) bool { 136 + // Expand any include: scopes in desired before comparing 137 + expandedDesired := ExpandIncludeScopes(desired) 138 + 107 139 // Handle nil/empty cases 108 - if len(stored) == 0 && len(desired) == 0 { 140 + if len(stored) == 0 && len(expandedDesired) == 0 { 109 141 return true 110 142 } 111 - if len(stored) != len(desired) { 143 + if len(stored) != len(expandedDesired) { 112 144 return false 113 145 } 114 146 115 147 // Build map of desired scopes for O(1) lookup 116 - desiredMap := make(map[string]bool, len(desired)) 117 - for _, scope := range desired { 148 + desiredMap := make(map[string]bool, len(expandedDesired)) 149 + for _, scope := range expandedDesired { 118 150 desiredMap[scope] = true 119 151 } 120 152
+2 -1
pkg/auth/oauth/client_test.go
··· 1 1 package oauth 2 2 3 3 import ( 4 + "testing" 5 + 4 6 "github.com/bluesky-social/indigo/atproto/auth/oauth" 5 - "testing" 6 7 ) 7 8 8 9 func TestNewClientApp(t *testing.T) {
+2 -11
pkg/auth/oauth/server_test.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "github.com/bluesky-social/indigo/atproto/auth/oauth" 6 5 "net/http" 7 6 "net/http/httptest" 8 7 "strings" 9 8 "testing" 10 9 "time" 10 + 11 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 11 12 ) 12 13 13 14 func TestNewServer(t *testing.T) { ··· 112 113 func (m *mockUISessionStore) DeleteByDID(did string) { 113 114 if m.deleteByDIDFunc != nil { 114 115 m.deleteByDIDFunc(did) 115 - } 116 - } 117 - 118 - type mockRefresher struct { 119 - invalidateSessionFunc func(did string) 120 - } 121 - 122 - func (m *mockRefresher) InvalidateSession(did string) { 123 - if m.invalidateSessionFunc != nil { 124 - m.invalidateSessionFunc(did) 125 116 } 126 117 } 127 118
+23 -5
pkg/auth/session.go
··· 9 9 "crypto/sha256" 10 10 "encoding/hex" 11 11 "encoding/json" 12 + "errors" 12 13 "fmt" 13 14 "io" 14 15 "log/slog" ··· 17 18 "time" 18 19 19 20 "atcr.io/pkg/atproto" 21 + ) 22 + 23 + // Sentinel errors for authentication failures 24 + var ( 25 + // ErrIdentityResolution indicates handle/DID resolution failed 26 + ErrIdentityResolution = errors.New("identity resolution failed") 27 + // ErrInvalidCredentials indicates PDS returned 401 (bad password/app-password) 28 + ErrInvalidCredentials = errors.New("invalid credentials") 29 + // ErrPDSUnavailable indicates PDS is unreachable or returned a server error 30 + ErrPDSUnavailable = errors.New("PDS unavailable") 20 31 ) 21 32 22 33 // CachedSession represents a cached session ··· 99 110 // Resolve identifier to PDS endpoint 100 111 _, _, pds, err := atproto.ResolveIdentity(ctx, identifier) 101 112 if err != nil { 102 - return "", "", "", err 113 + return "", "", "", fmt.Errorf("%w: %v", ErrIdentityResolution, err) 103 114 } 104 115 105 116 // Create session 106 117 sessionResp, err := v.createSession(ctx, pds, identifier, password) 107 118 if err != nil { 108 - return "", "", "", fmt.Errorf("authentication failed: %w", err) 119 + // Pass through typed errors from createSession 120 + return "", "", "", err 109 121 } 110 122 111 123 // Cache the session (ATProto sessions typically last 2 hours) ··· 146 158 resp, err := v.httpClient.Do(req) 147 159 if err != nil { 148 160 slog.Debug("Session creation HTTP request failed", "error", err) 149 - return nil, fmt.Errorf("failed to create session: %w", err) 161 + return nil, fmt.Errorf("%w: %v", ErrPDSUnavailable, err) 150 162 } 151 163 defer resp.Body.Close() 152 164 ··· 155 167 if resp.StatusCode == http.StatusUnauthorized { 156 168 bodyBytes, _ := io.ReadAll(resp.Body) 157 169 slog.Debug("Session creation unauthorized", "response", string(bodyBytes)) 158 - return nil, fmt.Errorf("invalid credentials") 170 + return nil, ErrInvalidCredentials 171 + } 172 + 173 + if resp.StatusCode >= 500 { 174 + bodyBytes, _ := io.ReadAll(resp.Body) 175 + slog.Debug("PDS server error", "status", resp.StatusCode, "response", string(bodyBytes)) 176 + return nil, fmt.Errorf("%w: server returned %d", ErrPDSUnavailable, resp.StatusCode) 159 177 } 160 178 161 179 if resp.StatusCode != http.StatusOK { 162 180 bodyBytes, _ := io.ReadAll(resp.Body) 163 181 slog.Debug("Session creation failed", "status", resp.StatusCode, "response", string(bodyBytes)) 164 - return nil, fmt.Errorf("create session failed with status %d: %s", resp.StatusCode, string(bodyBytes)) 182 + return nil, fmt.Errorf("%w: unexpected status %d: %s", ErrPDSUnavailable, resp.StatusCode, string(bodyBytes)) 165 183 } 166 184 167 185 var sessionResp SessionResponse
+1
pkg/auth/token/claims.go
··· 1 + // Package token provides JWT claims and token handling for registry authentication. 1 2 package token 2 3 3 4 import (
+15 -2
pkg/auth/token/handler.go
··· 3 3 import ( 4 4 "context" 5 5 "encoding/json" 6 + "errors" 6 7 "fmt" 7 8 "log/slog" 8 9 "net/http" ··· 194 195 slog.Debug("Trying app password authentication", "username", username) 195 196 did, handle, accessToken, err = h.validator.CreateSessionAndGetToken(r.Context(), username, password) 196 197 if err != nil { 197 - slog.Debug("App password validation failed", "error", err, "username", username) 198 - sendAuthError(w, r, "authentication failed") 198 + // Log at WARN level with specific error type 199 + if errors.Is(err, auth.ErrIdentityResolution) { 200 + slog.Warn("Identity resolution failed", "error", err, "username", username) 201 + sendAuthError(w, r, "authentication failed: could not resolve handle") 202 + } else if errors.Is(err, auth.ErrInvalidCredentials) { 203 + slog.Warn("Invalid credentials", "username", username) 204 + sendAuthError(w, r, "authentication failed: invalid credentials") 205 + } else if errors.Is(err, auth.ErrPDSUnavailable) { 206 + slog.Warn("PDS unavailable", "error", err, "username", username) 207 + sendAuthError(w, r, "authentication failed: PDS unavailable") 208 + } else { 209 + slog.Warn("Authentication failed", "error", err, "username", username) 210 + sendAuthError(w, r, "authentication failed") 211 + } 199 212 return 200 213 } 201 214
+1 -3
pkg/auth/token/handler_test.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "crypto/rsa" 6 5 "crypto/tls" 7 6 "database/sql" 8 7 "encoding/base64" ··· 22 21 // Shared test key to avoid generating a new RSA key for each test 23 22 // Generating a 2048-bit RSA key takes ~0.15s, so reusing one key saves ~4.5s for 32 tests 24 23 var ( 25 - sharedTestKey *rsa.PrivateKey 26 24 sharedTestKeyPath string 27 25 sharedTestKeyOnce sync.Once 28 26 sharedTestKeyDir string ··· 513 511 } 514 512 515 513 // Verify JSON structure 516 - var decoded map[string]interface{} 514 + var decoded map[string]any 517 515 if err := json.Unmarshal(data, &decoded); err != nil { 518 516 t.Fatalf("Failed to unmarshal JSON: %v", err) 519 517 }
+8 -11
pkg/auth/token/issuer_test.go
··· 19 19 // Shared test key to avoid generating a new RSA key for each test 20 20 // Generating a 2048-bit RSA key takes ~0.15s, so reusing one key saves significant time 21 21 var ( 22 - issuerSharedTestKey *rsa.PrivateKey 23 22 issuerSharedTestKeyPath string 24 23 issuerSharedTestKeyOnce sync.Once 25 24 issuerSharedTestKeyDir string ··· 207 206 } 208 207 209 208 // Parse and validate the token 210 - token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { 209 + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) { 211 210 return issuer.publicKey, nil 212 211 }) 213 212 if err != nil { ··· 289 288 } 290 289 291 290 // x5c should be a slice of base64-encoded certificates 292 - x5cSlice, ok := x5c.([]interface{}) 291 + x5cSlice, ok := x5c.([]any) 293 292 if !ok { 294 293 t.Fatal("Expected x5c to be a slice") 295 294 } ··· 379 378 // Issue tokens concurrently 380 379 const numGoroutines = 10 381 380 var wg sync.WaitGroup 382 - wg.Add(numGoroutines) 383 381 384 382 tokens := make([]string, numGoroutines) 385 383 errors := make([]error, numGoroutines) 386 384 387 385 for i := 0; i < numGoroutines; i++ { 388 - go func(idx int) { 389 - defer wg.Done() 390 - subject := "did:plc:user" + string(rune('0'+idx)) 386 + wg.Go(func() { 387 + subject := "did:plc:user" + string(rune('0'+i)) 391 388 token, err := issuer.Issue(subject, nil, AuthMethodOAuth) 392 - tokens[idx] = token 393 - errors[idx] = err 394 - }(i) 389 + tokens[i] = token 390 + errors[i] = err 391 + }) 395 392 } 396 393 397 394 wg.Wait() ··· 575 572 } 576 573 577 574 // Parse token and verify expiration 578 - token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { 575 + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) { 579 576 return issuer.publicKey, nil 580 577 }) 581 578 if err != nil {
+349
pkg/hold/admin/admin.go
··· 1 + // Package admin provides an owner-only web UI for managing the hold service. 2 + // It includes OAuth-based authentication, crew management, settings configuration, 3 + // and usage metrics. The admin panel is embedded directly in the hold service binary. 4 + package admin 5 + 6 + //go:generate curl -fsSL -o static/js/htmx.min.js https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js 7 + //go:generate curl -fsSL -o static/js/lucide.min.js https://unpkg.com/lucide@latest/dist/umd/lucide.min.js 8 + 9 + import ( 10 + "context" 11 + "crypto/rand" 12 + "embed" 13 + "encoding/base64" 14 + "encoding/json" 15 + "fmt" 16 + "html/template" 17 + "io/fs" 18 + "log/slog" 19 + "net" 20 + "net/http" 21 + "net/url" 22 + "sync" 23 + "time" 24 + 25 + "atcr.io/pkg/atproto" 26 + "atcr.io/pkg/hold/pds" 27 + "atcr.io/pkg/hold/quota" 28 + 29 + indigooauth "github.com/bluesky-social/indigo/atproto/auth/oauth" 30 + "github.com/go-chi/chi/v5" 31 + ) 32 + 33 + //go:embed templates/* 34 + var templatesFS embed.FS 35 + 36 + //go:embed static/* 37 + var staticFS embed.FS 38 + 39 + // AdminConfig holds admin panel configuration 40 + type AdminConfig struct { 41 + // Enabled controls whether the admin panel is accessible 42 + Enabled bool 43 + // PublicURL is the hold's public URL (for DID resolution, AppView communication) 44 + PublicURL string 45 + } 46 + 47 + // DefaultAdminConfig returns sensible defaults 48 + func DefaultAdminConfig() AdminConfig { 49 + return AdminConfig{ 50 + Enabled: false, 51 + } 52 + } 53 + 54 + // AdminSession represents an authenticated admin session 55 + type AdminSession struct { 56 + DID string 57 + Handle string 58 + } 59 + 60 + // AdminUI manages the admin web interface 61 + type AdminUI struct { 62 + pds *pds.HoldPDS 63 + quotaMgr *quota.Manager 64 + clientApp *indigooauth.ClientApp 65 + templates *template.Template 66 + config AdminConfig 67 + 68 + // In-memory session storage (single user, no persistence needed) 69 + sessions map[string]*AdminSession 70 + sessionsMu sync.RWMutex 71 + } 72 + 73 + // adminContextKey is used to store session data in request context 74 + type adminContextKey struct{} 75 + 76 + // NewAdminUI creates a new admin UI instance 77 + func NewAdminUI(ctx context.Context, holdPDS *pds.HoldPDS, quotaMgr *quota.Manager, cfg AdminConfig) (*AdminUI, error) { 78 + if !cfg.Enabled { 79 + return nil, nil 80 + } 81 + 82 + // Validate required config 83 + if cfg.PublicURL == "" { 84 + return nil, fmt.Errorf("PublicURL is required for admin panel") 85 + } 86 + 87 + // Determine OAuth configuration based on URL type 88 + u, err := url.Parse(cfg.PublicURL) 89 + if err != nil { 90 + return nil, fmt.Errorf("invalid PublicURL: %w", err) 91 + } 92 + 93 + // Use in-memory store for OAuth sessions 94 + oauthStore := indigooauth.NewMemStore() 95 + 96 + // Use minimal scopes for admin (only need basic auth, no blob access) 97 + adminScopes := []string{"atproto"} 98 + 99 + var oauthConfig indigooauth.ClientConfig 100 + var redirectURI string 101 + 102 + host := u.Hostname() 103 + if isIPAddress(host) || host == "localhost" || host == "127.0.0.1" { 104 + // Development mode: IP address or localhost - use localhost OAuth config 105 + // Substitute 127.0.0.1 for Docker network IPs 106 + port := u.Port() 107 + if port == "" { 108 + port = "8080" 109 + } 110 + oauthBaseURL := "http://127.0.0.1:" + port 111 + redirectURI = oauthBaseURL + "/admin/auth/oauth/callback" 112 + oauthConfig = indigooauth.NewLocalhostConfig(redirectURI, adminScopes) 113 + 114 + slog.Info("Admin OAuth configured (localhost mode)", 115 + "redirect_uri", redirectURI, 116 + "public_url", cfg.PublicURL) 117 + } else { 118 + // Production mode: real domain - use public client with metadata endpoint 119 + clientID := cfg.PublicURL + "/admin/oauth-client-metadata.json" 120 + redirectURI = cfg.PublicURL + "/admin/auth/oauth/callback" 121 + oauthConfig = indigooauth.NewPublicConfig(clientID, redirectURI, adminScopes) 122 + 123 + slog.Info("Admin OAuth configured (production mode)", 124 + "client_id", clientID, 125 + "redirect_uri", redirectURI) 126 + } 127 + 128 + clientApp := indigooauth.NewClientApp(&oauthConfig, oauthStore) 129 + clientApp.Dir = atproto.GetDirectory() 130 + 131 + // Parse templates 132 + templates, err := parseTemplates() 133 + if err != nil { 134 + return nil, fmt.Errorf("failed to parse templates: %w", err) 135 + } 136 + 137 + ui := &AdminUI{ 138 + pds: holdPDS, 139 + quotaMgr: quotaMgr, 140 + clientApp: clientApp, 141 + templates: templates, 142 + config: cfg, 143 + sessions: make(map[string]*AdminSession), 144 + } 145 + 146 + slog.Info("Admin panel initialized", "publicURL", cfg.PublicURL) 147 + 148 + return ui, nil 149 + } 150 + 151 + // Session management 152 + 153 + func (ui *AdminUI) createSession(did, handle string) string { 154 + b := make([]byte, 32) 155 + rand.Read(b) 156 + token := base64.URLEncoding.EncodeToString(b) 157 + 158 + ui.sessionsMu.Lock() 159 + ui.sessions[token] = &AdminSession{DID: did, Handle: handle} 160 + ui.sessionsMu.Unlock() 161 + 162 + return token 163 + } 164 + 165 + func (ui *AdminUI) getSession(token string) *AdminSession { 166 + ui.sessionsMu.RLock() 167 + defer ui.sessionsMu.RUnlock() 168 + return ui.sessions[token] 169 + } 170 + 171 + func (ui *AdminUI) deleteSession(token string) { 172 + ui.sessionsMu.Lock() 173 + delete(ui.sessions, token) 174 + ui.sessionsMu.Unlock() 175 + } 176 + 177 + // Cookie helpers 178 + 179 + const sessionCookieName = "hold_admin_session" 180 + 181 + func (ui *AdminUI) setSessionCookie(w http.ResponseWriter, r *http.Request, token string) { 182 + secure := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" 183 + http.SetCookie(w, &http.Cookie{ 184 + Name: sessionCookieName, 185 + Value: token, 186 + Path: "/admin", 187 + MaxAge: 86400, // 24 hours 188 + HttpOnly: true, 189 + Secure: secure, 190 + SameSite: http.SameSiteLaxMode, 191 + }) 192 + } 193 + 194 + func clearSessionCookie(w http.ResponseWriter) { 195 + http.SetCookie(w, &http.Cookie{ 196 + Name: sessionCookieName, 197 + Value: "", 198 + Path: "/admin", 199 + MaxAge: -1, 200 + HttpOnly: true, 201 + SameSite: http.SameSiteLaxMode, 202 + }) 203 + } 204 + 205 + func getSessionCookie(r *http.Request) (string, bool) { 206 + cookie, err := r.Cookie(sessionCookieName) 207 + if err != nil { 208 + return "", false 209 + } 210 + return cookie.Value, true 211 + } 212 + 213 + // parseTemplates loads and parses all HTML templates 214 + func parseTemplates() (*template.Template, error) { 215 + funcMap := template.FuncMap{ 216 + "truncate": func(s string, n int) string { 217 + if len(s) <= n { 218 + return s 219 + } 220 + return s[:n] + "..." 221 + }, 222 + "formatBytes": formatHumanBytes, 223 + "formatTime": func(t time.Time) string { 224 + return t.Format("2006-01-02 15:04") 225 + }, 226 + "contains": func(slice []string, item string) bool { 227 + for _, s := range slice { 228 + if s == item { 229 + return true 230 + } 231 + } 232 + return false 233 + }, 234 + } 235 + 236 + tmpl := template.New("").Funcs(funcMap) 237 + 238 + err := fs.WalkDir(templatesFS, "templates", func(path string, d fs.DirEntry, err error) error { 239 + if err != nil { 240 + return err 241 + } 242 + if d.IsDir() { 243 + return nil 244 + } 245 + if len(path) < 5 || path[len(path)-5:] != ".html" { 246 + return nil 247 + } 248 + 249 + content, err := templatesFS.ReadFile(path) 250 + if err != nil { 251 + return fmt.Errorf("failed to read template %s: %w", path, err) 252 + } 253 + 254 + name := path[len("templates/"):] 255 + _, err = tmpl.New(name).Parse(string(content)) 256 + if err != nil { 257 + return fmt.Errorf("failed to parse template %s: %w", path, err) 258 + } 259 + 260 + return nil 261 + }) 262 + 263 + if err != nil { 264 + return nil, err 265 + } 266 + 267 + return tmpl, nil 268 + } 269 + 270 + // formatHumanBytes formats bytes as human-readable string 271 + func formatHumanBytes(bytes int64) string { 272 + const unit = 1024 273 + if bytes < unit { 274 + return fmt.Sprintf("%d B", bytes) 275 + } 276 + div, exp := int64(unit), 0 277 + for n := bytes / unit; n >= unit; n /= unit { 278 + div *= unit 279 + exp++ 280 + } 281 + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) 282 + } 283 + 284 + // isIPAddress returns true if the host is an IP address (not a domain name) 285 + func isIPAddress(host string) bool { 286 + return net.ParseIP(host) != nil 287 + } 288 + 289 + // RegisterRoutes registers all admin routes with the router 290 + func (ui *AdminUI) RegisterRoutes(r chi.Router) { 291 + // Static files (public) 292 + staticSub, _ := fs.Sub(staticFS, "static") 293 + r.Handle("/admin/static/*", http.StripPrefix("/admin/static/", http.FileServer(http.FS(staticSub)))) 294 + 295 + // OAuth client metadata endpoint (required for production OAuth) 296 + r.Get("/admin/oauth-client-metadata.json", ui.handleClientMetadata) 297 + 298 + // Public auth routes 299 + r.Get("/admin/auth/login", ui.handleLogin) 300 + r.Get("/admin/auth/oauth/authorize", ui.handleAuthorize) 301 + r.Get("/admin/auth/oauth/callback", ui.handleCallback) 302 + 303 + // Protected routes (require owner) 304 + r.Group(func(r chi.Router) { 305 + r.Use(ui.requireOwner) 306 + 307 + // Dashboard 308 + r.Get("/admin", ui.handleDashboard) 309 + r.Get("/admin/", ui.handleDashboard) 310 + 311 + // Crew management 312 + r.Get("/admin/crew", ui.handleCrewList) 313 + r.Get("/admin/crew/add", ui.handleCrewAddForm) 314 + r.Post("/admin/crew/add", ui.handleCrewAdd) 315 + r.Get("/admin/crew/{rkey}", ui.handleCrewEditForm) 316 + r.Post("/admin/crew/{rkey}/update", ui.handleCrewUpdate) 317 + r.Post("/admin/crew/{rkey}/delete", ui.handleCrewDelete) 318 + 319 + // Settings 320 + r.Get("/admin/settings", ui.handleSettings) 321 + r.Post("/admin/settings/update", ui.handleSettingsUpdate) 322 + 323 + // API endpoints (for HTMX) 324 + r.Get("/admin/api/stats", ui.handleStatsAPI) 325 + r.Get("/admin/api/top-users", ui.handleTopUsersAPI) 326 + 327 + // Logout 328 + r.Get("/admin/auth/logout", ui.handleLogout) 329 + }) 330 + } 331 + 332 + // handleClientMetadata serves the OAuth client metadata for production deployments 333 + func (ui *AdminUI) handleClientMetadata(w http.ResponseWriter, r *http.Request) { 334 + metadata := ui.clientApp.Config.ClientMetadata() 335 + 336 + // Set client name for display in OAuth consent screen 337 + clientName := "Hold Admin Panel" 338 + metadata.ClientName = &clientName 339 + metadata.ClientURI = &ui.config.PublicURL 340 + 341 + w.Header().Set("Content-Type", "application/json") 342 + w.Header().Set("Cache-Control", "public, max-age=3600") 343 + json.NewEncoder(w).Encode(metadata) 344 + } 345 + 346 + // Close cleans up resources (no-op now, but keeps interface consistent) 347 + func (ui *AdminUI) Close() error { 348 + return nil 349 + }
+115
pkg/hold/admin/auth.go
··· 1 + package admin 2 + 3 + import ( 4 + "context" 5 + "log/slog" 6 + "net/http" 7 + ) 8 + 9 + // requireOwner middleware ensures the request is from the hold owner 10 + func (ui *AdminUI) requireOwner(next http.Handler) http.Handler { 11 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 + // Get session cookie 13 + token, ok := getSessionCookie(r) 14 + if !ok { 15 + http.Redirect(w, r, "/admin/auth/login?return_to="+r.URL.Path, http.StatusFound) 16 + return 17 + } 18 + 19 + // Validate session 20 + session := ui.getSession(token) 21 + if session == nil { 22 + clearSessionCookie(w) 23 + http.Redirect(w, r, "/admin/auth/login", http.StatusFound) 24 + return 25 + } 26 + 27 + // Double-check DID still matches captain.Owner 28 + _, captain, err := ui.pds.GetCaptainRecord(r.Context()) 29 + if err != nil { 30 + slog.Error("Failed to get captain record for admin auth", "error", err) 31 + http.Error(w, "Failed to verify ownership", http.StatusInternalServerError) 32 + return 33 + } 34 + 35 + if session.DID != captain.Owner { 36 + slog.Warn("Admin session DID doesn't match captain owner", 37 + "sessionDID", session.DID, 38 + "captainOwner", captain.Owner) 39 + ui.deleteSession(token) 40 + clearSessionCookie(w) 41 + http.Error(w, "Access denied: ownership verification failed", http.StatusForbidden) 42 + return 43 + } 44 + 45 + // Add session to context for handlers 46 + ctx := context.WithValue(r.Context(), adminContextKey{}, session) 47 + next.ServeHTTP(w, r.WithContext(ctx)) 48 + }) 49 + } 50 + 51 + // getSessionFromContext retrieves the admin session from context 52 + func getSessionFromContext(ctx context.Context) *AdminSession { 53 + session, ok := ctx.Value(adminContextKey{}).(*AdminSession) 54 + if !ok { 55 + return nil 56 + } 57 + return session 58 + } 59 + 60 + // PageData contains common data for all admin pages 61 + type PageData struct { 62 + Title string 63 + ActivePage string 64 + User *AdminSession 65 + HoldDID string 66 + Flash *Flash 67 + } 68 + 69 + // Flash represents a flash message 70 + type Flash struct { 71 + Category string // "success", "error", "warning", "info" 72 + Message string 73 + } 74 + 75 + // newPageData creates PageData with common values 76 + func (ui *AdminUI) newPageData(r *http.Request, title, activePage string) PageData { 77 + session := getSessionFromContext(r.Context()) 78 + flash := getFlash(r, ui) 79 + 80 + return PageData{ 81 + Title: title, 82 + ActivePage: activePage, 83 + User: session, 84 + HoldDID: ui.pds.DID(), 85 + Flash: flash, 86 + } 87 + } 88 + 89 + // renderTemplate renders a template with the given data 90 + func (ui *AdminUI) renderTemplate(w http.ResponseWriter, name string, data interface{}) { 91 + w.Header().Set("Content-Type", "text/html; charset=utf-8") 92 + 93 + if err := ui.templates.ExecuteTemplate(w, name, data); err != nil { 94 + slog.Error("Failed to render template", "template", name, "error", err) 95 + http.Error(w, "Internal server error", http.StatusInternalServerError) 96 + } 97 + } 98 + 99 + // renderError renders an error page 100 + func (ui *AdminUI) renderError(w http.ResponseWriter, r *http.Request, message string, statusCode int) { 101 + w.Header().Set("Content-Type", "text/html; charset=utf-8") 102 + w.WriteHeader(statusCode) 103 + 104 + data := struct { 105 + PageData 106 + Error string 107 + }{ 108 + PageData: ui.newPageData(r, "Error", ""), 109 + Error: message, 110 + } 111 + 112 + if err := ui.templates.ExecuteTemplate(w, "pages/error.html", data); err != nil { 113 + http.Error(w, message, statusCode) 114 + } 115 + }
+67
pkg/hold/admin/flash.go
··· 1 + package admin 2 + 3 + import ( 4 + "encoding/base64" 5 + "encoding/json" 6 + "net/http" 7 + ) 8 + 9 + const flashCookieName = "hold_admin_flash" 10 + 11 + // setFlash sets a flash message cookie 12 + func setFlash(w http.ResponseWriter, r *http.Request, category, message string) { 13 + flash := Flash{ 14 + Category: category, 15 + Message: message, 16 + } 17 + 18 + data, err := json.Marshal(flash) 19 + if err != nil { 20 + return 21 + } 22 + 23 + encoded := base64.URLEncoding.EncodeToString(data) 24 + secure := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" 25 + 26 + http.SetCookie(w, &http.Cookie{ 27 + Name: flashCookieName, 28 + Value: encoded, 29 + Path: "/admin", 30 + MaxAge: 60, // 1 minute - should be consumed on next page load 31 + HttpOnly: true, 32 + Secure: secure, 33 + SameSite: http.SameSiteLaxMode, 34 + }) 35 + } 36 + 37 + // getFlash retrieves and clears the flash message 38 + func getFlash(r *http.Request, ui *AdminUI) *Flash { 39 + cookie, err := r.Cookie(flashCookieName) 40 + if err != nil { 41 + return nil 42 + } 43 + 44 + data, err := base64.URLEncoding.DecodeString(cookie.Value) 45 + if err != nil { 46 + return nil 47 + } 48 + 49 + var flash Flash 50 + if err := json.Unmarshal(data, &flash); err != nil { 51 + return nil 52 + } 53 + 54 + return &flash 55 + } 56 + 57 + // clearFlash clears the flash cookie (called after displaying) 58 + func clearFlash(w http.ResponseWriter) { 59 + http.SetCookie(w, &http.Cookie{ 60 + Name: flashCookieName, 61 + Value: "", 62 + Path: "/admin", 63 + MaxAge: -1, 64 + HttpOnly: true, 65 + SameSite: http.SameSiteLaxMode, 66 + }) 67 + }
+196
pkg/hold/admin/handlers.go
··· 1 + package admin 2 + 3 + import ( 4 + "encoding/json" 5 + "log/slog" 6 + "net/http" 7 + "sort" 8 + "strconv" 9 + 10 + "atcr.io/pkg/atproto" 11 + ) 12 + 13 + // DashboardStats contains dashboard statistics 14 + type DashboardStats struct { 15 + TotalCrewMembers int 16 + TierDistribution map[string]int 17 + } 18 + 19 + // handleDashboard renders the main dashboard 20 + func (ui *AdminUI) handleDashboard(w http.ResponseWriter, r *http.Request) { 21 + ctx := r.Context() 22 + 23 + // Clear flash after reading 24 + defer clearFlash(w) 25 + 26 + // Get crew members 27 + crew, err := ui.pds.ListCrewMembers(ctx) 28 + if err != nil { 29 + slog.Warn("Failed to list crew members for dashboard", "error", err) 30 + } 31 + 32 + stats := DashboardStats{ 33 + TotalCrewMembers: len(crew), 34 + TierDistribution: make(map[string]int), 35 + } 36 + 37 + // Count tier distribution 38 + defaultTier := "default" 39 + if ui.quotaMgr != nil && ui.quotaMgr.IsEnabled() { 40 + defaultTier = ui.quotaMgr.GetDefaultTier() 41 + } 42 + 43 + for _, member := range crew { 44 + tier := member.Record.Tier 45 + if tier == "" { 46 + tier = defaultTier 47 + } 48 + stats.TierDistribution[tier]++ 49 + } 50 + 51 + data := struct { 52 + PageData 53 + Stats DashboardStats 54 + }{ 55 + PageData: ui.newPageData(r, "Dashboard", "dashboard"), 56 + Stats: stats, 57 + } 58 + 59 + ui.renderTemplate(w, "pages/dashboard.html", data) 60 + } 61 + 62 + // StorageStats contains storage statistics 63 + type StorageStats struct { 64 + TotalBlobs int `json:"totalBlobs"` 65 + TotalSize int64 `json:"totalSize"` 66 + TotalHuman string `json:"totalHuman"` 67 + UniqueDigests int `json:"uniqueDigests"` 68 + } 69 + 70 + // handleStatsAPI returns storage statistics (for HTMX lazy loading) 71 + func (ui *AdminUI) handleStatsAPI(w http.ResponseWriter, r *http.Request) { 72 + ctx := r.Context() 73 + 74 + // Get layer record count from the index 75 + recordsIndex := ui.pds.RecordsIndex() 76 + if recordsIndex == nil { 77 + http.Error(w, "Records index not available", http.StatusInternalServerError) 78 + return 79 + } 80 + 81 + // Count total layer records 82 + totalBlobs, err := recordsIndex.Count(atproto.LayerCollection) 83 + if err != nil { 84 + slog.Error("Failed to count layer records", "error", err) 85 + http.Error(w, "Failed to load stats", http.StatusInternalServerError) 86 + return 87 + } 88 + 89 + // Calculate total storage by summing quota for all crew members 90 + var totalSize int64 91 + uniqueDigests := 0 92 + 93 + crew, err := ui.pds.ListCrewMembers(ctx) 94 + if err != nil { 95 + slog.Warn("Failed to list crew for stats", "error", err) 96 + } else { 97 + // Get usage for each crew member 98 + for _, member := range crew { 99 + quotaStats, err := ui.pds.GetQuotaForUser(ctx, member.Record.Member) 100 + if err != nil { 101 + continue 102 + } 103 + totalSize += quotaStats.TotalSize 104 + uniqueDigests += quotaStats.UniqueBlobs 105 + } 106 + } 107 + 108 + stats := StorageStats{ 109 + TotalBlobs: totalBlobs, 110 + TotalSize: totalSize, 111 + TotalHuman: formatHumanBytes(totalSize), 112 + UniqueDigests: uniqueDigests, 113 + } 114 + 115 + // If HTMX request, return HTML partial 116 + if r.Header.Get("HX-Request") == "true" { 117 + data := struct { 118 + Stats StorageStats 119 + }{Stats: stats} 120 + ui.renderTemplate(w, "partials/usage_stats.html", data) 121 + return 122 + } 123 + 124 + // Otherwise return JSON 125 + w.Header().Set("Content-Type", "application/json") 126 + json.NewEncoder(w).Encode(stats) 127 + } 128 + 129 + // UserUsage represents storage usage for a user 130 + type UserUsage struct { 131 + DID string `json:"did"` 132 + Handle string `json:"handle"` 133 + Usage int64 `json:"usage"` 134 + UsageHuman string `json:"usageHuman"` 135 + BlobCount int `json:"blobCount"` 136 + } 137 + 138 + // handleTopUsersAPI returns top users by storage (for HTMX lazy loading) 139 + func (ui *AdminUI) handleTopUsersAPI(w http.ResponseWriter, r *http.Request) { 140 + ctx := r.Context() 141 + 142 + limit := 10 143 + if l := r.URL.Query().Get("limit"); l != "" { 144 + if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 { 145 + limit = parsed 146 + } 147 + } 148 + 149 + // Get all crew members and their usage 150 + crew, err := ui.pds.ListCrewMembers(ctx) 151 + if err != nil { 152 + slog.Error("Failed to list crew members for top users", "error", err) 153 + http.Error(w, "Failed to load top users", http.StatusInternalServerError) 154 + return 155 + } 156 + 157 + var users []UserUsage 158 + for _, member := range crew { 159 + quotaStats, err := ui.pds.GetQuotaForUser(ctx, member.Record.Member) 160 + if err != nil { 161 + slog.Warn("Failed to get quota for user", "did", member.Record.Member, "error", err) 162 + continue 163 + } 164 + 165 + users = append(users, UserUsage{ 166 + DID: member.Record.Member, 167 + Handle: resolveHandle(ctx, member.Record.Member), 168 + Usage: quotaStats.TotalSize, 169 + UsageHuman: formatHumanBytes(quotaStats.TotalSize), 170 + BlobCount: quotaStats.UniqueBlobs, 171 + }) 172 + } 173 + 174 + // Sort by usage (highest first) 175 + sort.Slice(users, func(i, j int) bool { 176 + return users[i].Usage > users[j].Usage 177 + }) 178 + 179 + // Limit results 180 + if len(users) > limit { 181 + users = users[:limit] 182 + } 183 + 184 + // If HTMX request, return HTML partial 185 + if r.Header.Get("HX-Request") == "true" { 186 + data := struct { 187 + Users []UserUsage 188 + }{Users: users} 189 + ui.renderTemplate(w, "partials/top_users.html", data) 190 + return 191 + } 192 + 193 + // Otherwise return JSON 194 + w.Header().Set("Content-Type", "application/json") 195 + json.NewEncoder(w).Encode(users) 196 + }
+135
pkg/hold/admin/handlers_auth.go
··· 1 + package admin 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + "strings" 7 + 8 + "atcr.io/pkg/atproto" 9 + ) 10 + 11 + // handleLogin renders the login page 12 + func (ui *AdminUI) handleLogin(w http.ResponseWriter, r *http.Request) { 13 + // If already logged in, redirect to dashboard 14 + if token, ok := getSessionCookie(r); ok { 15 + if session := ui.getSession(token); session != nil { 16 + // Verify still owner 17 + if _, captain, err := ui.pds.GetCaptainRecord(r.Context()); err == nil && session.DID == captain.Owner { 18 + http.Redirect(w, r, "/admin", http.StatusFound) 19 + return 20 + } 21 + } 22 + } 23 + 24 + returnTo := r.URL.Query().Get("return_to") 25 + if returnTo == "" { 26 + returnTo = "/admin" 27 + } 28 + 29 + data := struct { 30 + PageData 31 + ReturnTo string 32 + Error string 33 + }{ 34 + PageData: PageData{ 35 + Title: "Login", 36 + ActivePage: "login", 37 + HoldDID: ui.pds.DID(), 38 + }, 39 + ReturnTo: returnTo, 40 + Error: r.URL.Query().Get("error"), 41 + } 42 + 43 + ui.renderTemplate(w, "pages/login.html", data) 44 + } 45 + 46 + // handleAuthorize starts the OAuth flow 47 + func (ui *AdminUI) handleAuthorize(w http.ResponseWriter, r *http.Request) { 48 + handle := strings.TrimSpace(r.URL.Query().Get("handle")) 49 + if handle == "" { 50 + http.Redirect(w, r, "/admin/auth/login?error=Handle+is+required", http.StatusFound) 51 + return 52 + } 53 + 54 + // Normalize handle 55 + handle = strings.TrimPrefix(handle, "@") 56 + 57 + // Resolve handle to DID 58 + did, _, _, err := atproto.ResolveIdentity(r.Context(), handle) 59 + if err != nil { 60 + slog.Warn("Failed to resolve handle for admin login", "handle", handle, "error", err) 61 + http.Redirect(w, r, "/admin/auth/login?error=Could+not+resolve+handle", http.StatusFound) 62 + return 63 + } 64 + 65 + slog.Info("Starting admin OAuth flow", "handle", handle, "did", did) 66 + 67 + // Start OAuth flow 68 + authURL, err := ui.clientApp.StartAuthFlow(r.Context(), did) 69 + if err != nil { 70 + slog.Error("Failed to start OAuth flow", "error", err) 71 + http.Redirect(w, r, "/admin/auth/login?error=OAuth+initialization+failed", http.StatusFound) 72 + return 73 + } 74 + 75 + http.Redirect(w, r, authURL, http.StatusFound) 76 + } 77 + 78 + // handleCallback processes the OAuth callback 79 + func (ui *AdminUI) handleCallback(w http.ResponseWriter, r *http.Request) { 80 + ctx := r.Context() 81 + 82 + // Process OAuth callback 83 + sessionData, err := ui.clientApp.ProcessCallback(ctx, r.URL.Query()) 84 + if err != nil { 85 + slog.Error("OAuth callback failed", "error", err) 86 + http.Redirect(w, r, "/admin/auth/login?error=OAuth+authentication+failed", http.StatusFound) 87 + return 88 + } 89 + 90 + did := sessionData.AccountDID.String() 91 + 92 + // Resolve handle from DID 93 + _, handle, _, err := atproto.ResolveIdentity(ctx, did) 94 + if err != nil { 95 + slog.Warn("Failed to resolve handle from DID", "did", did, "error", err) 96 + handle = did // Fallback to DID 97 + } 98 + 99 + slog.Info("OAuth callback successful", "did", did, "handle", handle) 100 + 101 + // Get captain record to check owner 102 + _, captain, err := ui.pds.GetCaptainRecord(ctx) 103 + if err != nil { 104 + slog.Error("Failed to get captain record during OAuth callback", "error", err) 105 + http.Redirect(w, r, "/admin/auth/login?error=Failed+to+verify+ownership", http.StatusFound) 106 + return 107 + } 108 + 109 + // CRITICAL: Only allow the hold owner 110 + if did != captain.Owner { 111 + slog.Warn("Non-owner attempted admin access", 112 + "did", did, 113 + "handle", handle, 114 + "owner", captain.Owner) 115 + http.Redirect(w, r, "/admin/auth/login?error=Access+denied:+Only+the+hold+owner+can+access+the+admin+panel", http.StatusFound) 116 + return 117 + } 118 + 119 + // Create session and set cookie 120 + token := ui.createSession(did, handle) 121 + ui.setSessionCookie(w, r, token) 122 + 123 + slog.Info("Admin login successful", "did", did, "handle", handle) 124 + 125 + http.Redirect(w, r, "/admin", http.StatusFound) 126 + } 127 + 128 + // handleLogout clears the session and redirects to login 129 + func (ui *AdminUI) handleLogout(w http.ResponseWriter, r *http.Request) { 130 + if token, ok := getSessionCookie(r); ok { 131 + ui.deleteSession(token) 132 + } 133 + clearSessionCookie(w) 134 + http.Redirect(w, r, "/admin/auth/login", http.StatusFound) 135 + }
+422
pkg/hold/admin/handlers_crew.go
··· 1 + package admin 2 + 3 + import ( 4 + "context" 5 + "log/slog" 6 + "net/http" 7 + "sort" 8 + "strings" 9 + "time" 10 + 11 + "atcr.io/pkg/atproto" 12 + "github.com/go-chi/chi/v5" 13 + ) 14 + 15 + // CrewMemberView represents a crew member for display 16 + type CrewMemberView struct { 17 + RKey string 18 + DID string 19 + Handle string 20 + Role string 21 + Permissions []string 22 + Tier string 23 + TierLimit string 24 + CurrentUsage int64 25 + UsageHuman string 26 + UsagePercent int 27 + AddedAt time.Time 28 + } 29 + 30 + // resolveHandle attempts to resolve a DID to a handle 31 + // Returns empty string if resolution fails 32 + func resolveHandle(ctx context.Context, did string) string { 33 + _, handle, _, err := atproto.ResolveIdentity(ctx, did) 34 + if err != nil { 35 + slog.Debug("Failed to resolve handle for DID", "did", did, "error", err) 36 + return "" // Empty string means no handle resolved 37 + } 38 + // If handle is the same as the DID (fallback), treat as no handle 39 + if handle == did { 40 + return "" 41 + } 42 + return handle 43 + } 44 + 45 + // TierOption represents a tier choice in forms 46 + type TierOption struct { 47 + Key string 48 + Name string 49 + Limit string 50 + } 51 + 52 + // handleCrewList displays all crew members 53 + func (ui *AdminUI) handleCrewList(w http.ResponseWriter, r *http.Request) { 54 + ctx := r.Context() 55 + defer clearFlash(w) 56 + 57 + crew, err := ui.pds.ListCrewMembers(ctx) 58 + if err != nil { 59 + ui.renderError(w, r, "Failed to list crew: "+err.Error(), http.StatusInternalServerError) 60 + return 61 + } 62 + 63 + // Get usage data for each crew member using GetQuotaForUser 64 + var crewViews []CrewMemberView 65 + userUsage := make(map[string]int64) 66 + 67 + // Pre-fetch usage for all crew members 68 + for _, member := range crew { 69 + quotaStats, err := ui.pds.GetQuotaForUser(ctx, member.Record.Member) 70 + if err != nil { 71 + slog.Warn("Failed to get quota for crew member", "did", member.Record.Member, "error", err) 72 + continue 73 + } 74 + userUsage[member.Record.Member] = quotaStats.TotalSize 75 + } 76 + 77 + defaultTier := "default" 78 + if ui.quotaMgr != nil && ui.quotaMgr.IsEnabled() { 79 + defaultTier = ui.quotaMgr.GetDefaultTier() 80 + } 81 + 82 + for _, member := range crew { 83 + tier := member.Record.Tier 84 + if tier == "" { 85 + tier = defaultTier 86 + } 87 + 88 + view := CrewMemberView{ 89 + RKey: member.Rkey, 90 + DID: member.Record.Member, 91 + Handle: resolveHandle(ctx, member.Record.Member), 92 + Role: member.Record.Role, 93 + Permissions: member.Record.Permissions, 94 + Tier: tier, 95 + AddedAt: parseTime(member.Record.AddedAt), 96 + } 97 + 98 + // Get tier limit 99 + if ui.quotaMgr != nil && ui.quotaMgr.IsEnabled() { 100 + if limit := ui.quotaMgr.GetTierLimit(tier); limit != nil { 101 + view.TierLimit = formatHumanBytes(*limit) 102 + if *limit > 0 { 103 + view.UsagePercent = int(float64(userUsage[view.DID]) / float64(*limit) * 100) 104 + } 105 + } else { 106 + view.TierLimit = "Unlimited" 107 + } 108 + } else { 109 + view.TierLimit = "Unlimited" 110 + } 111 + 112 + view.CurrentUsage = userUsage[view.DID] 113 + view.UsageHuman = formatHumanBytes(view.CurrentUsage) 114 + 115 + crewViews = append(crewViews, view) 116 + } 117 + 118 + // Sort by usage (highest first) 119 + sort.Slice(crewViews, func(i, j int) bool { 120 + return crewViews[i].CurrentUsage > crewViews[j].CurrentUsage 121 + }) 122 + 123 + data := struct { 124 + PageData 125 + Crew []CrewMemberView 126 + Tiers []TierOption 127 + }{ 128 + PageData: ui.newPageData(r, "Crew Management", "crew"), 129 + Crew: crewViews, 130 + Tiers: ui.getTierOptions(), 131 + } 132 + 133 + ui.renderTemplate(w, "pages/crew.html", data) 134 + } 135 + 136 + // handleCrewAddForm displays the add crew form 137 + func (ui *AdminUI) handleCrewAddForm(w http.ResponseWriter, r *http.Request) { 138 + defer clearFlash(w) 139 + 140 + data := struct { 141 + PageData 142 + Tiers []TierOption 143 + }{ 144 + PageData: ui.newPageData(r, "Add Crew Member", "crew"), 145 + Tiers: ui.getTierOptions(), 146 + } 147 + 148 + ui.renderTemplate(w, "pages/crew_add.html", data) 149 + } 150 + 151 + // handleCrewAdd processes adding a new crew member 152 + func (ui *AdminUI) handleCrewAdd(w http.ResponseWriter, r *http.Request) { 153 + ctx := r.Context() 154 + 155 + if err := r.ParseForm(); err != nil { 156 + setFlash(w, r, "error", "Invalid form data") 157 + http.Redirect(w, r, "/admin/crew/add", http.StatusFound) 158 + return 159 + } 160 + 161 + did := strings.TrimSpace(r.FormValue("did")) 162 + role := r.FormValue("role") 163 + tier := r.FormValue("tier") 164 + 165 + // Parse permissions checkboxes 166 + var permissions []string 167 + if r.FormValue("perm_read") == "on" { 168 + permissions = append(permissions, "blob:read") 169 + } 170 + if r.FormValue("perm_write") == "on" { 171 + permissions = append(permissions, "blob:write") 172 + } 173 + if r.FormValue("perm_admin") == "on" { 174 + permissions = append(permissions, "crew:admin") 175 + } 176 + 177 + // Validate DID format 178 + if !strings.HasPrefix(did, "did:") { 179 + setFlash(w, r, "error", "Invalid DID format (must start with did:)") 180 + http.Redirect(w, r, "/admin/crew/add", http.StatusFound) 181 + return 182 + } 183 + 184 + // Default role 185 + if role == "" { 186 + role = "member" 187 + } 188 + 189 + // Add crew member 190 + _, err := ui.pds.AddCrewMember(ctx, did, role, permissions) 191 + if err != nil { 192 + slog.Error("Failed to add crew member", "did", did, "error", err) 193 + setFlash(w, r, "error", "Failed to add crew member: "+err.Error()) 194 + http.Redirect(w, r, "/admin/crew/add", http.StatusFound) 195 + return 196 + } 197 + 198 + // Update tier if specified and different from default 199 + defaultTier := "default" 200 + if ui.quotaMgr != nil && ui.quotaMgr.IsEnabled() { 201 + defaultTier = ui.quotaMgr.GetDefaultTier() 202 + } 203 + 204 + if tier != "" && tier != defaultTier { 205 + if err := ui.pds.UpdateCrewMemberTier(ctx, did, tier); err != nil { 206 + slog.Warn("Failed to set tier for new crew member", "did", did, "tier", tier, "error", err) 207 + } 208 + } 209 + 210 + session := getSessionFromContext(ctx) 211 + slog.Info("Crew member added via admin panel", 212 + "did", did, 213 + "role", role, 214 + "permissions", permissions, 215 + "by", session.DID) 216 + 217 + setFlash(w, r, "success", "Crew member added successfully") 218 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 219 + } 220 + 221 + // handleCrewEditForm displays the edit crew form 222 + func (ui *AdminUI) handleCrewEditForm(w http.ResponseWriter, r *http.Request) { 223 + ctx := r.Context() 224 + rkey := chi.URLParam(r, "rkey") 225 + defer clearFlash(w) 226 + 227 + _, member, err := ui.pds.GetCrewMember(ctx, rkey) 228 + if err != nil { 229 + setFlash(w, r, "error", "Crew member not found") 230 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 231 + return 232 + } 233 + 234 + // Check if this is the owner 235 + _, captain, _ := ui.pds.GetCaptainRecord(ctx) 236 + isOwner := captain != nil && member.Member == captain.Owner 237 + 238 + // Resolve handle for display 239 + memberHandle := resolveHandle(ctx, member.Member) 240 + 241 + data := struct { 242 + PageData 243 + Member *atproto.CrewRecord 244 + MemberHandle string 245 + RKey string 246 + IsOwner bool 247 + Tiers []TierOption 248 + }{ 249 + PageData: ui.newPageData(r, "Edit Crew Member", "crew"), 250 + Member: member, 251 + MemberHandle: memberHandle, 252 + RKey: rkey, 253 + IsOwner: isOwner, 254 + Tiers: ui.getTierOptions(), 255 + } 256 + 257 + ui.renderTemplate(w, "pages/crew_edit.html", data) 258 + } 259 + 260 + // handleCrewUpdate processes updating a crew member 261 + func (ui *AdminUI) handleCrewUpdate(w http.ResponseWriter, r *http.Request) { 262 + ctx := r.Context() 263 + rkey := chi.URLParam(r, "rkey") 264 + 265 + if err := r.ParseForm(); err != nil { 266 + setFlash(w, r, "error", "Invalid form data") 267 + http.Redirect(w, r, "/admin/crew/"+rkey, http.StatusFound) 268 + return 269 + } 270 + 271 + // Get current crew member 272 + _, current, err := ui.pds.GetCrewMember(ctx, rkey) 273 + if err != nil { 274 + setFlash(w, r, "error", "Crew member not found") 275 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 276 + return 277 + } 278 + 279 + // Parse new values 280 + role := r.FormValue("role") 281 + tier := r.FormValue("tier") 282 + 283 + var permissions []string 284 + if r.FormValue("perm_read") == "on" { 285 + permissions = append(permissions, "blob:read") 286 + } 287 + if r.FormValue("perm_write") == "on" { 288 + permissions = append(permissions, "blob:write") 289 + } 290 + if r.FormValue("perm_admin") == "on" { 291 + permissions = append(permissions, "crew:admin") 292 + } 293 + 294 + // Update tier if changed 295 + if tier != current.Tier { 296 + if err := ui.pds.UpdateCrewMemberTier(ctx, current.Member, tier); err != nil { 297 + setFlash(w, r, "error", "Failed to update tier: "+err.Error()) 298 + http.Redirect(w, r, "/admin/crew/"+rkey, http.StatusFound) 299 + return 300 + } 301 + } 302 + 303 + // For role/permissions changes, need to delete and recreate 304 + // (ATProto records are immutable, updates require delete+create) 305 + if role != current.Role || !slicesEqual(permissions, current.Permissions) { 306 + // Delete old record 307 + if err := ui.pds.RemoveCrewMember(ctx, rkey); err != nil { 308 + setFlash(w, r, "error", "Failed to update: "+err.Error()) 309 + http.Redirect(w, r, "/admin/crew/"+rkey, http.StatusFound) 310 + return 311 + } 312 + 313 + // Create new record with updated values 314 + if _, err := ui.pds.AddCrewMember(ctx, current.Member, role, permissions); err != nil { 315 + setFlash(w, r, "error", "Failed to recreate crew record: "+err.Error()) 316 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 317 + return 318 + } 319 + 320 + // Re-apply tier to new record 321 + if tier != "" { 322 + ui.pds.UpdateCrewMemberTier(ctx, current.Member, tier) 323 + } 324 + } 325 + 326 + session := getSessionFromContext(ctx) 327 + slog.Info("Crew member updated via admin panel", 328 + "did", current.Member, 329 + "role", role, 330 + "permissions", permissions, 331 + "by", session.DID) 332 + 333 + setFlash(w, r, "success", "Crew member updated successfully") 334 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 335 + } 336 + 337 + // handleCrewDelete removes a crew member 338 + func (ui *AdminUI) handleCrewDelete(w http.ResponseWriter, r *http.Request) { 339 + ctx := r.Context() 340 + rkey := chi.URLParam(r, "rkey") 341 + 342 + // Get crew member to log who was deleted 343 + _, member, err := ui.pds.GetCrewMember(ctx, rkey) 344 + if err != nil { 345 + setFlash(w, r, "error", "Crew member not found") 346 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 347 + return 348 + } 349 + 350 + // Prevent deleting self (captain) 351 + session := getSessionFromContext(ctx) 352 + if member.Member == session.DID { 353 + setFlash(w, r, "error", "Cannot remove yourself from crew") 354 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 355 + return 356 + } 357 + 358 + // Delete 359 + if err := ui.pds.RemoveCrewMember(ctx, rkey); err != nil { 360 + setFlash(w, r, "error", "Failed to remove crew member: "+err.Error()) 361 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 362 + return 363 + } 364 + 365 + slog.Info("Crew member removed via admin panel", "did", member.Member, "by", session.DID) 366 + 367 + // For HTMX requests, return empty response (row will be removed) 368 + if r.Header.Get("HX-Request") == "true" { 369 + w.WriteHeader(http.StatusOK) 370 + return 371 + } 372 + 373 + setFlash(w, r, "success", "Crew member removed") 374 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 375 + } 376 + 377 + // getTierOptions returns available tier options for forms 378 + func (ui *AdminUI) getTierOptions() []TierOption { 379 + if ui.quotaMgr == nil || !ui.quotaMgr.IsEnabled() { 380 + return []TierOption{{Key: "default", Name: "Default", Limit: "Unlimited"}} 381 + } 382 + 383 + tiers := ui.quotaMgr.ListTiers() 384 + options := make([]TierOption, 0, len(tiers)) 385 + 386 + for _, t := range tiers { 387 + limit := "Unlimited" 388 + if t.Limit != nil { 389 + limit = formatHumanBytes(*t.Limit) 390 + } 391 + options = append(options, TierOption{ 392 + Key: t.Key, 393 + Name: t.Key, 394 + Limit: limit, 395 + }) 396 + } 397 + 398 + return options 399 + } 400 + 401 + // slicesEqual checks if two string slices contain the same elements 402 + func slicesEqual(a, b []string) bool { 403 + if len(a) != len(b) { 404 + return false 405 + } 406 + aMap := make(map[string]bool) 407 + for _, v := range a { 408 + aMap[v] = true 409 + } 410 + for _, v := range b { 411 + if !aMap[v] { 412 + return false 413 + } 414 + } 415 + return true 416 + } 417 + 418 + // parseTime parses an RFC3339 timestamp 419 + func parseTime(s string) time.Time { 420 + t, _ := time.Parse(time.RFC3339, s) 421 + return t 422 + }
+91
pkg/hold/admin/handlers_settings.go
··· 1 + package admin 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + ) 7 + 8 + // handleSettings displays the settings page 9 + func (ui *AdminUI) handleSettings(w http.ResponseWriter, r *http.Request) { 10 + ctx := r.Context() 11 + defer clearFlash(w) 12 + 13 + _, captain, err := ui.pds.GetCaptainRecord(ctx) 14 + if err != nil { 15 + ui.renderError(w, r, "Failed to load settings: "+err.Error(), http.StatusInternalServerError) 16 + return 17 + } 18 + 19 + // Resolve owner handle 20 + ownerHandle := resolveHandle(ctx, captain.Owner) 21 + 22 + // Get quota info 23 + quotasEnabled := ui.quotaMgr != nil && ui.quotaMgr.IsEnabled() 24 + tierCount := 0 25 + defaultTier := "" 26 + if quotasEnabled { 27 + tierCount = ui.quotaMgr.TierCount() 28 + defaultTier = ui.quotaMgr.GetDefaultTier() 29 + } 30 + 31 + data := struct { 32 + PageData 33 + Settings struct { 34 + Public bool 35 + AllowAllCrew bool 36 + EnableBlueskyPosts bool 37 + OwnerDID string 38 + OwnerHandle string 39 + HoldDID string 40 + QuotasEnabled bool 41 + TierCount int 42 + DefaultTier string 43 + } 44 + }{ 45 + PageData: ui.newPageData(r, "Settings", "settings"), 46 + } 47 + data.Settings.Public = captain.Public 48 + data.Settings.AllowAllCrew = captain.AllowAllCrew 49 + data.Settings.EnableBlueskyPosts = captain.EnableBlueskyPosts 50 + data.Settings.OwnerDID = captain.Owner 51 + data.Settings.OwnerHandle = ownerHandle 52 + data.Settings.HoldDID = ui.pds.DID() 53 + data.Settings.QuotasEnabled = quotasEnabled 54 + data.Settings.TierCount = tierCount 55 + data.Settings.DefaultTier = defaultTier 56 + 57 + ui.renderTemplate(w, "pages/settings.html", data) 58 + } 59 + 60 + // handleSettingsUpdate processes settings updates 61 + func (ui *AdminUI) handleSettingsUpdate(w http.ResponseWriter, r *http.Request) { 62 + ctx := r.Context() 63 + 64 + if err := r.ParseForm(); err != nil { 65 + setFlash(w, r, "error", "Invalid form data") 66 + http.Redirect(w, r, "/admin/settings", http.StatusFound) 67 + return 68 + } 69 + 70 + public := r.FormValue("public") == "on" 71 + allowAllCrew := r.FormValue("allow_all_crew") == "on" 72 + enablePosts := r.FormValue("enable_bluesky_posts") == "on" 73 + 74 + _, err := ui.pds.UpdateCaptainRecord(ctx, public, allowAllCrew, enablePosts) 75 + if err != nil { 76 + slog.Error("Failed to update captain record", "error", err) 77 + setFlash(w, r, "error", "Failed to update settings: "+err.Error()) 78 + http.Redirect(w, r, "/admin/settings", http.StatusFound) 79 + return 80 + } 81 + 82 + session := getSessionFromContext(ctx) 83 + slog.Info("Settings updated via admin panel", 84 + "public", public, 85 + "allowAllCrew", allowAllCrew, 86 + "enableBlueskyPosts", enablePosts, 87 + "by", session.DID) 88 + 89 + setFlash(w, r, "success", "Settings updated successfully") 90 + http.Redirect(w, r, "/admin/settings", http.StatusFound) 91 + }
+692
pkg/hold/admin/static/css/admin.css
··· 1 + /* Hold Admin Panel Styles */ 2 + 3 + :root { 4 + --primary: #2563eb; 5 + --primary-hover: #1d4ed8; 6 + --danger: #dc2626; 7 + --danger-hover: #b91c1c; 8 + --warning: #f59e0b; 9 + --success: #10b981; 10 + --gray-50: #f9fafb; 11 + --gray-100: #f3f4f6; 12 + --gray-200: #e5e7eb; 13 + --gray-300: #d1d5db; 14 + --gray-500: #6b7280; 15 + --gray-700: #374151; 16 + --gray-900: #111827; 17 + } 18 + 19 + * { 20 + box-sizing: border-box; 21 + margin: 0; 22 + padding: 0; 23 + } 24 + 25 + body { 26 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 27 + background: var(--gray-50); 28 + color: var(--gray-900); 29 + line-height: 1.5; 30 + } 31 + 32 + /* Navigation */ 33 + .nav { 34 + background: var(--gray-900); 35 + color: white; 36 + padding: 1rem 2rem; 37 + display: flex; 38 + align-items: center; 39 + gap: 2rem; 40 + } 41 + 42 + .nav-brand a { 43 + color: white; 44 + text-decoration: none; 45 + font-weight: 600; 46 + font-size: 1.25rem; 47 + } 48 + 49 + .nav-links { 50 + list-style: none; 51 + display: flex; 52 + gap: 1rem; 53 + } 54 + 55 + .nav-links a { 56 + color: var(--gray-300); 57 + text-decoration: none; 58 + padding: 0.5rem 1rem; 59 + border-radius: 0.375rem; 60 + transition: background 0.2s; 61 + } 62 + 63 + .nav-links a:hover, 64 + .nav-links a.active { 65 + background: rgba(255, 255, 255, 0.1); 66 + color: white; 67 + } 68 + 69 + .nav-user { 70 + margin-left: auto; 71 + display: flex; 72 + align-items: center; 73 + gap: 1rem; 74 + color: var(--gray-300); 75 + } 76 + 77 + /* Container */ 78 + .container { 79 + max-width: 1200px; 80 + margin: 0 auto; 81 + padding: 2rem; 82 + } 83 + 84 + /* Page Header */ 85 + .page-header { 86 + display: flex; 87 + justify-content: space-between; 88 + align-items: center; 89 + margin-bottom: 2rem; 90 + } 91 + 92 + .page-header h1 { 93 + margin: 0; 94 + } 95 + 96 + /* Buttons */ 97 + .btn { 98 + display: inline-flex; 99 + align-items: center; 100 + justify-content: center; 101 + padding: 0.5rem 1rem; 102 + border: none; 103 + border-radius: 0.375rem; 104 + font-size: 0.875rem; 105 + font-weight: 500; 106 + text-decoration: none; 107 + cursor: pointer; 108 + transition: background 0.2s; 109 + background: var(--gray-200); 110 + color: var(--gray-700); 111 + } 112 + 113 + .btn:hover { 114 + background: var(--gray-300); 115 + } 116 + 117 + .btn-primary { 118 + background: var(--primary); 119 + color: white; 120 + } 121 + 122 + .btn-primary:hover { 123 + background: var(--primary-hover); 124 + } 125 + 126 + .btn-danger { 127 + background: var(--danger); 128 + color: white; 129 + } 130 + 131 + .btn-danger:hover { 132 + background: var(--danger-hover); 133 + } 134 + 135 + .btn-sm { 136 + padding: 0.25rem 0.5rem; 137 + font-size: 0.75rem; 138 + } 139 + 140 + .btn-icon { 141 + padding: 0.375rem; 142 + line-height: 1; 143 + } 144 + 145 + .btn-icon i { 146 + width: 16px; 147 + height: 16px; 148 + } 149 + 150 + .btn i { 151 + width: 16px; 152 + height: 16px; 153 + margin-right: 0.25rem; 154 + } 155 + 156 + .btn-icon i { 157 + margin-right: 0; 158 + } 159 + 160 + .btn-block { 161 + width: 100%; 162 + } 163 + 164 + /* Cards */ 165 + .card { 166 + background: white; 167 + border-radius: 0.5rem; 168 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 169 + overflow: hidden; 170 + } 171 + 172 + .card-header { 173 + padding: 1rem; 174 + background: var(--gray-50); 175 + border-bottom: 1px solid var(--gray-200); 176 + } 177 + 178 + .member-header { 179 + display: flex; 180 + justify-content: space-between; 181 + align-items: center; 182 + } 183 + 184 + .member-info { 185 + display: flex; 186 + flex-direction: column; 187 + gap: 0.25rem; 188 + } 189 + 190 + .member-info strong { 191 + font-size: 1.1rem; 192 + } 193 + 194 + /* Stats Grid */ 195 + .stats-grid { 196 + display: grid; 197 + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 198 + gap: 1rem; 199 + margin-bottom: 2rem; 200 + } 201 + 202 + .stat-card { 203 + background: white; 204 + padding: 1.5rem; 205 + border-radius: 0.5rem; 206 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 207 + } 208 + 209 + .stat-card h3 { 210 + font-size: 0.875rem; 211 + color: var(--gray-500); 212 + margin-bottom: 0.5rem; 213 + } 214 + 215 + .stat-value { 216 + font-size: 2rem; 217 + font-weight: 600; 218 + } 219 + 220 + .stat-detail { 221 + font-size: 0.875rem; 222 + color: var(--gray-500); 223 + } 224 + 225 + /* Sections */ 226 + .section { 227 + background: white; 228 + padding: 1.5rem; 229 + border-radius: 0.5rem; 230 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 231 + margin-bottom: 1.5rem; 232 + } 233 + 234 + .section h2 { 235 + font-size: 1.125rem; 236 + margin-bottom: 1rem; 237 + } 238 + 239 + /* Tables */ 240 + .table { 241 + width: 100%; 242 + border-collapse: collapse; 243 + background: white; 244 + border-radius: 0.5rem; 245 + overflow: hidden; 246 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 247 + } 248 + 249 + .table th, 250 + .table td { 251 + padding: 0.75rem 1rem; 252 + text-align: left; 253 + border-bottom: 1px solid var(--gray-200); 254 + } 255 + 256 + .table th { 257 + background: var(--gray-50); 258 + font-weight: 600; 259 + font-size: 0.75rem; 260 + text-transform: uppercase; 261 + color: var(--gray-500); 262 + } 263 + 264 + .table td { 265 + font-size: 0.875rem; 266 + } 267 + 268 + .table tbody tr:hover { 269 + background: var(--gray-50); 270 + } 271 + 272 + .actions { 273 + display: flex; 274 + gap: 0.25rem; 275 + justify-content: flex-end; 276 + } 277 + 278 + .actions-header { 279 + text-align: right; 280 + } 281 + 282 + .member-cell { 283 + line-height: 1.4; 284 + } 285 + 286 + .member-cell strong { 287 + color: var(--gray-900); 288 + } 289 + 290 + .did-code { 291 + font-size: 0.75rem; 292 + color: var(--gray-500); 293 + word-break: break-all; 294 + } 295 + 296 + .permissions-cell .badge { 297 + margin-right: 0.25rem; 298 + margin-bottom: 0.25rem; 299 + } 300 + 301 + .tier-limit { 302 + color: var(--gray-500); 303 + } 304 + 305 + /* Badges */ 306 + .badge { 307 + display: inline-block; 308 + padding: 0.125rem 0.5rem; 309 + font-size: 0.75rem; 310 + border-radius: 9999px; 311 + background: var(--gray-200); 312 + color: var(--gray-700); 313 + } 314 + 315 + .badge-tier { 316 + background: var(--primary); 317 + color: white; 318 + } 319 + 320 + .badge-gold { 321 + background: #fbbf24; 322 + color: #78350f; 323 + } 324 + 325 + /* Progress Bar */ 326 + .usage-cell { 327 + display: flex; 328 + flex-direction: column; 329 + gap: 0.25rem; 330 + } 331 + 332 + .progress-bar { 333 + width: 100%; 334 + height: 4px; 335 + background: var(--gray-200); 336 + border-radius: 2px; 337 + overflow: hidden; 338 + } 339 + 340 + .progress-fill { 341 + height: 100%; 342 + background: var(--primary); 343 + transition: width 0.3s; 344 + } 345 + 346 + .progress-fill.warning { 347 + background: var(--warning); 348 + } 349 + 350 + .progress-fill.danger { 351 + background: var(--danger); 352 + } 353 + 354 + /* Forms */ 355 + .form { 356 + max-width: 600px; 357 + } 358 + 359 + .form-group { 360 + margin-bottom: 1.5rem; 361 + } 362 + 363 + .form-group label { 364 + display: block; 365 + font-weight: 500; 366 + margin-bottom: 0.5rem; 367 + } 368 + 369 + .form-group input[type="text"], 370 + .form-group input[type="email"], 371 + .form-group select { 372 + width: 100%; 373 + padding: 0.5rem 0.75rem; 374 + border: 1px solid var(--gray-300); 375 + border-radius: 0.375rem; 376 + font-size: 1rem; 377 + } 378 + 379 + .form-group input:focus, 380 + .form-group select:focus { 381 + outline: none; 382 + border-color: var(--primary); 383 + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); 384 + } 385 + 386 + .form-group small { 387 + display: block; 388 + margin-top: 0.25rem; 389 + font-size: 0.75rem; 390 + color: var(--gray-500); 391 + } 392 + 393 + /* Input with lookup button */ 394 + .input-with-lookup { 395 + display: flex; 396 + gap: 0.5rem; 397 + } 398 + 399 + .input-with-lookup input { 400 + flex: 1; 401 + } 402 + 403 + .handle-lookup-result { 404 + margin-top: 0.5rem; 405 + font-size: 0.875rem; 406 + } 407 + 408 + .handle-lookup-result .success { 409 + color: var(--success); 410 + display: flex; 411 + align-items: center; 412 + gap: 0.25rem; 413 + } 414 + 415 + .handle-lookup-result .success i { 416 + width: 16px; 417 + height: 16px; 418 + } 419 + 420 + .handle-lookup-result .error { 421 + color: var(--danger); 422 + } 423 + 424 + .handle-lookup-result .warning { 425 + color: var(--warning); 426 + } 427 + 428 + .handle-lookup-result .loading { 429 + color: var(--gray-500); 430 + font-style: italic; 431 + } 432 + 433 + .checkbox-group { 434 + display: flex; 435 + flex-direction: column; 436 + gap: 0.75rem; 437 + } 438 + 439 + .checkbox { 440 + display: flex; 441 + align-items: flex-start; 442 + gap: 0.5rem; 443 + cursor: pointer; 444 + } 445 + 446 + .checkbox input { 447 + margin-top: 0.25rem; 448 + } 449 + 450 + .checkbox span { 451 + font-weight: 500; 452 + } 453 + 454 + .checkbox small { 455 + display: block; 456 + font-weight: normal; 457 + color: var(--gray-500); 458 + } 459 + 460 + .form-actions { 461 + display: flex; 462 + gap: 1rem; 463 + margin-top: 2rem; 464 + } 465 + 466 + /* Toggle Settings */ 467 + .toggle-setting { 468 + display: flex; 469 + align-items: flex-start; 470 + gap: 1rem; 471 + padding: 1rem; 472 + background: var(--gray-50); 473 + border-radius: 0.375rem; 474 + margin-bottom: 1rem; 475 + cursor: pointer; 476 + } 477 + 478 + .toggle-setting input { 479 + margin-top: 0.25rem; 480 + } 481 + 482 + .toggle-label strong { 483 + display: block; 484 + } 485 + 486 + .toggle-label small { 487 + color: var(--gray-500); 488 + } 489 + 490 + /* Info List */ 491 + .info-list { 492 + display: grid; 493 + grid-template-columns: auto 1fr; 494 + gap: 0.5rem 1rem; 495 + } 496 + 497 + .info-list dt { 498 + font-weight: 500; 499 + color: var(--gray-500); 500 + } 501 + 502 + .info-list dd { 503 + font-family: monospace; 504 + } 505 + 506 + /* Flash Messages */ 507 + .flash { 508 + padding: 1rem; 509 + border-radius: 0.375rem; 510 + margin-bottom: 1rem; 511 + } 512 + 513 + .flash-success { 514 + background: #d1fae5; 515 + color: #065f46; 516 + } 517 + 518 + .flash-error { 519 + background: #fee2e2; 520 + color: #991b1b; 521 + } 522 + 523 + .flash-warning { 524 + background: #fef3c7; 525 + color: #92400e; 526 + } 527 + 528 + .flash-info { 529 + background: #dbeafe; 530 + color: #1e40af; 531 + } 532 + 533 + /* Empty State */ 534 + .empty { 535 + text-align: center; 536 + padding: 2rem; 537 + color: var(--gray-500); 538 + } 539 + 540 + /* Loading */ 541 + .loading { 542 + color: var(--gray-500); 543 + font-style: italic; 544 + } 545 + 546 + /* Note */ 547 + .note { 548 + padding: 1rem; 549 + background: var(--gray-100); 550 + border-radius: 0.375rem; 551 + color: var(--gray-500); 552 + font-style: italic; 553 + } 554 + 555 + /* Footer */ 556 + .footer { 557 + text-align: center; 558 + padding: 2rem; 559 + color: var(--gray-500); 560 + font-size: 0.875rem; 561 + } 562 + 563 + .footer code { 564 + font-size: 0.75rem; 565 + background: var(--gray-200); 566 + padding: 0.125rem 0.375rem; 567 + border-radius: 0.25rem; 568 + } 569 + 570 + /* Login Page */ 571 + .login-page { 572 + background: var(--gray-100); 573 + min-height: 100vh; 574 + display: flex; 575 + align-items: center; 576 + justify-content: center; 577 + } 578 + 579 + .login-container { 580 + width: 100%; 581 + max-width: 400px; 582 + padding: 1rem; 583 + } 584 + 585 + .login-card { 586 + background: white; 587 + padding: 2rem; 588 + border-radius: 0.5rem; 589 + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 590 + } 591 + 592 + .login-card h1 { 593 + text-align: center; 594 + margin-bottom: 0.5rem; 595 + } 596 + 597 + .login-subtitle { 598 + text-align: center; 599 + color: var(--gray-500); 600 + margin-bottom: 1.5rem; 601 + } 602 + 603 + .login-form .form-group { 604 + margin-bottom: 1rem; 605 + } 606 + 607 + .login-note { 608 + text-align: center; 609 + font-size: 0.875rem; 610 + color: var(--gray-500); 611 + margin-top: 1rem; 612 + } 613 + 614 + .login-footer { 615 + text-align: center; 616 + margin-top: 2rem; 617 + color: var(--gray-500); 618 + font-size: 0.75rem; 619 + } 620 + 621 + /* Error Page */ 622 + .error-page { 623 + text-align: center; 624 + padding: 4rem 2rem; 625 + } 626 + 627 + .error-message { 628 + color: var(--danger); 629 + margin: 1rem 0 2rem; 630 + } 631 + 632 + /* Tier Chart */ 633 + .tier-chart { 634 + display: flex; 635 + flex-direction: column; 636 + gap: 0.5rem; 637 + } 638 + 639 + .tier-bar { 640 + display: flex; 641 + justify-content: space-between; 642 + padding: 0.5rem 1rem; 643 + background: var(--gray-100); 644 + border-radius: 0.25rem; 645 + } 646 + 647 + .tier-name { 648 + font-weight: 500; 649 + } 650 + 651 + .tier-count { 652 + color: var(--gray-500); 653 + } 654 + 655 + /* Code */ 656 + code { 657 + font-family: "SF Mono", Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", monospace; 658 + font-size: 0.875em; 659 + background: var(--gray-100); 660 + padding: 0.125rem 0.25rem; 661 + border-radius: 0.25rem; 662 + } 663 + 664 + /* Responsive */ 665 + @media (max-width: 768px) { 666 + .nav { 667 + flex-wrap: wrap; 668 + padding: 1rem; 669 + } 670 + 671 + .nav-links { 672 + order: 3; 673 + width: 100%; 674 + justify-content: center; 675 + margin-top: 0.5rem; 676 + } 677 + 678 + .container { 679 + padding: 1rem; 680 + } 681 + 682 + .page-header { 683 + flex-direction: column; 684 + align-items: flex-start; 685 + gap: 1rem; 686 + } 687 + 688 + .table { 689 + display: block; 690 + overflow-x: auto; 691 + } 692 + }
+1
pkg/hold/admin/static/js/htmx.min.js
··· 1 + var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=dn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true,historyRestoreAsHxRequest:true,reportValidityOfForms:false},parseInterval:null,location:location,_:null,version:"2.0.8"};Q.onLoad=V;Q.process=Ft;Q.on=xe;Q.off=be;Q.trigger=ae;Q.ajax=Ln;Q.find=f;Q.findAll=x;Q.closest=g;Q.remove=_;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=ze;Q.defineExtension=_n;Q.removeExtension=zn;Q.logAll=j;Q.logNone=$;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:se,canAccessLocalStorage:X,findThisElement:Se,filterValues:yn,swap:ze,hasAttribute:s,getAttributeValue:a,getClosestAttributeValue:ne,getClosestMatch:q,getExpressionVars:Rn,getHeaders:mn,getInputValues:dn,getInternalData:oe,getSwapSpecification:bn,getTriggerSpecs:st,getTarget:Ee,makeFragment:D,mergeObjects:le,makeSettleInfo:Sn,oobSwap:Te,querySelectorExt:ue,settleImmediately:Yt,shouldCancel:ht,triggerEvent:ae,triggerErrorEvent:fe,withExtensions:Vt};const de=["get","post","put","delete","patch"];const R=de.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function a(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function te(){return document}function y(e,t){return e.getRootNode?e.getRootNode({composed:t}):te()}function q(e,t){while(e&&!t(e)){e=u(e)}return e||null}function o(e,t,n){const r=a(t,n);const o=a(t,"hx-disinherit");var i=a(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function ne(t,n){let r=null;q(t,function(e){return!!(r=o(t,ce(e),n))});if(r!=="unset"){return r}}function h(e,t){return e instanceof Element&&e.matches(t)}function A(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function L(e){if("parseHTMLUnsafe"in Document){return Document.parseHTMLUnsafe(e)}const t=new DOMParser;return t.parseFromString(e,"text/html")}function N(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function r(e){const t=te().createElement("script");ie(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function i(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(i(e)){const t=r(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){H(e)}finally{e.remove()}}})}function D(e){const t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,"");const n=A(t);let r;if(n==="html"){r=new DocumentFragment;const i=L(e);N(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=L(t);N(r,i.body);r.title=i.title}else{const i=L('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function re(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function P(e){return typeof e==="function"}function k(e){return t(e,"Object")}function oe(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e<t.length;e++){n.push(t[e])}}return n}function ie(t,n){if(t){for(let e=0;e<t.length;e++){n(t[e])}}}function F(e){const t=e.getBoundingClientRect();const n=t.top;const r=t.bottom;return n<window.innerHeight&&r>=0}function se(e){return e.getRootNode({composed:true})===document}function B(e){return e.trim().split(/\s+/)}function le(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function v(e){try{return JSON.parse(e)}catch(e){H(e);return null}}function X(){const e="htmx:sessionStorageTest";try{sessionStorage.setItem(e,e);sessionStorage.removeItem(e);return true}catch(e){return false}}function U(e){const t=new URL(e,"http://x");if(t){e=t.pathname+t.search}if(e!="/"){e=e.replace(/\/+$/,"")}return e}function e(e){return On(te().body,function(){return eval(e)})}function V(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function j(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function $(){Q.logger=null}function f(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return f(te(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(te(),e)}}function b(){return window}function _(e,t){e=w(e);if(t){b().setTimeout(function(){_(e);e=null},t)}else{u(e).removeChild(e)}}function ce(e){return e instanceof Element?e:null}function z(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function p(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ce(w(e));if(!e){return}if(n){b().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ce(w(e));if(!r){return}if(n){b().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=w(e);e.classList.toggle(t)}function Z(e,t){e=w(e);ie(e.parentElement.children,function(e){G(e,t)});K(ce(e),t)}function g(e,t){e=ce(w(e));if(e){return e.closest(t)}return null}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function pe(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function m(t,r,n){if(r.indexOf("global ")===0){return m(t,r.slice(7),true)}t=w(t);const o=[];{let t=0;let n=0;for(let e=0;e<r.length;e++){const l=r[e];if(l===","&&t===0){o.push(r.substring(n,e));n=e+1;continue}if(l==="<"){t++}else if(l==="/"&&e<r.length-1&&r[e+1]===">"){t--}}if(n<r.length){o.push(r.substring(n))}}const i=[];const s=[];while(o.length>0){const r=pe(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ce(t),pe(r.slice(8)))}else if(r.indexOf("find ")===0){e=f(p(t),pe(r.slice(5)))}else if(r==="next"||r==="nextElementSibling"){e=ce(t).nextElementSibling}else if(r.indexOf("next ")===0){e=ge(t,pe(r.slice(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ce(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=me(t,pe(r.slice(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=y(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const c=p(y(t,!!n));i.push(...M(c.querySelectorAll(e)))}return i}var ge=function(t,e,n){const r=p(y(t,n)).querySelectorAll(e);for(let e=0;e<r.length;e++){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_PRECEDING){return o}}};var me=function(t,e,n){const r=p(y(t,n)).querySelectorAll(e);for(let e=r.length-1;e>=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ue(e,t){if(typeof e!=="string"){return m(e,t)[0]}else{return m(te().body,e)[0]}}function w(e,t){if(typeof e==="string"){return f(p(t)||document,e)}else{return e}}function ye(e,t,n,r){if(P(t)){return{target:te().body,event:J(e),listener:t,options:n}}else{return{target:w(e),event:J(t),listener:n,options:r}}}function xe(t,n,r,o){Gn(function(){const e=ye(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=P(n);return e?n:r}function be(t,n,r){Gn(function(){const e=ye(t,n,r);e.target.removeEventListener(e.event,e.listener)});return P(n)?n:r}const ve=te().createElement("output");function we(t,n){const e=ne(t,n);if(e){if(e==="this"){return[Se(t,n)]}else{const r=m(t,e);const o=/(^|,)(\s*)inherit(\s*)($|,)/.test(e);if(o){const i=ce(q(t,function(e){return e!==t&&s(ce(e),n)}));if(i){r.push(...we(i,n))}}if(r.length===0){H('The selector "'+e+'" on '+n+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ce(q(e,function(e){return a(ce(e),t)!=null}))}function Ee(e){const t=ne(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ue(e,t)}}else{const n=oe(e);if(n.boosted){return te().body}else{return e}}}function Ce(e){return Q.config.attributesToSettle.includes(e)}function Oe(t,n){ie(Array.from(t.attributes),function(e){if(!n.hasAttribute(e.name)&&Ce(e.name)){t.removeAttribute(e.name)}});ie(n.attributes,function(e){if(Ce(e.name)){t.setAttribute(e.name,e.value)}})}function He(t,e){const n=Jn(e);for(let e=0;e<n.length;e++){const r=n[e];try{if(r.isInlineSwap(t)){return true}}catch(e){H(e)}}return t==="outerHTML"}function Te(e,o,i,t){t=t||te();let n="#"+CSS.escape(ee(o,"id"));let s="outerHTML";if(e==="true"){}else if(e.indexOf(":")>0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=m(t,n,false);if(r.length){ie(r,function(e){let t;const n=o.cloneNode(true);t=te().createDocumentFragment();t.appendChild(n);if(!He(s,e)){t=p(n)}const r={shouldSwap:true,target:e,fragment:t};if(!ae(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);$e(s,e,e,t,i);Re()}ie(i.elts,function(e){ae(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(te().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Re(){const e=f("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=f("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){ie(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=a(e,"id");const n=te().getElementById(t);if(n!=null){if(e.moveBefore){let e=f("#--htmx-preserve-pantry--");if(e==null){te().body.insertAdjacentHTML("afterend","<div id='--htmx-preserve-pantry--'></div>");e=f("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Ae(l,e,c){ie(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=p(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Le(e){return function(){G(e,Q.config.addedClass);Ft(ce(e));Ne(p(e));ae(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=z(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function c(e,t,n,r){Ae(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ce(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Le(o))}}}function Ie(e,t){let n=0;while(n<e.length){t=(t<<5)-t+e.charCodeAt(n++)|0}return t}function De(t){let n=0;for(let e=0;e<t.attributes.length;e++){const r=t.attributes[e];if(r.value){n=Ie(r.name,n);n=Ie(r.value,n)}}return n}function Pe(t){const n=oe(t);if(n.onHandlers){for(let e=0;e<n.onHandlers.length;e++){const r=n.onHandlers[e];be(t,r.event,r.listener)}delete n.onHandlers}}function ke(e){const t=oe(e);if(t.timeout){clearTimeout(t.timeout)}if(t.listenerInfos){ie(t.listenerInfos,function(e){if(e.on){be(e.on,e.trigger,e.listener)}})}Pe(e);ie(Object.keys(t),function(e){if(e!=="firstInitCompleted")delete t[e]})}function S(e){ae(e,"htmx:beforeCleanupElement");ke(e);ie(e.children,function(e){S(e)})}function Me(t,e,n){if(t.tagName==="BODY"){return je(t,e,n)}let r;const o=t.previousSibling;const i=u(t);if(!i){return}c(i,t,e,n);if(o==null){r=i.firstChild}else{r=o.nextSibling}n.elts=n.elts.filter(function(e){return e!==t});while(r&&r!==t){if(r instanceof Element){n.elts.push(r)}r=r.nextSibling}S(t);t.remove()}function Fe(e,t,n){return c(e,e.firstChild,t,n)}function Be(e,t,n){return c(u(e),e,t,n)}function Xe(e,t,n){return c(e,null,t,n)}function Ue(e,t,n){return c(u(e),e.nextSibling,t,n)}function Ve(e){S(e);const t=u(e);if(t){return t.removeChild(e)}}function je(e,t,n){const r=e.firstChild;c(e,r,t,n);if(r){while(r.nextSibling){S(r.nextSibling);e.removeChild(r.nextSibling)}S(r);e.removeChild(r)}}function $e(t,e,n,r,o){switch(t){case"none":return;case"outerHTML":Me(n,r,o);return;case"afterbegin":Fe(n,r,o);return;case"beforebegin":Be(n,r,o);return;case"beforeend":Xe(n,r,o);return;case"afterend":Ue(n,r,o);return;case"delete":Ve(n);return;default:var i=Jn(e);for(let e=0;e<i.length;e++){const s=i[e];try{const l=s.handleSwap(t,n,r,o);if(l){if(Array.isArray(l)){for(let e=0;e<l.length;e++){const c=l[e];if(c.nodeType!==Node.TEXT_NODE&&c.nodeType!==Node.COMMENT_NODE){o.tasks.push(Le(c))}}}return}}catch(e){H(e)}}if(t==="innerHTML"){je(n,r,o)}else{$e(Q.config.defaultSwapStyle,e,n,r,o)}}}function _e(e,n,r){var t=x(e,"[hx-swap-oob], [data-hx-swap-oob]");ie(t,function(e){if(Q.config.allowNestedOobSwaps||e.parentElement===null){const t=a(e,"hx-swap-oob");if(t!=null){Te(t,e,n,r)}}else{e.removeAttribute("hx-swap-oob");e.removeAttribute("data-hx-swap-oob")}});return t.length>0}function ze(h,d,p,g){if(!g){g={}}let m=null;let n=null;let e=function(){re(g.beforeSwapCallback);h=w(h);const r=g.contextElement?y(g.contextElement,false):te();const e=document.activeElement;let t={};t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null};const o=Sn(h);if(p.swapStyle==="textContent"){h.textContent=d}else{let n=D(d);o.title=g.title||n.title;if(g.historyRequest){n=n.querySelector("[hx-history-elt],[data-hx-history-elt]")||n}if(g.selectOOB){const i=g.selectOOB.split(",");for(let t=0;t<i.length;t++){const s=i[t].split(":",2);let e=s[0].trim();if(e.indexOf("#")===0){e=e.substring(1)}const l=s[1]||"true";const c=n.querySelector("#"+e);if(c){Te(l,c,o,r)}}}_e(n,o,r);ie(x(n,"template"),function(e){if(e.content&&_e(e.content,o,r)){e.remove()}});if(g.select){const u=te().createDocumentFragment();ie(n.querySelectorAll(g.select),function(e){u.appendChild(e)});n=u}qe(n);$e(p.swapStyle,g.contextElement,h,n,o);Re()}if(t.elt&&!se(t.elt)&&ee(t.elt,"id")){const f=document.getElementById(ee(t.elt,"id"));const a={preventScroll:p.focusScroll!==undefined?!p.focusScroll:!Q.config.defaultFocusScroll};if(f){if(t.start&&f.setSelectionRange){try{f.setSelectionRange(t.start,t.end)}catch(e){}}f.focus(a)}}h.classList.remove(Q.config.swappingClass);ie(o.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}ae(e,"htmx:afterSwap",g.eventInfo)});re(g.afterSwapCallback);if(!p.ignoreTitle){Xn(o.title)}const n=function(){ie(o.tasks,function(e){e.call()});ie(o.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}ae(e,"htmx:afterSettle",g.eventInfo)});if(g.anchor){const e=ce(w("#"+g.anchor));if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}En(o.elts,p);re(g.afterSettleCallback);re(m)};if(p.settleDelay>0){b().setTimeout(n,p.settleDelay)}else{n()}};let t=Q.config.globalViewTransitions;if(p.hasOwnProperty("transition")){t=p.transition}const r=g.contextElement||te();if(t&&ae(r,"htmx:beforeTransition",g.eventInfo)&&typeof Promise!=="undefined"&&document.startViewTransition){const o=new Promise(function(e,t){m=e;n=t});const i=e;e=function(){document.startViewTransition(function(){i();return o})}}try{if(p?.swapDelay&&p.swapDelay>0){b().setTimeout(e,p.swapDelay)}else{e()}}catch(e){fe(r,"htmx:swapError",g.eventInfo);re(n);throw e}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=v(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(k(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}ae(n,i,e)}}}else{const s=r.split(",");for(let e=0;e<s.length;e++){ae(n,s[e].trim(),[])}}}const Ke=/\s/;const E=/[\s,]/;const Ge=/[_$a-zA-Z]/;const We=/[_$a-zA-Z0-9]/;const Ze=['"',"'","/"];const C=/[^\s]/;const Ye=/[{(]/;const Qe=/[})]/;function et(e){const t=[];let n=0;while(n<e.length){if(Ge.exec(e.charAt(n))){var r=n;while(We.exec(e.charAt(n+1))){n++}t.push(e.substring(r,n+1))}else if(Ze.indexOf(e.charAt(n))!==-1){const o=e.charAt(n);var r=n;n++;while(n<e.length&&e.charAt(n)!==o){if(e.charAt(n)==="\\"){n++}n++}t.push(e.substring(r,n+1))}else{const i=e.charAt(n);t.push(i)}n++}return t}function tt(e,t,n){return Ge.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==n&&t!=="."}function nt(r,o,i){if(o[0]==="["){o.shift();let e=1;let t=" return (function("+i+"){ return (";let n=null;while(o.length>0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=On(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(te().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function O(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=O(e,Qe).trim();e.shift()}else{t=O(e,E)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{O(o,C);const l=o.length;const c=O(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};O(o,C);u.pollInterval=d(O(o,/[,\[\s]/));O(o,C);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const f={trigger:c};var i=nt(e,o,"event");if(i){f.eventFilter=i}O(o,C);while(o.length>0&&o[0]!==","){const a=o.shift();if(a==="changed"){f.changed=true}else if(a==="once"){f.once=true}else if(a==="consume"){f.consume=true}else if(a==="delay"&&o[0]===":"){o.shift();f.delay=d(O(o,E))}else if(a==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=O(o,E);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}f.from=s}else if(a==="target"&&o[0]===":"){o.shift();f.target=rt(o)}else if(a==="throttle"&&o[0]===":"){o.shift();f.throttle=d(O(o,E))}else if(a==="queue"&&o[0]===":"){o.shift();f.queue=O(o,E)}else if(a==="root"&&o[0]===":"){o.shift();f[a]=rt(o)}else if(a==="threshold"&&o[0]===":"){o.shift();f[a]=O(o,E)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,C)}r.push(f)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,C)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=a(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){oe(e).cancelled=true}function ct(e,t,n){const r=oe(e);r.timeout=b().setTimeout(function(){if(se(e)&&r.cancelled!==true){if(!pt(n,e,Xt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function ft(e){return g(e,Q.config.disableSelector)}function at(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){gt(t,function(e,t){const n=ce(e);if(ft(n)){S(n);return}he(r,o,n,t)},n,e,true)})}}function ht(e,t){if(e.type==="submit"&&t.tagName==="FORM"){return true}else if(e.type==="click"){const n=t.closest('input[type="submit"], button');if(n&&n.form&&n.type==="submit"){return true}const r=t.closest("a");const o=/^#.+/;if(r&&r.href&&!o.test(r.getAttribute("href"))){return true}}return false}function dt(e,t){return oe(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function pt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(te().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function gt(l,c,e,u,f){const a=oe(l);let t;if(u.from){t=m(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in a)){a.lastValue=new WeakMap}t.forEach(function(e){if(!a.lastValue.has(u)){a.lastValue.set(u,new WeakMap)}a.lastValue.get(u).set(e,e.value)})}ie(t,function(i){const s=function(e){if(!se(l)){i.removeEventListener(u.trigger,s);return}if(dt(l,e)){return}if(f||ht(e,i)){e.preventDefault()}if(pt(u,l,e)){return}const t=oe(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!h(ce(e.target),u.target)){return}}if(u.once){if(a.triggeredOnce){return}else{a.triggeredOnce=true}}if(u.changed){const n=e.target;const r=n.value;const o=a.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(a.delayed){clearTimeout(a.delayed)}if(a.throttle){return}if(u.throttle>0){if(!a.throttle){ae(l,"htmx:trigger");c(l,e);a.throttle=b().setTimeout(function(){a.throttle=null},u.throttle)}}else if(u.delay>0){a.delayed=b().setTimeout(function(){ae(l,"htmx:trigger");c(l,e)},u.delay)}else{ae(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let yt=null;function xt(){if(!yt){yt=function(){mt=true};window.addEventListener("scroll",yt);window.addEventListener("resize",yt);setInterval(function(){if(mt){mt=false;ie(te().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&F(e)){e.setAttribute("data-hx-revealed","true");const t=oe(e);if(t.initHash){ae(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){ae(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;ae(e,"htmx:trigger");t(e)}};if(r>0){b().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;ie(de,function(r){if(s(t,"hx-"+r)){const o=a(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ce(e);if(ft(n)){S(n);return}he(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){xt();gt(r,n,t,e);bt(ce(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ue(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e<t.length;e++){const n=t[e];if(n.isIntersecting){ae(r,"intersect");break}}},o);i.observe(ce(r));gt(ce(r),n,t,e)}else if(!t.firstInitCompleted&&e.trigger==="load"){if(!pt(e,r,Xt("load",{elt:r}))){vt(ce(r),n,t,e.delay)}}else if(e.pollInterval>0){t.polling=true;ct(ce(r),n,e)}else{gt(r,n,t,e)}}function Et(e){const t=ce(e);if(!t){return false}const n=t.attributes;for(let e=0;e<n.length;e++){const r=n[e].name;if(l(r,"hx-on:")||l(r,"data-hx-on:")||l(r,"hx-on-")||l(r,"data-hx-on-")){return true}}return false}const Ct=(new XPathEvaluator).createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or'+' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]');function Ot(e,t){if(Et(e)){t.push(ce(e))}const n=Ct.evaluate(e);let r=null;while(r=n.iterateNext())t.push(ce(r))}function Ht(e){const t=[];if(e instanceof DocumentFragment){for(const n of e.childNodes){Ot(n,t)}}else{Ot(e,t)}return t}function Tt(e){if(e.querySelectorAll){const n=", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]";const r=[];for(const i in jn){const s=jn[i];if(s.getSelectors){var t=s.getSelectors();if(t){r.push(t)}}}const o=e.querySelectorAll(R+n+", form, [type='submit'],"+" [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]"+r.flat().map(e=>", "+e).join(""));return o}else{return[]}}function Rt(e){const t=At(e.target);const n=Nt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Nt(e);if(t){t.lastButtonClicked=null}}function At(e){return g(ce(e),"button, input[type='submit']")}function Lt(e){return e.form||g(e,"form")}function Nt(e){const t=At(e.target);if(!t){return}const n=Lt(t);if(!n){return}return oe(n)}function It(e){e.addEventListener("click",Rt);e.addEventListener("focusin",Rt);e.addEventListener("focusout",qt)}function Dt(t,e,n){const r=oe(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){On(t,function(){if(ft(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function Pt(t){Pe(t);for(let e=0;e<t.attributes.length;e++){const n=t.attributes[e].name;const r=t.attributes[e].value;if(l(n,"hx-on")||l(n,"data-hx-on")){const o=n.indexOf("-on")+3;const i=n.slice(o,o+1);if(i==="-"||i===":"){let e=n.slice(o+1);if(l(e,":")){e="htmx"+e}else if(l(e,"-")){e="htmx:"+e.slice(1)}else if(l(e,"htmx-")){e="htmx:"+e.slice(5)}Dt(t,e,r)}}}}function kt(t){ae(t,"htmx:beforeProcessNode");const n=oe(t);const e=st(t);const r=wt(t,n,e);if(!r){if(ne(t,"hx-boost")==="true"){at(t,n,e)}else if(s(t,"hx-trigger")){e.forEach(function(e){St(t,e,n,function(){})})}}if(t.tagName==="FORM"||ee(t,"type")==="submit"&&s(t,"form")){It(t)}n.firstInitCompleted=true;ae(t,"htmx:afterProcessNode")}function Mt(e){if(!(e instanceof Element)){return false}const t=oe(e);const n=De(e);if(t.initHash!==n){ke(e);t.initHash=n;return true}return false}function Ft(e){e=w(e);if(ft(e)){S(e);return}const t=[];if(Mt(e)){t.push(e)}ie(Tt(e),function(e){if(ft(e)){S(e);return}if(Mt(e)){t.push(e)}});ie(Ht(e),Pt);ie(t,kt)}function Bt(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function Xt(e,t){return new CustomEvent(e,{bubbles:true,cancelable:true,composed:true,detail:t})}function fe(e,t,n){ae(e,t,le({error:t},n))}function Ut(e){return e==="htmx:afterProcessNode"}function Vt(e,t,n){ie(Jn(e,[],n),function(e){try{t(e)}catch(e){H(e)}})}function H(e){console.error(e)}function ae(e,t,n){e=w(e);if(n==null){n={}}n.elt=e;const r=Xt(t,n);if(Q.logger&&!Ut(t)){Q.logger(e,t,n)}if(n.error){H(n.error);ae(e,"htmx:error",{errorInfo:n})}let o=e.dispatchEvent(r);const i=Bt(t);if(o&&i!==t){const s=Xt(i,r.detail);o=o&&e.dispatchEvent(s)}Vt(ce(e),function(e){o=o&&(e.onEvent(t,r)!==false&&!r.defaultPrevented)});return o}let jt;function $t(e){jt=e;if(X()){sessionStorage.setItem("htmx-current-path-for-history",e)}}$t(location.pathname+location.search);function _t(){const e=te().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||te().body}function zt(t,e){if(!X()){return}const n=Kt(e);const r=te().title;const o=window.scrollY;if(Q.config.historyCacheSize<=0){sessionStorage.removeItem("htmx-history-cache");return}t=U(t);const i=v(sessionStorage.getItem("htmx-history-cache"))||[];for(let e=0;e<i.length;e++){if(i[e].url===t){i.splice(e,1);break}}const s={url:t,content:n,title:r,scroll:o};ae(te().body,"htmx:historyItemCreated",{item:s,cache:i});i.push(s);while(i.length>Q.config.historyCacheSize){i.shift()}while(i.length>0){try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(te().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Jt(t){if(!X()){return null}t=U(t);const n=v(sessionStorage.getItem("htmx-history-cache"))||[];for(let e=0;e<n.length;e++){if(n[e].url===t){return n[e]}}return null}function Kt(e){const t=Q.config.requestClass;const n=e.cloneNode(true);ie(x(n,"."+t),function(e){G(e,t)});ie(x(n,"[data-disabled-by-htmx]"),function(e){e.removeAttribute("disabled")});return n.innerHTML}function Gt(){const e=_t();let t=jt;if(X()){t=sessionStorage.getItem("htmx-current-path-for-history")}t=t||location.pathname+location.search;const n=te().querySelector('[hx-history="false" i],[data-hx-history="false" i]');if(!n){ae(te().body,"htmx:beforeHistorySave",{path:t,historyElt:e});zt(t,e)}if(Q.config.historyEnabled)history.replaceState({htmx:true},te().title,location.href)}function Wt(e){if(Q.config.getCacheBusterParam){e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,"");if(Y(e,"&")||Y(e,"?")){e=e.slice(0,-1)}}if(Q.config.historyEnabled){history.pushState({htmx:true},"",e)}$t(e)}function Zt(e){if(Q.config.historyEnabled)history.replaceState({htmx:true},"",e);$t(e)}function Yt(e){ie(e,function(e){e.call(undefined)})}function Qt(e){const t=new XMLHttpRequest;const n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0};const r={path:e,xhr:t,historyElt:_t(),swapSpec:n};t.open("GET",e,true);if(Q.config.historyRestoreAsHxRequest){t.setRequestHeader("HX-Request","true")}t.setRequestHeader("HX-History-Restore-Request","true");t.setRequestHeader("HX-Current-URL",location.href);t.onload=function(){if(this.status>=200&&this.status<400){r.response=this.response;ae(te().body,"htmx:historyCacheMissLoad",r);ze(r.historyElt,r.response,n,{contextElement:r.historyElt,historyRequest:true});$t(r.path);ae(te().body,"htmx:historyRestore",{path:e,cacheMiss:true,serverResponse:r.response})}else{fe(te().body,"htmx:historyCacheMissLoadError",r)}};if(ae(te().body,"htmx:historyCacheMiss",r)){t.send()}}function en(e){Gt();e=e||location.pathname+location.search;const t=Jt(e);if(t){const n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:t.scroll};const r={path:e,item:t,historyElt:_t(),swapSpec:n};if(ae(te().body,"htmx:historyCacheHit",r)){ze(r.historyElt,t.content,n,{contextElement:r.historyElt,title:t.title});$t(r.path);ae(te().body,"htmx:historyRestore",r)}}else{if(Q.config.refreshOnHistoryMiss){Q.location.reload(true)}else{Qt(e)}}}function tn(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function nn(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function rn(e,t){ie(e.concat(t),function(e){const t=oe(e);t.requestCount=(t.requestCount||1)-1});ie(e,function(e){const t=oe(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});ie(t,function(e){const t=oe(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function on(t,n){for(let e=0;e<t.length;e++){const r=t[e];if(r.isSameNode(n)){return true}}return false}function sn(e){const t=e;if(t.name===""||t.name==null||t.disabled||g(t,"fieldset[disabled]")){return false}if(t.type==="button"||t.type==="submit"||t.tagName==="image"||t.tagName==="reset"||t.tagName==="file"){return false}if(t.type==="checkbox"||t.type==="radio"){return t.checked}return true}function ln(t,e,n){if(t!=null&&e!=null){if(Array.isArray(e)){e.forEach(function(e){n.append(t,e)})}else{n.append(t,e)}}}function cn(t,n,r){if(t!=null&&n!=null){let e=r.getAll(t);if(Array.isArray(n)){e=e.filter(e=>n.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);ie(e,e=>r.append(t,e))}}function un(e){if(e instanceof HTMLSelectElement&&e.multiple){return M(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e instanceof HTMLInputElement&&e.files){return M(e.files)}return e.value}function fn(t,n,r,e,o){if(e==null||on(t,e)){return}else{t.push(e)}if(sn(e)){const i=ee(e,"name");ln(i,un(e),n);if(o){an(e,r)}}if(e instanceof HTMLFormElement){ie(e.elements,function(e){if(t.indexOf(e)>=0){cn(e.name,un(e),n)}else{t.push(e)}if(o){an(e,r)}});new FormData(e).forEach(function(e,t){if(e instanceof File&&e.name===""){return}ln(t,e,n)})}}function an(e,t){const n=e;if(n.willValidate){ae(n,"htmx:validation:validate");if(!n.checkValidity()){if(ae(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})&&!t.length&&Q.config.reportValidityOfForms){n.reportValidity()}t.push({elt:n,message:n.validationMessage,validity:n.validity})}}}function hn(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function dn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=oe(e);if(s.lastButtonClicked&&!se(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||a(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){fn(n,o,i,Lt(e),l)}fn(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const f=ee(u,"name");ln(f,u.value,o)}const c=we(e,"hx-include");ie(c,function(e){fn(n,r,i,ce(e),l);if(!h(e,"form")){ie(p(e).querySelectorAll(ot),function(e){fn(n,r,i,e,l)})}});hn(r,o);return{errors:i,formData:r,values:kn(r)}}function pn(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function gn(e){e=Dn(e);let n="";e.forEach(function(e,t){n=pn(n,t,e)});return n}function mn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":a(t,"id"),"HX-Current-URL":location.href};Cn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(oe(e).boosted){r["HX-Boosted"]="true"}return r}function yn(n,e){const t=ne(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){ie(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;ie(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function xn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function bn(e,t){const n=t||ne(e,"hx-swap");const r={swapStyle:oe(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&oe(e).boosted&&!xn(e)){r.show="top"}if(n){const s=B(n);if(s.length>0){for(let e=0;e<s.length;e++){const l=s[e];if(l.indexOf("swap:")===0){r.swapDelay=d(l.slice(5))}else if(l.indexOf("settle:")===0){r.settleDelay=d(l.slice(7))}else if(l.indexOf("transition:")===0){r.transition=l.slice(11)==="true"}else if(l.indexOf("ignoreTitle:")===0){r.ignoreTitle=l.slice(12)==="true"}else if(l.indexOf("scroll:")===0){const c=l.slice(7);var o=c.split(":");const u=o.pop();var i=o.length>0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const f=l.slice(5);var o=f.split(":");const a=o.pop();var i=o.length>0?o.join(":"):null;r.show=a;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{H("Unknown modifier in hx-swap: "+l)}}}}return r}function vn(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function wn(t,n,r){let o=null;Vt(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(vn(n)){return hn(new FormData,Dn(r))}else{return gn(r)}}}function Sn(e){return{tasks:[],elts:[e]}}function En(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ce(ue(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}if(typeof t.scroll==="number"){b().setTimeout(function(){window.scrollTo(0,t.scroll)},0)}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ce(ue(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Cn(r,e,o,i,s){if(i==null){i={}}if(r==null){return i}const l=a(r,e);if(l){let e=l.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=On(r,function(){if(s){return Function("event","return ("+e+")").call(r,s)}else{return Function("return ("+e+")").call(r)}},{})}else{n=v(e)}for(const c in n){if(n.hasOwnProperty(c)){if(i[c]==null){i[c]=n[c]}}}}return Cn(ce(u(r)),e,o,i,s)}function On(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function Hn(e,t,n){return Cn(e,"hx-vars",true,n,t)}function Tn(e,t,n){return Cn(e,"hx-vals",false,n,t)}function Rn(e,t){return le(Hn(e,t),Tn(e,t))}function qn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function An(t){if(t.responseURL){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(te().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function T(e,t){return t.test(e.getAllResponseHeaders())}function Ln(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return he(t,n,null,null,{targetOverride:w(r)||ve,returnPromise:true})}else{let e=w(r.target);if(r.target&&!e||r.source&&!e&&!w(r.source)){e=ve}return he(t,n,w(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true,push:r.push,replace:r.replace,selectOOB:r.selectOOB})}}else{return he(t,n,null,null,{returnPromise:true})}}function Nn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function In(e,t,n){const r=new URL(t,location.protocol!=="about:"?location.href:window.origin);const o=location.protocol!=="about:"?location.origin:window.origin;const i=o===r.origin;if(Q.config.selfRequestsOnly){if(!i){return false}}return ae(e,"htmx:validateUrl",le({url:r,sameHost:i},n))}function Dn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Pn(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function kn(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Pn(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,k){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=te().body}const M=i.handler||Vn;const F=i.select||null;if(!se(r)){re(s);return e}const c=i.targetOverride||ce(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:ne(r,"hx-target")});re(l);return e}let u=oe(r);const f=u.lastButtonClicked;if(f){const A=ee(f,"formaction");if(A!=null){n=A}const L=ee(f,"formmethod");if(L!=null){if(de.includes(L.toLowerCase())){t=L}else{re(s);return e}}}const a=ne(r,"hx-confirm");if(k===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:a};if(ae(r,"htmx:confirm",G)===false){re(s);return e}}let h=r;let d=ne(r,"hx-sync");let p=null;let B=false;if(d){const N=d.split(":");const I=N[0].trim();if(I==="this"){h=Se(r,"hx-sync")}else{h=ce(ue(r,I))}d=(N[1]||"drop").trim();u=oe(h);if(d==="drop"&&u.xhr&&u.abortable!==true){re(s);return e}else if(d==="abort"){if(u.xhr){re(s);return e}else{B=true}}else if(d==="replace"){ae(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");p=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){ae(h,"htmx:abort")}else{if(p==null){if(o){const D=oe(o);if(D&&D.triggerSpec&&D.triggerSpec.queue){p=D.triggerSpec.queue}}if(p==null){p="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(p==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="all"){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){he(t,n,r,o,i)})}re(s);return e}}const g=new XMLHttpRequest;u.xhr=g;u.abortable=B;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const X=ne(r,"hx-prompt");if(X){var y=prompt(X);if(y===null||!ae(r,"htmx:prompt",{prompt:y,target:c})){re(s);m();return e}}if(a&&!k){if(!confirm(a)){re(s);m();return e}}let x=mn(r,c,y);if(t!=="get"&&!vn(r)){x["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){x=le(x,i.headers)}const U=dn(r,t);let b=U.errors;const V=U.formData;if(i.values){hn(V,Dn(i.values))}const j=Dn(Rn(r,o));const v=hn(V,j);let w=yn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=location.href}const S=Cn(r,"hx-request");const $=oe(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:$,useUrlParams:E,formData:w,parameters:kn(w),unfilteredFormData:v,unfilteredParameters:kn(v),headers:x,elt:r,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!ae(r,"htmx:configRequest",C)){re(s);m();return e}n=C.path;t=C.verb;x=C.headers;w=Dn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){ae(r,"htmx:validation:halted",C);re(s);m();return e}const _=n.split("#");const z=_[0];const O=_[1];let H=n;if(E){H=z;const Z=!w.keys().next().done;if(Z){if(H.indexOf("?")<0){H+="?"}else{H+="&"}H+=gn(w);if(O){H+="#"+O}}}if(!In(r,H,C)){fe(r,"htmx:invalidPath",C);re(l);m();return e}g.open(t.toUpperCase(),H,true);g.overrideMimeType("text/html");g.withCredentials=C.withCredentials;g.timeout=C.timeout;if(S.noHeaders){}else{for(const P in x){if(x.hasOwnProperty(P)){const Y=x[P];qn(g,P,Y)}}}const T={xhr:g,target:c,requestConfig:C,etc:i,boosted:$,select:F,pathInfo:{requestPath:n,finalRequestPath:H,responsePath:null,anchor:O}};g.onload=function(){try{const t=Nn(r);T.pathInfo.responsePath=An(g);M(r,T);if(T.keepIndicators!==true){rn(R,q)}ae(r,"htmx:afterRequest",T);ae(r,"htmx:afterOnLoad",T);if(!se(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(se(n)){e=n}}if(e){ae(e,"htmx:afterRequest",T);ae(e,"htmx:afterOnLoad",T)}}re(s)}catch(e){fe(r,"htmx:onLoadError",le({error:e},T));throw e}finally{m()}};g.onerror=function(){rn(R,q);fe(r,"htmx:afterRequest",T);fe(r,"htmx:sendError",T);re(l);m()};g.onabort=function(){rn(R,q);fe(r,"htmx:afterRequest",T);fe(r,"htmx:sendAbort",T);re(l);m()};g.ontimeout=function(){rn(R,q);fe(r,"htmx:afterRequest",T);fe(r,"htmx:timeout",T);re(l);m()};if(!ae(r,"htmx:beforeRequest",T)){re(s);m();return e}var R=tn(r);var q=nn(r);ie(["loadstart","loadend","progress","abort"],function(t){ie([g,g.upload],function(e){e.addEventListener(t,function(e){ae(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ae(r,"htmx:beforeSend",T);const J=E?null:wn(g,r,w);g.send(J);return e}function Mn(e,t){const n=t.xhr;let r=null;let o=null;if(T(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(T(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(T(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=t.etc.push||ne(e,"hx-push-url");const c=t.etc.replace||ne(e,"hx-replace-url");const u=oe(e).boosted;let f=null;let a=null;if(l){f="push";a=l}else if(c){f="replace";a=c}else if(u){f="push";a=s||i}if(a){if(a==="false"){return{}}if(a==="true"){a=s||i}if(t.pathInfo.anchor&&a.indexOf("#")===-1){a=a+"#"+t.pathInfo.anchor}return{type:f,path:a}}else{return{}}}function Fn(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Bn(e){for(var t=0;t<Q.config.responseHandling.length;t++){var n=Q.config.responseHandling[t];if(Fn(n,e.status)){return n}}return{swap:false}}function Xn(e){if(e){const t=f("title");if(t){t.textContent=e}else{window.document.title=e}}}function Un(e,t){if(t==="this"){return e}const n=ce(ue(e,t));if(n==null){fe(e,"htmx:targetError",{target:t});throw new Error(`Invalid re-target ${t}`)}return n}function Vn(t,e){const n=e.xhr;let r=e.target;const o=e.etc;const i=e.select;if(!ae(t,"htmx:beforeOnLoad",e))return;if(T(n,/HX-Trigger:/i)){Je(n,"HX-Trigger",t)}if(T(n,/HX-Location:/i)){let e=n.getResponseHeader("HX-Location");var s={};if(e.indexOf("{")===0){s=v(e);e=s.path;delete s.path}s.push=s.push||"true";Ln("get",e,s);return}const l=T(n,/HX-Refresh:/i)&&n.getResponseHeader("HX-Refresh")==="true";if(T(n,/HX-Redirect:/i)){e.keepIndicators=true;Q.location.href=n.getResponseHeader("HX-Redirect");l&&Q.location.reload();return}if(l){e.keepIndicators=true;Q.location.reload();return}const c=Mn(t,e);const u=Bn(n);const f=u.swap;let a=!!u.error;let h=Q.config.ignoreTitle||u.ignoreTitle;let d=u.select;if(u.target){e.target=Un(t,u.target)}var p=o.swapOverride;if(p==null&&u.swapOverride){p=u.swapOverride}if(T(n,/HX-Retarget:/i)){e.target=Un(t,n.getResponseHeader("HX-Retarget"))}if(T(n,/HX-Reswap:/i)){p=n.getResponseHeader("HX-Reswap")}var g=n.response;var m=le({shouldSwap:f,serverResponse:g,isError:a,ignoreTitle:h,selectOverride:d,swapOverride:p},e);if(u.event&&!ae(r,u.event,m))return;if(!ae(r,"htmx:beforeSwap",m))return;r=m.target;g=m.serverResponse;a=m.isError;h=m.ignoreTitle;d=m.selectOverride;p=m.swapOverride;e.target=r;e.failed=a;e.successful=!a;if(m.shouldSwap){if(n.status===286){lt(t)}Vt(t,function(e){g=e.transformResponse(g,n,t)});if(c.type){Gt()}var y=bn(t,p);if(!y.hasOwnProperty("ignoreTitle")){y.ignoreTitle=h}r.classList.add(Q.config.swappingClass);if(i){d=i}if(T(n,/HX-Reselect:/i)){d=n.getResponseHeader("HX-Reselect")}const x=o.selectOOB||ne(t,"hx-select-oob");const b=ne(t,"hx-select");ze(r,g,y,{select:d==="unset"?null:d||b,selectOOB:x,eventInfo:e,anchor:e.pathInfo.anchor,contextElement:t,afterSwapCallback:function(){if(T(n,/HX-Trigger-After-Swap:/i)){let e=t;if(!se(t)){e=te().body}Je(n,"HX-Trigger-After-Swap",e)}},afterSettleCallback:function(){if(T(n,/HX-Trigger-After-Settle:/i)){let e=t;if(!se(t)){e=te().body}Je(n,"HX-Trigger-After-Settle",e)}},beforeSwapCallback:function(){if(c.type){ae(te().body,"htmx:beforeHistoryUpdate",le({history:c},e));if(c.type==="push"){Wt(c.path);ae(te().body,"htmx:pushedIntoHistory",{path:c.path})}else{Zt(c.path);ae(te().body,"htmx:replacedInHistory",{path:c.path})}}}})}if(a){fe(t,"htmx:responseError",le({error:"Response Status Error Code "+n.status+" from "+e.pathInfo.requestPath},e))}}const jn={};function $n(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function _n(e,t){if(t.init){t.init(n)}jn[e]=le($n(),t)}function zn(e){delete jn[e]}function Jn(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=a(e,"hx-ext");if(t){ie(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=jn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Jn(ce(u(e)),n,r)}var Kn=false;te().addEventListener("DOMContentLoaded",function(){Kn=true});function Gn(e){if(Kn||te().readyState==="complete"){e()}else{te().addEventListener("DOMContentLoaded",e)}}function Wn(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";const t=Q.config.indicatorClass;const n=Q.config.requestClass;te().head.insertAdjacentHTML("beforeend",`<style${e}>`+`.${t}{opacity:0;visibility: hidden} `+`.${n} .${t}, .${n}.${t}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}`+"</style>")}}function Zn(){const e=te().querySelector('meta[name="htmx-config"]');if(e){return v(e.content)}else{return null}}function Yn(){const e=Zn();if(e){Q.config=le(Q.config,e)}}Gn(function(){Yn();Wn();let e=te().body;Ft(e);const t=te().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.detail.elt||e.target;const n=oe(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){en();ie(t,function(e){ae(e,"htmx:restored",{document:te(),triggerEvent:ae})})}else{if(n){n(e)}}};b().setTimeout(function(){ae(e,"htmx:load",{});e=null},0)});return Q}();
+12
pkg/hold/admin/static/js/lucide.min.js
··· 1 + /** 2 + * @license lucide v0.562.0 - ISC 3 + * 4 + * This source code is licensed under the ISC license. 5 + * See the LICENSE file in the root directory of this source tree. 6 + */ 7 + 8 + (function(a,n){typeof exports=="object"&&typeof module<"u"?n(exports):typeof define=="function"&&define.amd?define(["exports"],n):(a=typeof globalThis<"u"?globalThis:a||self,n(a.lucide={}))})(this,(function(a){"use strict";const n={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"},Sa=([t,h,d])=>{const c=document.createElementNS("http://www.w3.org/2000/svg",t);return Object.keys(h).forEach(M=>{c.setAttribute(M,String(h[M]))}),d?.length&&d.forEach(M=>{const p=Sa(M);c.appendChild(p)}),c},La=(t,h={})=>{const d={...n,...h};return Sa(["svg",d,t])},ru=t=>Array.from(t.attributes).reduce((h,d)=>(h[d.name]=d.value,h),{}),ou=t=>typeof t=="string"?t:!t||!t.class?"":t.class&&typeof t.class=="string"?t.class.split(" "):t.class&&Array.isArray(t.class)?t.class:"",vu=t=>t.flatMap(ou).map(h=>h.trim()).filter(Boolean).filter((h,d,c)=>c.indexOf(h)===d).join(" "),$u=t=>t.replace(/(\w)(\w*)(_|-|\s*)/g,(h,d,c)=>d.toUpperCase()+c.toLowerCase()),fa=(t,{nameAttr:h,icons:d,attrs:c})=>{const M=t.getAttribute(h);if(M==null)return;const p=$u(M),Va=d[p];if(!Va)return console.warn(`${t.outerHTML} icon name was not found in the provided icons object.`);const nu=ru(t),lu={...n,"data-lucide":M,...c,...nu},eu=vu(["lucide",`lucide-${M}`,nu,c]);eu&&Object.assign(lu,{class:eu});const mu=La(Va,lu);return t.parentNode?.replaceChild(mu,t)},ka=[["path",{d:"m14 12 4 4 4-4"}],["path",{d:"M18 16V7"}],["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M3.304 13h6.392"}]],Pa=[["path",{d:"m14 11 4-4 4 4"}],["path",{d:"M18 16V7"}],["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M3.304 13h6.392"}]],Ba=[["path",{d:"m15 16 2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16"}],["path",{d:"M15.697 14h5.606"}],["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M3.304 13h6.392"}]],za=[["circle",{cx:"16",cy:"4",r:"1"}],["path",{d:"m18 19 1-7-6 1"}],["path",{d:"m5 8 3-3 5.5 3-2.36 3.5"}],["path",{d:"M4.24 14.5a5 5 0 0 0 6.88 6"}],["path",{d:"M13.76 17.5a5 5 0 0 0-6.88-6"}]],Fa=[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"}]],Da=[["path",{d:"M18 17.5a2.5 2.5 0 1 1-4 2.03V12"}],["path",{d:"M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"}],["path",{d:"M6 8h12"}],["path",{d:"M6.6 15.572A2 2 0 1 0 10 17v-5"}]],s=[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}],["path",{d:"m9 13 2 2 4-4"}]],ba=[["path",{d:"M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"}],["path",{d:"m12 15 5 6H7Z"}]],g=[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}],["path",{d:"M9 13h6"}]],Ra=[["path",{d:"M6.87 6.87a8 8 0 1 0 11.26 11.26"}],["path",{d:"M19.9 14.25a8 8 0 0 0-9.15-9.15"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.26 18.67 4 21"}],["path",{d:"m2 2 20 20"}],["path",{d:"M4 4 2 6"}]],C=[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}],["path",{d:"M12 10v6"}],["path",{d:"M9 13h6"}]],Ta=[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M12 9v4l2 2"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}]],qa=[["path",{d:"M11 21c0-2.5 2-2.5 2-5"}],["path",{d:"M16 21c0-2.5 2-2.5 2-5"}],["path",{d:"m19 8-.8 3a1.25 1.25 0 0 1-1.2 1H7a1.25 1.25 0 0 1-1.2-1L5 8"}],["path",{d:"M21 3a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a1 1 0 0 1 1-1z"}],["path",{d:"M6 21c0-2.5 2-2.5 2-5"}]],Ua=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["polyline",{points:"11 3 11 11 14 8 17 11 17 3"}]],Oa=[["path",{d:"M2 12h20"}],["path",{d:"M10 16v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-4"}],["path",{d:"M10 8V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v4"}],["path",{d:"M20 16v1a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-1"}],["path",{d:"M14 8V7c0-1.1.9-2 2-2h2a2 2 0 0 1 2 2v1"}]],Za=[["path",{d:"M12 2v20"}],["path",{d:"M8 10H4a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h4"}],["path",{d:"M16 10h4a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-4"}],["path",{d:"M8 20H7a2 2 0 0 1-2-2v-2c0-1.1.9-2 2-2h1"}],["path",{d:"M16 14h1a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2h-1"}]],Ga=[["rect",{width:"6",height:"16",x:"4",y:"2",rx:"2"}],["rect",{width:"6",height:"9",x:"14",y:"9",rx:"2"}],["path",{d:"M22 22H2"}]],Wa=[["rect",{width:"16",height:"6",x:"2",y:"4",rx:"2"}],["rect",{width:"9",height:"6",x:"9",y:"14",rx:"2"}],["path",{d:"M22 22V2"}]],Ia=[["rect",{width:"6",height:"14",x:"4",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"14",y:"7",rx:"2"}],["path",{d:"M17 22v-5"}],["path",{d:"M17 7V2"}],["path",{d:"M7 22v-3"}],["path",{d:"M7 5V2"}]],Ea=[["rect",{width:"6",height:"14",x:"4",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"14",y:"7",rx:"2"}],["path",{d:"M4 2v20"}],["path",{d:"M14 2v20"}]],Xa=[["rect",{width:"6",height:"14",x:"4",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"14",y:"7",rx:"2"}],["path",{d:"M10 2v20"}],["path",{d:"M20 2v20"}]],ja=[["rect",{width:"6",height:"14",x:"2",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"16",y:"7",rx:"2"}],["path",{d:"M12 2v20"}]],Na=[["rect",{width:"6",height:"14",x:"2",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"12",y:"7",rx:"2"}],["path",{d:"M22 2v20"}]],Ka=[["rect",{width:"6",height:"14",x:"6",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"16",y:"7",rx:"2"}],["path",{d:"M2 2v20"}]],Qa=[["rect",{width:"6",height:"10",x:"9",y:"7",rx:"2"}],["path",{d:"M4 22V2"}],["path",{d:"M20 22V2"}]],Ja=[["rect",{width:"6",height:"14",x:"3",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"15",y:"7",rx:"2"}],["path",{d:"M3 2v20"}],["path",{d:"M21 2v20"}]],Ya=[["rect",{width:"6",height:"16",x:"4",y:"6",rx:"2"}],["rect",{width:"6",height:"9",x:"14",y:"6",rx:"2"}],["path",{d:"M22 2H2"}]],_a=[["rect",{width:"9",height:"6",x:"6",y:"14",rx:"2"}],["rect",{width:"16",height:"6",x:"6",y:"4",rx:"2"}],["path",{d:"M2 2v20"}]],xa=[["path",{d:"M22 17h-3"}],["path",{d:"M22 7h-5"}],["path",{d:"M5 17H2"}],["path",{d:"M7 7H2"}],["rect",{x:"5",y:"14",width:"14",height:"6",rx:"2"}],["rect",{x:"7",y:"4",width:"10",height:"6",rx:"2"}]],at=[["rect",{width:"14",height:"6",x:"5",y:"14",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"4",rx:"2"}],["path",{d:"M2 20h20"}],["path",{d:"M2 10h20"}]],tt=[["rect",{width:"14",height:"6",x:"5",y:"14",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"4",rx:"2"}],["path",{d:"M2 14h20"}],["path",{d:"M2 4h20"}]],ht=[["rect",{width:"14",height:"6",x:"5",y:"16",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"2",rx:"2"}],["path",{d:"M2 12h20"}]],dt=[["rect",{width:"14",height:"6",x:"5",y:"12",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"2",rx:"2"}],["path",{d:"M2 22h20"}]],ct=[["rect",{width:"14",height:"6",x:"5",y:"16",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"6",rx:"2"}],["path",{d:"M2 2h20"}]],Mt=[["rect",{width:"10",height:"6",x:"7",y:"9",rx:"2"}],["path",{d:"M22 20H2"}],["path",{d:"M22 4H2"}]],pt=[["rect",{width:"14",height:"6",x:"5",y:"15",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"3",rx:"2"}],["path",{d:"M2 21h20"}],["path",{d:"M2 3h20"}]],it=[["path",{d:"M10 10H6"}],["path",{d:"M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"}],["path",{d:"M19 18h2a1 1 0 0 0 1-1v-3.28a1 1 0 0 0-.684-.948l-1.923-.641a1 1 0 0 1-.578-.502l-1.539-3.076A1 1 0 0 0 16.382 8H14"}],["path",{d:"M8 8v4"}],["path",{d:"M9 18h6"}],["circle",{cx:"17",cy:"18",r:"2"}],["circle",{cx:"7",cy:"18",r:"2"}]],nt=[["path",{d:"M16 12h3"}],["path",{d:"M17.5 12a8 8 0 0 1-8 8A4.5 4.5 0 0 1 5 15.5c0-6 8-4 8-8.5a3 3 0 1 0-6 0c0 3 2.5 8.5 12 13"}]],lt=[["path",{d:"M10 17c-5-3-7-7-7-9a2 2 0 0 1 4 0c0 2.5-5 2.5-5 6 0 1.7 1.3 3 3 3 2.8 0 5-2.2 5-5"}],["path",{d:"M22 17c-5-3-7-7-7-9a2 2 0 0 1 4 0c0 2.5-5 2.5-5 6 0 1.7 1.3 3 3 3 2.8 0 5-2.2 5-5"}]],et=[["path",{d:"M10 2v5.632c0 .424-.272.795-.653.982A6 6 0 0 0 6 14c.006 4 3 7 5 8"}],["path",{d:"M10 5H8a2 2 0 0 0 0 4h.68"}],["path",{d:"M14 2v5.632c0 .424.272.795.652.982A6 6 0 0 1 18 14c0 4-3 7-5 8"}],["path",{d:"M14 5h2a2 2 0 0 1 0 4h-.68"}],["path",{d:"M18 22H6"}],["path",{d:"M9 2h6"}]],rt=[["path",{d:"M12 6v16"}],["path",{d:"m19 13 2-1a9 9 0 0 1-18 0l2 1"}],["path",{d:"M9 11h6"}],["circle",{cx:"12",cy:"4",r:"2"}]],ot=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M16 16s-1.5-2-4-2-4 2-4 2"}],["path",{d:"M7.5 8 10 9"}],["path",{d:"m14 9 2.5-1"}],["path",{d:"M9 10h.01"}],["path",{d:"M15 10h.01"}]],vt=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M8 15h8"}],["path",{d:"M8 9h2"}],["path",{d:"M14 9h2"}]],$t=[["path",{d:"M2 12 7 2"}],["path",{d:"m7 12 5-10"}],["path",{d:"m12 12 5-10"}],["path",{d:"m17 12 5-10"}],["path",{d:"M4.5 7h15"}],["path",{d:"M12 16v6"}]],mt=[["path",{d:"M7 10H6a4 4 0 0 1-4-4 1 1 0 0 1 1-1h4"}],["path",{d:"M7 5a1 1 0 0 1 1-1h13a1 1 0 0 1 1 1 7 7 0 0 1-7 7H8a1 1 0 0 1-1-1z"}],["path",{d:"M9 12v5"}],["path",{d:"M15 12v5"}],["path",{d:"M5 20a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3 1 1 0 0 1-1 1H6a1 1 0 0 1-1-1"}]],yt=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m14.31 8 5.74 9.94"}],["path",{d:"M9.69 8h11.48"}],["path",{d:"m7.38 12 5.74-9.94"}],["path",{d:"M9.69 16 3.95 6.06"}],["path",{d:"M14.31 16H2.83"}],["path",{d:"m16.62 12-5.74 9.94"}]],st=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M6 8h.01"}],["path",{d:"M10 8h.01"}],["path",{d:"M14 8h.01"}]],gt=[["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2"}],["path",{d:"M10 4v4"}],["path",{d:"M2 8h20"}],["path",{d:"M6 4v4"}]],Ct=[["path",{d:"M12 6.528V3a1 1 0 0 1 1-1h0"}],["path",{d:"M18.237 21A15 15 0 0 0 22 11a6 6 0 0 0-10-4.472A6 6 0 0 0 2 11a15.1 15.1 0 0 0 3.763 10 3 3 0 0 0 3.648.648 5.5 5.5 0 0 1 5.178 0A3 3 0 0 0 18.237 21"}]],ut=[["rect",{width:"20",height:"5",x:"2",y:"3",rx:"1"}],["path",{d:"M4 8v11a2 2 0 0 0 2 2h2"}],["path",{d:"M20 8v11a2 2 0 0 1-2 2h-2"}],["path",{d:"m9 15 3-3 3 3"}],["path",{d:"M12 12v9"}]],Ht=[["rect",{width:"20",height:"5",x:"2",y:"3",rx:"1"}],["path",{d:"M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"}],["path",{d:"m9.5 17 5-5"}],["path",{d:"m9.5 12 5 5"}]],At=[["rect",{width:"20",height:"5",x:"2",y:"3",rx:"1"}],["path",{d:"M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"}],["path",{d:"M10 12h4"}]],wt=[["path",{d:"M19 9V6a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v3"}],["path",{d:"M3 16a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5a2 2 0 0 0-4 0v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V11a2 2 0 0 0-4 0z"}],["path",{d:"M5 18v2"}],["path",{d:"M19 18v2"}]],Vt=[["path",{d:"M15 11a1 1 0 0 0 1 1h2.939a1 1 0 0 1 .75 1.811l-6.835 6.836a1.207 1.207 0 0 1-1.707 0L4.31 13.81a1 1 0 0 1 .75-1.811H8a1 1 0 0 0 1-1V9a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1z"}],["path",{d:"M9 4h6"}]],St=[["path",{d:"M15 11a1 1 0 0 0 1 1h2.939a1 1 0 0 1 .75 1.811l-6.835 6.836a1.207 1.207 0 0 1-1.707 0L4.31 13.81a1 1 0 0 1 .75-1.811H8a1 1 0 0 0 1-1V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1z"}]],Lt=[["path",{d:"M13 9a1 1 0 0 1-1-1V5.061a1 1 0 0 0-1.811-.75l-6.835 6.836a1.207 1.207 0 0 0 0 1.707l6.835 6.835a1 1 0 0 0 1.811-.75V16a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1z"}],["path",{d:"M20 9v6"}]],ft=[["path",{d:"M13 9a1 1 0 0 1-1-1V5.061a1 1 0 0 0-1.811-.75l-6.835 6.836a1.207 1.207 0 0 0 0 1.707l6.835 6.835a1 1 0 0 0 1.811-.75V16a1 1 0 0 1 1-1h6a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1z"}]],kt=[["path",{d:"M11 9a1 1 0 0 0 1-1V5.061a1 1 0 0 1 1.811-.75l6.836 6.836a1.207 1.207 0 0 1 0 1.707l-6.836 6.835a1 1 0 0 1-1.811-.75V16a1 1 0 0 0-1-1H9a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1z"}],["path",{d:"M4 9v6"}]],Pt=[["path",{d:"M11 9a1 1 0 0 0 1-1V5.061a1 1 0 0 1 1.811-.75l6.836 6.836a1.207 1.207 0 0 1 0 1.707l-6.836 6.835a1 1 0 0 1-1.811-.75V16a1 1 0 0 0-1-1H5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1z"}]],Bt=[["path",{d:"M9 13a1 1 0 0 0-1-1H5.061a1 1 0 0 1-.75-1.811l6.836-6.835a1.207 1.207 0 0 1 1.707 0l6.835 6.835a1 1 0 0 1-.75 1.811H16a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1z"}],["path",{d:"M9 20h6"}]],zt=[["path",{d:"M9 13a1 1 0 0 0-1-1H5.061a1 1 0 0 1-.75-1.811l6.836-6.835a1.207 1.207 0 0 1 1.707 0l6.835 6.835a1 1 0 0 1-.75 1.811H16a1 1 0 0 0-1 1v6a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1z"}]],Ft=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["rect",{x:"15",y:"4",width:"4",height:"6",ry:"2"}],["path",{d:"M17 20v-6h-2"}],["path",{d:"M15 20h4"}]],Dt=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"M17 10V4h-2"}],["path",{d:"M15 10h4"}],["rect",{x:"15",y:"14",width:"4",height:"6",ry:"2"}]],u=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"M20 8h-5"}],["path",{d:"M15 10V6.5a2.5 2.5 0 0 1 5 0V10"}],["path",{d:"M15 14h5l-5 6h5"}]],bt=[["path",{d:"M19 3H5"}],["path",{d:"M12 21V7"}],["path",{d:"m6 15 6 6 6-6"}]],Rt=[["path",{d:"M17 7 7 17"}],["path",{d:"M17 17H7V7"}]],Tt=[["path",{d:"m7 7 10 10"}],["path",{d:"M17 7v10H7"}]],qt=[["path",{d:"M12 2v14"}],["path",{d:"m19 9-7 7-7-7"}],["circle",{cx:"12",cy:"21",r:"1"}]],Ut=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"M11 4h4"}],["path",{d:"M11 8h7"}],["path",{d:"M11 12h10"}]],Ot=[["path",{d:"M12 17V3"}],["path",{d:"m6 11 6 6 6-6"}],["path",{d:"M19 21H5"}]],Zt=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"m21 8-4-4-4 4"}],["path",{d:"M17 4v16"}]],H=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"M11 4h10"}],["path",{d:"M11 8h7"}],["path",{d:"M11 12h4"}]],A=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 4v16"}],["path",{d:"M15 4h5l-5 6h5"}],["path",{d:"M15 20v-3.5a2.5 2.5 0 0 1 5 0V20"}],["path",{d:"M20 18h-5"}]],Gt=[["path",{d:"M12 5v14"}],["path",{d:"m19 12-7 7-7-7"}]],Wt=[["path",{d:"M8 3 4 7l4 4"}],["path",{d:"M4 7h16"}],["path",{d:"m16 21 4-4-4-4"}],["path",{d:"M20 17H4"}]],It=[["path",{d:"m9 6-6 6 6 6"}],["path",{d:"M3 12h14"}],["path",{d:"M21 19V5"}]],Et=[["path",{d:"M3 19V5"}],["path",{d:"m13 6-6 6 6 6"}],["path",{d:"M7 12h14"}]],Xt=[["path",{d:"m12 19-7-7 7-7"}],["path",{d:"M19 12H5"}]],jt=[["path",{d:"M3 5v14"}],["path",{d:"M21 12H7"}],["path",{d:"m15 18 6-6-6-6"}]],Nt=[["path",{d:"m16 3 4 4-4 4"}],["path",{d:"M20 7H4"}],["path",{d:"m8 21-4-4 4-4"}],["path",{d:"M4 17h16"}]],Kt=[["path",{d:"M17 12H3"}],["path",{d:"m11 18 6-6-6-6"}],["path",{d:"M21 5v14"}]],Qt=[["path",{d:"M5 12h14"}],["path",{d:"m12 5 7 7-7 7"}]],Jt=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["rect",{x:"15",y:"4",width:"4",height:"6",ry:"2"}],["path",{d:"M17 20v-6h-2"}],["path",{d:"M15 20h4"}]],Yt=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M17 10V4h-2"}],["path",{d:"M15 10h4"}],["rect",{x:"15",y:"14",width:"4",height:"6",ry:"2"}]],w=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M20 8h-5"}],["path",{d:"M15 10V6.5a2.5 2.5 0 0 1 5 0V10"}],["path",{d:"M15 14h5l-5 6h5"}]],_t=[["path",{d:"m21 16-4 4-4-4"}],["path",{d:"M17 20V4"}],["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}]],xt=[["path",{d:"m5 9 7-7 7 7"}],["path",{d:"M12 16V2"}],["circle",{cx:"12",cy:"21",r:"1"}]],ah=[["path",{d:"m18 9-6-6-6 6"}],["path",{d:"M12 3v14"}],["path",{d:"M5 21h14"}]],th=[["path",{d:"M7 17V7h10"}],["path",{d:"M17 17 7 7"}]],V=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M11 12h4"}],["path",{d:"M11 16h7"}],["path",{d:"M11 20h10"}]],hh=[["path",{d:"M7 7h10v10"}],["path",{d:"M7 17 17 7"}]],dh=[["path",{d:"M5 3h14"}],["path",{d:"m18 13-6-6-6 6"}],["path",{d:"M12 7v14"}]],ch=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M11 12h10"}],["path",{d:"M11 16h7"}],["path",{d:"M11 20h4"}]],S=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M15 4h5l-5 6h5"}],["path",{d:"M15 20v-3.5a2.5 2.5 0 0 1 5 0V20"}],["path",{d:"M20 18h-5"}]],Mh=[["path",{d:"m5 12 7-7 7 7"}],["path",{d:"M12 19V5"}]],ph=[["path",{d:"m4 6 3-3 3 3"}],["path",{d:"M7 17V3"}],["path",{d:"m14 6 3-3 3 3"}],["path",{d:"M17 17V3"}],["path",{d:"M4 21h16"}]],ih=[["path",{d:"M12 6v12"}],["path",{d:"M17.196 9 6.804 15"}],["path",{d:"m6.804 9 10.392 6"}]],nh=[["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"}]],lh=[["circle",{cx:"12",cy:"12",r:"1"}],["path",{d:"M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"}],["path",{d:"M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"}]],eh=[["path",{d:"M2 10v3"}],["path",{d:"M6 6v11"}],["path",{d:"M10 3v18"}],["path",{d:"M14 8v7"}],["path",{d:"M18 5v13"}],["path",{d:"M22 10v3"}]],rh=[["path",{d:"m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526"}],["circle",{cx:"12",cy:"8",r:"6"}]],oh=[["path",{d:"M2 13a2 2 0 0 0 2-2V7a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0V4a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0v-4a2 2 0 0 1 2-2"}]],vh=[["path",{d:"m14 12-8.381 8.38a1 1 0 0 1-3.001-3L11 9"}],["path",{d:"M15 15.5a.5.5 0 0 0 .5.5A6.5 6.5 0 0 0 22 9.5a.5.5 0 0 0-.5-.5h-1.672a2 2 0 0 1-1.414-.586l-5.062-5.062a1.205 1.205 0 0 0-1.704 0L9.352 5.648a1.205 1.205 0 0 0 0 1.704l5.062 5.062A2 2 0 0 1 15 13.828z"}]],L=[["path",{d:"M13.5 10.5 15 9"}],["path",{d:"M4 4v15a1 1 0 0 0 1 1h15"}],["path",{d:"M4.293 19.707 6 18"}],["path",{d:"m9 15 1.5-1.5"}]],$h=[["path",{d:"M10 16c.5.3 1.2.5 2 .5s1.5-.2 2-.5"}],["path",{d:"M15 12h.01"}],["path",{d:"M19.38 6.813A9 9 0 0 1 20.8 10.2a2 2 0 0 1 0 3.6 9 9 0 0 1-17.6 0 2 2 0 0 1 0-3.6A9 9 0 0 1 12 3c2 0 3.5 1.1 3.5 2.5s-.9 2.5-2 2.5c-.8 0-1.5-.4-1.5-1"}],["path",{d:"M9 12h.01"}]],mh=[["path",{d:"M4 10a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"}],["path",{d:"M8 10h8"}],["path",{d:"M8 18h8"}],["path",{d:"M8 22v-6a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v6"}],["path",{d:"M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"}]],yh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16"}]],sh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M12 7v10"}],["path",{d:"M15.4 10a4 4 0 1 0 0 4"}]],f=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"m9 12 2 2 4-4"}]],gh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"}],["path",{d:"M12 18V6"}]],Ch=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M7 12h5"}],["path",{d:"M15 9.4a4 4 0 1 0 0 5.2"}]],uh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M8 8h8"}],["path",{d:"M8 12h8"}],["path",{d:"m13 17-5-1h1a4 4 0 0 0 0-8"}]],Hh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"12",x2:"12",y1:"16",y2:"12"}],["line",{x1:"12",x2:"12.01",y1:"8",y2:"8"}]],Ah=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"m9 8 3 3v7"}],["path",{d:"m12 11 3-3"}],["path",{d:"M9 12h6"}],["path",{d:"M9 16h6"}]],wh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12"}]],Vh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"m15 9-6 6"}],["path",{d:"M9 9h.01"}],["path",{d:"M15 15h.01"}]],Sh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"12",x2:"12",y1:"8",y2:"16"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12"}]],Lh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M8 12h4"}],["path",{d:"M10 16V9.5a2.5 2.5 0 0 1 5 0"}],["path",{d:"M8 16h7"}]],k=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"}],["line",{x1:"12",x2:"12.01",y1:"17",y2:"17"}]],fh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M9 16h5"}],["path",{d:"M9 12h5a2 2 0 1 0 0-4h-3v9"}]],kh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M11 17V8h4"}],["path",{d:"M11 12h3"}],["path",{d:"M9 16h4"}]],Ph=[["path",{d:"M11 7v10a5 5 0 0 0 5-5"}],["path",{d:"m15 8-6 3"}],["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76"}]],Bh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"15",x2:"9",y1:"9",y2:"15"}],["line",{x1:"9",x2:"15",y1:"9",y2:"15"}]],zh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}]],Fh=[["path",{d:"M22 18H6a2 2 0 0 1-2-2V7a2 2 0 0 0-2-2"}],["path",{d:"M17 14V4a2 2 0 0 0-2-2h-1a2 2 0 0 0-2 2v10"}],["rect",{width:"13",height:"8",x:"8",y:"6",rx:"1"}],["circle",{cx:"18",cy:"20",r:"2"}],["circle",{cx:"9",cy:"20",r:"2"}]],Dh=[["path",{d:"M12 16v1a2 2 0 0 0 2 2h1a2 2 0 0 1 2 2v1"}],["path",{d:"M12 6a2 2 0 0 1 2 2"}],["path",{d:"M18 8c0 4-3.5 8-6 8s-6-4-6-8a6 6 0 0 1 12 0"}]],bh=[["path",{d:"M4.929 4.929 19.07 19.071"}],["circle",{cx:"12",cy:"12",r:"10"}]],Rh=[["path",{d:"M4 13c3.5-2 8-2 10 2a5.5 5.5 0 0 1 8 5"}],["path",{d:"M5.15 17.89c5.52-1.52 8.65-6.89 7-12C11.55 4 11.5 2 13 2c3.22 0 5 5.5 5 8 0 6.5-4.2 12-10.49 12C5.11 22 2 22 2 20c0-1.5 1.14-1.55 3.15-2.11Z"}]],Th=[["path",{d:"M10 10.01h.01"}],["path",{d:"M10 14.01h.01"}],["path",{d:"M14 10.01h.01"}],["path",{d:"M14 14.01h.01"}],["path",{d:"M18 6v11.5"}],["path",{d:"M6 6v12"}],["rect",{x:"2",y:"6",width:"20",height:"12",rx:"2"}]],qh=[["path",{d:"M12 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5"}],["path",{d:"m16 19 3 3 3-3"}],["path",{d:"M18 12h.01"}],["path",{d:"M19 16v6"}],["path",{d:"M6 12h.01"}],["circle",{cx:"12",cy:"12",r:"2"}]],Uh=[["path",{d:"M12 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5"}],["path",{d:"M18 12h.01"}],["path",{d:"M19 22v-6"}],["path",{d:"m22 19-3-3-3 3"}],["path",{d:"M6 12h.01"}],["circle",{cx:"12",cy:"12",r:"2"}]],Oh=[["path",{d:"M13 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5"}],["path",{d:"m17 17 5 5"}],["path",{d:"M18 12h.01"}],["path",{d:"m22 17-5 5"}],["path",{d:"M6 12h.01"}],["circle",{cx:"12",cy:"12",r:"2"}]],Zh=[["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}],["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"M6 12h.01M18 12h.01"}]],Gh=[["path",{d:"M3 5v14"}],["path",{d:"M8 5v14"}],["path",{d:"M12 5v14"}],["path",{d:"M17 5v14"}],["path",{d:"M21 5v14"}]],Wh=[["path",{d:"M10 3a41 41 0 0 0 0 18"}],["path",{d:"M14 3a41 41 0 0 1 0 18"}],["path",{d:"M17 3a2 2 0 0 1 1.68.92 15.25 15.25 0 0 1 0 16.16A2 2 0 0 1 17 21H7a2 2 0 0 1-1.68-.92 15.25 15.25 0 0 1 0-16.16A2 2 0 0 1 7 3z"}],["path",{d:"M3.84 17h16.32"}],["path",{d:"M3.84 7h16.32"}]],Ih=[["path",{d:"M4 20h16"}],["path",{d:"m6 16 6-12 6 12"}],["path",{d:"M8 12h8"}]],Eh=[["path",{d:"M10 4 8 6"}],["path",{d:"M17 19v2"}],["path",{d:"M2 12h20"}],["path",{d:"M7 19v2"}],["path",{d:"M9 5 7.621 3.621A2.121 2.121 0 0 0 4 5v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5"}]],Xh=[["path",{d:"m11 7-3 5h4l-3 5"}],["path",{d:"M14.856 6H16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.935"}],["path",{d:"M22 14v-4"}],["path",{d:"M5.14 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.936"}]],jh=[["path",{d:"M10 10v4"}],["path",{d:"M14 10v4"}],["path",{d:"M22 14v-4"}],["path",{d:"M6 10v4"}],["rect",{x:"2",y:"6",width:"16",height:"12",rx:"2"}]],Nh=[["path",{d:"M22 14v-4"}],["path",{d:"M6 14v-4"}],["rect",{x:"2",y:"6",width:"16",height:"12",rx:"2"}]],Kh=[["path",{d:"M10 9v6"}],["path",{d:"M12.543 6H16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-3.605"}],["path",{d:"M22 14v-4"}],["path",{d:"M7 12h6"}],["path",{d:"M7.606 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3.606"}]],Qh=[["path",{d:"M10 14v-4"}],["path",{d:"M22 14v-4"}],["path",{d:"M6 14v-4"}],["rect",{x:"2",y:"6",width:"16",height:"12",rx:"2"}]],Jh=[["path",{d:"M10 17h.01"}],["path",{d:"M10 7v6"}],["path",{d:"M14 6h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2"}],["path",{d:"M22 14v-4"}],["path",{d:"M6 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2"}]],Yh=[["path",{d:"M 22 14 L 22 10"}],["rect",{x:"2",y:"6",width:"16",height:"12",rx:"2"}]],_h=[["path",{d:"M4.5 3h15"}],["path",{d:"M6 3v16a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V3"}],["path",{d:"M6 14h12"}]],xh=[["path",{d:"M9 9c-.64.64-1.521.954-2.402 1.165A6 6 0 0 0 8 22a13.96 13.96 0 0 0 9.9-4.1"}],["path",{d:"M10.75 5.093A6 6 0 0 1 22 8c0 2.411-.61 4.68-1.683 6.66"}],["path",{d:"M5.341 10.62a4 4 0 0 0 6.487 1.208M10.62 5.341a4.015 4.015 0 0 1 2.039 2.04"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],a5=[["path",{d:"M10.165 6.598C9.954 7.478 9.64 8.36 9 9c-.64.64-1.521.954-2.402 1.165A6 6 0 0 0 8 22c7.732 0 14-6.268 14-14a6 6 0 0 0-11.835-1.402Z"}],["path",{d:"M5.341 10.62a4 4 0 1 0 5.279-5.28"}]],t5=[["path",{d:"M2 20v-8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v8"}],["path",{d:"M4 10V6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4"}],["path",{d:"M12 4v6"}],["path",{d:"M2 18h20"}]],h5=[["path",{d:"M3 20v-8a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v8"}],["path",{d:"M5 10V6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"}],["path",{d:"M3 18h18"}]],d5=[["path",{d:"M2 4v16"}],["path",{d:"M2 8h18a2 2 0 0 1 2 2v10"}],["path",{d:"M2 17h20"}],["path",{d:"M6 8v9"}]],c5=[["path",{d:"M16.4 13.7A6.5 6.5 0 1 0 6.28 6.6c-1.1 3.13-.78 3.9-3.18 6.08A3 3 0 0 0 5 18c4 0 8.4-1.8 11.4-4.3"}],["path",{d:"m18.5 6 2.19 4.5a6.48 6.48 0 0 1-2.29 7.2C15.4 20.2 11 22 7 22a3 3 0 0 1-2.68-1.66L2.4 16.5"}],["circle",{cx:"12.5",cy:"8.5",r:"2.5"}]],M5=[["path",{d:"M13 13v5"}],["path",{d:"M17 11.47V8"}],["path",{d:"M17 11h1a3 3 0 0 1 2.745 4.211"}],["path",{d:"m2 2 20 20"}],["path",{d:"M5 8v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3"}],["path",{d:"M7.536 7.535C6.766 7.649 6.154 8 5.5 8a2.5 2.5 0 0 1-1.768-4.268"}],["path",{d:"M8.727 3.204C9.306 2.767 9.885 2 11 2c1.56 0 2 1.5 3 1.5s1.72-.5 2.5-.5a1 1 0 1 1 0 5c-.78 0-1.5-.5-2.5-.5a3.149 3.149 0 0 0-.842.12"}],["path",{d:"M9 14.6V18"}]],p5=[["path",{d:"M17 11h1a3 3 0 0 1 0 6h-1"}],["path",{d:"M9 12v6"}],["path",{d:"M13 12v6"}],["path",{d:"M14 7.5c-1 0-1.44.5-3 .5s-2-.5-3-.5-1.72.5-2.5.5a2.5 2.5 0 0 1 0-5c.78 0 1.57.5 2.5.5S9.44 2 11 2s2 1.5 3 1.5 1.72-.5 2.5-.5a2.5 2.5 0 0 1 0 5c-.78 0-1.5-.5-2.5-.5Z"}],["path",{d:"M5 8v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8"}]],i5=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M13.916 2.314A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.74 7.327A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673 9 9 0 0 1-.585-.665"}],["circle",{cx:"18",cy:"8",r:"3"}]],n5=[["path",{d:"M18.518 17.347A7 7 0 0 1 14 19"}],["path",{d:"M18.8 4A11 11 0 0 1 20 9"}],["path",{d:"M9 9h.01"}],["circle",{cx:"20",cy:"16",r:"2"}],["circle",{cx:"9",cy:"9",r:"7"}],["rect",{x:"4",y:"16",width:"10",height:"6",rx:"2"}]],l5=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M15 8h6"}],["path",{d:"M16.243 3.757A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673A9.4 9.4 0 0 1 18.667 12"}]],e5=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M17 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 .258-1.742"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.668 3.01A6 6 0 0 1 18 8c0 2.687.77 4.653 1.707 6.05"}]],r5=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M15 8h6"}],["path",{d:"M18 5v6"}],["path",{d:"M20.002 14.464a9 9 0 0 0 .738.863A1 1 0 0 1 20 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 8.75-5.332"}]],o5=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M22 8c0-2.3-.8-4.3-2-6"}],["path",{d:"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"}],["path",{d:"M4 2C2.8 3.7 2 5.7 2 8"}]],v5=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"}]],P=[["rect",{width:"13",height:"7",x:"3",y:"3",rx:"1"}],["path",{d:"m22 15-3-3 3-3"}],["rect",{width:"13",height:"7",x:"3",y:"14",rx:"1"}]],B=[["rect",{width:"13",height:"7",x:"8",y:"3",rx:"1"}],["path",{d:"m2 9 3 3-3 3"}],["rect",{width:"13",height:"7",x:"8",y:"14",rx:"1"}]],$5=[["rect",{width:"7",height:"13",x:"3",y:"3",rx:"1"}],["path",{d:"m9 22 3-3 3 3"}],["rect",{width:"7",height:"13",x:"14",y:"3",rx:"1"}]],m5=[["rect",{width:"7",height:"13",x:"3",y:"8",rx:"1"}],["path",{d:"m15 2-3 3-3-3"}],["rect",{width:"7",height:"13",x:"14",y:"8",rx:"1"}]],y5=[["path",{d:"M12.409 13.017A5 5 0 0 1 22 15c0 3.866-4 7-9 7-4.077 0-8.153-.82-10.371-2.462-.426-.316-.631-.832-.62-1.362C2.118 12.723 2.627 2 10 2a3 3 0 0 1 3 3 2 2 0 0 1-2 2c-1.105 0-1.64-.444-2-1"}],["path",{d:"M15 14a5 5 0 0 0-7.584 2"}],["path",{d:"M9.964 6.825C8.019 7.977 9.5 13 8 15"}]],s5=[["circle",{cx:"18.5",cy:"17.5",r:"3.5"}],["circle",{cx:"5.5",cy:"17.5",r:"3.5"}],["circle",{cx:"15",cy:"5",r:"1"}],["path",{d:"M12 17.5V14l-3-3 4-3 2 3h2"}]],g5=[["rect",{x:"14",y:"14",width:"4",height:"6",rx:"2"}],["rect",{x:"6",y:"4",width:"4",height:"6",rx:"2"}],["path",{d:"M6 20h4"}],["path",{d:"M14 10h4"}],["path",{d:"M6 14h2v6"}],["path",{d:"M14 4h2v6"}]],C5=[["circle",{cx:"12",cy:"11.9",r:"2"}],["path",{d:"M6.7 3.4c-.9 2.5 0 5.2 2.2 6.7C6.5 9 3.7 9.6 2 11.6"}],["path",{d:"m8.9 10.1 1.4.8"}],["path",{d:"M17.3 3.4c.9 2.5 0 5.2-2.2 6.7 2.4-1.2 5.2-.6 6.9 1.5"}],["path",{d:"m15.1 10.1-1.4.8"}],["path",{d:"M16.7 20.8c-2.6-.4-4.6-2.6-4.7-5.3-.2 2.6-2.1 4.8-4.7 5.2"}],["path",{d:"M12 13.9v1.6"}],["path",{d:"M13.5 5.4c-1-.2-2-.2-3 0"}],["path",{d:"M17 16.4c.7-.7 1.2-1.6 1.5-2.5"}],["path",{d:"M5.5 13.9c.3.9.8 1.8 1.5 2.5"}]],u5=[["path",{d:"M10 10h4"}],["path",{d:"M19 7V4a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v3"}],["path",{d:"M20 21a2 2 0 0 0 2-2v-3.851c0-1.39-2-2.962-2-4.829V8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v11a2 2 0 0 0 2 2z"}],["path",{d:"M 22 16 L 2 16"}],["path",{d:"M4 21a2 2 0 0 1-2-2v-3.851c0-1.39 2-2.962 2-4.829V8a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v11a2 2 0 0 1-2 2z"}],["path",{d:"M9 7V4a1 1 0 0 0-1-1H6a1 1 0 0 0-1 1v3"}]],H5=[["path",{d:"M12 18v4"}],["path",{d:"m17 18 1.956-11.468"}],["path",{d:"m3 8 7.82-5.615a2 2 0 0 1 2.36 0L21 8"}],["path",{d:"M4 18h16"}],["path",{d:"M7 18 5.044 6.532"}],["circle",{cx:"12",cy:"10",r:"2"}]],A5=[["path",{d:"M16 7h.01"}],["path",{d:"M3.4 18H12a8 8 0 0 0 8-8V7a4 4 0 0 0-7.28-2.3L2 20"}],["path",{d:"m20 7 2 .5-2 .5"}],["path",{d:"M10 18v3"}],["path",{d:"M14 17.75V21"}],["path",{d:"M7 18a6 6 0 0 0 3.84-10.61"}]],w5=[["path",{d:"M11.767 19.089c4.924.868 6.14-6.025 1.216-6.894m-1.216 6.894L5.86 18.047m5.908 1.042-.347 1.97m1.563-8.864c4.924.869 6.14-6.025 1.215-6.893m-1.215 6.893-3.94-.694m5.155-6.2L8.29 4.26m5.908 1.042.348-1.97M7.48 20.364l3.126-17.727"}]],V5=[["circle",{cx:"9",cy:"9",r:"7"}],["circle",{cx:"15",cy:"15",r:"7"}]],S5=[["path",{d:"M3 3h18"}],["path",{d:"M20 7H8"}],["path",{d:"M20 11H8"}],["path",{d:"M10 19h10"}],["path",{d:"M8 15h12"}],["path",{d:"M4 3v14"}],["circle",{cx:"4",cy:"19",r:"2"}]],L5=[["path",{d:"M10 22V7a1 1 0 0 0-1-1H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5a1 1 0 0 0-1-1H2"}],["rect",{x:"14",y:"2",width:"8",height:"8",rx:"1"}]],f5=[["path",{d:"m7 7 10 10-5 5V2l5 5L7 17"}],["line",{x1:"18",x2:"21",y1:"12",y2:"12"}],["line",{x1:"3",x2:"6",y1:"12",y2:"12"}]],k5=[["path",{d:"m17 17-5 5V12l-5 5"}],["path",{d:"m2 2 20 20"}],["path",{d:"M14.5 9.5 17 7l-5-5v4.5"}]],P5=[["path",{d:"m7 7 10 10-5 5V2l5 5L7 17"}],["path",{d:"M20.83 14.83a4 4 0 0 0 0-5.66"}],["path",{d:"M18 12h.01"}]],B5=[["path",{d:"m7 7 10 10-5 5V2l5 5L7 17"}]],z5=[["path",{d:"M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"}]],F5=[["path",{d:"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"}],["circle",{cx:"12",cy:"12",r:"4"}]],D5=[["circle",{cx:"11",cy:"13",r:"9"}],["path",{d:"M14.35 4.65 16.3 2.7a2.41 2.41 0 0 1 3.4 0l1.6 1.6a2.4 2.4 0 0 1 0 3.4l-1.95 1.95"}],["path",{d:"m22 2-1.5 1.5"}]],b5=[["path",{d:"M17 10c.7-.7 1.69 0 2.5 0a2.5 2.5 0 1 0 0-5 .5.5 0 0 1-.5-.5 2.5 2.5 0 1 0-5 0c0 .81.7 1.8 0 2.5l-7 7c-.7.7-1.69 0-2.5 0a2.5 2.5 0 0 0 0 5c.28 0 .5.22.5.5a2.5 2.5 0 1 0 5 0c0-.81-.7-1.8 0-2.5Z"}]],R5=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m8 13 4-7 4 7"}],["path",{d:"M9.1 11h5.7"}]],T5=[["path",{d:"M12 13h.01"}],["path",{d:"M12 6v3"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}]],q5=[["path",{d:"M12 6v7"}],["path",{d:"M16 8v3"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M8 8v3"}]],U5=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m9 9.5 2 2 4-4"}]],O5=[["path",{d:"M5 7a2 2 0 0 0-2 2v11"}],["path",{d:"M5.803 18H5a2 2 0 0 0 0 4h9.5a.5.5 0 0 0 .5-.5V21"}],["path",{d:"M9 15V4a2 2 0 0 1 2-2h9.5a.5.5 0 0 1 .5.5v14a.5.5 0 0 1-.5.5H11a2 2 0 0 1 0-4h10"}]],z=[["path",{d:"M12 17h1.5"}],["path",{d:"M12 22h1.5"}],["path",{d:"M12 2h1.5"}],["path",{d:"M17.5 22H19a1 1 0 0 0 1-1"}],["path",{d:"M17.5 2H19a1 1 0 0 1 1 1v1.5"}],["path",{d:"M20 14v3h-2.5"}],["path",{d:"M20 8.5V10"}],["path",{d:"M4 10V8.5"}],["path",{d:"M4 19.5V14"}],["path",{d:"M4 4.5A2.5 2.5 0 0 1 6.5 2H8"}],["path",{d:"M8 22H6.5a1 1 0 0 1 0-5H8"}]],Z5=[["path",{d:"M12 13V7"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m9 10 3 3 3-3"}]],G5=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M8 12v-2a4 4 0 0 1 8 0v2"}],["circle",{cx:"15",cy:"12",r:"1"}],["circle",{cx:"9",cy:"12",r:"1"}]],W5=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M8.62 9.8A2.25 2.25 0 1 1 12 6.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a.998.998 0 0 1-1.507 0z"}]],I5=[["path",{d:"m19 3 1 1"}],["path",{d:"m20 2-4.5 4.5"}],["path",{d:"M20 7.898V21a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2h7.844"}],["circle",{cx:"14",cy:"8",r:"2"}]],E5=[["path",{d:"m20 13.7-2.1-2.1a2 2 0 0 0-2.8 0L9.7 17"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["circle",{cx:"10",cy:"8",r:"2"}]],X5=[["path",{d:"M18 6V4a2 2 0 1 0-4 0v2"}],["path",{d:"M20 15v6a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H10"}],["rect",{x:"12",y:"6",width:"8",height:"5",rx:"1"}]],j5=[["path",{d:"M10 2v8l3-3 3 3V2"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}]],N5=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M9 10h6"}]],K5=[["path",{d:"M12 21V7"}],["path",{d:"m16 12 2 2 4-4"}],["path",{d:"M22 6V4a1 1 0 0 0-1-1h-5a4 4 0 0 0-4 4 4 4 0 0 0-4-4H3a1 1 0 0 0-1 1v13a1 1 0 0 0 1 1h6a3 3 0 0 1 3 3 3 3 0 0 1 3-3h6a1 1 0 0 0 1-1v-1.3"}]],Q5=[["path",{d:"M12 7v14"}],["path",{d:"M16 12h2"}],["path",{d:"M16 8h2"}],["path",{d:"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"}],["path",{d:"M6 12h2"}],["path",{d:"M6 8h2"}]],J5=[["path",{d:"M12 7v14"}],["path",{d:"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"}]],Y5=[["path",{d:"M12 7v6"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M9 10h6"}]],_5=[["path",{d:"M11 22H5.5a1 1 0 0 1 0-5h4.501"}],["path",{d:"m21 22-1.879-1.878"}],["path",{d:"M3 19.5v-15A2.5 2.5 0 0 1 5.5 2H18a1 1 0 0 1 1 1v8"}],["circle",{cx:"17",cy:"18",r:"3"}]],x5=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M8 11h8"}],["path",{d:"M8 7h6"}]],a4=[["path",{d:"M12 13V7"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m9 10 3-3 3 3"}]],t4=[["path",{d:"M10 13h4"}],["path",{d:"M12 6v7"}],["path",{d:"M16 8V6H8v2"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}]],h4=[["path",{d:"M12 13V7"}],["path",{d:"M18 2h1a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2"}],["path",{d:"m9 10 3-3 3 3"}],["path",{d:"m9 5 3-3 3 3"}]],d4=[["path",{d:"M15 13a3 3 0 1 0-6 0"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["circle",{cx:"12",cy:"8",r:"2"}]],c4=[["path",{d:"m14.5 7-5 5"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m9.5 7 5 5"}]],M4=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}]],p4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2Z"}],["path",{d:"m9 10 2 2 4-4"}]],i4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"}],["line",{x1:"15",x2:"9",y1:"10",y2:"10"}]],n4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"}],["line",{x1:"12",x2:"12",y1:"7",y2:"13"}],["line",{x1:"15",x2:"9",y1:"10",y2:"10"}]],l4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2Z"}],["path",{d:"m14.5 7.5-5 5"}],["path",{d:"m9.5 7.5 5 5"}]],e4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"}]],r4=[["path",{d:"M4 9V5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4"}],["path",{d:"M8 8v1"}],["path",{d:"M12 8v1"}],["path",{d:"M16 8v1"}],["rect",{width:"20",height:"12",x:"2",y:"9",rx:"2"}],["circle",{cx:"8",cy:"15",r:"2"}],["circle",{cx:"16",cy:"15",r:"2"}]],o4=[["path",{d:"M12 6V2H8"}],["path",{d:"M15 11v2"}],["path",{d:"M2 12h2"}],["path",{d:"M20 12h2"}],["path",{d:"M20 16a2 2 0 0 1-2 2H8.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 4 20.286V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2z"}],["path",{d:"M9 11v2"}]],v4=[["path",{d:"M13.67 8H18a2 2 0 0 1 2 2v4.33"}],["path",{d:"M2 14h2"}],["path",{d:"M20 14h2"}],["path",{d:"M22 22 2 2"}],["path",{d:"M8 8H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 1.414-.586"}],["path",{d:"M9 13v2"}],["path",{d:"M9.67 4H12v2.33"}]],$4=[["path",{d:"M12 8V4H8"}],["rect",{width:"16",height:"12",x:"4",y:"8",rx:"2"}],["path",{d:"M2 14h2"}],["path",{d:"M20 14h2"}],["path",{d:"M15 13v2"}],["path",{d:"M9 13v2"}]],m4=[["path",{d:"M10 3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a6 6 0 0 0 1.2 3.6l.6.8A6 6 0 0 1 17 13v8a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1v-8a6 6 0 0 1 1.2-3.6l.6-.8A6 6 0 0 0 10 5z"}],["path",{d:"M17 13h-4a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h4"}]],y4=[["path",{d:"M17 3h4v4"}],["path",{d:"M18.575 11.082a13 13 0 0 1 1.048 9.027 1.17 1.17 0 0 1-1.914.597L14 17"}],["path",{d:"M7 10 3.29 6.29a1.17 1.17 0 0 1 .6-1.91 13 13 0 0 1 9.03 1.05"}],["path",{d:"M7 14a1.7 1.7 0 0 0-1.207.5l-2.646 2.646A.5.5 0 0 0 3.5 18H5a1 1 0 0 1 1 1v1.5a.5.5 0 0 0 .854.354L9.5 18.207A1.7 1.7 0 0 0 10 17v-2a1 1 0 0 0-1-1z"}],["path",{d:"M9.707 14.293 21 3"}]],s4=[["path",{d:"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"}],["path",{d:"m3.3 7 8.7 5 8.7-5"}],["path",{d:"M12 22V12"}]],g4=[["path",{d:"M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z"}],["path",{d:"m7 16.5-4.74-2.85"}],["path",{d:"m7 16.5 5-3"}],["path",{d:"M7 16.5v5.17"}],["path",{d:"M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z"}],["path",{d:"m17 16.5-5-3"}],["path",{d:"m17 16.5 4.74-2.85"}],["path",{d:"M17 16.5v5.17"}],["path",{d:"M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z"}],["path",{d:"M12 8 7.26 5.15"}],["path",{d:"m12 8 4.74-2.85"}],["path",{d:"M12 13.5V8"}]],F=[["path",{d:"M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1"}],["path",{d:"M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1"}]],C4=[["path",{d:"M16 3h3a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-3"}],["path",{d:"M8 21H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h3"}]],u4=[["path",{d:"M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"}],["path",{d:"M9 13a4.5 4.5 0 0 0 3-4"}],["path",{d:"M6.003 5.125A3 3 0 0 0 6.401 6.5"}],["path",{d:"M3.477 10.896a4 4 0 0 1 .585-.396"}],["path",{d:"M6 18a4 4 0 0 1-1.967-.516"}],["path",{d:"M12 13h4"}],["path",{d:"M12 18h6a2 2 0 0 1 2 2v1"}],["path",{d:"M12 8h8"}],["path",{d:"M16 8V5a2 2 0 0 1 2-2"}],["circle",{cx:"16",cy:"13",r:".5"}],["circle",{cx:"18",cy:"3",r:".5"}],["circle",{cx:"20",cy:"21",r:".5"}],["circle",{cx:"20",cy:"8",r:".5"}]],H4=[["path",{d:"m10.852 14.772-.383.923"}],["path",{d:"m10.852 9.228-.383-.923"}],["path",{d:"m13.148 14.772.382.924"}],["path",{d:"m13.531 8.305-.383.923"}],["path",{d:"m14.772 10.852.923-.383"}],["path",{d:"m14.772 13.148.923.383"}],["path",{d:"M17.598 6.5A3 3 0 1 0 12 5a3 3 0 0 0-5.63-1.446 3 3 0 0 0-.368 1.571 4 4 0 0 0-2.525 5.771"}],["path",{d:"M17.998 5.125a4 4 0 0 1 2.525 5.771"}],["path",{d:"M19.505 10.294a4 4 0 0 1-1.5 7.706"}],["path",{d:"M4.032 17.483A4 4 0 0 0 11.464 20c.18-.311.892-.311 1.072 0a4 4 0 0 0 7.432-2.516"}],["path",{d:"M4.5 10.291A4 4 0 0 0 6 18"}],["path",{d:"M6.002 5.125a3 3 0 0 0 .4 1.375"}],["path",{d:"m9.228 10.852-.923-.383"}],["path",{d:"m9.228 13.148-.923.383"}],["circle",{cx:"12",cy:"12",r:"3"}]],A4=[["path",{d:"M12 18V5"}],["path",{d:"M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4"}],["path",{d:"M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5"}],["path",{d:"M17.997 5.125a4 4 0 0 1 2.526 5.77"}],["path",{d:"M18 18a4 4 0 0 0 2-7.464"}],["path",{d:"M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517"}],["path",{d:"M6 18a4 4 0 0 1-2-7.464"}],["path",{d:"M6.003 5.125a4 4 0 0 0-2.526 5.77"}]],w4=[["path",{d:"M16 3v2.107"}],["path",{d:"M17 9c1 3 2.5 3.5 3.5 4.5A5 5 0 0 1 22 17a5 5 0 0 1-10 0c0-.3 0-.6.1-.9a2 2 0 1 0 3.3-2C13 11.5 16 9 17 9"}],["path",{d:"M21 8.274V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.938"}],["path",{d:"M3 15h5.253"}],["path",{d:"M3 9h8.228"}],["path",{d:"M8 15v6"}],["path",{d:"M8 3v6"}]],V4=[["path",{d:"M12 9v1.258"}],["path",{d:"M16 3v5.46"}],["path",{d:"M21 9.118V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h5.75"}],["path",{d:"M22 17.5c0 2.499-1.75 3.749-3.83 4.474a.5.5 0 0 1-.335-.005c-2.085-.72-3.835-1.97-3.835-4.47V14a.5.5 0 0 1 .5-.499c1 0 2.25-.6 3.12-1.36a.6.6 0 0 1 .76-.001c.875.765 2.12 1.36 3.12 1.36a.5.5 0 0 1 .5.5z"}],["path",{d:"M3 15h7"}],["path",{d:"M3 9h12.142"}],["path",{d:"M8 15v6"}],["path",{d:"M8 3v6"}]],S4=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 9v6"}],["path",{d:"M16 15v6"}],["path",{d:"M16 3v6"}],["path",{d:"M3 15h18"}],["path",{d:"M3 9h18"}],["path",{d:"M8 15v6"}],["path",{d:"M8 3v6"}]],L4=[["path",{d:"M12 12h.01"}],["path",{d:"M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"}],["path",{d:"M22 13a18.15 18.15 0 0 1-20 0"}],["rect",{width:"20",height:"14",x:"2",y:"6",rx:"2"}]],f4=[["path",{d:"M10 20v2"}],["path",{d:"M14 20v2"}],["path",{d:"M18 20v2"}],["path",{d:"M21 20H3"}],["path",{d:"M6 20v2"}],["path",{d:"M8 16V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v12"}],["rect",{x:"4",y:"6",width:"16",height:"10",rx:"2"}]],k4=[["path",{d:"M12 11v4"}],["path",{d:"M14 13h-4"}],["path",{d:"M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"}],["path",{d:"M18 6v14"}],["path",{d:"M6 6v14"}],["rect",{width:"20",height:"14",x:"2",y:"6",rx:"2"}]],P4=[["path",{d:"M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"}],["rect",{width:"20",height:"14",x:"2",y:"6",rx:"2"}]],B4=[["rect",{x:"8",y:"8",width:"8",height:"8",rx:"2"}],["path",{d:"M4 10a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2"}],["path",{d:"M14 20a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2"}]],z4=[["path",{d:"m16 22-1-4"}],["path",{d:"M19 14a1 1 0 0 0 1-1v-1a2 2 0 0 0-2-2h-3a1 1 0 0 1-1-1V4a2 2 0 0 0-4 0v5a1 1 0 0 1-1 1H6a2 2 0 0 0-2 2v1a1 1 0 0 0 1 1"}],["path",{d:"M19 14H5l-1.973 6.767A1 1 0 0 0 4 22h16a1 1 0 0 0 .973-1.233z"}],["path",{d:"m8 22 1-4"}]],F4=[["path",{d:"m11 10 3 3"}],["path",{d:"M6.5 21A3.5 3.5 0 1 0 3 17.5a2.62 2.62 0 0 1-.708 1.792A1 1 0 0 0 3 21z"}],["path",{d:"M9.969 17.031 21.378 5.624a1 1 0 0 0-3.002-3.002L6.967 14.031"}]],D4=[["path",{d:"M7.001 15.085A1.5 1.5 0 0 1 9 16.5"}],["circle",{cx:"18.5",cy:"8.5",r:"3.5"}],["circle",{cx:"7.5",cy:"16.5",r:"5.5"}],["circle",{cx:"7.5",cy:"4.5",r:"2.5"}]],b4=[["path",{d:"M12 20v-8"}],["path",{d:"M14.12 3.88 16 2"}],["path",{d:"M15 7.13V6a3 3 0 0 0-5.14-2.1L8 2"}],["path",{d:"M18 12.34V11a4 4 0 0 0-4-4h-1.3"}],["path",{d:"m2 2 20 20"}],["path",{d:"M21 5a4 4 0 0 1-3.55 3.97"}],["path",{d:"M22 13h-3.34"}],["path",{d:"M3 21a4 4 0 0 1 3.81-4"}],["path",{d:"M6 13H2"}],["path",{d:"M7.7 7.7A4 4 0 0 0 6 11v3a6 6 0 0 0 11.13 3.13"}]],R4=[["path",{d:"M10 19.655A6 6 0 0 1 6 14v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 3.97"}],["path",{d:"M14 15.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997a1 1 0 0 1-1.517-.86z"}],["path",{d:"M14.12 3.88 16 2"}],["path",{d:"M21 5a4 4 0 0 1-3.55 3.97"}],["path",{d:"M3 21a4 4 0 0 1 3.81-4"}],["path",{d:"M3 5a4 4 0 0 0 3.55 3.97"}],["path",{d:"M6 13H2"}],["path",{d:"m8 2 1.88 1.88"}],["path",{d:"M9 7.13V6a3 3 0 1 1 6 0v1.13"}]],T4=[["path",{d:"M12 20v-9"}],["path",{d:"M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z"}],["path",{d:"M14.12 3.88 16 2"}],["path",{d:"M21 21a4 4 0 0 0-3.81-4"}],["path",{d:"M21 5a4 4 0 0 1-3.55 3.97"}],["path",{d:"M22 13h-4"}],["path",{d:"M3 21a4 4 0 0 1 3.81-4"}],["path",{d:"M3 5a4 4 0 0 0 3.55 3.97"}],["path",{d:"M6 13H2"}],["path",{d:"m8 2 1.88 1.88"}],["path",{d:"M9 7.13V6a3 3 0 1 1 6 0v1.13"}]],q4=[["path",{d:"M10 12h4"}],["path",{d:"M10 8h4"}],["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2"}],["path",{d:"M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16"}]],U4=[["path",{d:"M12 10h.01"}],["path",{d:"M12 14h.01"}],["path",{d:"M12 6h.01"}],["path",{d:"M16 10h.01"}],["path",{d:"M16 14h.01"}],["path",{d:"M16 6h.01"}],["path",{d:"M8 10h.01"}],["path",{d:"M8 14h.01"}],["path",{d:"M8 6h.01"}],["path",{d:"M9 22v-3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"}],["rect",{x:"4",y:"2",width:"16",height:"20",rx:"2"}]],O4=[["path",{d:"M4 6 2 7"}],["path",{d:"M10 6h4"}],["path",{d:"m22 7-2-1"}],["rect",{width:"16",height:"16",x:"4",y:"3",rx:"2"}],["path",{d:"M4 11h16"}],["path",{d:"M8 15h.01"}],["path",{d:"M16 15h.01"}],["path",{d:"M6 19v2"}],["path",{d:"M18 21v-2"}]],Z4=[["path",{d:"M8 6v6"}],["path",{d:"M15 6v6"}],["path",{d:"M2 12h19.6"}],["path",{d:"M18 18h3s.5-1.7.8-2.8c.1-.4.2-.8.2-1.2 0-.4-.1-.8-.2-1.2l-1.4-5C20.1 6.8 19.1 6 18 6H4a2 2 0 0 0-2 2v10h3"}],["circle",{cx:"7",cy:"18",r:"2"}],["path",{d:"M9 18h5"}],["circle",{cx:"16",cy:"18",r:"2"}]],G4=[["path",{d:"M10 3h.01"}],["path",{d:"M14 2h.01"}],["path",{d:"m2 9 20-5"}],["path",{d:"M12 12V6.5"}],["rect",{width:"16",height:"10",x:"4",y:"12",rx:"3"}],["path",{d:"M9 12v5"}],["path",{d:"M15 12v5"}],["path",{d:"M4 17h16"}]],W4=[["path",{d:"M17 19a1 1 0 0 1-1-1v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2a1 1 0 0 1-1 1z"}],["path",{d:"M17 21v-2"}],["path",{d:"M19 14V6.5a1 1 0 0 0-7 0v11a1 1 0 0 1-7 0V10"}],["path",{d:"M21 21v-2"}],["path",{d:"M3 5V3"}],["path",{d:"M4 10a2 2 0 0 1-2-2V6a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2z"}],["path",{d:"M7 5V3"}]],I4=[["path",{d:"M16 13H3"}],["path",{d:"M16 17H3"}],["path",{d:"m7.2 7.9-3.388 2.5A2 2 0 0 0 3 12.01V20a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-8.654c0-2-2.44-6.026-6.44-8.026a1 1 0 0 0-1.082.057L10.4 5.6"}],["circle",{cx:"9",cy:"7",r:"2"}]],E4=[["path",{d:"M20 21v-8a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8"}],["path",{d:"M4 16s.5-1 2-1 2.5 2 4 2 2.5-2 4-2 2.5 2 4 2 2-1 2-1"}],["path",{d:"M2 21h20"}],["path",{d:"M7 8v3"}],["path",{d:"M12 8v3"}],["path",{d:"M17 8v3"}],["path",{d:"M7 4h.01"}],["path",{d:"M12 4h.01"}],["path",{d:"M17 4h.01"}]],X4=[["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["line",{x1:"8",x2:"16",y1:"6",y2:"6"}],["line",{x1:"16",x2:"16",y1:"14",y2:"18"}],["path",{d:"M16 10h.01"}],["path",{d:"M12 10h.01"}],["path",{d:"M8 10h.01"}],["path",{d:"M12 14h.01"}],["path",{d:"M8 14h.01"}],["path",{d:"M12 18h.01"}],["path",{d:"M8 18h.01"}]],j4=[["path",{d:"M11 14h1v4"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}],["rect",{x:"3",y:"4",width:"18",height:"18",rx:"2"}]],N4=[["path",{d:"m14 18 4 4 4-4"}],["path",{d:"M16 2v4"}],["path",{d:"M18 14v8"}],["path",{d:"M21 11.354V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7.343"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],K4=[["path",{d:"m14 18 4-4 4 4"}],["path",{d:"M16 2v4"}],["path",{d:"M18 22v-8"}],["path",{d:"M21 11.343V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h9"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],Q4=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["path",{d:"M21 14V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8"}],["path",{d:"M3 10h18"}],["path",{d:"m16 20 2 2 4-4"}]],J4=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"m9 16 2 2 4-4"}]],Y4=[["path",{d:"m15.228 16.852-.923-.383"}],["path",{d:"m15.228 19.148-.923.383"}],["path",{d:"M16 2v4"}],["path",{d:"m16.47 14.305.382.923"}],["path",{d:"m16.852 20.772-.383.924"}],["path",{d:"m19.148 15.228.383-.923"}],["path",{d:"m19.53 21.696-.382-.924"}],["path",{d:"m20.772 16.852.924-.383"}],["path",{d:"m20.772 19.148.924.383"}],["path",{d:"M21 10.592V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}],["circle",{cx:"18",cy:"18",r:"3"}]],_4=[["path",{d:"M16 14v2.2l1.6 1"}],["path",{d:"M16 2v4"}],["path",{d:"M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5"}],["path",{d:"M3 10h5"}],["path",{d:"M8 2v4"}],["circle",{cx:"16",cy:"16",r:"6"}]],x4=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"M8 14h.01"}],["path",{d:"M12 14h.01"}],["path",{d:"M16 14h.01"}],["path",{d:"M8 18h.01"}],["path",{d:"M12 18h.01"}],["path",{d:"M16 18h.01"}]],a3=[["path",{d:"M3 20a2 2 0 0 0 2 2h10a2.4 2.4 0 0 0 1.706-.706l3.588-3.588A2.4 2.4 0 0 0 21 16V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z"}],["path",{d:"M15 22v-5a1 1 0 0 1 1-1h5"}],["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h18"}]],t3=[["path",{d:"M12.127 22H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5.125"}],["path",{d:"M14.62 18.8A2.25 2.25 0 1 1 18 15.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a.998.998 0 0 1-1.507 0z"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],h3=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"M10 16h4"}]],d3=[["path",{d:"M16 19h6"}],["path",{d:"M16 2v4"}],["path",{d:"M21 15V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8.5"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],c3=[["path",{d:"M4.2 4.2A2 2 0 0 0 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 1.82-1.18"}],["path",{d:"M21 15.5V6a2 2 0 0 0-2-2H9.5"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h7"}],["path",{d:"M21 10h-5.5"}],["path",{d:"m2 2 20 20"}]],M3=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"M10 16h4"}],["path",{d:"M12 14v4"}]],p3=[["path",{d:"M16 19h6"}],["path",{d:"M16 2v4"}],["path",{d:"M19 16v6"}],["path",{d:"M21 12.598V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8.5"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],i3=[["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}],["path",{d:"M17 14h-6"}],["path",{d:"M13 18H7"}],["path",{d:"M7 14h.01"}],["path",{d:"M17 18h.01"}]],n3=[["path",{d:"M16 2v4"}],["path",{d:"M21 11.75V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7.25"}],["path",{d:"m22 22-1.875-1.875"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}],["circle",{cx:"18",cy:"18",r:"3"}]],l3=[["path",{d:"M11 10v4h4"}],["path",{d:"m11 14 1.535-1.605a5 5 0 0 1 8 1.5"}],["path",{d:"M16 2v4"}],["path",{d:"m21 18-1.535 1.605a5 5 0 0 1-8-1.5"}],["path",{d:"M21 22v-4h-4"}],["path",{d:"M21 8.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h4.3"}],["path",{d:"M3 10h4"}],["path",{d:"M8 2v4"}]],e3=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["path",{d:"M21 13V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8"}],["path",{d:"M3 10h18"}],["path",{d:"m17 22 5-5"}],["path",{d:"m17 17 5 5"}]],r3=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"m14 14-4 4"}],["path",{d:"m10 14 4 4"}]],o3=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}]],v3=[["path",{d:"M12 2v2"}],["path",{d:"M15.726 21.01A2 2 0 0 1 14 22H4a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2"}],["path",{d:"M18 2v2"}],["path",{d:"M2 13h2"}],["path",{d:"M8 8h14"}],["rect",{x:"8",y:"3",width:"14",height:"14",rx:"2"}]],$3=[["path",{d:"M14.564 14.558a3 3 0 1 1-4.122-4.121"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20 20H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 .819-.175"}],["path",{d:"M9.695 4.024A2 2 0 0 1 10.004 4h3.993a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v7.344"}]],m3=[["path",{d:"M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"}],["circle",{cx:"12",cy:"13",r:"3"}]],y3=[["path",{d:"M5.7 21a2 2 0 0 1-3.5-2l8.6-14a6 6 0 0 1 10.4 6 2 2 0 1 1-3.464-2 2 2 0 1 0-3.464-2Z"}],["path",{d:"M17.75 7 15 2.1"}],["path",{d:"M10.9 4.8 13 9"}],["path",{d:"m7.9 9.7 2 4.4"}],["path",{d:"M4.9 14.7 7 18.9"}]],s3=[["path",{d:"M10 10v7.9"}],["path",{d:"M11.802 6.145a5 5 0 0 1 6.053 6.053"}],["path",{d:"M14 6.1v2.243"}],["path",{d:"m15.5 15.571-.964.964a5 5 0 0 1-7.071 0 5 5 0 0 1 0-7.07l.964-.965"}],["path",{d:"M16 7V3a1 1 0 0 1 1.707-.707 2.5 2.5 0 0 0 2.152.717 1 1 0 0 1 1.131 1.131 2.5 2.5 0 0 0 .717 2.152A1 1 0 0 1 21 8h-4"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8 17v4a1 1 0 0 1-1.707.707 2.5 2.5 0 0 0-2.152-.717 1 1 0 0 1-1.131-1.131 2.5 2.5 0 0 0-.717-2.152A1 1 0 0 1 3 16h4"}]],g3=[["path",{d:"M10 7v10.9"}],["path",{d:"M14 6.1V17"}],["path",{d:"M16 7V3a1 1 0 0 1 1.707-.707 2.5 2.5 0 0 0 2.152.717 1 1 0 0 1 1.131 1.131 2.5 2.5 0 0 0 .717 2.152A1 1 0 0 1 21 8h-4"}],["path",{d:"M16.536 7.465a5 5 0 0 0-7.072 0l-2 2a5 5 0 0 0 0 7.07 5 5 0 0 0 7.072 0l2-2a5 5 0 0 0 0-7.07"}],["path",{d:"M8 17v4a1 1 0 0 1-1.707.707 2.5 2.5 0 0 0-2.152-.717 1 1 0 0 1-1.131-1.131 2.5 2.5 0 0 0-.717-2.152A1 1 0 0 1 3 16h4"}]],C3=[["path",{d:"M12 22v-4c1.5 1.5 3.5 3 6 3 0-1.5-.5-3.5-2-5"}],["path",{d:"M13.988 8.327C13.902 6.054 13.365 3.82 12 2a9.3 9.3 0 0 0-1.445 2.9"}],["path",{d:"M17.375 11.725C18.882 10.53 21 7.841 21 6c-2.324 0-5.08 1.296-6.662 2.684"}],["path",{d:"m2 2 20 20"}],["path",{d:"M21.024 15.378A15 15 0 0 0 22 15c-.426-1.279-2.67-2.557-4.25-2.907"}],["path",{d:"M6.995 6.992C5.714 6.4 4.29 6 3 6c0 2 2.5 5 4 6-1.5 0-4.5 1.5-5 3 3.5 1.5 6 1 6 1-1.5 1.5-2 3.5-2 5 2.5 0 4.5-1.5 6-3"}]],u3=[["path",{d:"M12 22v-4"}],["path",{d:"M7 12c-1.5 0-4.5 1.5-5 3 3.5 1.5 6 1 6 1-1.5 1.5-2 3.5-2 5 2.5 0 4.5-1.5 6-3 1.5 1.5 3.5 3 6 3 0-1.5-.5-3.5-2-5 0 0 2.5.5 6-1-.5-1.5-3.5-3-5-3 1.5-1 4-4 4-6-2.5 0-5.5 1.5-7 3 0-2.5-.5-5-2-7-1.5 2-2 4.5-2 7-1.5-1.5-4.5-3-7-3 0 2 2.5 5 4 6"}]],H3=[["path",{d:"M10.5 5H19a2 2 0 0 1 2 2v8.5"}],["path",{d:"M17 11h-.5"}],["path",{d:"M19 19H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2"}],["path",{d:"m2 2 20 20"}],["path",{d:"M7 11h4"}],["path",{d:"M7 15h2.5"}]],D=[["rect",{width:"18",height:"14",x:"3",y:"5",rx:"2",ry:"2"}],["path",{d:"M7 15h4M15 15h2M7 11h2M13 11h4"}]],A3=[["path",{d:"m21 8-2 2-1.5-3.7A2 2 0 0 0 15.646 5H8.4a2 2 0 0 0-1.903 1.257L5 10 3 8"}],["path",{d:"M7 14h.01"}],["path",{d:"M17 14h.01"}],["rect",{width:"18",height:"8",x:"3",y:"10",rx:"2"}],["path",{d:"M5 18v2"}],["path",{d:"M19 18v2"}]],w3=[["path",{d:"M10 2h4"}],["path",{d:"m21 8-2 2-1.5-3.7A2 2 0 0 0 15.646 5H8.4a2 2 0 0 0-1.903 1.257L5 10 3 8"}],["path",{d:"M7 14h.01"}],["path",{d:"M17 14h.01"}],["rect",{width:"18",height:"8",x:"3",y:"10",rx:"2"}],["path",{d:"M5 18v2"}],["path",{d:"M19 18v2"}]],V3=[["path",{d:"M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2"}],["circle",{cx:"7",cy:"17",r:"2"}],["path",{d:"M9 17h6"}],["circle",{cx:"17",cy:"17",r:"2"}]],S3=[["path",{d:"M18 19V9a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v8a2 2 0 0 0 2 2h2"}],["path",{d:"M2 9h3a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H2"}],["path",{d:"M22 17v1a1 1 0 0 1-1 1H10v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9"}],["circle",{cx:"8",cy:"19",r:"2"}]],L3=[["path",{d:"M12 14v4"}],["path",{d:"M14.172 2a2 2 0 0 1 1.414.586l3.828 3.828A2 2 0 0 1 20 7.828V20a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z"}],["path",{d:"M8 14h8"}],["rect",{x:"8",y:"10",width:"8",height:"8",rx:"1"}]],f3=[["path",{d:"M2.27 21.7s9.87-3.5 12.73-6.36a4.5 4.5 0 0 0-6.36-6.37C5.77 11.84 2.27 21.7 2.27 21.7zM8.64 14l-2.05-2.04M15.34 15l-2.46-2.46"}],["path",{d:"M22 9s-1.33-2-3.5-2C16.86 7 15 9 15 9s1.33 2 3.5 2S22 9 22 9z"}],["path",{d:"M15 2s-2 1.33-2 3.5S15 9 15 9s2-1.84 2-3.5C17 3.33 15 2 15 2z"}]],k3=[["path",{d:"M10 9v7"}],["path",{d:"M14 6v10"}],["circle",{cx:"17.5",cy:"12.5",r:"3.5"}],["circle",{cx:"6.5",cy:"12.5",r:"3.5"}]],P3=[["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M22 9v7"}],["path",{d:"M3.304 13h6.392"}],["circle",{cx:"18.5",cy:"12.5",r:"3.5"}]],B3=[["path",{d:"M15 11h4.5a1 1 0 0 1 0 5h-4a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h3a1 1 0 0 1 0 5"}],["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M3.304 13h6.392"}]],z3=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["circle",{cx:"8",cy:"10",r:"2"}],["path",{d:"M8 12h8"}],["circle",{cx:"16",cy:"10",r:"2"}],["path",{d:"m6 20 .7-2.9A1.4 1.4 0 0 1 8.1 16h7.8a1.4 1.4 0 0 1 1.4 1l.7 3"}]],F3=[["path",{d:"M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"}],["path",{d:"M2 12a9 9 0 0 1 8 8"}],["path",{d:"M2 16a5 5 0 0 1 4 4"}],["line",{x1:"2",x2:"2.01",y1:"20",y2:"20"}]],D3=[["path",{d:"M10 5V3"}],["path",{d:"M14 5V3"}],["path",{d:"M15 21v-3a3 3 0 0 0-6 0v3"}],["path",{d:"M18 3v8"}],["path",{d:"M18 5H6"}],["path",{d:"M22 11H2"}],["path",{d:"M22 9v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9"}],["path",{d:"M6 3v8"}]],b3=[["path",{d:"M12 5c.67 0 1.35.09 2 .26 1.78-2 5.03-2.84 6.42-2.26 1.4.58-.42 7-.42 7 .57 1.07 1 2.24 1 3.44C21 17.9 16.97 21 12 21s-9-3-9-7.56c0-1.25.5-2.4 1-3.44 0 0-1.89-6.42-.5-7 1.39-.58 4.72.23 6.5 2.23A9.04 9.04 0 0 1 12 5Z"}],["path",{d:"M8 14v.5"}],["path",{d:"M16 14v.5"}],["path",{d:"M11.25 16.25h1.5L12 17l-.75-.75Z"}]],R3=[["path",{d:"M16.75 12h3.632a1 1 0 0 1 .894 1.447l-2.034 4.069a1 1 0 0 1-1.708.134l-2.124-2.97"}],["path",{d:"M17.106 9.053a1 1 0 0 1 .447 1.341l-3.106 6.211a1 1 0 0 1-1.342.447L3.61 12.3a2.92 2.92 0 0 1-1.3-3.91L3.69 5.6a2.92 2.92 0 0 1 3.92-1.3z"}],["path",{d:"M2 19h3.76a2 2 0 0 0 1.8-1.1L9 15"}],["path",{d:"M2 21v-4"}],["path",{d:"M7 9h.01"}]],b=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 11.207a.5.5 0 0 1 .146-.353l2-2a.5.5 0 0 1 .708 0l3.292 3.292a.5.5 0 0 0 .708 0l4.292-4.292a.5.5 0 0 1 .854.353V16a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1z"}]],R=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["rect",{x:"7",y:"13",width:"9",height:"4",rx:"1"}],["rect",{x:"7",y:"5",width:"12",height:"4",rx:"1"}]],T3=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 11h8"}],["path",{d:"M7 16h3"}],["path",{d:"M7 6h12"}]],q3=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 11h8"}],["path",{d:"M7 16h12"}],["path",{d:"M7 6h3"}]],U3=[["path",{d:"M11 13v4"}],["path",{d:"M15 5v4"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["rect",{x:"7",y:"13",width:"9",height:"4",rx:"1"}],["rect",{x:"7",y:"5",width:"12",height:"4",rx:"1"}]],T=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 16h8"}],["path",{d:"M7 11h12"}],["path",{d:"M7 6h3"}]],q=[["path",{d:"M9 5v4"}],["rect",{width:"4",height:"6",x:"7",y:"9",rx:"1"}],["path",{d:"M9 15v2"}],["path",{d:"M17 3v2"}],["rect",{width:"4",height:"8",x:"15",y:"5",rx:"1"}],["path",{d:"M17 13v3"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}]],U=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["rect",{x:"15",y:"5",width:"4",height:"12",rx:"1"}],["rect",{x:"7",y:"8",width:"4",height:"9",rx:"1"}]],O3=[["path",{d:"M13 17V9"}],["path",{d:"M18 17v-3"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M8 17V5"}]],O=[["path",{d:"M13 17V9"}],["path",{d:"M18 17V5"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M8 17v-3"}]],Z3=[["path",{d:"M11 13H7"}],["path",{d:"M19 9h-4"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["rect",{x:"15",y:"5",width:"4",height:"12",rx:"1"}],["rect",{x:"7",y:"8",width:"4",height:"9",rx:"1"}]],Z=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M18 17V9"}],["path",{d:"M13 17V5"}],["path",{d:"M8 17v-3"}]],G3=[["path",{d:"M10 6h8"}],["path",{d:"M12 16h6"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M8 11h7"}]],G=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"m19 9-5 5-4-4-3 3"}]],W3=[["path",{d:"m13.11 7.664 1.78 2.672"}],["path",{d:"m14.162 12.788-3.324 1.424"}],["path",{d:"m20 4-6.06 1.515"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["circle",{cx:"12",cy:"6",r:"2"}],["circle",{cx:"16",cy:"12",r:"2"}],["circle",{cx:"9",cy:"15",r:"2"}]],I3=[["path",{d:"M5 21V3"}],["path",{d:"M12 21V9"}],["path",{d:"M19 21v-6"}]],W=[["path",{d:"M5 21v-6"}],["path",{d:"M12 21V9"}],["path",{d:"M19 21V3"}]],I=[["path",{d:"M5 21v-6"}],["path",{d:"M12 21V3"}],["path",{d:"M19 21V9"}]],E3=[["path",{d:"M12 16v5"}],["path",{d:"M16 14v7"}],["path",{d:"M20 10v11"}],["path",{d:"m22 3-8.646 8.646a.5.5 0 0 1-.708 0L9.354 8.354a.5.5 0 0 0-.707 0L2 15"}],["path",{d:"M4 18v3"}],["path",{d:"M8 14v7"}]],E=[["path",{d:"M6 5h12"}],["path",{d:"M4 12h10"}],["path",{d:"M12 19h8"}]],X=[["path",{d:"M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z"}],["path",{d:"M21.21 15.89A10 10 0 1 1 8 2.83"}]],j=[["circle",{cx:"7.5",cy:"7.5",r:".5",fill:"currentColor"}],["circle",{cx:"18.5",cy:"5.5",r:".5",fill:"currentColor"}],["circle",{cx:"11.5",cy:"11.5",r:".5",fill:"currentColor"}],["circle",{cx:"7.5",cy:"16.5",r:".5",fill:"currentColor"}],["circle",{cx:"17.5",cy:"14.5",r:".5",fill:"currentColor"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}]],X3=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 16c.5-2 1.5-7 4-7 2 0 2 3 4 3 2.5 0 4.5-5 5-7"}]],j3=[["path",{d:"M18 6 7 17l-5-5"}],["path",{d:"m22 10-7.5 7.5L13 16"}]],N3=[["path",{d:"M20 4L9 15"}],["path",{d:"M21 19L3 19"}],["path",{d:"M9 15L4 10"}]],K3=[["path",{d:"M20 6 9 17l-5-5"}]],Q3=[["path",{d:"M17 21a1 1 0 0 0 1-1v-5.35c0-.457.316-.844.727-1.041a4 4 0 0 0-2.134-7.589 5 5 0 0 0-9.186 0 4 4 0 0 0-2.134 7.588c.411.198.727.585.727 1.041V20a1 1 0 0 0 1 1Z"}],["path",{d:"M6 17h12"}]],J3=[["path",{d:"M2 17a5 5 0 0 0 10 0c0-2.76-2.5-5-5-3-2.5-2-5 .24-5 3Z"}],["path",{d:"M12 17a5 5 0 0 0 10 0c0-2.76-2.5-5-5-3-2.5-2-5 .24-5 3Z"}],["path",{d:"M7 14c3.22-2.91 4.29-8.75 5-12 1.66 2.38 4.94 9 5 12"}],["path",{d:"M22 9c-4.29 0-7.14-2.33-10-7 5.71 0 10 4.67 10 7Z"}]],Y3=[["path",{d:"M5 20a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1z"}],["path",{d:"M15 18c1.5-.615 3-2.461 3-4.923C18 8.769 14.5 4.462 12 2 9.5 4.462 6 8.77 6 13.077 6 15.539 7.5 17.385 9 18"}],["path",{d:"m16 7-2.5 2.5"}],["path",{d:"M9 2h6"}]],_3=[["path",{d:"M4 20a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z"}],["path",{d:"m6.7 18-1-1C4.35 15.682 3 14.09 3 12a5 5 0 0 1 4.95-5c1.584 0 2.7.455 4.05 1.818C13.35 7.455 14.466 7 16.05 7A5 5 0 0 1 21 12c0 2.082-1.359 3.673-2.7 5l-1 1"}],["path",{d:"M10 4h4"}],["path",{d:"M12 2v6.818"}]],x3=[["path",{d:"M5 20a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1z"}],["path",{d:"M16.5 18c1-2 2.5-5 2.5-9a7 7 0 0 0-7-7H6.635a1 1 0 0 0-.768 1.64L7 5l-2.32 5.802a2 2 0 0 0 .95 2.526l2.87 1.456"}],["path",{d:"m15 5 1.425-1.425"}],["path",{d:"m17 8 1.53-1.53"}],["path",{d:"M9.713 12.185 7 18"}]],ad=[["path",{d:"M5 20a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1z"}],["path",{d:"m14.5 10 1.5 8"}],["path",{d:"M7 10h10"}],["path",{d:"m8 18 1.5-8"}],["circle",{cx:"12",cy:"6",r:"4"}]],td=[["path",{d:"M4 20a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z"}],["path",{d:"m12.474 5.943 1.567 5.34a1 1 0 0 0 1.75.328l2.616-3.402"}],["path",{d:"m20 9-3 9"}],["path",{d:"m5.594 8.209 2.615 3.403a1 1 0 0 0 1.75-.329l1.567-5.34"}],["path",{d:"M7 18 4 9"}],["circle",{cx:"12",cy:"4",r:"2"}],["circle",{cx:"20",cy:"7",r:"2"}],["circle",{cx:"4",cy:"7",r:"2"}]],hd=[["path",{d:"m6 9 6 6 6-6"}]],dd=[["path",{d:"m17 18-6-6 6-6"}],["path",{d:"M7 6v12"}]],cd=[["path",{d:"M5 20a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1z"}],["path",{d:"M10 2v2"}],["path",{d:"M14 2v2"}],["path",{d:"m17 18-1-9"}],["path",{d:"M6 2v5a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2"}],["path",{d:"M6 4h12"}],["path",{d:"m7 18 1-9"}]],Md=[["path",{d:"m7 18 6-6-6-6"}],["path",{d:"M17 6v12"}]],pd=[["path",{d:"m15 18-6-6 6-6"}]],id=[["path",{d:"m9 18 6-6-6-6"}]],nd=[["path",{d:"m18 15-6-6-6 6"}]],ld=[["path",{d:"m7 20 5-5 5 5"}],["path",{d:"m7 4 5 5 5-5"}]],ed=[["path",{d:"m7 6 5 5 5-5"}],["path",{d:"m7 13 5 5 5-5"}]],rd=[["path",{d:"M12 12h.01"}],["path",{d:"M16 12h.01"}],["path",{d:"m17 7 5 5-5 5"}],["path",{d:"m7 7-5 5 5 5"}],["path",{d:"M8 12h.01"}]],od=[["path",{d:"m9 7-5 5 5 5"}],["path",{d:"m15 7 5 5-5 5"}]],vd=[["path",{d:"m11 17-5-5 5-5"}],["path",{d:"m18 17-5-5 5-5"}]],$d=[["path",{d:"m20 17-5-5 5-5"}],["path",{d:"m4 17 5-5-5-5"}]],md=[["path",{d:"m6 17 5-5-5-5"}],["path",{d:"m13 17 5-5-5-5"}]],yd=[["path",{d:"m7 15 5 5 5-5"}],["path",{d:"m7 9 5-5 5 5"}]],sd=[["path",{d:"m17 11-5-5-5 5"}],["path",{d:"m17 18-5-5-5 5"}]],gd=[["path",{d:"M10 9h4"}],["path",{d:"M12 7v5"}],["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"m18 9 3.52 2.147a1 1 0 0 1 .48.854V19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-6.999a1 1 0 0 1 .48-.854L6 9"}],["path",{d:"M6 21V7a1 1 0 0 1 .376-.782l5-3.999a1 1 0 0 1 1.249.001l5 4A1 1 0 0 1 18 7v14"}]],Cd=[["path",{d:"M12 12H3a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h13"}],["path",{d:"M18 8c0-2.5-2-2.5-2-5"}],["path",{d:"m2 2 20 20"}],["path",{d:"M21 12a1 1 0 0 1 1 1v2a1 1 0 0 1-.5.866"}],["path",{d:"M22 8c0-2.5-2-2.5-2-5"}],["path",{d:"M7 12v4"}]],N=[["path",{d:"M10.88 21.94 15.46 14"}],["path",{d:"M21.17 8H12"}],["path",{d:"M3.95 6.06 8.54 14"}],["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"4"}]],ud=[["path",{d:"M17 12H3a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h14"}],["path",{d:"M18 8c0-2.5-2-2.5-2-5"}],["path",{d:"M21 16a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1"}],["path",{d:"M22 8c0-2.5-2-2.5-2-5"}],["path",{d:"M7 12v4"}]],K=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16"}]],Q=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 8v8"}],["path",{d:"m8 12 4 4 4-4"}]],J=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m12 8-4 4 4 4"}],["path",{d:"M16 12H8"}]],Y=[["path",{d:"M2 12a10 10 0 1 1 10 10"}],["path",{d:"m2 22 10-10"}],["path",{d:"M8 22H2v-6"}]],_=[["path",{d:"M12 22a10 10 0 1 1 10-10"}],["path",{d:"M22 22 12 12"}],["path",{d:"M22 16v6h-6"}]],x=[["path",{d:"M2 8V2h6"}],["path",{d:"m2 2 10 10"}],["path",{d:"M12 2A10 10 0 1 1 2 12"}]],a1=[["path",{d:"M22 12A10 10 0 1 1 12 2"}],["path",{d:"M22 2 12 12"}],["path",{d:"M16 2h6v6"}]],t1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m12 16 4-4-4-4"}],["path",{d:"M8 12h8"}]],h1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m16 12-4-4-4 4"}],["path",{d:"M12 16V8"}]],d1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m9 12 2 2 4-4"}]],c1=[["path",{d:"M21.801 10A10 10 0 1 1 17 3.335"}],["path",{d:"m9 11 3 3L22 4"}]],M1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m16 10-4 4-4-4"}]],p1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m14 16-4-4 4-4"}]],i1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m10 8 4 4-4 4"}]],n1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m8 14 4-4 4 4"}]],Hd=[["path",{d:"M10.1 2.182a10 10 0 0 1 3.8 0"}],["path",{d:"M13.9 21.818a10 10 0 0 1-3.8 0"}],["path",{d:"M17.609 3.721a10 10 0 0 1 2.69 2.7"}],["path",{d:"M2.182 13.9a10 10 0 0 1 0-3.8"}],["path",{d:"M20.279 17.609a10 10 0 0 1-2.7 2.69"}],["path",{d:"M21.818 10.1a10 10 0 0 1 0 3.8"}],["path",{d:"M3.721 6.391a10 10 0 0 1 2.7-2.69"}],["path",{d:"M6.391 20.279a10 10 0 0 1-2.69-2.7"}]],l1=[["line",{x1:"8",x2:"16",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"16",y2:"16"}],["line",{x1:"12",x2:"12",y1:"8",y2:"8"}],["circle",{cx:"12",cy:"12",r:"10"}]],Ad=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"}],["path",{d:"M12 18V6"}]],wd=[["path",{d:"M10.1 2.18a9.93 9.93 0 0 1 3.8 0"}],["path",{d:"M17.6 3.71a9.95 9.95 0 0 1 2.69 2.7"}],["path",{d:"M21.82 10.1a9.93 9.93 0 0 1 0 3.8"}],["path",{d:"M20.29 17.6a9.95 9.95 0 0 1-2.7 2.69"}],["path",{d:"M13.9 21.82a9.94 9.94 0 0 1-3.8 0"}],["path",{d:"M6.4 20.29a9.95 9.95 0 0 1-2.69-2.7"}],["path",{d:"M2.18 13.9a9.93 9.93 0 0 1 0-3.8"}],["path",{d:"M3.71 6.4a9.95 9.95 0 0 1 2.7-2.69"}],["circle",{cx:"12",cy:"12",r:"1"}]],Vd=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"1"}]],Sd=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M17 12h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M7 12h.01"}]],Ld=[["path",{d:"M7 10h10"}],["path",{d:"M7 14h10"}],["circle",{cx:"12",cy:"12",r:"10"}]],fd=[["path",{d:"M12 2a10 10 0 0 1 7.38 16.75"}],["path",{d:"m16 12-4-4-4 4"}],["path",{d:"M12 16V8"}],["path",{d:"M2.5 8.875a10 10 0 0 0-.5 3"}],["path",{d:"M2.83 16a10 10 0 0 0 2.43 3.4"}],["path",{d:"M4.636 5.235a10 10 0 0 1 .891-.857"}],["path",{d:"M8.644 21.42a10 10 0 0 0 7.631-.38"}]],kd=[["path",{d:"M12 2a10 10 0 0 1 7.38 16.75"}],["path",{d:"M12 8v8"}],["path",{d:"M16 12H8"}],["path",{d:"M2.5 8.875a10 10 0 0 0-.5 3"}],["path",{d:"M2.83 16a10 10 0 0 0 2.43 3.4"}],["path",{d:"M4.636 5.235a10 10 0 0 1 .891-.857"}],["path",{d:"M8.644 21.42a10 10 0 0 0 7.631-.38"}]],e1=[["path",{d:"M15.6 2.7a10 10 0 1 0 5.7 5.7"}],["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"M13.4 10.6 19 5"}]],r1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M8 12h8"}]],Pd=[["path",{d:"m2 2 20 20"}],["path",{d:"M8.35 2.69A10 10 0 0 1 21.3 15.65"}],["path",{d:"M19.08 19.08A10 10 0 1 1 4.92 4.92"}]],o1=[["path",{d:"M12.656 7H13a3 3 0 0 1 2.984 3.307"}],["path",{d:"M13 13H9"}],["path",{d:"M19.071 19.071A1 1 0 0 1 4.93 4.93"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.357 2.687a10 10 0 0 1 12.956 12.956"}],["path",{d:"M9 17V9"}]],v1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M9 17V7h4a3 3 0 0 1 0 6H9"}]],$1=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"10",x2:"10",y1:"15",y2:"9"}],["line",{x1:"14",x2:"14",y1:"15",y2:"9"}]],m1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m15 9-6 6"}],["path",{d:"M9 9h.01"}],["path",{d:"M15 15h.01"}]],Bd=[["circle",{cx:"12",cy:"19",r:"2"}],["circle",{cx:"12",cy:"5",r:"2"}],["circle",{cx:"16",cy:"12",r:"2"}],["circle",{cx:"20",cy:"19",r:"2"}],["circle",{cx:"4",cy:"19",r:"2"}],["circle",{cx:"8",cy:"12",r:"2"}]],y1=[["path",{d:"M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z"}],["circle",{cx:"12",cy:"12",r:"10"}]],zd=[["path",{d:"M10 16V9.5a1 1 0 0 1 5 0"}],["path",{d:"M8 12h4"}],["path",{d:"M8 16h7"}],["circle",{cx:"12",cy:"12",r:"10"}]],s1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M8 12h8"}],["path",{d:"M12 8v8"}]],g1=[["path",{d:"M12 7v4"}],["path",{d:"M7.998 9.003a5 5 0 1 0 8-.005"}],["circle",{cx:"12",cy:"12",r:"10"}]],l=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"}],["path",{d:"M12 17h.01"}]],C1=[["path",{d:"M22 2 2 22"}],["circle",{cx:"12",cy:"12",r:"10"}]],Fd=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"9",x2:"15",y1:"15",y2:"9"}]],Dd=[["circle",{cx:"12",cy:"12",r:"6"}]],bd=[["path",{d:"M11.051 7.616a1 1 0 0 1 1.909.024l.737 1.452a1 1 0 0 0 .737.535l1.634.256a1 1 0 0 1 .588 1.806l-1.172 1.168a1 1 0 0 0-.282.866l.259 1.613a1 1 0 0 1-1.541 1.134l-1.465-.75a1 1 0 0 0-.912 0l-1.465.75a1 1 0 0 1-1.539-1.133l.258-1.613a1 1 0 0 0-.282-.867l-1.156-1.152a1 1 0 0 1 .572-1.822l1.633-.256a1 1 0 0 0 .737-.535z"}],["circle",{cx:"12",cy:"12",r:"10"}]],u1=[["circle",{cx:"12",cy:"12",r:"10"}],["rect",{x:"9",y:"9",width:"6",height:"6",rx:"1"}]],H1=[["path",{d:"M18 20a6 6 0 0 0-12 0"}],["circle",{cx:"12",cy:"10",r:"4"}],["circle",{cx:"12",cy:"12",r:"10"}]],A1=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"}]],w1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m15 9-6 6"}],["path",{d:"m9 9 6 6"}]],Rd=[["circle",{cx:"12",cy:"12",r:"10"}]],Td=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M11 9h4a2 2 0 0 0 2-2V3"}],["circle",{cx:"9",cy:"9",r:"2"}],["path",{d:"M7 21v-4a2 2 0 0 1 2-2h4"}],["circle",{cx:"15",cy:"15",r:"2"}]],qd=[["path",{d:"M21.66 17.67a1.08 1.08 0 0 1-.04 1.6A12 12 0 0 1 4.73 2.38a1.1 1.1 0 0 1 1.61-.04z"}],["path",{d:"M19.65 15.66A8 8 0 0 1 8.35 4.34"}],["path",{d:"m14 10-5.5 5.5"}],["path",{d:"M14 17.85V10H6.15"}]],Ud=[["path",{d:"M20.2 6 3 11l-.9-2.4c-.3-1.1.3-2.2 1.3-2.5l13.5-4c1.1-.3 2.2.3 2.5 1.3Z"}],["path",{d:"m6.2 5.3 3.1 3.9"}],["path",{d:"m12.4 3.4 3.1 4"}],["path",{d:"M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"}]],Od=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"m9 14 2 2 4-4"}]],Zd=[["path",{d:"M16 14v2.2l1.6 1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v.832"}],["path",{d:"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h2"}],["circle",{cx:"16",cy:"16",r:"6"}],["rect",{x:"8",y:"2",width:"8",height:"4",rx:"1"}]],Gd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v4"}],["path",{d:"M21 14H11"}],["path",{d:"m15 10-4 4 4 4"}]],Wd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"M12 11h4"}],["path",{d:"M12 16h4"}],["path",{d:"M8 11h.01"}],["path",{d:"M8 16h.01"}]],Id=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"M9 14h6"}]],Ed=[["path",{d:"M11 14h10"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v1.344"}],["path",{d:"m17 18 4-4-4-4"}],["path",{d:"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 1.793-1.113"}],["rect",{x:"8",y:"2",width:"8",height:"4",rx:"1"}]],V1=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1"}],["path",{d:"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-.5"}],["path",{d:"M16 4h2a2 2 0 0 1 1.73 1"}],["path",{d:"M8 18h1"}],["path",{d:"M21.378 12.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}]],S1=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-5.5"}],["path",{d:"M4 13.5V6a2 2 0 0 1 2-2h2"}],["path",{d:"M13.378 15.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}]],Xd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"M9 14h6"}],["path",{d:"M12 17v-6"}]],jd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"M9 12v-1h6v1"}],["path",{d:"M11 17h2"}],["path",{d:"M12 11v6"}]],Nd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"m15 11-6 6"}],["path",{d:"m9 11 6 6"}]],Kd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}]],Qd=[["path",{d:"M12 6v6l2-4"}],["circle",{cx:"12",cy:"12",r:"10"}]],Jd=[["path",{d:"M12 6v6l-4-2"}],["circle",{cx:"12",cy:"12",r:"10"}]],Yd=[["path",{d:"M12 6v6l-2-4"}],["circle",{cx:"12",cy:"12",r:"10"}]],_d=[["path",{d:"M12 6v6"}],["circle",{cx:"12",cy:"12",r:"10"}]],xd=[["path",{d:"M12 6v6l4-2"}],["circle",{cx:"12",cy:"12",r:"10"}]],a8=[["path",{d:"M12 6v6h4"}],["circle",{cx:"12",cy:"12",r:"10"}]],t8=[["path",{d:"M12 6v6l4 2"}],["circle",{cx:"12",cy:"12",r:"10"}]],h8=[["path",{d:"M12 6v6l2 4"}],["circle",{cx:"12",cy:"12",r:"10"}]],d8=[["path",{d:"M12 6v10"}],["circle",{cx:"12",cy:"12",r:"10"}]],c8=[["path",{d:"M12 6v6l-2 4"}],["circle",{cx:"12",cy:"12",r:"10"}]],M8=[["path",{d:"M12 6v6l-4 2"}],["circle",{cx:"12",cy:"12",r:"10"}]],p8=[["path",{d:"M12 6v6H8"}],["circle",{cx:"12",cy:"12",r:"10"}]],i8=[["path",{d:"M12 6v6l4 2"}],["path",{d:"M20 12v5"}],["path",{d:"M20 21h.01"}],["path",{d:"M21.25 8.2A10 10 0 1 0 16 21.16"}]],n8=[["path",{d:"M12 6v6l2 1"}],["path",{d:"M12.337 21.994a10 10 0 1 1 9.588-8.767"}],["path",{d:"m14 18 4 4 4-4"}],["path",{d:"M18 14v8"}]],l8=[["path",{d:"M12 6v6l1.56.78"}],["path",{d:"M13.227 21.925a10 10 0 1 1 8.767-9.588"}],["path",{d:"m14 18 4-4 4 4"}],["path",{d:"M18 22v-8"}]],e8=[["path",{d:"M12 6v6l4 2"}],["path",{d:"M22 12a10 10 0 1 0-11 9.95"}],["path",{d:"m22 16-5.5 5.5L14 19"}]],r8=[["path",{d:"M12 2a10 10 0 0 1 7.38 16.75"}],["path",{d:"M12 6v6l4 2"}],["path",{d:"M2.5 8.875a10 10 0 0 0-.5 3"}],["path",{d:"M2.83 16a10 10 0 0 0 2.43 3.4"}],["path",{d:"M4.636 5.235a10 10 0 0 1 .891-.857"}],["path",{d:"M8.644 21.42a10 10 0 0 0 7.631-.38"}]],o8=[["path",{d:"M12 6v6l3.644 1.822"}],["path",{d:"M16 19h6"}],["path",{d:"M19 16v6"}],["path",{d:"M21.92 13.267a10 10 0 1 0-8.653 8.653"}]],v8=[["path",{d:"M12 6v6l4 2"}],["circle",{cx:"12",cy:"12",r:"10"}]],$8=[["path",{d:"M10 9.17a3 3 0 1 0 0 5.66"}],["path",{d:"M17 9.17a3 3 0 1 0 0 5.66"}],["rect",{x:"2",y:"5",width:"20",height:"14",rx:"2"}]],m8=[["path",{d:"M12 12v4"}],["path",{d:"M12 20h.01"}],["path",{d:"M17 18h.5a1 1 0 0 0 0-9h-1.79A7 7 0 1 0 7 17.708"}]],y8=[["path",{d:"M21 15.251A4.5 4.5 0 0 0 17.5 8h-1.79A7 7 0 1 0 3 13.607"}],["path",{d:"M7 11v4h4"}],["path",{d:"M8 19a5 5 0 0 0 9-3 4.5 4.5 0 0 0-4.5-4.5 4.82 4.82 0 0 0-3.41 1.41L7 15"}]],s8=[["path",{d:"m17 15-5.5 5.5L9 18"}],["path",{d:"M5 17.743A7 7 0 1 1 15.71 10h1.79a4.5 4.5 0 0 1 1.5 8.742"}]],g8=[["path",{d:"m10.852 19.772-.383.924"}],["path",{d:"m13.148 14.228.383-.923"}],["path",{d:"M13.148 19.772a3 3 0 1 0-2.296-5.544l-.383-.923"}],["path",{d:"m13.53 20.696-.382-.924a3 3 0 1 1-2.296-5.544"}],["path",{d:"m14.772 15.852.923-.383"}],["path",{d:"m14.772 18.148.923.383"}],["path",{d:"M4.2 15.1a7 7 0 1 1 9.93-9.858A7 7 0 0 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.2"}],["path",{d:"m9.228 15.852-.923-.383"}],["path",{d:"m9.228 18.148-.923.383"}]],L1=[["path",{d:"M12 13v8l-4-4"}],["path",{d:"m12 21 4-4"}],["path",{d:"M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284"}]],C8=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M8 19v1"}],["path",{d:"M8 14v1"}],["path",{d:"M16 19v1"}],["path",{d:"M16 14v1"}],["path",{d:"M12 21v1"}],["path",{d:"M12 16v1"}]],u8=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M16 17H7"}],["path",{d:"M17 21H9"}]],H8=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M16 14v2"}],["path",{d:"M8 14v2"}],["path",{d:"M16 20h.01"}],["path",{d:"M8 20h.01"}],["path",{d:"M12 16v2"}],["path",{d:"M12 22h.01"}]],A8=[["path",{d:"M6 16.326A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 .5 8.973"}],["path",{d:"m13 12-3 5h4l-3 5"}]],w8=[["path",{d:"M11 20v2"}],["path",{d:"M18.376 14.512a6 6 0 0 0 3.461-4.127c.148-.625-.659-.97-1.248-.714a4 4 0 0 1-5.259-5.26c.255-.589-.09-1.395-.716-1.248a6 6 0 0 0-4.594 5.36"}],["path",{d:"M3 20a5 5 0 1 1 8.9-4H13a3 3 0 0 1 2 5.24"}],["path",{d:"M7 19v2"}]],V8=[["path",{d:"M13 16a3 3 0 0 1 0 6H7a5 5 0 1 1 4.9-6z"}],["path",{d:"M18.376 14.512a6 6 0 0 0 3.461-4.127c.148-.625-.659-.97-1.248-.714a4 4 0 0 1-5.259-5.26c.255-.589-.09-1.395-.716-1.248a6 6 0 0 0-4.594 5.36"}]],S8=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"m9.2 22 3-7"}],["path",{d:"m9 13-3 7"}],["path",{d:"m17 13-3 7"}]],L8=[["path",{d:"m2 2 20 20"}],["path",{d:"M5.782 5.782A7 7 0 0 0 9 19h8.5a4.5 4.5 0 0 0 1.307-.193"}],["path",{d:"M21.532 16.5A4.5 4.5 0 0 0 17.5 10h-1.79A7.008 7.008 0 0 0 10 5.07"}]],f8=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M16 14v6"}],["path",{d:"M8 14v6"}],["path",{d:"M12 16v6"}]],k8=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M8 15h.01"}],["path",{d:"M8 19h.01"}],["path",{d:"M12 17h.01"}],["path",{d:"M12 21h.01"}],["path",{d:"M16 15h.01"}],["path",{d:"M16 19h.01"}]],P8=[["path",{d:"M12 2v2"}],["path",{d:"m4.93 4.93 1.41 1.41"}],["path",{d:"M20 12h2"}],["path",{d:"m19.07 4.93-1.41 1.41"}],["path",{d:"M15.947 12.65a4 4 0 0 0-5.925-4.128"}],["path",{d:"M3 20a5 5 0 1 1 8.9-4H13a3 3 0 0 1 2 5.24"}],["path",{d:"M11 20v2"}],["path",{d:"M7 19v2"}]],B8=[["path",{d:"M12 2v2"}],["path",{d:"m4.93 4.93 1.41 1.41"}],["path",{d:"M20 12h2"}],["path",{d:"m19.07 4.93-1.41 1.41"}],["path",{d:"M15.947 12.65a4 4 0 0 0-5.925-4.128"}],["path",{d:"M13 22H7a5 5 0 1 1 4.9-6H13a3 3 0 0 1 0 6Z"}]],z8=[["path",{d:"m17 18-1.535 1.605a5 5 0 0 1-8-1.5"}],["path",{d:"M17 22v-4h-4"}],["path",{d:"M20.996 15.251A4.5 4.5 0 0 0 17.495 8h-1.79a7 7 0 1 0-12.709 5.607"}],["path",{d:"M7 10v4h4"}],["path",{d:"m7 14 1.535-1.605a5 5 0 0 1 8 1.5"}]],f1=[["path",{d:"M12 13v8"}],["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"m8 17 4-4 4 4"}]],F8=[["path",{d:"M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"}]],D8=[["path",{d:"M17.5 21H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"}],["path",{d:"M22 10a3 3 0 0 0-3-3h-2.207a5.502 5.502 0 0 0-10.702.5"}]],b8=[["path",{d:"M16.17 7.83 2 22"}],["path",{d:"M4.02 12a2.827 2.827 0 1 1 3.81-4.17A2.827 2.827 0 1 1 12 4.02a2.827 2.827 0 1 1 4.17 3.81A2.827 2.827 0 1 1 19.98 12a2.827 2.827 0 1 1-3.81 4.17A2.827 2.827 0 1 1 12 19.98a2.827 2.827 0 1 1-4.17-3.81A1 1 0 1 1 4 12"}],["path",{d:"m7.83 7.83 8.34 8.34"}]],R8=[["path",{d:"M17.28 9.05a5.5 5.5 0 1 0-10.56 0A5.5 5.5 0 1 0 12 17.66a5.5 5.5 0 1 0 5.28-8.6Z"}],["path",{d:"M12 17.66L12 22"}]],k1=[["path",{d:"m18 16 4-4-4-4"}],["path",{d:"m6 8-4 4 4 4"}],["path",{d:"m14.5 4-5 16"}]],T8=[["path",{d:"m16 18 6-6-6-6"}],["path",{d:"m8 6-6 6 6 6"}]],q8=[["polygon",{points:"12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"}],["line",{x1:"12",x2:"12",y1:"22",y2:"15.5"}],["polyline",{points:"22 8.5 12 15.5 2 8.5"}],["polyline",{points:"2 15.5 12 8.5 22 15.5"}],["line",{x1:"12",x2:"12",y1:"2",y2:"8.5"}]],U8=[["path",{d:"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"}],["polyline",{points:"7.5 4.21 12 6.81 16.5 4.21"}],["polyline",{points:"7.5 19.79 7.5 14.6 3 12"}],["polyline",{points:"21 12 16.5 14.6 16.5 19.79"}],["polyline",{points:"3.27 6.96 12 12.01 20.73 6.96"}],["line",{x1:"12",x2:"12",y1:"22.08",y2:"12"}]],O8=[["path",{d:"M10 2v2"}],["path",{d:"M14 2v2"}],["path",{d:"M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1"}],["path",{d:"M6 2v2"}]],Z8=[["circle",{cx:"8",cy:"8",r:"6"}],["path",{d:"M18.09 10.37A6 6 0 1 1 10.34 18"}],["path",{d:"M7 6h1v4"}],["path",{d:"m16.71 13.88.7.71-2.82 2.82"}]],G8=[["path",{d:"M11 10.27 7 3.34"}],["path",{d:"m11 13.73-4 6.93"}],["path",{d:"M12 22v-2"}],["path",{d:"M12 2v2"}],["path",{d:"M14 12h8"}],["path",{d:"m17 20.66-1-1.73"}],["path",{d:"m17 3.34-1 1.73"}],["path",{d:"M2 12h2"}],["path",{d:"m20.66 17-1.73-1"}],["path",{d:"m20.66 7-1.73 1"}],["path",{d:"m3.34 17 1.73-1"}],["path",{d:"m3.34 7 1.73 1"}],["circle",{cx:"12",cy:"12",r:"2"}],["circle",{cx:"12",cy:"12",r:"8"}]],P1=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 3v18"}]],e=[["path",{d:"M10.5 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5.5"}],["path",{d:"m14.3 19.6 1-.4"}],["path",{d:"M15 3v7.5"}],["path",{d:"m15.2 16.9-.9-.3"}],["path",{d:"m16.6 21.7.3-.9"}],["path",{d:"m16.8 15.3-.4-1"}],["path",{d:"m19.1 15.2.3-.9"}],["path",{d:"m19.6 21.7-.4-1"}],["path",{d:"m20.7 16.8 1-.4"}],["path",{d:"m21.7 19.4-.9-.3"}],["path",{d:"M9 3v18"}],["circle",{cx:"18",cy:"18",r:"3"}]],B1=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}],["path",{d:"M15 3v18"}]],W8=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7.5 3v18"}],["path",{d:"M12 3v18"}],["path",{d:"M16.5 3v18"}]],I8=[["path",{d:"M14 3a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1"}],["path",{d:"M19 3a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1"}],["path",{d:"m7 15 3 3"}],["path",{d:"m7 21 3-3H5a2 2 0 0 1-2-2v-2"}],["rect",{x:"14",y:"14",width:"7",height:"7",rx:"1"}],["rect",{x:"3",y:"3",width:"7",height:"7",rx:"1"}]],E8=[["path",{d:"M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"}]],X8=[["path",{d:"m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z"}],["circle",{cx:"12",cy:"12",r:"10"}]],j8=[["path",{d:"M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"}],["path",{d:"M2.297 11.293a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0z"}],["path",{d:"M8.916 17.912a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0z"}],["path",{d:"M8.916 4.674a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"}]],N8=[["rect",{width:"14",height:"8",x:"5",y:"2",rx:"2"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2"}],["path",{d:"M6 18h2"}],["path",{d:"M12 18h6"}]],K8=[["path",{d:"M3 20a1 1 0 0 1-1-1v-1a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1Z"}],["path",{d:"M20 16a8 8 0 1 0-16 0"}],["path",{d:"M12 4v4"}],["path",{d:"M10 4h4"}]],Q8=[["path",{d:"m20.9 18.55-8-15.98a1 1 0 0 0-1.8 0l-8 15.98"}],["ellipse",{cx:"12",cy:"19",rx:"9",ry:"3"}]],J8=[["rect",{x:"2",y:"6",width:"20",height:"8",rx:"1"}],["path",{d:"M17 14v7"}],["path",{d:"M7 14v7"}],["path",{d:"M17 3v3"}],["path",{d:"M7 3v3"}],["path",{d:"M10 14 2.3 6.3"}],["path",{d:"m14 6 7.7 7.7"}],["path",{d:"m8 6 8 8"}]],z1=[["path",{d:"M16 2v2"}],["path",{d:"M17.915 22a6 6 0 0 0-12 0"}],["path",{d:"M8 2v2"}],["circle",{cx:"12",cy:"12",r:"4"}],["rect",{x:"3",y:"4",width:"18",height:"18",rx:"2"}]],Y8=[["path",{d:"M16 2v2"}],["path",{d:"M7 22v-2a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2"}],["path",{d:"M8 2v2"}],["circle",{cx:"12",cy:"11",r:"3"}],["rect",{x:"3",y:"4",width:"18",height:"18",rx:"2"}]],_8=[["path",{d:"M22 7.7c0-.6-.4-1.2-.8-1.5l-6.3-3.9a1.72 1.72 0 0 0-1.7 0l-10.3 6c-.5.2-.9.8-.9 1.4v6.6c0 .5.4 1.2.8 1.5l6.3 3.9a1.72 1.72 0 0 0 1.7 0l10.3-6c.5-.3.9-1 .9-1.5Z"}],["path",{d:"M10 21.9V14L2.1 9.1"}],["path",{d:"m10 14 11.9-6.9"}],["path",{d:"M14 19.8v-8.1"}],["path",{d:"M18 17.5V9.4"}]],x8=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 18a6 6 0 0 0 0-12v12z"}]],a6=[["path",{d:"M12 2a10 10 0 1 0 10 10 4 4 0 0 1-5-5 4 4 0 0 1-5-5"}],["path",{d:"M8.5 8.5v.01"}],["path",{d:"M16 15.5v.01"}],["path",{d:"M12 12v.01"}],["path",{d:"M11 17v.01"}],["path",{d:"M7 14v.01"}]],t6=[["path",{d:"M2 12h20"}],["path",{d:"M20 12v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8"}],["path",{d:"m4 8 16-4"}],["path",{d:"m8.86 6.78-.45-1.81a2 2 0 0 1 1.45-2.43l1.94-.48a2 2 0 0 1 2.43 1.46l.45 1.8"}]],h6=[["path",{d:"m12 15 2 2 4-4"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],d6=[["line",{x1:"12",x2:"18",y1:"15",y2:"15"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],c6=[["line",{x1:"15",x2:"15",y1:"12",y2:"18"}],["line",{x1:"12",x2:"18",y1:"15",y2:"15"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],M6=[["line",{x1:"12",x2:"18",y1:"18",y2:"12"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],p6=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],i6=[["line",{x1:"12",x2:"18",y1:"12",y2:"18"}],["line",{x1:"12",x2:"18",y1:"18",y2:"12"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],n6=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M9.17 14.83a4 4 0 1 0 0-5.66"}]],l6=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M14.83 14.83a4 4 0 1 1 0-5.66"}]],e6=[["path",{d:"m15 10 5 5-5 5"}],["path",{d:"M4 4v7a4 4 0 0 0 4 4h12"}]],r6=[["path",{d:"M20 4v7a4 4 0 0 1-4 4H4"}],["path",{d:"m9 10-5 5 5 5"}]],o6=[["path",{d:"m14 15-5 5-5-5"}],["path",{d:"M20 4h-7a4 4 0 0 0-4 4v12"}]],v6=[["path",{d:"M14 9 9 4 4 9"}],["path",{d:"M20 20h-7a4 4 0 0 1-4-4V4"}]],$6=[["path",{d:"m10 15 5 5 5-5"}],["path",{d:"M4 4h7a4 4 0 0 1 4 4v12"}]],m6=[["path",{d:"m10 9 5-5 5 5"}],["path",{d:"M4 20h7a4 4 0 0 0 4-4V4"}]],y6=[["path",{d:"M20 20v-7a4 4 0 0 0-4-4H4"}],["path",{d:"M9 14 4 9l5-5"}]],s6=[["path",{d:"m15 14 5-5-5-5"}],["path",{d:"M4 20v-7a4 4 0 0 1 4-4h12"}]],g6=[["path",{d:"M12 20v2"}],["path",{d:"M12 2v2"}],["path",{d:"M17 20v2"}],["path",{d:"M17 2v2"}],["path",{d:"M2 12h2"}],["path",{d:"M2 17h2"}],["path",{d:"M2 7h2"}],["path",{d:"M20 12h2"}],["path",{d:"M20 17h2"}],["path",{d:"M20 7h2"}],["path",{d:"M7 20v2"}],["path",{d:"M7 2v2"}],["rect",{x:"4",y:"4",width:"16",height:"16",rx:"2"}],["rect",{x:"8",y:"8",width:"8",height:"8",rx:"1"}]],C6=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M10 9.3a2.8 2.8 0 0 0-3.5 1 3.1 3.1 0 0 0 0 3.4 2.7 2.7 0 0 0 3.5 1"}],["path",{d:"M17 9.3a2.8 2.8 0 0 0-3.5 1 3.1 3.1 0 0 0 0 3.4 2.7 2.7 0 0 0 3.5 1"}]],u6=[["rect",{width:"20",height:"14",x:"2",y:"5",rx:"2"}],["line",{x1:"2",x2:"22",y1:"10",y2:"10"}]],H6=[["path",{d:"M10.2 18H4.774a1.5 1.5 0 0 1-1.352-.97 11 11 0 0 1 .132-6.487"}],["path",{d:"M18 10.2V4.774a1.5 1.5 0 0 0-.97-1.352 11 11 0 0 0-6.486.132"}],["path",{d:"M18 5a4 3 0 0 1 4 3 2 2 0 0 1-2 2 10 10 0 0 0-5.139 1.42"}],["path",{d:"M5 18a3 4 0 0 0 3 4 2 2 0 0 0 2-2 10 10 0 0 1 1.42-5.14"}],["path",{d:"M8.709 2.554a10 10 0 0 0-6.155 6.155 1.5 1.5 0 0 0 .676 1.626l9.807 5.42a2 2 0 0 0 2.718-2.718l-5.42-9.807a1.5 1.5 0 0 0-1.626-.676"}]],A6=[["path",{d:"M6 2v14a2 2 0 0 0 2 2h14"}],["path",{d:"M18 22V8a2 2 0 0 0-2-2H2"}]],w6=[["path",{d:"M4 9a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h4a1 1 0 0 1 1 1v4a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-4a1 1 0 0 1 1-1h4a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2h-4a1 1 0 0 1-1-1V4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4a1 1 0 0 1-1 1z"}]],V6=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"22",x2:"18",y1:"12",y2:"12"}],["line",{x1:"6",x2:"2",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"6",y2:"2"}],["line",{x1:"12",x2:"12",y1:"22",y2:"18"}]],S6=[["path",{d:"M11.562 3.266a.5.5 0 0 1 .876 0L15.39 8.87a1 1 0 0 0 1.516.294L21.183 5.5a.5.5 0 0 1 .798.519l-2.834 10.246a1 1 0 0 1-.956.734H5.81a1 1 0 0 1-.957-.734L2.02 6.02a.5.5 0 0 1 .798-.519l4.276 3.664a1 1 0 0 0 1.516-.294z"}],["path",{d:"M5 21h14"}]],L6=[["path",{d:"m21.12 6.4-6.05-4.06a2 2 0 0 0-2.17-.05L2.95 8.41a2 2 0 0 0-.95 1.7v5.82a2 2 0 0 0 .88 1.66l6.05 4.07a2 2 0 0 0 2.17.05l9.95-6.12a2 2 0 0 0 .95-1.7V8.06a2 2 0 0 0-.88-1.66Z"}],["path",{d:"M10 22v-8L2.25 9.15"}],["path",{d:"m10 14 11.77-6.87"}]],f6=[["path",{d:"m6 8 1.75 12.28a2 2 0 0 0 2 1.72h4.54a2 2 0 0 0 2-1.72L18 8"}],["path",{d:"M5 8h14"}],["path",{d:"M7 15a6.47 6.47 0 0 1 5 0 6.47 6.47 0 0 0 5 0"}],["path",{d:"m12 8 1-6h2"}]],k6=[["circle",{cx:"12",cy:"12",r:"8"}],["line",{x1:"3",x2:"6",y1:"3",y2:"6"}],["line",{x1:"21",x2:"18",y1:"3",y2:"6"}],["line",{x1:"3",x2:"6",y1:"21",y2:"18"}],["line",{x1:"21",x2:"18",y1:"21",y2:"18"}]],P6=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3"}],["path",{d:"M3 5v14a9 3 0 0 0 18 0V5"}]],B6=[["path",{d:"M11 11.31c1.17.56 1.54 1.69 3.5 1.69 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M11.75 18c.35.5 1.45 1 2.75 1 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["path",{d:"M2 6h4"}],["path",{d:"M7 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1L10 4a1 1 0 0 0-1-1z"}]],z6=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3"}],["path",{d:"M3 12a9 3 0 0 0 5 2.69"}],["path",{d:"M21 9.3V5"}],["path",{d:"M3 5v14a9 3 0 0 0 6.47 2.88"}],["path",{d:"M12 12v4h4"}],["path",{d:"M13 20a5 5 0 0 0 9-3 4.5 4.5 0 0 0-4.5-4.5c-1.33 0-2.54.54-3.41 1.41L12 16"}]],F6=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3"}],["path",{d:"M3 5V19A9 3 0 0 0 15 21.84"}],["path",{d:"M21 5V8"}],["path",{d:"M21 12L18 17H22L19 22"}],["path",{d:"M3 12A9 3 0 0 0 14.59 14.87"}]],D6=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5"}],["path",{d:"M3 12A9 3 0 0 0 21 12"}]],b6=[["path",{d:"m13 21-3-3 3-3"}],["path",{d:"M20 18H10"}],["path",{d:"M3 11h.01"}],["rect",{x:"6",y:"3",width:"5",height:"8",rx:"2.5"}]],R6=[["path",{d:"M10 18h10"}],["path",{d:"m17 21 3-3-3-3"}],["path",{d:"M3 11h.01"}],["rect",{x:"15",y:"3",width:"5",height:"8",rx:"2.5"}],["rect",{x:"6",y:"3",width:"5",height:"8",rx:"2.5"}]],T6=[["path",{d:"M10 5a2 2 0 0 0-1.344.519l-6.328 5.74a1 1 0 0 0 0 1.481l6.328 5.741A2 2 0 0 0 10 19h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z"}],["path",{d:"m12 9 6 6"}],["path",{d:"m18 9-6 6"}]],q6=[["path",{d:"M10.162 3.167A10 10 0 0 0 2 13a2 2 0 0 0 4 0v-1a2 2 0 0 1 4 0v4a2 2 0 0 0 4 0v-4a2 2 0 0 1 4 0v1a2 2 0 0 0 4-.006 10 10 0 0 0-8.161-9.826"}],["path",{d:"M20.804 14.869a9 9 0 0 1-17.608 0"}],["circle",{cx:"12",cy:"4",r:"2"}]],U6=[["circle",{cx:"19",cy:"19",r:"2"}],["circle",{cx:"5",cy:"5",r:"2"}],["path",{d:"M6.48 3.66a10 10 0 0 1 13.86 13.86"}],["path",{d:"m6.41 6.41 11.18 11.18"}],["path",{d:"M3.66 6.48a10 10 0 0 0 13.86 13.86"}]],O6=[["path",{d:"M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41L13.7 2.71a2.41 2.41 0 0 0-3.41 0z"}],["path",{d:"M8 12h8"}]],F1=[["path",{d:"M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41L13.7 2.71a2.41 2.41 0 0 0-3.41 0Z"}],["path",{d:"M9.2 9.2h.01"}],["path",{d:"m14.5 9.5-5 5"}],["path",{d:"M14.7 14.8h.01"}]],Z6=[["path",{d:"M12 8v8"}],["path",{d:"M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41L13.7 2.71a2.41 2.41 0 0 0-3.41 0z"}],["path",{d:"M8 12h8"}]],G6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M12 12h.01"}]],W6=[["path",{d:"M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41l-7.59-7.59a2.41 2.41 0 0 0-3.41 0Z"}]],I6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M15 9h.01"}],["path",{d:"M9 15h.01"}]],E6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M16 8h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M8 16h.01"}]],X6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M16 8h.01"}],["path",{d:"M8 8h.01"}],["path",{d:"M8 16h.01"}],["path",{d:"M16 16h.01"}],["path",{d:"M12 12h.01"}]],j6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M16 8h.01"}],["path",{d:"M16 12h.01"}],["path",{d:"M16 16h.01"}],["path",{d:"M8 8h.01"}],["path",{d:"M8 12h.01"}],["path",{d:"M8 16h.01"}]],N6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M16 8h.01"}],["path",{d:"M8 8h.01"}],["path",{d:"M8 16h.01"}],["path",{d:"M16 16h.01"}]],K6=[["rect",{width:"12",height:"12",x:"2",y:"10",rx:"2",ry:"2"}],["path",{d:"m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6"}],["path",{d:"M6 18h.01"}],["path",{d:"M10 14h.01"}],["path",{d:"M15 6h.01"}],["path",{d:"M18 9h.01"}]],Q6=[["path",{d:"M12 3v14"}],["path",{d:"M5 10h14"}],["path",{d:"M5 21h14"}]],J6=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M12 12h.01"}]],Y6=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M6 12c0-1.7.7-3.2 1.8-4.2"}],["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"M18 12c0 1.7-.7 3.2-1.8 4.2"}]],_6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"12",cy:"12",r:"5"}],["path",{d:"M12 12h.01"}]],x6=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"2"}]],ac=[["circle",{cx:"12",cy:"6",r:"1"}],["line",{x1:"5",x2:"19",y1:"12",y2:"12"}],["circle",{cx:"12",cy:"18",r:"1"}]],tc=[["path",{d:"M15 2c-1.35 1.5-2.092 3-2.5 4.5L14 8"}],["path",{d:"m17 6-2.891-2.891"}],["path",{d:"M2 15c3.333-3 6.667-3 10-3"}],["path",{d:"m2 2 20 20"}],["path",{d:"m20 9 .891.891"}],["path",{d:"M22 9c-1.5 1.35-3 2.092-4.5 2.5l-1-1"}],["path",{d:"M3.109 14.109 4 15"}],["path",{d:"m6.5 12.5 1 1"}],["path",{d:"m7 18 2.891 2.891"}],["path",{d:"M9 22c1.35-1.5 2.092-3 2.5-4.5L10 16"}]],hc=[["path",{d:"M2 8h20"}],["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M6 16h12"}]],dc=[["path",{d:"m10 16 1.5 1.5"}],["path",{d:"m14 8-1.5-1.5"}],["path",{d:"M15 2c-1.798 1.998-2.518 3.995-2.807 5.993"}],["path",{d:"m16.5 10.5 1 1"}],["path",{d:"m17 6-2.891-2.891"}],["path",{d:"M2 15c6.667-6 13.333 0 20-6"}],["path",{d:"m20 9 .891.891"}],["path",{d:"M3.109 14.109 4 15"}],["path",{d:"m6.5 12.5 1 1"}],["path",{d:"m7 18 2.891 2.891"}],["path",{d:"M9 22c1.798-1.998 2.518-3.995 2.807-5.993"}]],cc=[["path",{d:"M11.25 16.25h1.5L12 17z"}],["path",{d:"M16 14v.5"}],["path",{d:"M4.42 11.247A13.152 13.152 0 0 0 4 14.556C4 18.728 7.582 21 12 21s8-2.272 8-6.444a11.702 11.702 0 0 0-.493-3.309"}],["path",{d:"M8 14v.5"}],["path",{d:"M8.5 8.5c-.384 1.05-1.083 2.028-2.344 2.5-1.931.722-3.576-.297-3.656-1-.113-.994 1.177-6.53 4-7 1.923-.321 3.651.845 3.651 2.235A7.497 7.497 0 0 1 14 5.277c0-1.39 1.844-2.598 3.767-2.277 2.823.47 4.113 6.006 4 7-.08.703-1.725 1.722-3.656 1-1.261-.472-1.855-1.45-2.239-2.5"}]],Mc=[["line",{x1:"12",x2:"12",y1:"2",y2:"22"}],["path",{d:"M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"}]],pc=[["path",{d:"M20.5 10a2.5 2.5 0 0 1-2.4-3H18a2.95 2.95 0 0 1-2.6-4.4 10 10 0 1 0 6.3 7.1c-.3.2-.8.3-1.2.3"}],["circle",{cx:"12",cy:"12",r:"3"}]],ic=[["path",{d:"M10 12h.01"}],["path",{d:"M18 9V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14"}],["path",{d:"M2 20h8"}],["path",{d:"M20 17v-2a2 2 0 1 0-4 0v2"}],["rect",{x:"14",y:"17",width:"8",height:"5",rx:"1"}]],nc=[["path",{d:"M10 12h.01"}],["path",{d:"M18 20V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14"}],["path",{d:"M2 20h20"}]],lc=[["circle",{cx:"12.1",cy:"12.1",r:"1"}]],ec=[["path",{d:"M11 20H2"}],["path",{d:"M11 4.562v16.157a1 1 0 0 0 1.242.97L19 20V5.562a2 2 0 0 0-1.515-1.94l-4-1A2 2 0 0 0 11 4.561z"}],["path",{d:"M11 4H8a2 2 0 0 0-2 2v14"}],["path",{d:"M14 12h.01"}],["path",{d:"M22 20h-3"}]],rc=[["path",{d:"M12 15V3"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}],["path",{d:"m7 10 5 5 5-5"}]],oc=[["path",{d:"m12.99 6.74 1.93 3.44"}],["path",{d:"M19.136 12a10 10 0 0 1-14.271 0"}],["path",{d:"m21 21-2.16-3.84"}],["path",{d:"m3 21 8.02-14.26"}],["circle",{cx:"12",cy:"5",r:"2"}]],vc=[["path",{d:"M10 11h.01"}],["path",{d:"M14 6h.01"}],["path",{d:"M18 6h.01"}],["path",{d:"M6.5 13.1h.01"}],["path",{d:"M22 5c0 9-4 12-6 12s-6-3-6-12c0-2 2-3 6-3s6 1 6 3"}],["path",{d:"M17.4 9.9c-.8.8-2 .8-2.8 0"}],["path",{d:"M10.1 7.1C9 7.2 7.7 7.7 6 8.6c-3.5 2-4.7 3.9-3.7 5.6 4.5 7.8 9.5 8.4 11.2 7.4.9-.5 1.9-2.1 1.9-4.7"}],["path",{d:"M9.1 16.5c.3-1.1 1.4-1.7 2.4-1.4"}]],$c=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M19.13 5.09C15.22 9.14 10 10.44 2.25 10.94"}],["path",{d:"M21.75 12.84c-6.62-1.41-12.14 1-16.38 6.32"}],["path",{d:"M8.56 2.75c4.37 6 6 9.42 8 17.72"}]],mc=[["path",{d:"M10 18a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H5a3 3 0 0 1-3-3 1 1 0 0 1 1-1z"}],["path",{d:"M13 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1l-.81 3.242a1 1 0 0 1-.97.758H8"}],["path",{d:"M14 4h3a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-3"}],["path",{d:"M18 6h4"}],["path",{d:"m5 10-2 8"}],["path",{d:"m7 18 2-8"}]],yc=[["path",{d:"M10 10 7 7"}],["path",{d:"m10 14-3 3"}],["path",{d:"m14 10 3-3"}],["path",{d:"m14 14 3 3"}],["path",{d:"M14.205 4.139a4 4 0 1 1 5.439 5.863"}],["path",{d:"M19.637 14a4 4 0 1 1-5.432 5.868"}],["path",{d:"M4.367 10a4 4 0 1 1 5.438-5.862"}],["path",{d:"M9.795 19.862a4 4 0 1 1-5.429-5.873"}],["rect",{x:"10",y:"8",width:"4",height:"8",rx:"1"}]],sc=[["path",{d:"M18.715 13.186C18.29 11.858 17.384 10.607 16 9.5c-2-1.6-3.5-4-4-6.5a10.7 10.7 0 0 1-.884 2.586"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.795 8.797A11 11 0 0 1 8 9.5C6 11.1 5 13 5 15a7 7 0 0 0 13.222 3.208"}]],gc=[["path",{d:"M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z"}]],Cc=[["path",{d:"M7 16.3c2.2 0 4-1.83 4-4.05 0-1.16-.57-2.26-1.71-3.19S7.29 6.75 7 5.3c-.29 1.45-1.14 2.84-2.29 3.76S3 11.1 3 12.25c0 2.22 1.8 4.05 4 4.05z"}],["path",{d:"M12.56 6.6A10.97 10.97 0 0 0 14 3.02c.5 2.5 2 4.9 4 6.5s3 3.5 3 5.5a6.98 6.98 0 0 1-11.91 4.97"}]],uc=[["path",{d:"m2 2 8 8"}],["path",{d:"m22 2-8 8"}],["ellipse",{cx:"12",cy:"9",rx:"10",ry:"5"}],["path",{d:"M7 13.4v7.9"}],["path",{d:"M12 14v8"}],["path",{d:"M17 13.4v7.9"}],["path",{d:"M2 9v8a10 5 0 0 0 20 0V9"}]],Hc=[["path",{d:"M15.4 15.63a7.875 6 135 1 1 6.23-6.23 4.5 3.43 135 0 0-6.23 6.23"}],["path",{d:"m8.29 12.71-2.6 2.6a2.5 2.5 0 1 0-1.65 4.65A2.5 2.5 0 1 0 8.7 18.3l2.59-2.59"}]],Ac=[["path",{d:"M17.596 12.768a2 2 0 1 0 2.829-2.829l-1.768-1.767a2 2 0 0 0 2.828-2.829l-2.828-2.828a2 2 0 0 0-2.829 2.828l-1.767-1.768a2 2 0 1 0-2.829 2.829z"}],["path",{d:"m2.5 21.5 1.4-1.4"}],["path",{d:"m20.1 3.9 1.4-1.4"}],["path",{d:"M5.343 21.485a2 2 0 1 0 2.829-2.828l1.767 1.768a2 2 0 1 0 2.829-2.829l-6.364-6.364a2 2 0 1 0-2.829 2.829l1.768 1.767a2 2 0 0 0-2.828 2.829z"}],["path",{d:"m9.6 14.4 4.8-4.8"}]],wc=[["path",{d:"M6 18.5a3.5 3.5 0 1 0 7 0c0-1.57.92-2.52 2.04-3.46"}],["path",{d:"M6 8.5c0-.75.13-1.47.36-2.14"}],["path",{d:"M8.8 3.15A6.5 6.5 0 0 1 19 8.5c0 1.63-.44 2.81-1.09 3.76"}],["path",{d:"M12.5 6A2.5 2.5 0 0 1 15 8.5M10 13a2 2 0 0 0 1.82-1.18"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],Vc=[["path",{d:"M6 8.5a6.5 6.5 0 1 1 13 0c0 6-6 6-6 10a3.5 3.5 0 1 1-7 0"}],["path",{d:"M15 8.5a2.5 2.5 0 0 0-5 0v1a2 2 0 1 1 0 4"}]],Sc=[["path",{d:"M7 3.34V5a3 3 0 0 0 3 3"}],["path",{d:"M11 21.95V18a2 2 0 0 0-2-2 2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05"}],["path",{d:"M21.54 15H17a2 2 0 0 0-2 2v4.54"}],["path",{d:"M12 2a10 10 0 1 0 9.54 13"}],["path",{d:"M20 6V4a2 2 0 1 0-4 0v2"}],["rect",{width:"8",height:"5",x:"14",y:"6",rx:"1"}]],D1=[["path",{d:"M21.54 15H17a2 2 0 0 0-2 2v4.54"}],["path",{d:"M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17"}],["path",{d:"M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05"}],["circle",{cx:"12",cy:"12",r:"10"}]],Lc=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 2a7 7 0 1 0 10 10"}]],fc=[["circle",{cx:"11.5",cy:"12.5",r:"3.5"}],["path",{d:"M3 8c0-3.5 2.5-6 6.5-6 5 0 4.83 3 7.5 5s5 2 5 6c0 4.5-2.5 6.5-7 6.5-2.5 0-2.5 2.5-6 2.5s-7-2-7-5.5c0-3 1.5-3 1.5-5C3.5 10 3 9 3 8Z"}]],kc=[["path",{d:"m2 2 20 20"}],["path",{d:"M20 14.347V14c0-6-4-12-8-12-1.078 0-2.157.436-3.157 1.19"}],["path",{d:"M6.206 6.21C4.871 8.4 4 11.2 4 14a8 8 0 0 0 14.568 4.568"}]],Pc=[["path",{d:"M12 2C8 2 4 8 4 14a8 8 0 0 0 16 0c0-6-4-12-8-12"}]],b1=[["circle",{cx:"12",cy:"12",r:"1"}],["circle",{cx:"12",cy:"5",r:"1"}],["circle",{cx:"12",cy:"19",r:"1"}]],R1=[["circle",{cx:"12",cy:"12",r:"1"}],["circle",{cx:"19",cy:"12",r:"1"}],["circle",{cx:"5",cy:"12",r:"1"}]],Bc=[["path",{d:"M5 15a6.5 6.5 0 0 1 7 0 6.5 6.5 0 0 0 7 0"}],["path",{d:"M5 9a6.5 6.5 0 0 1 7 0 6.5 6.5 0 0 0 7 0"}]],zc=[["line",{x1:"5",x2:"19",y1:"9",y2:"9"}],["line",{x1:"5",x2:"19",y1:"15",y2:"15"}],["line",{x1:"19",x2:"5",y1:"5",y2:"19"}]],Fc=[["path",{d:"M21 21H8a2 2 0 0 1-1.42-.587l-3.994-3.999a2 2 0 0 1 0-2.828l10-10a2 2 0 0 1 2.829 0l5.999 6a2 2 0 0 1 0 2.828L12.834 21"}],["path",{d:"m5.082 11.09 8.828 8.828"}]],Dc=[["line",{x1:"5",x2:"19",y1:"9",y2:"9"}],["line",{x1:"5",x2:"19",y1:"15",y2:"15"}]],bc=[["path",{d:"m15 20 3-3h2a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2l3 3z"}],["path",{d:"M6 8v1"}],["path",{d:"M10 8v1"}],["path",{d:"M14 8v1"}],["path",{d:"M18 8v1"}]],Rc=[["path",{d:"M4 10h12"}],["path",{d:"M4 14h9"}],["path",{d:"M19 6a7.7 7.7 0 0 0-5.2-2A7.9 7.9 0 0 0 6 12c0 4.4 3.5 8 7.8 8 2 0 3.8-.8 5.2-2"}]],Tc=[["path",{d:"M14 13h2a2 2 0 0 1 2 2v2a2 2 0 0 0 4 0v-6.998a2 2 0 0 0-.59-1.42L18 5"}],["path",{d:"M14 21V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v16"}],["path",{d:"M2 21h13"}],["path",{d:"M3 7h11"}],["path",{d:"m9 11-2 3h3l-2 3"}]],qc=[["path",{d:"M15 3h6v6"}],["path",{d:"M10 14 21 3"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"}]],Uc=[["path",{d:"m15 15 6 6"}],["path",{d:"m15 9 6-6"}],["path",{d:"M21 16v5h-5"}],["path",{d:"M21 8V3h-5"}],["path",{d:"M3 16v5h5"}],["path",{d:"m3 21 6-6"}],["path",{d:"M3 8V3h5"}],["path",{d:"M9 9 3 3"}]],Oc=[["path",{d:"m15 18-.722-3.25"}],["path",{d:"M2 8a10.645 10.645 0 0 0 20 0"}],["path",{d:"m20 15-1.726-2.05"}],["path",{d:"m4 15 1.726-2.05"}],["path",{d:"m9 18 .722-3.25"}]],Zc=[["path",{d:"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"}],["path",{d:"M14.084 14.158a3 3 0 0 1-4.242-4.242"}],["path",{d:"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"}],["path",{d:"m2 2 20 20"}]],Gc=[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"}],["circle",{cx:"12",cy:"12",r:"3"}]],Wc=[["path",{d:"M12 16h.01"}],["path",{d:"M16 16h.01"}],["path",{d:"M3 19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8.5a.5.5 0 0 0-.769-.422l-4.462 2.844A.5.5 0 0 1 15 10.5v-2a.5.5 0 0 0-.769-.422L9.77 10.922A.5.5 0 0 1 9 10.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z"}],["path",{d:"M8 16h.01"}]],Ic=[["path",{d:"M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"}]],Ec=[["path",{d:"M10.827 16.379a6.082 6.082 0 0 1-8.618-7.002l5.412 1.45a6.082 6.082 0 0 1 7.002-8.618l-1.45 5.412a6.082 6.082 0 0 1 8.618 7.002l-5.412-1.45a6.082 6.082 0 0 1-7.002 8.618l1.45-5.412Z"}],["path",{d:"M12 12v.01"}]],Xc=[["path",{d:"M12 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 12 18z"}],["path",{d:"M2 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 2 18z"}]],jc=[["path",{d:"M12.67 19a2 2 0 0 0 1.416-.588l6.154-6.172a6 6 0 0 0-8.49-8.49L5.586 9.914A2 2 0 0 0 5 11.328V18a1 1 0 0 0 1 1z"}],["path",{d:"M16 8 2 22"}],["path",{d:"M17.5 15H9"}]],Nc=[["path",{d:"M4 3 2 5v15c0 .6.4 1 1 1h2c.6 0 1-.4 1-1V5Z"}],["path",{d:"M6 8h4"}],["path",{d:"M6 18h4"}],["path",{d:"m12 3-2 2v15c0 .6.4 1 1 1h2c.6 0 1-.4 1-1V5Z"}],["path",{d:"M14 8h4"}],["path",{d:"M14 18h4"}],["path",{d:"m20 3-2 2v15c0 .6.4 1 1 1h2c.6 0 1-.4 1-1V5Z"}]],Kc=[["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"M12 2v4"}],["path",{d:"m6.8 15-3.5 2"}],["path",{d:"m20.7 7-3.5 2"}],["path",{d:"M6.8 9 3.3 7"}],["path",{d:"m20.7 17-3.5-2"}],["path",{d:"m9 22 3-8 3 8"}],["path",{d:"M8 22h8"}],["path",{d:"M18 18.7a9 9 0 1 0-12 0"}]],Qc=[["path",{d:"M5 5.5A3.5 3.5 0 0 1 8.5 2H12v7H8.5A3.5 3.5 0 0 1 5 5.5z"}],["path",{d:"M12 2h3.5a3.5 3.5 0 1 1 0 7H12V2z"}],["path",{d:"M12 12.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 1 1-7 0z"}],["path",{d:"M5 19.5A3.5 3.5 0 0 1 8.5 16H12v3.5a3.5 3.5 0 1 1-7 0z"}],["path",{d:"M5 12.5A3.5 3.5 0 0 1 8.5 9H12v7H8.5A3.5 3.5 0 0 1 5 12.5z"}]],Jc=[["path",{d:"M13.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v11.5"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 12v-1"}],["path",{d:"M8 18v-2"}],["path",{d:"M8 7V6"}],["circle",{cx:"8",cy:"20",r:"2"}]],T1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m8 18 4-4"}],["path",{d:"M8 10v8h8"}]],q1=[["path",{d:"M13 22h5a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v3.3"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m7.69 16.479 1.29 4.88a.5.5 0 0 1-.698.591l-1.843-.849a1 1 0 0 0-.879.001l-1.846.85a.5.5 0 0 1-.692-.593l1.29-4.88"}],["circle",{cx:"6",cy:"14",r:"3"}]],U1=[["path",{d:"M14 22h4a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M5 14a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1 1 1 0 0 1 1 1v2a1 1 0 0 0 1 1"}],["path",{d:"M9 22a1 1 0 0 0 1-1v-2a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-2a1 1 0 0 0-1-1"}]],Yc=[["path",{d:"M14.5 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v3.8"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M11.7 14.2 7 17l-4.7-2.8"}],["path",{d:"M3 13.1a2 2 0 0 0-.999 1.76v3.24a2 2 0 0 0 .969 1.78L6 21.7a2 2 0 0 0 2.03.01L11 19.9a2 2 0 0 0 1-1.76V14.9a2 2 0 0 0-.97-1.78L8 11.3a2 2 0 0 0-2.03-.01z"}],["path",{d:"M7 17v5"}]],O1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1"}],["path",{d:"M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1"}]],Z1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 18v-2"}],["path",{d:"M12 18v-4"}],["path",{d:"M16 18v-6"}]],G1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 18v-1"}],["path",{d:"M12 18v-6"}],["path",{d:"M16 18v-3"}]],W1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m16 13-3.5 3.5-2-2L8 17"}]],I1=[["path",{d:"M15.941 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.704l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v3.512"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M4.017 11.512a6 6 0 1 0 8.466 8.475"}],["path",{d:"M9 16a1 1 0 0 1-1-1v-4c0-.552.45-1.008.995-.917a6 6 0 0 1 4.922 4.922c.091.544-.365.995-.917.995z"}]],E1=[["path",{d:"M10.5 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m14 20 2 2 4-4"}]],_c=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m9 15 2 2 4-4"}]],xc=[["path",{d:"M16 22h2a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v2.85"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 14v2.2l1.6 1"}],["circle",{cx:"8",cy:"16",r:"6"}]],X1=[["path",{d:"M4 12.15V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-3.35"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m5 16-3 3 3 3"}],["path",{d:"m9 22 3-3-3-3"}]],a7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M10 12.5 8 15l2 2.5"}],["path",{d:"m14 12.5 2 2.5-2 2.5"}]],j1=[["path",{d:"M13.85 22H18a2 2 0 0 0 2-2V8a2 2 0 0 0-.586-1.414l-4-4A2 2 0 0 0 14 2H6a2 2 0 0 0-2 2v6.6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m3.305 19.53.923-.382"}],["path",{d:"m4.228 16.852-.924-.383"}],["path",{d:"m5.852 15.228-.383-.923"}],["path",{d:"m5.852 20.772-.383.924"}],["path",{d:"m8.148 15.228.383-.923"}],["path",{d:"m8.53 21.696-.382-.924"}],["path",{d:"m9.773 16.852.922-.383"}],["path",{d:"m9.773 19.148.922.383"}],["circle",{cx:"7",cy:"18",r:"3"}]],t7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M9 10h6"}],["path",{d:"M12 13V7"}],["path",{d:"M9 17h6"}]],h7=[["path",{d:"M4 12V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M10 16h2v6"}],["path",{d:"M10 22h4"}],["rect",{x:"2",y:"16",width:"4",height:"6",rx:"2"}]],d7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M12 18v-6"}],["path",{d:"m9 15 3 3 3-3"}]],N1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M12 9v4"}],["path",{d:"M12 17h.01"}]],r=[["path",{d:"M4 6.835V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-.343"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M2 19a2 2 0 0 1 4 0v1a2 2 0 0 1-4 0v-4a6 6 0 0 1 12 0v4a2 2 0 0 1-4 0v-1a2 2 0 0 1 4 0"}]],c7=[["path",{d:"M13 22h5a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v7"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M3.62 18.8A2.25 2.25 0 1 1 7 15.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a1 1 0 0 1-1.507 0z"}]],M7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["circle",{cx:"10",cy:"12",r:"2"}],["path",{d:"m20 17-1.296-1.296a2.41 2.41 0 0 0-3.408 0L9 22"}]],p7=[["path",{d:"M4 11V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-1"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M2 15h10"}],["path",{d:"m9 18 3-3-3-3"}]],K1=[["path",{d:"M10.65 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v10.1"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m10 15 1 1"}],["path",{d:"m11 14-4.586 4.586"}],["circle",{cx:"5",cy:"20",r:"2"}]],Q1=[["path",{d:"M4 9.8V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-3"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M9 17v-2a2 2 0 0 0-4 0v2"}],["rect",{width:"8",height:"5",x:"3",y:"17",rx:"1"}]],J1=[["path",{d:"M20 14V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M14 18h6"}]],i7=[["path",{d:"M11.65 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v10.35"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 20v-7l3 1.474"}],["circle",{cx:"6",cy:"20",r:"2"}]],n7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M9 15h6"}]],l7=[["path",{d:"M4.226 20.925A2 2 0 0 0 6 22h12a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v3.127"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m5 11-3 3"}],["path",{d:"m5 17-3-3h10"}]],Y1=[["path",{d:"m18.226 5.226-2.52-2.52A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-.351"}],["path",{d:"M21.378 12.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["path",{d:"M8 18h1"}]],_1=[["path",{d:"M12.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v9.34"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M10.378 12.622a1 1 0 0 1 3 3.003L8.36 20.637a2 2 0 0 1-.854.506l-2.867.837a.5.5 0 0 1-.62-.62l.836-2.869a2 2 0 0 1 .506-.853z"}]],x1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M15.033 13.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56v-4.704a.645.645 0 0 1 .967-.56z"}]],e7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M9 15h6"}],["path",{d:"M12 18v-6"}]],a2=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M12 17h.01"}],["path",{d:"M9.1 9a3 3 0 0 1 5.82 1c0 2-3 3-3 3"}]],t2=[["path",{d:"M11.35 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v5.35"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M14 19h6"}],["path",{d:"M17 16v6"}]],r7=[["path",{d:"M20 10V8a2.4 2.4 0 0 0-.706-1.704l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.35"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M16 14a2 2 0 0 0-2 2"}],["path",{d:"M16 22a2 2 0 0 1-2-2"}],["path",{d:"M20 14a2 2 0 0 1 2 2"}],["path",{d:"M20 22a2 2 0 0 0 2-2"}]],h2=[["path",{d:"M11.1 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.589 3.588A2.4 2.4 0 0 1 20 8v3.25"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m21 22-2.88-2.88"}],["circle",{cx:"16",cy:"17",r:"3"}]],o7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["circle",{cx:"11.5",cy:"14.5",r:"2.5"}],["path",{d:"M13.3 16.3 15 18"}]],d2=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 15h.01"}],["path",{d:"M11.5 13.5a2.5 2.5 0 0 1 0 3"}],["path",{d:"M15 12a5 5 0 0 1 0 6"}]],v7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 13h2"}],["path",{d:"M14 13h2"}],["path",{d:"M8 17h2"}],["path",{d:"M14 17h2"}]],$7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 12h8"}],["path",{d:"M10 11v2"}],["path",{d:"M8 17h8"}],["path",{d:"M14 16v2"}]],m7=[["path",{d:"M11 21a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1"}],["path",{d:"M16 16a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1"}],["path",{d:"M21 6a2 2 0 0 0-.586-1.414l-2-2A2 2 0 0 0 17 2h-3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1z"}]],y7=[["path",{d:"M4 11V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h7"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m10 18 3-3-3-3"}]],s7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m8 16 2-2-2-2"}],["path",{d:"M12 18h4"}]],g7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M10 9H8"}],["path",{d:"M16 13H8"}],["path",{d:"M16 17H8"}]],c2=[["path",{d:"M12 22h6a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M3 16v-1.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5V16"}],["path",{d:"M6 22h2"}],["path",{d:"M7 14v8"}]],C7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M11 18h2"}],["path",{d:"M12 12v6"}],["path",{d:"M9 13v-.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v.5"}]],u7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M12 12v6"}],["path",{d:"m15 15-3-3-3 3"}]],H7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M16 22a4 4 0 0 0-8 0"}],["circle",{cx:"12",cy:"15",r:"3"}]],A7=[["path",{d:"M4 11.55V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-1.95"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M12 15a5 5 0 0 1 0 6"}],["path",{d:"M8 14.502a.5.5 0 0 0-.826-.381l-1.893 1.631a1 1 0 0 1-.651.243H3.5a.5.5 0 0 0-.5.501v3.006a.5.5 0 0 0 .5.501h1.129a1 1 0 0 1 .652.243l1.893 1.633a.5.5 0 0 0 .826-.38z"}]],M2=[["path",{d:"M4 12V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m10 17.843 3.033-1.755a.64.64 0 0 1 .967.56v4.704a.65.65 0 0 1-.967.56L10 20.157"}],["rect",{width:"7",height:"6",x:"3",y:"16",rx:"1"}]],p2=[["path",{d:"M11 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v5"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m15 17 5 5"}],["path",{d:"m20 17-5 5"}]],w7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m14.5 12.5-5 5"}],["path",{d:"m9.5 12.5 5 5"}]],V7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}]],S7=[["path",{d:"M15 2h-4a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8"}],["path",{d:"M16.706 2.706A2.4 2.4 0 0 0 15 2v5a1 1 0 0 0 1 1h5a2.4 2.4 0 0 0-.706-1.706z"}],["path",{d:"M5 7a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h8a2 2 0 0 0 1.732-1"}]],L7=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 3v18"}],["path",{d:"M3 7.5h4"}],["path",{d:"M3 12h18"}],["path",{d:"M3 16.5h4"}],["path",{d:"M17 3v18"}],["path",{d:"M17 7.5h4"}],["path",{d:"M17 16.5h4"}]],i2=[["path",{d:"M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4"}],["path",{d:"M14 13.12c0 2.38 0 6.38-1 8.88"}],["path",{d:"M17.29 21.02c.12-.6.43-2.3.5-3.02"}],["path",{d:"M2 12a10 10 0 0 1 18-6"}],["path",{d:"M2 16h.01"}],["path",{d:"M21.8 16c.2-2 .131-5.354 0-6"}],["path",{d:"M5 19.5C5.5 18 6 15 6 12a6 6 0 0 1 .34-2"}],["path",{d:"M8.65 22c.21-.66.45-1.32.57-2"}],["path",{d:"M9 6.8a6 6 0 0 1 9 5.2v2"}]],f7=[["path",{d:"M15 6.5V3a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v3.5"}],["path",{d:"M9 18h8"}],["path",{d:"M18 3h-3"}],["path",{d:"M11 3a6 6 0 0 0-6 6v11"}],["path",{d:"M5 13h4"}],["path",{d:"M17 10a4 4 0 0 0-8 0v10a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2Z"}]],k7=[["path",{d:"M18 12.47v.03m0-.5v.47m-.475 5.056A6.744 6.744 0 0 1 15 18c-3.56 0-7.56-2.53-8.5-6 .348-1.28 1.114-2.433 2.121-3.38m3.444-2.088A8.802 8.802 0 0 1 15 6c3.56 0 6.06 2.54 7 6-.309 1.14-.786 2.177-1.413 3.058"}],["path",{d:"M7 10.67C7 8 5.58 5.97 2.73 5.5c-1 1.5-1 5 .23 6.5-1.24 1.5-1.24 5-.23 6.5C5.58 18.03 7 16 7 13.33m7.48-4.372A9.77 9.77 0 0 1 16 6.07m0 11.86a9.77 9.77 0 0 1-1.728-3.618"}],["path",{d:"m16.01 17.93-.23 1.4A2 2 0 0 1 13.8 21H9.5a5.96 5.96 0 0 0 1.49-3.98M8.53 3h5.27a2 2 0 0 1 1.98 1.67l.23 1.4M2 2l20 20"}]],P7=[["path",{d:"M2 16s9-15 20-4C11 23 2 8 2 8"}]],B7=[["path",{d:"M6.5 12c.94-3.46 4.94-6 8.5-6 3.56 0 6.06 2.54 7 6-.94 3.47-3.44 6-7 6s-7.56-2.53-8.5-6Z"}],["path",{d:"M18 12v.5"}],["path",{d:"M16 17.93a9.77 9.77 0 0 1 0-11.86"}],["path",{d:"M7 10.67C7 8 5.58 5.97 2.73 5.5c-1 1.5-1 5 .23 6.5-1.24 1.5-1.24 5-.23 6.5C5.58 18.03 7 16 7 13.33"}],["path",{d:"M10.46 7.26C10.2 5.88 9.17 4.24 8 3h5.8a2 2 0 0 1 1.98 1.67l.23 1.4"}],["path",{d:"m16.01 17.93-.23 1.4A2 2 0 0 1 13.8 21H9.5a5.96 5.96 0 0 0 1.49-3.98"}]],z7=[["path",{d:"m17.586 11.414-5.93 5.93a1 1 0 0 1-8-8l3.137-3.137a.707.707 0 0 1 1.207.5V10"}],["path",{d:"M20.414 8.586 22 7"}],["circle",{cx:"19",cy:"10",r:"2"}]],F7=[["path",{d:"M16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528"}],["path",{d:"m2 2 20 20"}],["path",{d:"M4 22V4"}],["path",{d:"M7.656 2H8c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10.347"}]],D7=[["path",{d:"M18 22V2.8a.8.8 0 0 0-1.17-.71L5.45 7.78a.8.8 0 0 0 0 1.44L18 15.5"}]],b7=[["path",{d:"M6 22V2.8a.8.8 0 0 1 1.17-.71l11.38 5.69a.8.8 0 0 1 0 1.44L6 15.5"}]],R7=[["path",{d:"M12 2c1 3 2.5 3.5 3.5 4.5A5 5 0 0 1 17 10a5 5 0 1 1-10 0c0-.3 0-.6.1-.9a2 2 0 1 0 3.3-2C8 4.5 11 2 12 2Z"}],["path",{d:"m5 22 14-4"}],["path",{d:"m5 18 14 4"}]],T7=[["path",{d:"M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528"}]],q7=[["path",{d:"M12 3q1 4 4 6.5t3 5.5a1 1 0 0 1-14 0 5 5 0 0 1 1-3 1 1 0 0 0 5 0c0-2-1.5-3-1.5-5q0-2 2.5-4"}]],U7=[["path",{d:"M11.652 6H18"}],["path",{d:"M12 13v1"}],["path",{d:"M16 16v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-8a4 4 0 0 0-.8-2.4l-.6-.8A3 3 0 0 1 6 7V6"}],["path",{d:"m2 2 20 20"}],["path",{d:"M7.649 2H17a1 1 0 0 1 1 1v4a3 3 0 0 1-.6 1.8l-.6.8a4 4 0 0 0-.55 1.007"}]],O7=[["path",{d:"M12 13v1"}],["path",{d:"M17 2a1 1 0 0 1 1 1v4a3 3 0 0 1-.6 1.8l-.6.8A4 4 0 0 0 16 12v8a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2v-8a4 4 0 0 0-.8-2.4l-.6-.8A3 3 0 0 1 6 7V3a1 1 0 0 1 1-1z"}],["path",{d:"M6 6h12"}]],Z7=[["path",{d:"M10 2v2.343"}],["path",{d:"M14 2v6.343"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20 20a2 2 0 0 1-2 2H6a2 2 0 0 1-1.755-2.96l5.227-9.563"}],["path",{d:"M6.453 15H15"}],["path",{d:"M8.5 2h7"}]],G7=[["path",{d:"M10 2v6.292a7 7 0 1 0 4 0V2"}],["path",{d:"M5 15h14"}],["path",{d:"M8.5 2h7"}]],W7=[["path",{d:"M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2"}],["path",{d:"M6.453 15h11.094"}],["path",{d:"M8.5 2h7"}]],I7=[["path",{d:"M8 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h3"}],["path",{d:"M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3"}],["path",{d:"M12 20v2"}],["path",{d:"M12 14v2"}],["path",{d:"M12 8v2"}],["path",{d:"M12 2v2"}]],E7=[["path",{d:"m3 7 5 5-5 5V7"}],["path",{d:"m21 7-5 5 5 5V7"}],["path",{d:"M12 20v2"}],["path",{d:"M12 14v2"}],["path",{d:"M12 8v2"}],["path",{d:"M12 2v2"}]],X7=[["path",{d:"m17 3-5 5-5-5h10"}],["path",{d:"m17 21-5-5-5 5h10"}],["path",{d:"M4 12H2"}],["path",{d:"M10 12H8"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}]],j7=[["path",{d:"M21 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v3"}],["path",{d:"M21 16v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3"}],["path",{d:"M4 12H2"}],["path",{d:"M10 12H8"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}]],N7=[["path",{d:"M12 5a3 3 0 1 1 3 3m-3-3a3 3 0 1 0-3 3m3-3v1M9 8a3 3 0 1 0 3 3M9 8h1m5 0a3 3 0 1 1-3 3m3-3h-1m-2 3v-1"}],["circle",{cx:"12",cy:"8",r:"2"}],["path",{d:"M12 10v12"}],["path",{d:"M12 22c4.2 0 7-1.667 7-5-4.2 0-7 1.667-7 5Z"}],["path",{d:"M12 22c-4.2 0-7-1.667-7-5 4.2 0 7 1.667 7 5Z"}]],K7=[["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"M12 16.5A4.5 4.5 0 1 1 7.5 12 4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 1 1 4.5 4.5 4.5 4.5 0 1 1-4.5 4.5"}],["path",{d:"M12 7.5V9"}],["path",{d:"M7.5 12H9"}],["path",{d:"M16.5 12H15"}],["path",{d:"M12 16.5V15"}],["path",{d:"m8 8 1.88 1.88"}],["path",{d:"M14.12 9.88 16 8"}],["path",{d:"m8 16 1.88-1.88"}],["path",{d:"M14.12 14.12 16 16"}]],Q7=[["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}]],J7=[["path",{d:"M2 12h6"}],["path",{d:"M22 12h-6"}],["path",{d:"M12 2v2"}],["path",{d:"M12 8v2"}],["path",{d:"M12 14v2"}],["path",{d:"M12 20v2"}],["path",{d:"m19 9-3 3 3 3"}],["path",{d:"m5 15 3-3-3-3"}]],Y7=[["path",{d:"M12 22v-6"}],["path",{d:"M12 8V2"}],["path",{d:"M4 12H2"}],["path",{d:"M10 12H8"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}],["path",{d:"m15 19-3-3-3 3"}],["path",{d:"m15 5-3 3-3-3"}]],_7=[["circle",{cx:"15",cy:"19",r:"2"}],["path",{d:"M20.9 19.8A2 2 0 0 0 22 18V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h5.1"}],["path",{d:"M15 11v-1"}],["path",{d:"M15 17v-2"}]],x7=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"m9 13 2 2 4-4"}]],aM=[["path",{d:"M16 14v2.2l1.6 1"}],["path",{d:"M7 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2"}],["circle",{cx:"16",cy:"16",r:"6"}]],tM=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M2 10h20"}]],hM=[["path",{d:"M10 10.5 8 13l2 2.5"}],["path",{d:"m14 10.5 2 2.5-2 2.5"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2z"}]],n2=[["path",{d:"M10.3 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.98a2 2 0 0 1 1.69.9l.66 1.2A2 2 0 0 0 12 6h8a2 2 0 0 1 2 2v3.3"}],["path",{d:"m14.305 19.53.923-.382"}],["path",{d:"m15.228 16.852-.923-.383"}],["path",{d:"m16.852 15.228-.383-.923"}],["path",{d:"m16.852 20.772-.383.924"}],["path",{d:"m19.148 15.228.383-.923"}],["path",{d:"m19.53 21.696-.382-.924"}],["path",{d:"m20.772 16.852.924-.383"}],["path",{d:"m20.772 19.148.924.383"}],["circle",{cx:"18",cy:"18",r:"3"}]],dM=[["path",{d:"M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"}],["circle",{cx:"12",cy:"13",r:"1"}]],cM=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M12 10v6"}],["path",{d:"m15 13-3 3-3-3"}]],MM=[["path",{d:"M18 19a5 5 0 0 1-5-5v8"}],["path",{d:"M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v5"}],["circle",{cx:"13",cy:"12",r:"2"}],["circle",{cx:"20",cy:"19",r:"2"}]],pM=[["circle",{cx:"12",cy:"13",r:"2"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M14 13h3"}],["path",{d:"M7 13h3"}]],iM=[["path",{d:"M10.638 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v3.417"}],["path",{d:"M14.62 18.8A2.25 2.25 0 1 1 18 15.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a.998.998 0 0 1-1.507 0z"}]],nM=[["path",{d:"M2 9V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-1"}],["path",{d:"M2 13h10"}],["path",{d:"m9 16 3-3-3-3"}]],lM=[["path",{d:"M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"}],["path",{d:"M8 10v4"}],["path",{d:"M12 10v2"}],["path",{d:"M16 10v6"}]],eM=[["circle",{cx:"16",cy:"20",r:"2"}],["path",{d:"M10 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v2"}],["path",{d:"m22 14-4.5 4.5"}],["path",{d:"m21 15 1 1"}]],rM=[["rect",{width:"8",height:"5",x:"14",y:"17",rx:"1"}],["path",{d:"M10 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v2.5"}],["path",{d:"M20 17v-2a2 2 0 1 0-4 0v2"}]],oM=[["path",{d:"M9 13h6"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}]],vM=[["path",{d:"m6 14 1.45-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.55 6a2 2 0 0 1-1.94 1.5H4a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2h3.93a2 2 0 0 1 1.66.9l.82 1.2a2 2 0 0 0 1.66.9H18a2 2 0 0 1 2 2v2"}],["circle",{cx:"14",cy:"15",r:"1"}]],$M=[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"}]],mM=[["path",{d:"M2 7.5V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-1.5"}],["path",{d:"M2 13h10"}],["path",{d:"m5 10-3 3 3 3"}]],l2=[["path",{d:"M2 11.5V5a2 2 0 0 1 2-2h3.9c.7 0 1.3.3 1.7.9l.8 1.2c.4.6 1 .9 1.7.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-9.5"}],["path",{d:"M11.378 13.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}]],yM=[["path",{d:"M12 10v6"}],["path",{d:"M9 13h6"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}]],sM=[["path",{d:"M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"}],["circle",{cx:"12",cy:"13",r:"2"}],["path",{d:"M12 15v5"}]],gM=[["circle",{cx:"11.5",cy:"12.5",r:"2.5"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M13.3 14.3 15 16"}]],CM=[["path",{d:"M10.7 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v4.1"}],["path",{d:"m21 21-1.9-1.9"}],["circle",{cx:"17",cy:"17",r:"3"}]],uM=[["path",{d:"M2 9.35V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h7"}],["path",{d:"m8 16 3-3-3-3"}]],HM=[["path",{d:"M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v.5"}],["path",{d:"M12 10v4h4"}],["path",{d:"m12 14 1.535-1.605a5 5 0 0 1 8 1.5"}],["path",{d:"M22 22v-4h-4"}],["path",{d:"m22 18-1.535 1.605a5 5 0 0 1-8-1.5"}]],AM=[["path",{d:"M20 10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2.5a1 1 0 0 1-.8-.4l-.9-1.2A1 1 0 0 0 15 3h-2a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"}],["path",{d:"M20 21a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2.9a1 1 0 0 1-.88-.55l-.42-.85a1 1 0 0 0-.92-.6H13a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"}],["path",{d:"M3 5a2 2 0 0 0 2 2h3"}],["path",{d:"M3 3v13a2 2 0 0 0 2 2h3"}]],wM=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M12 10v6"}],["path",{d:"m9 13 3-3 3 3"}]],VM=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"m9.5 10.5 5 5"}],["path",{d:"m14.5 10.5-5 5"}]],SM=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}]],LM=[["path",{d:"M20 5a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h2.5a1.5 1.5 0 0 1 1.2.6l.6.8a1.5 1.5 0 0 0 1.2.6z"}],["path",{d:"M3 8.268a2 2 0 0 0-1 1.738V19a2 2 0 0 0 2 2h11a2 2 0 0 0 1.732-1"}]],fM=[["path",{d:"M4 16v-2.38C4 11.5 2.97 10.5 3 8c.03-2.72 1.49-6 4.5-6C9.37 2 10 3.8 10 5.5c0 3.11-2 5.66-2 8.68V16a2 2 0 1 1-4 0Z"}],["path",{d:"M20 20v-2.38c0-2.12 1.03-3.12 1-5.62-.03-2.72-1.49-6-4.5-6C14.63 6 14 7.8 14 9.5c0 3.11 2 5.66 2 8.68V20a2 2 0 1 0 4 0Z"}],["path",{d:"M16 17h4"}],["path",{d:"M4 13h4"}]],kM=[["path",{d:"M12 12H5a2 2 0 0 0-2 2v5"}],["circle",{cx:"13",cy:"19",r:"2"}],["circle",{cx:"5",cy:"19",r:"2"}],["path",{d:"M8 19h3m5-17v17h6M6 12V7c0-1.1.9-2 2-2h3l5 5"}]],PM=[["path",{d:"M4 14h6"}],["path",{d:"M4 2h10"}],["rect",{x:"4",y:"18",width:"16",height:"4",rx:"1"}],["rect",{x:"4",y:"6",width:"16",height:"4",rx:"1"}]],BM=[["path",{d:"m15 17 5-5-5-5"}],["path",{d:"M4 18v-2a4 4 0 0 1 4-4h12"}]],zM=[["line",{x1:"22",x2:"2",y1:"6",y2:"6"}],["line",{x1:"22",x2:"2",y1:"18",y2:"18"}],["line",{x1:"6",x2:"6",y1:"2",y2:"22"}],["line",{x1:"18",x2:"18",y1:"2",y2:"22"}]],FM=[["path",{d:"M5 16V9h14V2H5l14 14h-7m-7 0 7 7v-7m-7 0h7"}]],DM=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M16 16s-1.5-2-4-2-4 2-4 2"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}]],bM=[["path",{d:"M14 13h2a2 2 0 0 1 2 2v2a2 2 0 0 0 4 0v-6.998a2 2 0 0 0-.59-1.42L18 5"}],["path",{d:"M14 21V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v16"}],["path",{d:"M2 21h13"}],["path",{d:"M3 9h11"}]],RM=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["rect",{width:"10",height:"8",x:"7",y:"8",rx:"1"}]],TM=[["path",{d:"M13.354 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14v6a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341l1.218-1.348"}],["path",{d:"M16 6h6"}],["path",{d:"M19 3v6"}]],e2=[["path",{d:"M12.531 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14v6a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341l.427-.473"}],["path",{d:"m16.5 3.5 5 5"}],["path",{d:"m21.5 3.5-5 5"}]],r2=[["path",{d:"M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z"}]],qM=[["path",{d:"M2 7v10"}],["path",{d:"M6 5v14"}],["rect",{width:"12",height:"18",x:"10",y:"3",rx:"2"}]],UM=[["path",{d:"M2 3v18"}],["rect",{width:"12",height:"18",x:"6",y:"3",rx:"2"}],["path",{d:"M22 3v18"}]],OM=[["rect",{width:"18",height:"14",x:"3",y:"3",rx:"2"}],["path",{d:"M4 21h1"}],["path",{d:"M9 21h1"}],["path",{d:"M14 21h1"}],["path",{d:"M19 21h1"}]],ZM=[["path",{d:"M7 2h10"}],["path",{d:"M5 6h14"}],["rect",{width:"18",height:"12",x:"3",y:"10",rx:"2"}]],GM=[["path",{d:"M3 2h18"}],["rect",{width:"18",height:"12",x:"3",y:"6",rx:"2"}],["path",{d:"M3 22h18"}]],WM=[["line",{x1:"6",x2:"10",y1:"11",y2:"11"}],["line",{x1:"8",x2:"8",y1:"9",y2:"13"}],["line",{x1:"15",x2:"15.01",y1:"12",y2:"12"}],["line",{x1:"18",x2:"18.01",y1:"10",y2:"10"}],["path",{d:"M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z"}]],IM=[["path",{d:"M11.146 15.854a1.207 1.207 0 0 1 1.708 0l1.56 1.56A2 2 0 0 1 15 18.828V21a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1v-2.172a2 2 0 0 1 .586-1.414z"}],["path",{d:"M18.828 15a2 2 0 0 1-1.414-.586l-1.56-1.56a1.207 1.207 0 0 1 0-1.708l1.56-1.56A2 2 0 0 1 18.828 9H21a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1z"}],["path",{d:"M6.586 14.414A2 2 0 0 1 5.172 15H3a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2.172a2 2 0 0 1 1.414.586l1.56 1.56a1.207 1.207 0 0 1 0 1.708z"}],["path",{d:"M9 3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2.172a2 2 0 0 1-.586 1.414l-1.56 1.56a1.207 1.207 0 0 1-1.708 0l-1.56-1.56A2 2 0 0 1 9 5.172z"}]],EM=[["line",{x1:"6",x2:"10",y1:"12",y2:"12"}],["line",{x1:"8",x2:"8",y1:"10",y2:"14"}],["line",{x1:"15",x2:"15.01",y1:"13",y2:"13"}],["line",{x1:"18",x2:"18.01",y1:"11",y2:"11"}],["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}]],XM=[["path",{d:"m12 14 4-4"}],["path",{d:"M3.34 19a10 10 0 1 1 17.32 0"}]],jM=[["path",{d:"m14 13-8.381 8.38a1 1 0 0 1-3.001-3l8.384-8.381"}],["path",{d:"m16 16 6-6"}],["path",{d:"m21.5 10.5-8-8"}],["path",{d:"m8 8 6-6"}],["path",{d:"m8.5 7.5 8 8"}]],NM=[["path",{d:"M10.5 3 8 9l4 13 4-13-2.5-6"}],["path",{d:"M17 3a2 2 0 0 1 1.6.8l3 4a2 2 0 0 1 .013 2.382l-7.99 10.986a2 2 0 0 1-3.247 0l-7.99-10.986A2 2 0 0 1 2.4 7.8l2.998-3.997A2 2 0 0 1 7 3z"}],["path",{d:"M2 9h20"}]],KM=[["path",{d:"M11.5 21a7.5 7.5 0 1 1 7.35-9"}],["path",{d:"M13 12V3"}],["path",{d:"M4 21h16"}],["path",{d:"M9 12V3"}]],QM=[["path",{d:"M9 10h.01"}],["path",{d:"M15 10h.01"}],["path",{d:"M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z"}]],JM=[["rect",{x:"3",y:"8",width:"18",height:"4",rx:"1"}],["path",{d:"M12 8v13"}],["path",{d:"M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7"}],["path",{d:"M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5"}]],YM=[["path",{d:"M15 6a9 9 0 0 0-9 9V3"}],["path",{d:"M21 18h-6"}],["circle",{cx:"18",cy:"6",r:"3"}],["circle",{cx:"6",cy:"18",r:"3"}]],_M=[["line",{x1:"6",x2:"6",y1:"3",y2:"15"}],["circle",{cx:"18",cy:"6",r:"3"}],["circle",{cx:"6",cy:"18",r:"3"}],["path",{d:"M18 9a9 9 0 0 1-9 9"}]],xM=[["path",{d:"M6 3v12"}],["path",{d:"M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"}],["path",{d:"M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"}],["path",{d:"M15 6a9 9 0 0 0-9 9"}],["path",{d:"M18 15v6"}],["path",{d:"M21 18h-6"}]],o2=[["circle",{cx:"12",cy:"12",r:"3"}],["line",{x1:"3",x2:"9",y1:"12",y2:"12"}],["line",{x1:"15",x2:"21",y1:"12",y2:"12"}]],a9=[["path",{d:"M12 3v6"}],["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"M12 15v6"}]],t9=[["circle",{cx:"5",cy:"6",r:"3"}],["path",{d:"M12 6h5a2 2 0 0 1 2 2v7"}],["path",{d:"m15 9-3-3 3-3"}],["circle",{cx:"19",cy:"18",r:"3"}],["path",{d:"M12 18H7a2 2 0 0 1-2-2V9"}],["path",{d:"m9 15 3 3-3 3"}]],h9=[["circle",{cx:"18",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M13 6h3a2 2 0 0 1 2 2v7"}],["path",{d:"M11 18H8a2 2 0 0 1-2-2V9"}]],d9=[["circle",{cx:"12",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["circle",{cx:"18",cy:"6",r:"3"}],["path",{d:"M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9"}],["path",{d:"M12 12v3"}]],c9=[["circle",{cx:"5",cy:"6",r:"3"}],["path",{d:"M5 9v6"}],["circle",{cx:"5",cy:"18",r:"3"}],["path",{d:"M12 3v18"}],["circle",{cx:"19",cy:"6",r:"3"}],["path",{d:"M16 15.7A9 9 0 0 0 19 9"}]],M9=[["circle",{cx:"18",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M6 21V9a9 9 0 0 0 9 9"}]],p9=[["circle",{cx:"5",cy:"6",r:"3"}],["path",{d:"M5 9v12"}],["circle",{cx:"19",cy:"18",r:"3"}],["path",{d:"m15 9-3-3 3-3"}],["path",{d:"M12 6h5a2 2 0 0 1 2 2v7"}]],i9=[["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M6 9v12"}],["path",{d:"m21 3-6 6"}],["path",{d:"m21 9-6-6"}],["path",{d:"M18 11.5V15"}],["circle",{cx:"18",cy:"18",r:"3"}]],n9=[["circle",{cx:"5",cy:"6",r:"3"}],["path",{d:"M5 9v12"}],["path",{d:"m15 9-3-3 3-3"}],["path",{d:"M12 6h5a2 2 0 0 1 2 2v3"}],["path",{d:"M19 15v6"}],["path",{d:"M22 18h-6"}]],l9=[["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M6 9v12"}],["path",{d:"M13 6h3a2 2 0 0 1 2 2v3"}],["path",{d:"M18 15v6"}],["path",{d:"M21 18h-6"}]],e9=[["circle",{cx:"18",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M18 6V5"}],["path",{d:"M18 11v-1"}],["line",{x1:"6",x2:"6",y1:"9",y2:"21"}]],r9=[["circle",{cx:"18",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M13 6h3a2 2 0 0 1 2 2v7"}],["line",{x1:"6",x2:"6",y1:"9",y2:"21"}]],o9=[["path",{d:"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"}],["path",{d:"M9 18c-4.51 2-5-2-7-2"}]],v9=[["path",{d:"m22 13.29-3.33-10a.42.42 0 0 0-.14-.18.38.38 0 0 0-.22-.11.39.39 0 0 0-.23.07.42.42 0 0 0-.14.18l-2.26 6.67H8.32L6.1 3.26a.42.42 0 0 0-.1-.18.38.38 0 0 0-.26-.08.39.39 0 0 0-.23.07.42.42 0 0 0-.14.18L2 13.29a.74.74 0 0 0 .27.83L12 21l9.69-6.88a.71.71 0 0 0 .31-.83Z"}]],$9=[["path",{d:"M5.116 4.104A1 1 0 0 1 6.11 3h11.78a1 1 0 0 1 .994 1.105L17.19 20.21A2 2 0 0 1 15.2 22H8.8a2 2 0 0 1-2-1.79z"}],["path",{d:"M6 12a5 5 0 0 1 6 0 5 5 0 0 0 6 0"}]],m9=[["circle",{cx:"6",cy:"15",r:"4"}],["circle",{cx:"18",cy:"15",r:"4"}],["path",{d:"M14 15a2 2 0 0 0-2-2 2 2 0 0 0-2 2"}],["path",{d:"M2.5 13 5 7c.7-1.3 1.4-2 3-2"}],["path",{d:"M21.5 13 19 7c-.7-1.3-1.5-2-3-2"}]],y9=[["path",{d:"M15.686 15A14.5 14.5 0 0 1 12 22a14.5 14.5 0 0 1 0-20 10 10 0 1 0 9.542 13"}],["path",{d:"M2 12h8.5"}],["path",{d:"M20 6V4a2 2 0 1 0-4 0v2"}],["rect",{width:"8",height:"5",x:"14",y:"6",rx:"1"}]],s9=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"}],["path",{d:"M2 12h20"}]],g9=[["path",{d:"M12 13V2l8 4-8 4"}],["path",{d:"M20.561 10.222a9 9 0 1 1-12.55-5.29"}],["path",{d:"M8.002 9.997a5 5 0 1 0 8.9 2.02"}]],C9=[["path",{d:"M2 21V3"}],["path",{d:"M2 5h18a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2.26"}],["path",{d:"M7 17v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-3"}],["circle",{cx:"16",cy:"11",r:"2"}],["circle",{cx:"8",cy:"11",r:"2"}]],u9=[["path",{d:"M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z"}],["path",{d:"M22 10v6"}],["path",{d:"M6 12.5V16a6 3 0 0 0 12 0v-3.5"}]],H9=[["path",{d:"M22 5V2l-5.89 5.89"}],["circle",{cx:"16.6",cy:"15.89",r:"3"}],["circle",{cx:"8.11",cy:"7.4",r:"3"}],["circle",{cx:"12.35",cy:"11.65",r:"3"}],["circle",{cx:"13.91",cy:"5.85",r:"3"}],["circle",{cx:"18.15",cy:"10.09",r:"3"}],["circle",{cx:"6.56",cy:"13.2",r:"3"}],["circle",{cx:"10.8",cy:"17.44",r:"3"}],["circle",{cx:"5",cy:"19",r:"3"}]],v2=[["path",{d:"M12 3v17a1 1 0 0 1-1 1H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v6a1 1 0 0 1-1 1H3"}],["path",{d:"m16 19 2 2 4-4"}]],$2=[["path",{d:"M12 3v17a1 1 0 0 1-1 1H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v6a1 1 0 0 1-1 1H3"}],["path",{d:"M16 19h6"}],["path",{d:"M19 22v-6"}]],m2=[["path",{d:"M12 3v17a1 1 0 0 1-1 1H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v6a1 1 0 0 1-1 1H3"}],["path",{d:"m16 16 5 5"}],["path",{d:"m16 21 5-5"}]],y2=[["path",{d:"M12 3v18"}],["path",{d:"M3 12h18"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],A9=[["path",{d:"M15 3v18"}],["path",{d:"M3 12h18"}],["path",{d:"M9 3v18"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],o=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"M3 15h18"}],["path",{d:"M9 3v18"}],["path",{d:"M15 3v18"}]],w9=[["circle",{cx:"12",cy:"9",r:"1"}],["circle",{cx:"19",cy:"9",r:"1"}],["circle",{cx:"5",cy:"9",r:"1"}],["circle",{cx:"12",cy:"15",r:"1"}],["circle",{cx:"19",cy:"15",r:"1"}],["circle",{cx:"5",cy:"15",r:"1"}]],V9=[["circle",{cx:"9",cy:"12",r:"1"}],["circle",{cx:"9",cy:"5",r:"1"}],["circle",{cx:"9",cy:"19",r:"1"}],["circle",{cx:"15",cy:"12",r:"1"}],["circle",{cx:"15",cy:"5",r:"1"}],["circle",{cx:"15",cy:"19",r:"1"}]],S9=[["circle",{cx:"12",cy:"5",r:"1"}],["circle",{cx:"19",cy:"5",r:"1"}],["circle",{cx:"5",cy:"5",r:"1"}],["circle",{cx:"12",cy:"12",r:"1"}],["circle",{cx:"19",cy:"12",r:"1"}],["circle",{cx:"5",cy:"12",r:"1"}],["circle",{cx:"12",cy:"19",r:"1"}],["circle",{cx:"19",cy:"19",r:"1"}],["circle",{cx:"5",cy:"19",r:"1"}]],L9=[["path",{d:"M3 7V5c0-1.1.9-2 2-2h2"}],["path",{d:"M17 3h2c1.1 0 2 .9 2 2v2"}],["path",{d:"M21 17v2c0 1.1-.9 2-2 2h-2"}],["path",{d:"M7 21H5c-1.1 0-2-.9-2-2v-2"}],["rect",{width:"7",height:"5",x:"7",y:"7",rx:"1"}],["rect",{width:"7",height:"5",x:"10",y:"12",rx:"1"}]],f9=[["path",{d:"M13.144 21.144A7.274 10.445 45 1 0 2.856 10.856"}],["path",{d:"M13.144 21.144A7.274 4.365 45 0 0 2.856 10.856a7.274 4.365 45 0 0 10.288 10.288"}],["path",{d:"M16.565 10.435 18.6 8.4a2.501 2.501 0 1 0 1.65-4.65 2.5 2.5 0 1 0-4.66 1.66l-2.024 2.025"}],["path",{d:"m8.5 16.5-1-1"}]],k9=[["path",{d:"m11.9 12.1 4.514-4.514"}],["path",{d:"M20.1 2.3a1 1 0 0 0-1.4 0l-1.114 1.114A2 2 0 0 0 17 4.828v1.344a2 2 0 0 1-.586 1.414A2 2 0 0 1 17.828 7h1.344a2 2 0 0 0 1.414-.586L21.7 5.3a1 1 0 0 0 0-1.4z"}],["path",{d:"m6 16 2 2"}],["path",{d:"M8.23 9.85A3 3 0 0 1 11 8a5 5 0 0 1 5 5 3 3 0 0 1-1.85 2.77l-.92.38A2 2 0 0 0 12 18a4 4 0 0 1-4 4 6 6 0 0 1-6-6 4 4 0 0 1 4-4 2 2 0 0 0 1.85-1.23z"}]],P9=[["path",{d:"M12 16H4a2 2 0 1 1 0-4h16a2 2 0 1 1 0 4h-4.25"}],["path",{d:"M5 12a2 2 0 0 1-2-2 9 7 0 0 1 18 0 2 2 0 0 1-2 2"}],["path",{d:"M5 16a2 2 0 0 0-2 2 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 2 2 0 0 0-2-2q0 0 0 0"}],["path",{d:"m6.67 12 6.13 4.6a2 2 0 0 0 2.8-.4l3.15-4.2"}]],B9=[["path",{d:"m15 12-9.373 9.373a1 1 0 0 1-3.001-3L12 9"}],["path",{d:"m18 15 4-4"}],["path",{d:"m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172v-.344a2 2 0 0 0-.586-1.414l-1.657-1.657A6 6 0 0 0 12.516 3H9l1.243 1.243A6 6 0 0 1 12 8.485V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5"}]],z9=[["path",{d:"M11 15h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 17"}],["path",{d:"m7 21 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"}],["path",{d:"m2 16 6 6"}],["circle",{cx:"16",cy:"9",r:"2.9"}],["circle",{cx:"6",cy:"5",r:"3"}]],F9=[["path",{d:"M12.035 17.012a3 3 0 0 0-3-3l-.311-.002a.72.72 0 0 1-.505-1.229l1.195-1.195A2 2 0 0 1 10.828 11H12a2 2 0 0 0 0-4H9.243a3 3 0 0 0-2.122.879l-2.707 2.707A4.83 4.83 0 0 0 3 14a8 8 0 0 0 8 8h2a8 8 0 0 0 8-8V7a2 2 0 1 0-4 0v2a2 2 0 1 0 4 0"}],["path",{d:"M13.888 9.662A2 2 0 0 0 17 8V5A2 2 0 1 0 13 5"}],["path",{d:"M9 5A2 2 0 1 0 5 5V10"}],["path",{d:"M9 7V4A2 2 0 1 1 13 4V7.268"}]],s2=[["path",{d:"M18 11.5V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v1.4"}],["path",{d:"M14 10V8a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2"}],["path",{d:"M10 9.9V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v5"}],["path",{d:"M6 14a2 2 0 0 0-2-2a2 2 0 0 0-2 2"}],["path",{d:"M18 11a2 2 0 1 1 4 0v3a8 8 0 0 1-8 8h-4a8 8 0 0 1-8-8 2 2 0 1 1 4 0"}]],D9=[["path",{d:"M11 14h2a2 2 0 0 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 16"}],["path",{d:"m14.45 13.39 5.05-4.694C20.196 8 21 6.85 21 5.75a2.75 2.75 0 0 0-4.797-1.837.276.276 0 0 1-.406 0A2.75 2.75 0 0 0 11 5.75c0 1.2.802 2.248 1.5 2.946L16 11.95"}],["path",{d:"m2 15 6 6"}],["path",{d:"m7 20 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a1 1 0 0 0-2.75-2.91"}]],g2=[["path",{d:"M11 12h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 14"}],["path",{d:"m7 18 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"}],["path",{d:"m2 13 6 6"}]],b9=[["path",{d:"M18 12.5V10a2 2 0 0 0-2-2a2 2 0 0 0-2 2v1.4"}],["path",{d:"M14 11V9a2 2 0 1 0-4 0v2"}],["path",{d:"M10 10.5V5a2 2 0 1 0-4 0v9"}],["path",{d:"m7 15-1.76-1.76a2 2 0 0 0-2.83 2.82l3.6 3.6C7.5 21.14 9.2 22 12 22h2a8 8 0 0 0 8-8V7a2 2 0 1 0-4 0v5"}]],R9=[["path",{d:"M12 3V2"}],["path",{d:"m15.4 17.4 3.2-2.8a2 2 0 1 1 2.8 2.9l-3.6 3.3c-.7.8-1.7 1.2-2.8 1.2h-4c-1.1 0-2.1-.4-2.8-1.2l-1.302-1.464A1 1 0 0 0 6.151 19H5"}],["path",{d:"M2 14h12a2 2 0 0 1 0 4h-2"}],["path",{d:"M4 10h16"}],["path",{d:"M5 10a7 7 0 0 1 14 0"}],["path",{d:"M5 14v6a1 1 0 0 1-1 1H2"}]],T9=[["path",{d:"M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"}],["path",{d:"M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2"}],["path",{d:"M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"}],["path",{d:"M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"}]],q9=[["path",{d:"M2.048 18.566A2 2 0 0 0 4 21h16a2 2 0 0 0 1.952-2.434l-2-9A2 2 0 0 0 18 8H6a2 2 0 0 0-1.952 1.566z"}],["path",{d:"M8 11V6a4 4 0 0 1 8 0v5"}]],U9=[["path",{d:"m11 17 2 2a1 1 0 1 0 3-3"}],["path",{d:"m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4"}],["path",{d:"m21 3 1 11h-2"}],["path",{d:"M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3"}],["path",{d:"M3 4h8"}]],O9=[["path",{d:"M12 2v8"}],["path",{d:"m16 6-4 4-4-4"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2"}],["path",{d:"M6 18h.01"}],["path",{d:"M10 18h.01"}]],Z9=[["path",{d:"m16 6-4-4-4 4"}],["path",{d:"M12 2v8"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2"}],["path",{d:"M6 18h.01"}],["path",{d:"M10 18h.01"}]],G9=[["path",{d:"M10 10V5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v5"}],["path",{d:"M14 6a6 6 0 0 1 6 6v3"}],["path",{d:"M4 15v-3a6 6 0 0 1 6-6"}],["rect",{x:"2",y:"15",width:"20",height:"4",rx:"1"}]],W9=[["line",{x1:"4",x2:"20",y1:"9",y2:"9"}],["line",{x1:"4",x2:"20",y1:"15",y2:"15"}],["line",{x1:"10",x2:"8",y1:"3",y2:"21"}],["line",{x1:"16",x2:"14",y1:"3",y2:"21"}]],I9=[["line",{x1:"22",x2:"2",y1:"12",y2:"12"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"}],["line",{x1:"6",x2:"6.01",y1:"16",y2:"16"}],["line",{x1:"10",x2:"10.01",y1:"16",y2:"16"}]],E9=[["path",{d:"M14 18a2 2 0 0 0-4 0"}],["path",{d:"m19 11-2.11-6.657a2 2 0 0 0-2.752-1.148l-1.276.61A2 2 0 0 1 12 4H8.5a2 2 0 0 0-1.925 1.456L5 11"}],["path",{d:"M2 11h20"}],["circle",{cx:"17",cy:"18",r:"3"}],["circle",{cx:"7",cy:"18",r:"3"}]],X9=[["path",{d:"m5.2 6.2 1.4 1.4"}],["path",{d:"M2 13h2"}],["path",{d:"M20 13h2"}],["path",{d:"m17.4 7.6 1.4-1.4"}],["path",{d:"M22 17H2"}],["path",{d:"M22 21H2"}],["path",{d:"M16 13a4 4 0 0 0-8 0"}],["path",{d:"M12 5V2.5"}]],j9=[["path",{d:"M10 12H6"}],["path",{d:"M10 15V9"}],["path",{d:"M14 14.5a.5.5 0 0 0 .5.5h1a2.5 2.5 0 0 0 2.5-2.5v-1A2.5 2.5 0 0 0 15.5 9h-1a.5.5 0 0 0-.5.5z"}],["path",{d:"M6 15V9"}],["rect",{x:"2",y:"5",width:"20",height:"14",rx:"2"}]],N9=[["path",{d:"M22 9a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h1l2 2h12l2-2h1a1 1 0 0 0 1-1Z"}],["path",{d:"M7.5 12h9"}]],K9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["path",{d:"m17 12 3-2v8"}]],Q9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["path",{d:"M17.5 10.5c1.7-1 3.5 0 3.5 1.5a2 2 0 0 1-2 2"}],["path",{d:"M17 17.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2"}]],J9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["path",{d:"M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1"}]],Y9=[["path",{d:"M12 18V6"}],["path",{d:"M17 10v3a1 1 0 0 0 1 1h3"}],["path",{d:"M21 10v8"}],["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}]],_9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["path",{d:"M17 13v-3h4"}],["path",{d:"M17 17.7c.4.2.8.3 1.3.3 1.5 0 2.7-1.1 2.7-2.5S19.8 13 18.3 13H17"}]],x9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["circle",{cx:"19",cy:"16",r:"2"}],["path",{d:"M20 10c-2 2-3 3.5-3 6"}]],ap=[["path",{d:"M6 12h12"}],["path",{d:"M6 20V4"}],["path",{d:"M18 20V4"}]],tp=[["path",{d:"M21 14h-1.343"}],["path",{d:"M9.128 3.47A9 9 0 0 1 21 12v3.343"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20.414 20.414A2 2 0 0 1 19 21h-1a2 2 0 0 1-2-2v-3"}],["path",{d:"M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 2.636-6.364"}]],hp=[["path",{d:"M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3"}]],dp=[["path",{d:"M3 11h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-5Zm0 0a9 9 0 1 1 18 0m0 0v5a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3Z"}],["path",{d:"M21 16v2a4 4 0 0 1-4 4h-5"}]],cp=[["path",{d:"M12.409 5.824c-.702.792-1.15 1.496-1.415 2.166l2.153 2.156a.5.5 0 0 1 0 .707l-2.293 2.293a.5.5 0 0 0 0 .707L12 15"}],["path",{d:"M13.508 20.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 9.591-3.677.6.6 0 0 0 .818.001A5.5 5.5 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5z"}]],Mp=[["path",{d:"M19.414 14.414C21 12.828 22 11.5 22 9.5a5.5 5.5 0 0 0-9.591-3.676.6.6 0 0 1-.818.001A5.5 5.5 0 0 0 2 9.5c0 2.3 1.5 4 3 5.5l5.535 5.362a2 2 0 0 0 2.879.052 2.12 2.12 0 0 0-.004-3 2.124 2.124 0 1 0 3-3 2.124 2.124 0 0 0 3.004 0 2 2 0 0 0 0-2.828l-1.881-1.882a2.41 2.41 0 0 0-3.409 0l-1.71 1.71a2 2 0 0 1-2.828 0 2 2 0 0 1 0-2.828l2.823-2.762"}]],pp=[["path",{d:"m14.876 18.99-1.368 1.323a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5a5.2 5.2 0 0 1-.244 1.572"}],["path",{d:"M15 15h6"}]],ip=[["path",{d:"M10.5 4.893a5.5 5.5 0 0 1 1.091.931.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 1.872-1.002 3.356-2.187 4.655"}],["path",{d:"m16.967 16.967-3.459 3.346a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 2.747-4.761"}],["path",{d:"m2 2 20 20"}]],np=[["path",{d:"m14.479 19.374-.971.939a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5a5.2 5.2 0 0 1-.219 1.49"}],["path",{d:"M15 15h6"}],["path",{d:"M18 12v6"}]],lp=[["path",{d:"M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5"}],["path",{d:"M3.22 13H9.5l.5-1 2 4.5 2-7 1.5 3.5h5.27"}]],ep=[["path",{d:"M11 8c2-3-2-3 0-6"}],["path",{d:"M15.5 8c2-3-2-3 0-6"}],["path",{d:"M6 10h.01"}],["path",{d:"M6 14h.01"}],["path",{d:"M10 16v-4"}],["path",{d:"M14 16v-4"}],["path",{d:"M18 16v-4"}],["path",{d:"M20 6a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3"}],["path",{d:"M5 20v2"}],["path",{d:"M19 20v2"}]],rp=[["path",{d:"M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5"}]],op=[["path",{d:"M11 17v4"}],["path",{d:"M14 3v8a2 2 0 0 0 2 2h5.865"}],["path",{d:"M17 17v4"}],["path",{d:"M18 17a4 4 0 0 0 4-4 8 6 0 0 0-8-6 6 5 0 0 0-6 5v3a2 2 0 0 0 2 2z"}],["path",{d:"M2 10v5"}],["path",{d:"M6 3h16"}],["path",{d:"M7 21h14"}],["path",{d:"M8 13H2"}]],vp=[["path",{d:"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"}]],$p=[["path",{d:"m9 11-6 6v3h9l3-3"}],["path",{d:"m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"}]],mp=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}],["path",{d:"M12 7v5l4 2"}]],yp=[["path",{d:"M10.82 16.12c1.69.6 3.91.79 5.18.85.55.03 1-.42.97-.97-.06-1.27-.26-3.5-.85-5.18"}],["path",{d:"M11.5 6.5c1.64 0 5-.38 6.71-1.07.52-.2.55-.82.12-1.17A10 10 0 0 0 4.26 18.33c.35.43.96.4 1.17-.12.69-1.71 1.07-5.07 1.07-6.71 1.34.45 3.1.9 4.88.62a.88.88 0 0 0 .73-.74c.3-2.14-.15-3.5-.61-4.88"}],["path",{d:"M15.62 16.95c.2.85.62 2.76.5 4.28a.77.77 0 0 1-.9.7 16.64 16.64 0 0 1-4.08-1.36"}],["path",{d:"M16.13 21.05c1.65.63 3.68.84 4.87.91a.9.9 0 0 0 .96-.96 17.68 17.68 0 0 0-.9-4.87"}],["path",{d:"M16.94 15.62c.86.2 2.77.62 4.29.5a.77.77 0 0 0 .7-.9 16.64 16.64 0 0 0-1.36-4.08"}],["path",{d:"M17.99 5.52a20.82 20.82 0 0 1 3.15 4.5.8.8 0 0 1-.68 1.13c-2.33.2-5.3-.32-8.27-1.57"}],["path",{d:"M4.93 4.93 3 3a.7.7 0 0 1 0-1"}],["path",{d:"M9.58 12.18c1.24 2.98 1.77 5.95 1.57 8.28a.8.8 0 0 1-1.13.68 20.82 20.82 0 0 1-4.5-3.15"}]],sp=[["path",{d:"M10.82 16.12c1.69.6 3.91.79 5.18.85.28.01.53-.09.7-.27"}],["path",{d:"M11.14 20.57c.52.24 2.44 1.12 4.08 1.37.46.06.86-.25.9-.71.12-1.52-.3-3.43-.5-4.28"}],["path",{d:"M16.13 21.05c1.65.63 3.68.84 4.87.91a.9.9 0 0 0 .7-.26"}],["path",{d:"M17.99 5.52a20.83 20.83 0 0 1 3.15 4.5.8.8 0 0 1-.68 1.13c-1.17.1-2.5.02-3.9-.25"}],["path",{d:"M20.57 11.14c.24.52 1.12 2.44 1.37 4.08.04.3-.08.59-.31.75"}],["path",{d:"M4.93 4.93a10 10 0 0 0-.67 13.4c.35.43.96.4 1.17-.12.69-1.71 1.07-5.07 1.07-6.71 1.34.45 3.1.9 4.88.62a.85.85 0 0 0 .48-.24"}],["path",{d:"M5.52 17.99c1.05.95 2.91 2.42 4.5 3.15a.8.8 0 0 0 1.13-.68c.2-2.34-.33-5.3-1.57-8.28"}],["path",{d:"M8.35 2.68a10 10 0 0 1 9.98 1.58c.43.35.4.96-.12 1.17-1.5.6-4.3.98-6.07 1.05"}],["path",{d:"m2 2 20 20"}]],gp=[["path",{d:"M12 7v4"}],["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"M14 9h-4"}],["path",{d:"M18 11h2a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h2"}],["path",{d:"M18 21V5a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16"}]],Cp=[["path",{d:"M10 22v-6.57"}],["path",{d:"M12 11h.01"}],["path",{d:"M12 7h.01"}],["path",{d:"M14 15.43V22"}],["path",{d:"M15 16a5 5 0 0 0-6 0"}],["path",{d:"M16 11h.01"}],["path",{d:"M16 7h.01"}],["path",{d:"M8 11h.01"}],["path",{d:"M8 7h.01"}],["rect",{x:"4",y:"2",width:"16",height:"20",rx:"2"}]],up=[["path",{d:"M5 22h14"}],["path",{d:"M5 2h14"}],["path",{d:"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22"}],["path",{d:"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2"}]],Hp=[["path",{d:"M8.62 13.8A2.25 2.25 0 1 1 12 10.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a.998.998 0 0 1-1.507 0z"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"}]],Ap=[["path",{d:"M10 12V8.964"}],["path",{d:"M14 12V8.964"}],["path",{d:"M15 12a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2a1 1 0 0 1 1-1z"}],["path",{d:"M8.5 21H5a2 2 0 0 1-2-2v-9a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-2"}]],wp=[["path",{d:"M12.35 21H5a2 2 0 0 1-2-2v-9a2 2 0 0 1 .71-1.53l7-6a2 2 0 0 1 2.58 0l7 6A2 2 0 0 1 21 10v2.35"}],["path",{d:"M14.8 12.4A1 1 0 0 0 14 12h-4a1 1 0 0 0-1 1v8"}],["path",{d:"M15 18h6"}],["path",{d:"M18 15v6"}]],Vp=[["path",{d:"M9.5 13.866a4 4 0 0 1 5 .01"}],["path",{d:"M12 17h.01"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"}],["path",{d:"M7 10.754a8 8 0 0 1 10 0"}]],C2=[["path",{d:"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"}]],u2=[["path",{d:"M12 17c5 0 8-2.69 8-6H4c0 3.31 3 6 8 6m-4 4h8m-4-3v3M5.14 11a3.5 3.5 0 1 1 6.71 0"}],["path",{d:"M12.14 11a3.5 3.5 0 1 1 6.71 0"}],["path",{d:"M15.5 6.5a3.5 3.5 0 1 0-7 0"}]],H2=[["path",{d:"m7 11 4.08 10.35a1 1 0 0 0 1.84 0L17 11"}],["path",{d:"M17 7A5 5 0 0 0 7 7"}],["path",{d:"M17 7a2 2 0 0 1 0 4H7a2 2 0 0 1 0-4"}]],Sp=[["path",{d:"M13.5 8h-3"}],["path",{d:"m15 2-1 2h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h3"}],["path",{d:"M16.899 22A5 5 0 0 0 7.1 22"}],["path",{d:"m9 2 3 6"}],["circle",{cx:"12",cy:"15",r:"3"}]],Lp=[["path",{d:"M16 10h2"}],["path",{d:"M16 14h2"}],["path",{d:"M6.17 15a3 3 0 0 1 5.66 0"}],["circle",{cx:"9",cy:"11",r:"2"}],["rect",{x:"2",y:"5",width:"20",height:"14",rx:"2"}]],fp=[["path",{d:"M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21"}],["path",{d:"m14 19 3 3v-5.5"}],["path",{d:"m17 22 3-3"}],["circle",{cx:"9",cy:"9",r:"2"}]],kp=[["path",{d:"M21 9v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"}],["line",{x1:"16",x2:"22",y1:"5",y2:"5"}],["circle",{cx:"9",cy:"9",r:"2"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"}]],Pp=[["line",{x1:"2",x2:"22",y1:"2",y2:"22"}],["path",{d:"M10.41 10.41a2 2 0 1 1-2.83-2.83"}],["line",{x1:"13.5",x2:"6",y1:"13.5",y2:"21"}],["line",{x1:"18",x2:"21",y1:"12",y2:"15"}],["path",{d:"M3.59 3.59A1.99 1.99 0 0 0 3 5v14a2 2 0 0 0 2 2h14c.55 0 1.052-.22 1.41-.59"}],["path",{d:"M21 15V5a2 2 0 0 0-2-2H9"}]],Bp=[["path",{d:"M16 5h6"}],["path",{d:"M19 2v6"}],["path",{d:"M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"}],["circle",{cx:"9",cy:"9",r:"2"}]],zp=[["path",{d:"M15 15.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997a1 1 0 0 1-1.517-.86z"}],["path",{d:"M21 12.17V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"}],["path",{d:"m6 21 5-5"}],["circle",{cx:"9",cy:"9",r:"2"}]],Fp=[["path",{d:"M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21"}],["path",{d:"m14 19.5 3-3 3 3"}],["path",{d:"M17 22v-5.5"}],["circle",{cx:"9",cy:"9",r:"2"}]],Dp=[["path",{d:"M16 3h5v5"}],["path",{d:"M17 21h2a2 2 0 0 0 2-2"}],["path",{d:"M21 12v3"}],["path",{d:"m21 3-5 5"}],["path",{d:"M3 7V5a2 2 0 0 1 2-2"}],["path",{d:"m5 21 4.144-4.144a1.21 1.21 0 0 1 1.712 0L13 19"}],["path",{d:"M9 3h3"}],["rect",{x:"3",y:"11",width:"10",height:"10",rx:"1"}]],bp=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["circle",{cx:"9",cy:"9",r:"2"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"}]],Rp=[["path",{d:"m22 11-1.296-1.296a2.4 2.4 0 0 0-3.408 0L11 16"}],["path",{d:"M4 8a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2"}],["circle",{cx:"13",cy:"7",r:"1",fill:"currentColor"}],["rect",{x:"8",y:"2",width:"14",height:"14",rx:"2"}]],Tp=[["path",{d:"M12 3v12"}],["path",{d:"m8 11 4 4 4-4"}],["path",{d:"M8 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-4"}]],qp=[["polyline",{points:"22 12 16 12 14 15 10 15 8 12 2 12"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"}]],Up=[["path",{d:"M6 3h12"}],["path",{d:"M6 8h12"}],["path",{d:"m6 13 8.5 8"}],["path",{d:"M6 13h3"}],["path",{d:"M9 13c6.667 0 6.667-10 0-10"}]],Op=[["path",{d:"M6 16c5 0 7-8 12-8a4 4 0 0 1 0 8c-5 0-7-8-12-8a4 4 0 1 0 0 8"}]],Zp=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 16v-4"}],["path",{d:"M12 8h.01"}]],Gp=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 7h.01"}],["path",{d:"M17 7h.01"}],["path",{d:"M7 17h.01"}],["path",{d:"M17 17h.01"}]],Wp=[["rect",{width:"20",height:"20",x:"2",y:"2",rx:"5",ry:"5"}],["path",{d:"M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"}],["line",{x1:"17.5",x2:"17.51",y1:"6.5",y2:"6.5"}]],Ip=[["line",{x1:"19",x2:"10",y1:"4",y2:"4"}],["line",{x1:"14",x2:"5",y1:"20",y2:"20"}],["line",{x1:"15",x2:"9",y1:"4",y2:"20"}]],Ep=[["path",{d:"m16 14 4 4-4 4"}],["path",{d:"M20 10a8 8 0 1 0-8 8h8"}]],Xp=[["path",{d:"M4 10a8 8 0 1 1 8 8H4"}],["path",{d:"m8 22-4-4 4-4"}]],jp=[["path",{d:"M12 9.5V21m0-11.5L6 3m6 6.5L18 3"}],["path",{d:"M6 15h12"}],["path",{d:"M6 11h12"}]],Np=[["path",{d:"M21 17a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-2Z"}],["path",{d:"M6 15v-2"}],["path",{d:"M12 15V9"}],["circle",{cx:"12",cy:"6",r:"3"}]],Kp=[["path",{d:"M5 3v14"}],["path",{d:"M12 3v8"}],["path",{d:"M19 3v18"}]],Qp=[["path",{d:"M18 17a1 1 0 0 0-1 1v1a2 2 0 1 0 2-2z"}],["path",{d:"M20.97 3.61a.45.45 0 0 0-.58-.58C10.2 6.6 6.6 10.2 3.03 20.39a.45.45 0 0 0 .58.58C13.8 17.4 17.4 13.8 20.97 3.61"}],["path",{d:"m6.707 6.707 10.586 10.586"}],["path",{d:"M7 5a2 2 0 1 0-2 2h1a1 1 0 0 0 1-1z"}]],Jp=[["path",{d:"M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"}],["circle",{cx:"16.5",cy:"7.5",r:".5",fill:"currentColor"}]],Yp=[["path",{d:"M12.4 2.7a2.5 2.5 0 0 1 3.4 0l5.5 5.5a2.5 2.5 0 0 1 0 3.4l-3.7 3.7a2.5 2.5 0 0 1-3.4 0L8.7 9.8a2.5 2.5 0 0 1 0-3.4z"}],["path",{d:"m14 7 3 3"}],["path",{d:"m9.4 10.6-6.814 6.814A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814"}]],_p=[["path",{d:"m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"}],["path",{d:"m21 2-9.6 9.6"}],["circle",{cx:"7.5",cy:"15.5",r:"5.5"}]],xp=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M6 8h4"}],["path",{d:"M14 8h.01"}],["path",{d:"M18 8h.01"}],["path",{d:"M2 12h20"}],["path",{d:"M6 12v4"}],["path",{d:"M10 12v4"}],["path",{d:"M14 12v4"}],["path",{d:"M18 12v4"}]],ai=[["path",{d:"M 20 4 A2 2 0 0 1 22 6"}],["path",{d:"M 22 6 L 22 16.41"}],["path",{d:"M 7 16 L 16 16"}],["path",{d:"M 9.69 4 L 20 4"}],["path",{d:"M14 8h.01"}],["path",{d:"M18 8h.01"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20 20H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"}],["path",{d:"M6 8h.01"}],["path",{d:"M8 12h.01"}]],ti=[["path",{d:"M10 8h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M14 8h.01"}],["path",{d:"M16 12h.01"}],["path",{d:"M18 8h.01"}],["path",{d:"M6 8h.01"}],["path",{d:"M7 16h10"}],["path",{d:"M8 12h.01"}],["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}]],hi=[["path",{d:"M12 2v5"}],["path",{d:"M14.829 15.998a3 3 0 1 1-5.658 0"}],["path",{d:"M20.92 14.606A1 1 0 0 1 20 16H4a1 1 0 0 1-.92-1.394l3-7A1 1 0 0 1 7 7h10a1 1 0 0 1 .92.606z"}]],di=[["path",{d:"M10.293 2.293a1 1 0 0 1 1.414 0l2.5 2.5 5.994 1.227a1 1 0 0 1 .506 1.687l-7 7a1 1 0 0 1-1.687-.506l-1.227-5.994-2.5-2.5a1 1 0 0 1 0-1.414z"}],["path",{d:"m14.207 4.793-3.414 3.414"}],["path",{d:"M3 20a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"}],["path",{d:"m9.086 6.5-4.793 4.793a1 1 0 0 0-.18 1.17L7 18"}]],ci=[["path",{d:"M12 10v12"}],["path",{d:"M17.929 7.629A1 1 0 0 1 17 9H7a1 1 0 0 1-.928-1.371l2-5A1 1 0 0 1 9 2h6a1 1 0 0 1 .928.629z"}],["path",{d:"M9 22h6"}]],Mi=[["path",{d:"M19.929 18.629A1 1 0 0 1 19 20H9a1 1 0 0 1-.928-1.371l2-5A1 1 0 0 1 11 13h6a1 1 0 0 1 .928.629z"}],["path",{d:"M6 3a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"}],["path",{d:"M8 6h4a2 2 0 0 1 2 2v5"}]],pi=[["path",{d:"M19.929 9.629A1 1 0 0 1 19 11H9a1 1 0 0 1-.928-1.371l2-5A1 1 0 0 1 11 4h6a1 1 0 0 1 .928.629z"}],["path",{d:"M6 15a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1z"}],["path",{d:"M8 18h4a2 2 0 0 0 2-2v-5"}]],ii=[["path",{d:"M12 12v6"}],["path",{d:"M4.077 10.615A1 1 0 0 0 5 12h14a1 1 0 0 0 .923-1.385l-3.077-7.384A2 2 0 0 0 15 2H9a2 2 0 0 0-1.846 1.23Z"}],["path",{d:"M8 20a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1z"}]],ni=[["path",{d:"m12 8 6-3-6-3v10"}],["path",{d:"m8 11.99-5.5 3.14a1 1 0 0 0 0 1.74l8.5 4.86a2 2 0 0 0 2 0l8.5-4.86a1 1 0 0 0 0-1.74L16 12"}],["path",{d:"m6.49 12.85 11.02 6.3"}],["path",{d:"M17.51 12.85 6.5 19.15"}]],li=[["path",{d:"M10 18v-7"}],["path",{d:"M11.12 2.198a2 2 0 0 1 1.76.006l7.866 3.847c.476.233.31.949-.22.949H3.474c-.53 0-.695-.716-.22-.949z"}],["path",{d:"M14 18v-7"}],["path",{d:"M18 18v-7"}],["path",{d:"M3 22h18"}],["path",{d:"M6 18v-7"}]],ei=[["path",{d:"m5 8 6 6"}],["path",{d:"m4 14 6-6 2-3"}],["path",{d:"M2 5h12"}],["path",{d:"M7 2h1"}],["path",{d:"m22 22-5-10-5 10"}],["path",{d:"M14 18h6"}]],ri=[["path",{d:"M2 20h20"}],["path",{d:"m9 10 2 2 4-4"}],["rect",{x:"3",y:"4",width:"18",height:"12",rx:"2"}]],A2=[["rect",{width:"18",height:"12",x:"3",y:"4",rx:"2",ry:"2"}],["line",{x1:"2",x2:"22",y1:"20",y2:"20"}]],oi=[["path",{d:"M18 5a2 2 0 0 1 2 2v8.526a2 2 0 0 0 .212.897l1.068 2.127a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45l1.068-2.127A2 2 0 0 0 4 15.526V7a2 2 0 0 1 2-2z"}],["path",{d:"M20.054 15.987H3.946"}]],vi=[["path",{d:"M7 22a5 5 0 0 1-2-4"}],["path",{d:"M7 16.93c.96.43 1.96.74 2.99.91"}],["path",{d:"M3.34 14A6.8 6.8 0 0 1 2 10c0-4.42 4.48-8 10-8s10 3.58 10 8a7.19 7.19 0 0 1-.33 2"}],["path",{d:"M5 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"}],["path",{d:"M14.33 22h-.09a.35.35 0 0 1-.24-.32v-10a.34.34 0 0 1 .33-.34c.08 0 .15.03.21.08l7.34 6a.33.33 0 0 1-.21.59h-4.49l-2.57 3.85a.35.35 0 0 1-.28.14z"}]],$i=[["path",{d:"M3.704 14.467A10 8 0 0 1 2 10a10 8 0 0 1 20 0 10 8 0 0 1-10 8 10 8 0 0 1-5.181-1.158"}],["path",{d:"M7 22a5 5 0 0 1-2-3.994"}],["circle",{cx:"5",cy:"16",r:"2"}]],mi=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M18 13a6 6 0 0 1-6 5 6 6 0 0 1-6-5h12Z"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}]],yi=[["path",{d:"M13 13.74a2 2 0 0 1-2 0L2.5 8.87a1 1 0 0 1 0-1.74L11 2.26a2 2 0 0 1 2 0l8.5 4.87a1 1 0 0 1 0 1.74z"}],["path",{d:"m20 14.285 1.5.845a1 1 0 0 1 0 1.74L13 21.74a2 2 0 0 1-2 0l-8.5-4.87a1 1 0 0 1 0-1.74l1.5-.845"}]],w2=[["path",{d:"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"}],["path",{d:"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"}],["path",{d:"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"}]],si=[["path",{d:"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 .83.18 2 2 0 0 0 .83-.18l8.58-3.9a1 1 0 0 0 0-1.831z"}],["path",{d:"M16 17h6"}],["path",{d:"M19 14v6"}],["path",{d:"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 .825.178"}],["path",{d:"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l2.116-.962"}]],gi=[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1"}]],Ci=[["rect",{width:"7",height:"7",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"14",rx:"1"}],["rect",{width:"7",height:"7",x:"3",y:"14",rx:"1"}]],ui=[["rect",{width:"7",height:"7",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"3",y:"14",rx:"1"}],["path",{d:"M14 4h7"}],["path",{d:"M14 9h7"}],["path",{d:"M14 15h7"}],["path",{d:"M14 20h7"}]],Hi=[["rect",{width:"7",height:"18",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"14",rx:"1"}]],Ai=[["rect",{width:"18",height:"7",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"3",y:"14",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"14",rx:"1"}]],wi=[["rect",{width:"18",height:"7",x:"3",y:"3",rx:"1"}],["rect",{width:"9",height:"7",x:"3",y:"14",rx:"1"}],["rect",{width:"5",height:"7",x:"16",y:"14",rx:"1"}]],Vi=[["path",{d:"M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z"}],["path",{d:"M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"}]],Si=[["path",{d:"M2 22c1.25-.987 2.27-1.975 3.9-2.2a5.56 5.56 0 0 1 3.8 1.5 4 4 0 0 0 6.187-2.353 3.5 3.5 0 0 0 3.69-5.116A3.5 3.5 0 0 0 20.95 8 3.5 3.5 0 1 0 16 3.05a3.5 3.5 0 0 0-5.831 1.373 3.5 3.5 0 0 0-5.116 3.69 4 4 0 0 0-2.348 6.155C3.499 15.42 4.409 16.712 4.2 18.1 3.926 19.743 3.014 20.732 2 22"}],["path",{d:"M2 22 17 7"}]],Li=[["path",{d:"M16 12h3a2 2 0 0 0 1.902-1.38l1.056-3.333A1 1 0 0 0 21 6H3a1 1 0 0 0-.958 1.287l1.056 3.334A2 2 0 0 0 5 12h3"}],["path",{d:"M18 6V3a1 1 0 0 0-1-1h-3"}],["rect",{width:"8",height:"12",x:"8",y:"10",rx:"1"}]],fi=[["rect",{width:"8",height:"18",x:"3",y:"3",rx:"1"}],["path",{d:"M7 3v18"}],["path",{d:"M20.4 18.9c.2.5-.1 1.1-.6 1.3l-1.9.7c-.5.2-1.1-.1-1.3-.6L11.1 5.1c-.2-.5.1-1.1.6-1.3l1.9-.7c.5-.2 1.1.1 1.3.6Z"}]],ki=[["path",{d:"m16 6 4 14"}],["path",{d:"M12 6v14"}],["path",{d:"M8 8v12"}],["path",{d:"M4 4v16"}]],Pi=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m4.93 4.93 4.24 4.24"}],["path",{d:"m14.83 9.17 4.24-4.24"}],["path",{d:"m14.83 14.83 4.24 4.24"}],["path",{d:"m9.17 14.83-4.24 4.24"}],["circle",{cx:"12",cy:"12",r:"4"}]],Bi=[["path",{d:"M14 12h2v8"}],["path",{d:"M14 20h4"}],["path",{d:"M6 12h4"}],["path",{d:"M6 20h4"}],["path",{d:"M8 20V8a4 4 0 0 1 7.464-2"}]],zi=[["path",{d:"M16.8 11.2c.8-.9 1.2-2 1.2-3.2a6 6 0 0 0-9.3-5"}],["path",{d:"m2 2 20 20"}],["path",{d:"M6.3 6.3a4.67 4.67 0 0 0 1.2 5.2c.7.7 1.3 1.5 1.5 2.5"}],["path",{d:"M9 18h6"}],["path",{d:"M10 22h4"}]],Fi=[["path",{d:"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"}],["path",{d:"M9 18h6"}],["path",{d:"M10 22h4"}]],Di=[["path",{d:"M7 3.5c5-2 7 2.5 3 4C1.5 10 2 15 5 16c5 2 9-10 14-7s.5 13.5-4 12c-5-2.5.5-11 6-2"}]],bi=[["path",{d:"M9 17H7A5 5 0 0 1 7 7h2"}],["path",{d:"M15 7h2a5 5 0 1 1 0 10h-2"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12"}]],Ri=[["path",{d:"M9 17H7A5 5 0 0 1 7 7"}],["path",{d:"M15 7h2a5 5 0 0 1 4 8"}],["line",{x1:"8",x2:"12",y1:"12",y2:"12"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],Ti=[["path",{d:"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"}],["path",{d:"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"}]],qi=[["path",{d:"M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"}],["rect",{width:"4",height:"12",x:"2",y:"9"}],["circle",{cx:"4",cy:"4",r:"2"}]],Ui=[["path",{d:"M16 5H3"}],["path",{d:"M16 12H3"}],["path",{d:"M11 19H3"}],["path",{d:"m15 18 2 2 4-4"}]],Oi=[["path",{d:"M13 5h8"}],["path",{d:"M13 12h8"}],["path",{d:"M13 19h8"}],["path",{d:"m3 17 2 2 4-4"}],["path",{d:"m3 7 2 2 4-4"}]],Zi=[["path",{d:"M3 5h8"}],["path",{d:"M3 12h8"}],["path",{d:"M3 19h8"}],["path",{d:"m15 5 3 3 3-3"}],["path",{d:"m15 19 3-3 3 3"}]],Gi=[["path",{d:"M3 5h8"}],["path",{d:"M3 12h8"}],["path",{d:"M3 19h8"}],["path",{d:"m15 8 3-3 3 3"}],["path",{d:"m15 16 3 3 3-3"}]],Wi=[["path",{d:"M10 5h11"}],["path",{d:"M10 12h11"}],["path",{d:"M10 19h11"}],["path",{d:"m3 10 3-3-3-3"}],["path",{d:"m3 20 3-3-3-3"}]],Ii=[["path",{d:"M16 5H3"}],["path",{d:"M16 12H3"}],["path",{d:"M9 19H3"}],["path",{d:"m16 16-3 3 3 3"}],["path",{d:"M21 5v12a2 2 0 0 1-2 2h-6"}]],Ei=[["path",{d:"M12 5H2"}],["path",{d:"M6 12h12"}],["path",{d:"M9 19h6"}],["path",{d:"M16 5h6"}],["path",{d:"M19 8V2"}]],Xi=[["path",{d:"M2 5h20"}],["path",{d:"M6 12h12"}],["path",{d:"M9 19h6"}]],v=[["path",{d:"M21 5H11"}],["path",{d:"M21 12H11"}],["path",{d:"M21 19H11"}],["path",{d:"m7 8-4 4 4 4"}]],$=[["path",{d:"M21 5H11"}],["path",{d:"M21 12H11"}],["path",{d:"M21 19H11"}],["path",{d:"m3 8 4 4-4 4"}]],ji=[["path",{d:"M16 5H3"}],["path",{d:"M11 12H3"}],["path",{d:"M16 19H3"}],["path",{d:"M21 12h-6"}]],Ni=[["path",{d:"M16 5H3"}],["path",{d:"M11 12H3"}],["path",{d:"M11 19H3"}],["path",{d:"M21 16V5"}],["circle",{cx:"18",cy:"16",r:"3"}]],Ki=[["path",{d:"M11 5h10"}],["path",{d:"M11 12h10"}],["path",{d:"M11 19h10"}],["path",{d:"M4 4h1v5"}],["path",{d:"M4 9h2"}],["path",{d:"M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02"}]],Qi=[["path",{d:"M16 5H3"}],["path",{d:"M11 12H3"}],["path",{d:"M16 19H3"}],["path",{d:"M18 9v6"}],["path",{d:"M21 12h-6"}]],Ji=[["path",{d:"M21 5H3"}],["path",{d:"M7 12H3"}],["path",{d:"M7 19H3"}],["path",{d:"M12 18a5 5 0 0 0 9-3 4.5 4.5 0 0 0-4.5-4.5c-1.33 0-2.54.54-3.41 1.41L11 14"}],["path",{d:"M11 10v4h4"}]],Yi=[["path",{d:"M3 5h6"}],["path",{d:"M3 12h13"}],["path",{d:"M3 19h13"}],["path",{d:"m16 8-3-3 3-3"}],["path",{d:"M21 19V7a2 2 0 0 0-2-2h-6"}]],_i=[["path",{d:"M13 5h8"}],["path",{d:"M13 12h8"}],["path",{d:"M13 19h8"}],["path",{d:"m3 17 2 2 4-4"}],["rect",{x:"3",y:"4",width:"6",height:"6",rx:"1"}]],xi=[["path",{d:"M8 5h13"}],["path",{d:"M13 12h8"}],["path",{d:"M13 19h8"}],["path",{d:"M3 10a2 2 0 0 0 2 2h3"}],["path",{d:"M3 5v12a2 2 0 0 0 2 2h3"}]],an=[["path",{d:"M21 5H3"}],["path",{d:"M10 12H3"}],["path",{d:"M10 19H3"}],["path",{d:"M15 12.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997a1 1 0 0 1-1.517-.86z"}]],tn=[["path",{d:"M16 5H3"}],["path",{d:"M11 12H3"}],["path",{d:"M16 19H3"}],["path",{d:"m15.5 9.5 5 5"}],["path",{d:"m20.5 9.5-5 5"}]],V2=[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56"}]],hn=[["path",{d:"M3 5h.01"}],["path",{d:"M3 12h.01"}],["path",{d:"M3 19h.01"}],["path",{d:"M8 5h13"}],["path",{d:"M8 12h13"}],["path",{d:"M8 19h13"}]],dn=[["path",{d:"M22 12a1 1 0 0 1-10 0 1 1 0 0 0-10 0"}],["path",{d:"M7 20.7a1 1 0 1 1 5-8.7 1 1 0 1 0 5-8.6"}],["path",{d:"M7 3.3a1 1 0 1 1 5 8.6 1 1 0 1 0 5 8.6"}],["circle",{cx:"12",cy:"12",r:"10"}]],cn=[["path",{d:"M12 2v4"}],["path",{d:"m16.2 7.8 2.9-2.9"}],["path",{d:"M18 12h4"}],["path",{d:"m16.2 16.2 2.9 2.9"}],["path",{d:"M12 18v4"}],["path",{d:"m4.9 19.1 2.9-2.9"}],["path",{d:"M2 12h4"}],["path",{d:"m4.9 4.9 2.9 2.9"}]],Mn=[["line",{x1:"2",x2:"5",y1:"12",y2:"12"}],["line",{x1:"19",x2:"22",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"2",y2:"5"}],["line",{x1:"12",x2:"12",y1:"19",y2:"22"}],["circle",{cx:"12",cy:"12",r:"7"}],["circle",{cx:"12",cy:"12",r:"3"}]],pn=[["path",{d:"M12 19v3"}],["path",{d:"M12 2v3"}],["path",{d:"M18.89 13.24a7 7 0 0 0-8.13-8.13"}],["path",{d:"M19 12h3"}],["path",{d:"M2 12h3"}],["path",{d:"m2 2 20 20"}],["path",{d:"M7.05 7.05a7 7 0 0 0 9.9 9.9"}]],nn=[["line",{x1:"2",x2:"5",y1:"12",y2:"12"}],["line",{x1:"19",x2:"22",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"2",y2:"5"}],["line",{x1:"12",x2:"12",y1:"19",y2:"22"}],["circle",{cx:"12",cy:"12",r:"7"}]],S2=[["circle",{cx:"12",cy:"16",r:"1"}],["rect",{width:"18",height:"12",x:"3",y:"10",rx:"2"}],["path",{d:"M7 10V7a5 5 0 0 1 9.33-2.5"}]],ln=[["circle",{cx:"12",cy:"16",r:"1"}],["rect",{x:"3",y:"10",width:"18",height:"12",rx:"2"}],["path",{d:"M7 10V7a5 5 0 0 1 10 0v3"}]],L2=[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2"}],["path",{d:"M7 11V7a5 5 0 0 1 9.9-1"}]],en=[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4"}]],rn=[["path",{d:"m10 17 5-5-5-5"}],["path",{d:"M15 12H3"}],["path",{d:"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"}]],on=[["path",{d:"m16 17 5-5-5-5"}],["path",{d:"M21 12H9"}],["path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"}]],vn=[["path",{d:"M3 5h1"}],["path",{d:"M3 12h1"}],["path",{d:"M3 19h1"}],["path",{d:"M8 5h1"}],["path",{d:"M8 12h1"}],["path",{d:"M8 19h1"}],["path",{d:"M13 5h8"}],["path",{d:"M13 12h8"}],["path",{d:"M13 19h8"}]],$n=[["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}],["path",{d:"M11 11a2 2 0 0 0 4 0 4 4 0 0 0-8 0 6 6 0 0 0 12 0"}]],mn=[["path",{d:"M6 20a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2"}],["path",{d:"M8 18V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v14"}],["path",{d:"M10 20h4"}],["circle",{cx:"16",cy:"20",r:"2"}],["circle",{cx:"8",cy:"20",r:"2"}]],yn=[["path",{d:"m12 15 4 4"}],["path",{d:"M2.352 10.648a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.029-6.029a1 1 0 1 1 3 3l-6.029 6.029a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.365-6.367A1 1 0 0 0 8.716 4.282z"}],["path",{d:"m5 8 4 4"}]],sn=[["path",{d:"M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"m16 19 2 2 4-4"}]],gn=[["path",{d:"M22 15V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M16 19h6"}]],Cn=[["path",{d:"M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z"}],["path",{d:"m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"}]],un=[["path",{d:"M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M19 16v6"}],["path",{d:"M16 19h6"}]],f2=[["path",{d:"M22 10.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h12.5"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M18 15.28c.2-.4.5-.8.9-1a2.1 2.1 0 0 1 2.6.4c.3.4.5.8.5 1.3 0 1.3-2 2-2 2"}],["path",{d:"M20 22v.01"}]],Hn=[["path",{d:"M22 12.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h7.5"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M18 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"}],["circle",{cx:"18",cy:"18",r:"3"}],["path",{d:"m22 22-1.5-1.5"}]],An=[["path",{d:"M22 10.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h12.5"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M20 14v4"}],["path",{d:"M20 22v.01"}]],wn=[["path",{d:"M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h9"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"m17 17 4 4"}],["path",{d:"m21 17-4 4"}]],Vn=[["path",{d:"m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"}],["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2"}]],Sn=[["path",{d:"M22 17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9.5C2 7 4 5 6.5 5H18c2.2 0 4 1.8 4 4v8Z"}],["polyline",{points:"15,9 18,9 18,11"}],["path",{d:"M6.5 5C9 5 11 7 11 9.5V17a2 2 0 0 1-2 2"}],["line",{x1:"6",x2:"7",y1:"10",y2:"10"}]],Ln=[["path",{d:"M17 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 1-1.732"}],["path",{d:"m22 5.5-6.419 4.179a2 2 0 0 1-2.162 0L7 5.5"}],["rect",{x:"7",y:"3",width:"15",height:"12",rx:"2"}]],fn=[["path",{d:"m11 19-1.106-.552a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0l4.212 2.106a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619V14"}],["path",{d:"M15 5.764V14"}],["path",{d:"M21 18h-6"}],["path",{d:"M9 3.236v15"}]],kn=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["path",{d:"m9 10 2 2 4-4"}]],Pn=[["path",{d:"M19.43 12.935c.357-.967.57-1.955.57-2.935a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32.197 32.197 0 0 0 .813-.728"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"m16 18 2 2 4-4"}]],Bn=[["path",{d:"M15 22a1 1 0 0 1-1-1v-4a1 1 0 0 1 .445-.832l3-2a1 1 0 0 1 1.11 0l3 2A1 1 0 0 1 22 17v4a1 1 0 0 1-1 1z"}],["path",{d:"M18 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 .601.2"}],["path",{d:"M18 22v-3"}],["circle",{cx:"10",cy:"10",r:"3"}]],zn=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["path",{d:"M9 10h6"}]],Fn=[["path",{d:"M18.977 14C19.6 12.701 20 11.343 20 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32 32 0 0 0 .824-.738"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M16 18h6"}]],Dn=[["path",{d:"M12.75 7.09a3 3 0 0 1 2.16 2.16"}],["path",{d:"M17.072 17.072c-1.634 2.17-3.527 3.912-4.471 4.727a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 1.432-4.568"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.475 2.818A8 8 0 0 1 20 10c0 1.183-.31 2.377-.81 3.533"}],["path",{d:"M9.13 9.13a3 3 0 0 0 3.74 3.74"}]],k2=[["path",{d:"M17.97 9.304A8 8 0 0 0 2 10c0 4.69 4.887 9.562 7.022 11.468"}],["path",{d:"M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["circle",{cx:"10",cy:"10",r:"3"}]],bn=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["path",{d:"M12 7v6"}],["path",{d:"M9 10h6"}]],Rn=[["path",{d:"M19.914 11.105A7.298 7.298 0 0 0 20 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32 32 0 0 0 .824-.738"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M16 18h6"}],["path",{d:"M19 15v6"}]],Tn=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["path",{d:"m14.5 7.5-5 5"}],["path",{d:"m9.5 7.5 5 5"}]],qn=[["path",{d:"M19.752 11.901A7.78 7.78 0 0 0 20 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 19 19 0 0 0 .09-.077"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"m21.5 15.5-5 5"}],["path",{d:"m21.5 20.5-5-5"}]],Un=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["circle",{cx:"12",cy:"10",r:"3"}]],On=[["path",{d:"M18 8c0 3.613-3.869 7.429-5.393 8.795a1 1 0 0 1-1.214 0C9.87 15.429 6 11.613 6 8a6 6 0 0 1 12 0"}],["circle",{cx:"12",cy:"8",r:"2"}],["path",{d:"M8.714 14h-3.71a1 1 0 0 0-.948.683l-2.004 6A1 1 0 0 0 3 22h18a1 1 0 0 0 .948-1.316l-2-6a1 1 0 0 0-.949-.684h-3.712"}]],Zn=[["path",{d:"m11 19-1.106-.552a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0l4.212 2.106a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619V12"}],["path",{d:"M15 5.764V12"}],["path",{d:"M18 15v6"}],["path",{d:"M21 18h-6"}],["path",{d:"M9 3.236v15"}]],Gn=[["path",{d:"M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z"}],["path",{d:"M15 5.764v15"}],["path",{d:"M9 3.236v15"}]],Wn=[["path",{d:"m14 6 4 4"}],["path",{d:"M17 3h4v4"}],["path",{d:"m21 3-7.75 7.75"}],["circle",{cx:"9",cy:"15",r:"6"}]],In=[["path",{d:"M16 3h5v5"}],["path",{d:"m21 3-6.75 6.75"}],["circle",{cx:"10",cy:"14",r:"6"}]],En=[["path",{d:"M8 22h8"}],["path",{d:"M12 11v11"}],["path",{d:"m19 3-7 8-7-8Z"}]],Xn=[["path",{d:"M15 3h6v6"}],["path",{d:"m21 3-7 7"}],["path",{d:"m3 21 7-7"}],["path",{d:"M9 21H3v-6"}]],jn=[["path",{d:"M8 3H5a2 2 0 0 0-2 2v3"}],["path",{d:"M21 8V5a2 2 0 0 0-2-2h-3"}],["path",{d:"M3 16v3a2 2 0 0 0 2 2h3"}],["path",{d:"M16 21h3a2 2 0 0 0 2-2v-3"}]],Nn=[["path",{d:"M11.636 6A13 13 0 0 0 19.4 3.2 1 1 0 0 1 21 4v11.344"}],["path",{d:"M14.378 14.357A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h1"}],["path",{d:"m2 2 20 20"}],["path",{d:"M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"}],["path",{d:"M8 8v6"}]],Kn=[["path",{d:"M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"}],["path",{d:"M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"}],["path",{d:"M8 6v8"}]],Qn=[["path",{d:"M7.21 15 2.66 7.14a2 2 0 0 1 .13-2.2L4.4 2.8A2 2 0 0 1 6 2h12a2 2 0 0 1 1.6.8l1.6 2.14a2 2 0 0 1 .14 2.2L16.79 15"}],["path",{d:"M11 12 5.12 2.2"}],["path",{d:"m13 12 5.88-9.8"}],["path",{d:"M8 7h8"}],["circle",{cx:"12",cy:"17",r:"5"}],["path",{d:"M12 18v-2h-.5"}]],Jn=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"8",x2:"16",y1:"15",y2:"15"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}]],Yn=[["path",{d:"M12 12v-2"}],["path",{d:"M12 18v-2"}],["path",{d:"M16 12v-2"}],["path",{d:"M16 18v-2"}],["path",{d:"M2 11h1.5"}],["path",{d:"M20 18v-2"}],["path",{d:"M20.5 11H22"}],["path",{d:"M4 18v-2"}],["path",{d:"M8 12v-2"}],["path",{d:"M8 18v-2"}],["rect",{x:"2",y:"6",width:"20",height:"10",rx:"2"}]],_n=[["path",{d:"m8 6 4-4 4 4"}],["path",{d:"M12 2v10.3a4 4 0 0 1-1.172 2.872L4 22"}],["path",{d:"m20 22-5-5"}]],xn=[["path",{d:"M4 5h16"}],["path",{d:"M4 12h16"}],["path",{d:"M4 19h16"}]],al=[["path",{d:"m10 9-3 3 3 3"}],["path",{d:"m14 15 3-3-3-3"}],["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}]],tl=[["path",{d:"M10.1 2.182a10 10 0 0 1 3.8 0"}],["path",{d:"M13.9 21.818a10 10 0 0 1-3.8 0"}],["path",{d:"M17.609 3.72a10 10 0 0 1 2.69 2.7"}],["path",{d:"M2.182 13.9a10 10 0 0 1 0-3.8"}],["path",{d:"M20.28 17.61a10 10 0 0 1-2.7 2.69"}],["path",{d:"M21.818 10.1a10 10 0 0 1 0 3.8"}],["path",{d:"M3.721 6.391a10 10 0 0 1 2.7-2.69"}],["path",{d:"m6.163 21.117-2.906.85a1 1 0 0 1-1.236-1.169l.965-2.98"}]],hl=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M7.828 13.07A3 3 0 0 1 12 8.764a3 3 0 0 1 5.004 2.224 3 3 0 0 1-.832 2.083l-3.447 3.62a1 1 0 0 1-1.45-.001z"}]],dl=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M8 12h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M16 12h.01"}]],cl=[["path",{d:"m2 2 20 20"}],["path",{d:"M4.93 4.929a10 10 0 0 0-1.938 11.412 2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 0 0 11.302-1.989"}],["path",{d:"M8.35 2.69A10 10 0 0 1 21.3 15.65"}]],Ml=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M8 12h8"}],["path",{d:"M12 8v8"}]],P2=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"}],["path",{d:"M12 17h.01"}]],pl=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"m10 15-3-3 3-3"}],["path",{d:"M7 12h8a2 2 0 0 1 2 2v1"}]],il=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M12 8v4"}],["path",{d:"M12 16h.01"}]],nl=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"m15 9-6 6"}],["path",{d:"m9 9 6 6"}]],ll=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}]],el=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"m10 8-3 3 3 3"}],["path",{d:"m14 14 3-3-3-3"}]],rl=[["path",{d:"M12 19h.01"}],["path",{d:"M12 3h.01"}],["path",{d:"M16 19h.01"}],["path",{d:"M16 3h.01"}],["path",{d:"M2 13h.01"}],["path",{d:"M2 17v4.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H8"}],["path",{d:"M2 5a2 2 0 0 1 2-2"}],["path",{d:"M2 9h.01"}],["path",{d:"M20 3a2 2 0 0 1 2 2"}],["path",{d:"M22 13h.01"}],["path",{d:"M22 17a2 2 0 0 1-2 2"}],["path",{d:"M22 9h.01"}],["path",{d:"M8 3h.01"}]],ol=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M10 15h4"}],["path",{d:"M10 9h4"}],["path",{d:"M12 7v4"}]],vl=[["path",{d:"M12.7 3H4a2 2 0 0 0-2 2v16.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H20a2 2 0 0 0 2-2v-4.7"}],["circle",{cx:"19",cy:"6",r:"3"}]],$l=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M7.5 9.5c0 .687.265 1.383.697 1.844l3.009 3.264a1.14 1.14 0 0 0 .407.314 1 1 0 0 0 .783-.004 1.14 1.14 0 0 0 .398-.31l3.008-3.264A2.77 2.77 0 0 0 16.5 9.5 2.5 2.5 0 0 0 12 8a2.5 2.5 0 0 0-4.5 1.5"}]],ml=[["path",{d:"M22 8.5V5a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v16.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H10"}],["path",{d:"M20 15v-2a2 2 0 0 0-4 0v2"}],["rect",{x:"14",y:"15",width:"8",height:"5",rx:"1"}]],yl=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M12 11h.01"}],["path",{d:"M16 11h.01"}],["path",{d:"M8 11h.01"}]],sl=[["path",{d:"M19 19H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.7.7 0 0 1 2 21.286V5a2 2 0 0 1 1.184-1.826"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.656 3H20a2 2 0 0 1 2 2v11.344"}]],gl=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M12 8v6"}],["path",{d:"M9 11h6"}]],Cl=[["path",{d:"M14 14a2 2 0 0 0 2-2V8h-2"}],["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M8 14a2 2 0 0 0 2-2V8H8"}]],ul=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"m10 8-3 3 3 3"}],["path",{d:"M17 14v-1a2 2 0 0 0-2-2H7"}]],Hl=[["path",{d:"M12 3H4a2 2 0 0 0-2 2v16.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H20a2 2 0 0 0 2-2v-4"}],["path",{d:"M16 3h6v6"}],["path",{d:"m16 9 6-6"}]],Al=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M7 11h10"}],["path",{d:"M7 15h6"}],["path",{d:"M7 7h8"}]],wl=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M12 15h.01"}],["path",{d:"M12 7v4"}]],Vl=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"m14.5 8.5-5 5"}],["path",{d:"m9.5 8.5 5 5"}]],Sl=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}]],Ll=[["path",{d:"M16 10a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 14.286V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"}],["path",{d:"M20 9a2 2 0 0 1 2 2v10.286a.71.71 0 0 1-1.212.502l-2.202-2.202A2 2 0 0 0 17.172 19H10a2 2 0 0 1-2-2v-1"}]],fl=[["path",{d:"M12 19v3"}],["path",{d:"M15 9.34V5a3 3 0 0 0-5.68-1.33"}],["path",{d:"M16.95 16.95A7 7 0 0 1 5 12v-2"}],["path",{d:"M18.89 13.23A7 7 0 0 0 19 12v-2"}],["path",{d:"m2 2 20 20"}],["path",{d:"M9 9v3a3 3 0 0 0 5.12 2.12"}]],B2=[["path",{d:"m11 7.601-5.994 8.19a1 1 0 0 0 .1 1.298l.817.818a1 1 0 0 0 1.314.087L15.09 12"}],["path",{d:"M16.5 21.174C15.5 20.5 14.372 20 13 20c-2.058 0-3.928 2.356-6 2-2.072-.356-2.775-3.369-1.5-4.5"}],["circle",{cx:"16",cy:"7",r:"5"}]],kl=[["path",{d:"M12 19v3"}],["path",{d:"M19 10v2a7 7 0 0 1-14 0v-2"}],["rect",{x:"9",y:"2",width:"6",height:"13",rx:"3"}]],Pl=[["path",{d:"M10 12h4"}],["path",{d:"M10 17h4"}],["path",{d:"M10 7h4"}],["path",{d:"M18 12h2"}],["path",{d:"M18 18h2"}],["path",{d:"M18 6h2"}],["path",{d:"M4 12h2"}],["path",{d:"M4 18h2"}],["path",{d:"M4 6h2"}],["rect",{x:"6",y:"2",width:"12",height:"20",rx:"2"}]],Bl=[["path",{d:"M6 18h8"}],["path",{d:"M3 22h18"}],["path",{d:"M14 22a7 7 0 1 0 0-14h-1"}],["path",{d:"M9 14h2"}],["path",{d:"M9 12a2 2 0 0 1-2-2V6h6v4a2 2 0 0 1-2 2Z"}],["path",{d:"M12 6V3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3"}]],zl=[["rect",{width:"20",height:"15",x:"2",y:"4",rx:"2"}],["rect",{width:"8",height:"7",x:"6",y:"8",rx:"1"}],["path",{d:"M18 8v7"}],["path",{d:"M6 19v2"}],["path",{d:"M18 19v2"}]],Fl=[["path",{d:"M12 13v8"}],["path",{d:"M12 3v3"}],["path",{d:"M4 6a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h13a2 2 0 0 0 1.152-.365l3.424-2.317a1 1 0 0 0 0-1.635l-3.424-2.318A2 2 0 0 0 17 6z"}]],Dl=[["path",{d:"M8 2h8"}],["path",{d:"M9 2v1.343M15 2v2.789a4 4 0 0 0 .672 2.219l.656.984a4 4 0 0 1 .672 2.22v1.131M7.8 7.8l-.128.192A4 4 0 0 0 7 10.212V20a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-3"}],["path",{d:"M7 15a6.47 6.47 0 0 1 5 0 6.472 6.472 0 0 0 3.435.435"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],bl=[["path",{d:"M8 2h8"}],["path",{d:"M9 2v2.789a4 4 0 0 1-.672 2.219l-.656.984A4 4 0 0 0 7 10.212V20a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-9.789a4 4 0 0 0-.672-2.219l-.656-.984A4 4 0 0 1 15 4.788V2"}],["path",{d:"M7 15a6.472 6.472 0 0 1 5 0 6.47 6.47 0 0 0 5 0"}]],Rl=[["path",{d:"m14 10 7-7"}],["path",{d:"M20 10h-6V4"}],["path",{d:"m3 21 7-7"}],["path",{d:"M4 14h6v6"}]],Tl=[["path",{d:"M8 3v3a2 2 0 0 1-2 2H3"}],["path",{d:"M21 8h-3a2 2 0 0 1-2-2V3"}],["path",{d:"M3 16h3a2 2 0 0 1 2 2v3"}],["path",{d:"M16 21v-3a2 2 0 0 1 2-2h3"}]],ql=[["path",{d:"M5 12h14"}]],Ul=[["path",{d:"m9 10 2 2 4-4"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Ol=[["path",{d:"M11 13a3 3 0 1 1 2.83-4H14a2 2 0 0 1 0 4z"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}],["rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}]],Zl=[["path",{d:"M12 17v4"}],["path",{d:"m14.305 7.53.923-.382"}],["path",{d:"m15.228 4.852-.923-.383"}],["path",{d:"m16.852 3.228-.383-.924"}],["path",{d:"m16.852 8.772-.383.923"}],["path",{d:"m19.148 3.228.383-.924"}],["path",{d:"m19.53 9.696-.382-.924"}],["path",{d:"m20.772 4.852.924-.383"}],["path",{d:"m20.772 7.148.924.383"}],["path",{d:"M22 13v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"}],["path",{d:"M8 21h8"}],["circle",{cx:"18",cy:"6",r:"3"}]],Gl=[["path",{d:"M12 17v4"}],["path",{d:"M22 12.307V15a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8.693"}],["path",{d:"M8 21h8"}],["circle",{cx:"19",cy:"6",r:"3"}]],Wl=[["path",{d:"M12 13V7"}],["path",{d:"m15 10-3 3-3-3"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Il=[["path",{d:"M17 17H4a2 2 0 0 1-2-2V5c0-1.5 1-2 1-2"}],["path",{d:"M22 15V5a2 2 0 0 0-2-2H9"}],["path",{d:"M8 21h8"}],["path",{d:"M12 17v4"}],["path",{d:"m2 2 20 20"}]],El=[["path",{d:"M10 13V7"}],["path",{d:"M14 13V7"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Xl=[["path",{d:"M15.033 9.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56V7.648a.645.645 0 0 1 .967-.56z"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}],["rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}]],jl=[["path",{d:"M18 8V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h8"}],["path",{d:"M10 19v-3.96 3.15"}],["path",{d:"M7 19h5"}],["rect",{width:"6",height:"10",x:"16",y:"12",rx:"2"}]],Nl=[["path",{d:"M5.5 20H8"}],["path",{d:"M17 9h.01"}],["rect",{width:"10",height:"16",x:"12",y:"4",rx:"2"}],["path",{d:"M8 6H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h4"}],["circle",{cx:"17",cy:"15",r:"1"}]],Kl=[["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}],["rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}],["rect",{x:"9",y:"7",width:"6",height:"6",rx:"1"}]],Ql=[["path",{d:"m9 10 3-3 3 3"}],["path",{d:"M12 13V7"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Jl=[["path",{d:"m14.5 12.5-5-5"}],["path",{d:"m9.5 12.5 5-5"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Yl=[["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["line",{x1:"8",x2:"16",y1:"21",y2:"21"}],["line",{x1:"12",x2:"12",y1:"17",y2:"21"}]],_l=[["path",{d:"M18 5h4"}],["path",{d:"M20 3v4"}],["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"}]],xl=[["path",{d:"m18 14-1-3"}],["path",{d:"m3 9 6 2a2 2 0 0 1 2-2h2a2 2 0 0 1 1.99 1.81"}],["path",{d:"M8 17h3a1 1 0 0 0 1-1 6 6 0 0 1 6-6 1 1 0 0 0 1-1v-.75A5 5 0 0 0 17 5"}],["circle",{cx:"19",cy:"17",r:"3"}],["circle",{cx:"5",cy:"17",r:"3"}]],ae=[["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"}]],te=[["path",{d:"m8 3 4 8 5-5 5 15H2L8 3z"}],["path",{d:"M4.14 15.08c2.62-1.57 5.24-1.43 7.86.42 2.74 1.94 5.49 2 8.23.19"}]],he=[["path",{d:"m8 3 4 8 5-5 5 15H2L8 3z"}]],de=[["path",{d:"M12 6v.343"}],["path",{d:"M18.218 18.218A7 7 0 0 1 5 15V9a7 7 0 0 1 .782-3.218"}],["path",{d:"M19 13.343V9A7 7 0 0 0 8.56 2.902"}],["path",{d:"M22 22 2 2"}]],ce=[["path",{d:"m15.55 8.45 5.138 2.087a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063L8.45 15.551"}],["path",{d:"M22 2 2 22"}],["path",{d:"m6.816 11.528-2.779-6.84a.495.495 0 0 1 .651-.651l6.84 2.779"}]],Me=[["path",{d:"M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"}]],pe=[["path",{d:"M2.034 2.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.944L8.204 7.545a1 1 0 0 0-.66.66l-1.066 3.443a.5.5 0 0 1-.944.033z"}],["circle",{cx:"16",cy:"16",r:"6"}],["path",{d:"m11.8 11.8 8.4 8.4"}]],ie=[["path",{d:"M14 4.1 12 6"}],["path",{d:"m5.1 8-2.9-.8"}],["path",{d:"m6 12-1.9 2"}],["path",{d:"M7.2 2.2 8 5.1"}],["path",{d:"M9.037 9.69a.498.498 0 0 1 .653-.653l11 4.5a.5.5 0 0 1-.074.949l-4.349 1.041a1 1 0 0 0-.74.739l-1.04 4.35a.5.5 0 0 1-.95.074z"}]],ne=[["path",{d:"M12.586 12.586 19 19"}],["path",{d:"M3.688 3.037a.497.497 0 0 0-.651.651l6.5 15.999a.501.501 0 0 0 .947-.062l1.569-6.083a2 2 0 0 1 1.448-1.479l6.124-1.579a.5.5 0 0 0 .063-.947z"}]],le=[["rect",{x:"5",y:"2",width:"14",height:"20",rx:"7"}],["path",{d:"M12 6v4"}]],z2=[["path",{d:"M5 3v16h16"}],["path",{d:"m5 19 6-6"}],["path",{d:"m2 6 3-3 3 3"}],["path",{d:"m18 16 3 3-3 3"}]],ee=[["path",{d:"M19 13v6h-6"}],["path",{d:"M5 11V5h6"}],["path",{d:"m5 5 14 14"}]],re=[["path",{d:"M11 19H5v-6"}],["path",{d:"M13 5h6v6"}],["path",{d:"M19 5 5 19"}]],oe=[["path",{d:"M11 19H5V13"}],["path",{d:"M19 5L5 19"}]],ve=[["path",{d:"M19 13V19H13"}],["path",{d:"M5 5L19 19"}]],$e=[["path",{d:"M8 18L12 22L16 18"}],["path",{d:"M12 2V22"}]],me=[["path",{d:"M6 8L2 12L6 16"}],["path",{d:"M2 12H22"}]],ye=[["path",{d:"m18 8 4 4-4 4"}],["path",{d:"M2 12h20"}],["path",{d:"m6 8-4 4 4 4"}]],se=[["path",{d:"M18 8L22 12L18 16"}],["path",{d:"M2 12H22"}]],ge=[["path",{d:"M5 11V5H11"}],["path",{d:"M5 5L19 19"}]],Ce=[["path",{d:"M13 5H19V11"}],["path",{d:"M19 5L5 19"}]],ue=[["path",{d:"M8 6L12 2L16 6"}],["path",{d:"M12 2V22"}]],He=[["path",{d:"M12 2v20"}],["path",{d:"m8 18 4 4 4-4"}],["path",{d:"m8 6 4-4 4 4"}]],Ae=[["path",{d:"M12 2v20"}],["path",{d:"m15 19-3 3-3-3"}],["path",{d:"m19 9 3 3-3 3"}],["path",{d:"M2 12h20"}],["path",{d:"m5 9-3 3 3 3"}],["path",{d:"m9 5 3-3 3 3"}]],we=[["circle",{cx:"8",cy:"18",r:"4"}],["path",{d:"M12 18V2l7 4"}]],Ve=[["circle",{cx:"12",cy:"18",r:"4"}],["path",{d:"M16 18V2"}]],Se=[["path",{d:"M9 18V5l12-2v13"}],["path",{d:"m9 9 12-2"}],["circle",{cx:"6",cy:"18",r:"3"}],["circle",{cx:"18",cy:"16",r:"3"}]],Le=[["path",{d:"M9 18V5l12-2v13"}],["circle",{cx:"6",cy:"18",r:"3"}],["circle",{cx:"18",cy:"16",r:"3"}]],fe=[["path",{d:"M9.31 9.31 5 21l7-4 7 4-1.17-3.17"}],["path",{d:"M14.53 8.88 12 2l-1.17 3.17"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],ke=[["polygon",{points:"12 2 19 21 12 17 5 21 12 2"}]],Pe=[["path",{d:"M8.43 8.43 3 11l8 2 2 8 2.57-5.43"}],["path",{d:"M17.39 11.73 22 2l-9.73 4.61"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],Be=[["polygon",{points:"3 11 22 2 13 21 11 13 3 11"}]],ze=[["path",{d:"M15 18h-5"}],["path",{d:"M18 14h-8"}],["path",{d:"M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-4 0v-9a2 2 0 0 1 2-2h2"}],["rect",{width:"8",height:"4",x:"10",y:"6",rx:"1"}]],Fe=[["rect",{x:"16",y:"16",width:"6",height:"6",rx:"1"}],["rect",{x:"2",y:"16",width:"6",height:"6",rx:"1"}],["rect",{x:"9",y:"2",width:"6",height:"6",rx:"1"}],["path",{d:"M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"}],["path",{d:"M12 12V8"}]],De=[["path",{d:"M6 8.32a7.43 7.43 0 0 1 0 7.36"}],["path",{d:"M9.46 6.21a11.76 11.76 0 0 1 0 11.58"}],["path",{d:"M12.91 4.1a15.91 15.91 0 0 1 .01 15.8"}],["path",{d:"M16.37 2a20.16 20.16 0 0 1 0 20"}]],be=[["path",{d:"M12 2v10"}],["path",{d:"m8.5 4 7 4"}],["path",{d:"m8.5 8 7-4"}],["circle",{cx:"12",cy:"17",r:"5"}]],Re=[["path",{d:"M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4"}],["path",{d:"M2 6h4"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["path",{d:"M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}]],Te=[["path",{d:"M2 6h4"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["path",{d:"M15 2v20"}],["path",{d:"M15 7h5"}],["path",{d:"M15 12h5"}],["path",{d:"M15 17h5"}]],qe=[["path",{d:"M2 6h4"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["path",{d:"M9.5 8h5"}],["path",{d:"M9.5 12H16"}],["path",{d:"M9.5 16H14"}]],Ue=[["path",{d:"M2 6h4"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["path",{d:"M16 2v20"}]],Oe=[["path",{d:"M8 2v4"}],["path",{d:"M12 2v4"}],["path",{d:"M16 2v4"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v2"}],["path",{d:"M20 12v2"}],["path",{d:"M20 18v2a2 2 0 0 1-2 2h-1"}],["path",{d:"M13 22h-2"}],["path",{d:"M7 22H6a2 2 0 0 1-2-2v-2"}],["path",{d:"M4 14v-2"}],["path",{d:"M4 8V6a2 2 0 0 1 2-2h2"}],["path",{d:"M8 10h6"}],["path",{d:"M8 14h8"}],["path",{d:"M8 18h5"}]],Ze=[["path",{d:"M8 2v4"}],["path",{d:"M12 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"16",height:"18",x:"4",y:"4",rx:"2"}],["path",{d:"M8 10h6"}],["path",{d:"M8 14h8"}],["path",{d:"M8 18h5"}]],Ge=[["path",{d:"M12 4V2"}],["path",{d:"M5 10v4a7.004 7.004 0 0 0 5.277 6.787c.412.104.802.292 1.102.592L12 22l.621-.621c.3-.3.69-.488 1.102-.592a7.01 7.01 0 0 0 4.125-2.939"}],["path",{d:"M19 10v3.343"}],["path",{d:"M12 12c-1.349-.573-1.905-1.005-2.5-2-.546.902-1.048 1.353-2.5 2-1.018-.644-1.46-1.08-2-2-1.028.71-1.69.918-3 1 1.081-1.048 1.757-2.03 2-3 .194-.776.84-1.551 1.79-2.21m11.654 5.997c.887-.457 1.28-.891 1.556-1.787 1.032.916 1.683 1.157 3 1-1.297-1.036-1.758-2.03-2-3-.5-2-4-4-8-4-.74 0-1.461.068-2.15.192"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],We=[["path",{d:"M12 4V2"}],["path",{d:"M5 10v4a7.004 7.004 0 0 0 5.277 6.787c.412.104.802.292 1.102.592L12 22l.621-.621c.3-.3.69-.488 1.102-.592A7.003 7.003 0 0 0 19 14v-4"}],["path",{d:"M12 4C8 4 4.5 6 4 8c-.243.97-.919 1.952-2 3 1.31-.082 1.972-.29 3-1 .54.92.982 1.356 2 2 1.452-.647 1.954-1.098 2.5-2 .595.995 1.151 1.427 2.5 2 1.31-.621 1.862-1.058 2.5-2 .629.977 1.162 1.423 2.5 2 1.209-.548 1.68-.967 2-2 1.032.916 1.683 1.157 3 1-1.297-1.036-1.758-2.03-2-3-.5-2-4-4-8-4Z"}]],F2=[["path",{d:"M12 16h.01"}],["path",{d:"M12 8v4"}],["path",{d:"M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z"}]],Ie=[["path",{d:"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"}],["path",{d:"M8 12h8"}]],D2=[["path",{d:"M10 15V9"}],["path",{d:"M14 15V9"}],["path",{d:"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"}]],b2=[["path",{d:"m15 9-6 6"}],["path",{d:"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"}],["path",{d:"m9 9 6 6"}]],Ee=[["path",{d:"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"}]],Xe=[["path",{d:"M3 20h4.5a.5.5 0 0 0 .5-.5v-.282a.52.52 0 0 0-.247-.437 8 8 0 1 1 8.494-.001.52.52 0 0 0-.247.438v.282a.5.5 0 0 0 .5.5H21"}]],je=[["path",{d:"M3 3h6l6 18h6"}],["path",{d:"M14 3h7"}]],Ne=[["path",{d:"M20.341 6.484A10 10 0 0 1 10.266 21.85"}],["path",{d:"M3.659 17.516A10 10 0 0 1 13.74 2.152"}],["circle",{cx:"12",cy:"12",r:"3"}],["circle",{cx:"19",cy:"5",r:"2"}],["circle",{cx:"5",cy:"19",r:"2"}]],Ke=[["path",{d:"M12 12V4a1 1 0 0 1 1-1h6.297a1 1 0 0 1 .651 1.759l-4.696 4.025"}],["path",{d:"m12 21-7.414-7.414A2 2 0 0 1 4 12.172V6.415a1.002 1.002 0 0 1 1.707-.707L20 20.009"}],["path",{d:"m12.214 3.381 8.414 14.966a1 1 0 0 1-.167 1.199l-1.168 1.163a1 1 0 0 1-.706.291H6.351a1 1 0 0 1-.625-.219L3.25 18.8a1 1 0 0 1 .631-1.781l4.165.027"}]],Qe=[["path",{d:"M12 3v6"}],["path",{d:"M16.76 3a2 2 0 0 1 1.8 1.1l2.23 4.479a2 2 0 0 1 .21.891V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9.472a2 2 0 0 1 .211-.894L5.45 4.1A2 2 0 0 1 7.24 3z"}],["path",{d:"M3.054 9.013h17.893"}]],Je=[["path",{d:"m16 16 2 2 4-4"}],["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}]],Ye=[["path",{d:"M16 16h6"}],["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}]],_e=[["path",{d:"M12 22v-9"}],["path",{d:"M15.17 2.21a1.67 1.67 0 0 1 1.63 0L21 4.57a1.93 1.93 0 0 1 0 3.36L8.82 14.79a1.655 1.655 0 0 1-1.64 0L3 12.43a1.93 1.93 0 0 1 0-3.36z"}],["path",{d:"M20 13v3.87a2.06 2.06 0 0 1-1.11 1.83l-6 3.08a1.93 1.93 0 0 1-1.78 0l-6-3.08A2.06 2.06 0 0 1 4 16.87V13"}],["path",{d:"M21 12.43a1.93 1.93 0 0 0 0-3.36L8.83 2.2a1.64 1.64 0 0 0-1.63 0L3 4.57a1.93 1.93 0 0 0 0 3.36l12.18 6.86a1.636 1.636 0 0 0 1.63 0z"}]],xe=[["path",{d:"M16 16h6"}],["path",{d:"M19 13v6"}],["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}]],ar=[["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}],["circle",{cx:"18.5",cy:"15.5",r:"2.5"}],["path",{d:"M20.27 17.27 22 19"}]],tr=[["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}],["path",{d:"m17 13 5 5m-5 0 5-5"}]],hr=[["path",{d:"M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z"}],["path",{d:"M12 22V12"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["path",{d:"m7.5 4.27 9 5.15"}]],dr=[["path",{d:"M11 7 6 2"}],["path",{d:"M18.992 12H2.041"}],["path",{d:"M21.145 18.38A3.34 3.34 0 0 1 20 16.5a3.3 3.3 0 0 1-1.145 1.88c-.575.46-.855 1.02-.855 1.595A2 2 0 0 0 20 22a2 2 0 0 0 2-2.025c0-.58-.285-1.13-.855-1.595"}],["path",{d:"m8.5 4.5 2.148-2.148a1.205 1.205 0 0 1 1.704 0l7.296 7.296a1.205 1.205 0 0 1 0 1.704l-7.592 7.592a3.615 3.615 0 0 1-5.112 0l-3.888-3.888a3.615 3.615 0 0 1 0-5.112L5.67 7.33"}]],cr=[["rect",{width:"16",height:"6",x:"2",y:"2",rx:"2"}],["path",{d:"M10 16v-2a2 2 0 0 1 2-2h8a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"}],["rect",{width:"4",height:"6",x:"8",y:"16",rx:"1"}]],R2=[["path",{d:"M10 2v2"}],["path",{d:"M14 2v4"}],["path",{d:"M17 2a1 1 0 0 1 1 1v9H6V3a1 1 0 0 1 1-1z"}],["path",{d:"M6 12a1 1 0 0 0-1 1v1a2 2 0 0 0 2 2h2a1 1 0 0 1 1 1v2.9a2 2 0 1 0 4 0V17a1 1 0 0 1 1-1h2a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1"}]],Mr=[["path",{d:"m14.622 17.897-10.68-2.913"}],["path",{d:"M18.376 2.622a1 1 0 1 1 3.002 3.002L17.36 9.643a.5.5 0 0 0 0 .707l.944.944a2.41 2.41 0 0 1 0 3.408l-.944.944a.5.5 0 0 1-.707 0L8.354 7.348a.5.5 0 0 1 0-.707l.944-.944a2.41 2.41 0 0 1 3.408 0l.944.944a.5.5 0 0 0 .707 0z"}],["path",{d:"M9 8c-1.804 2.71-3.97 3.46-6.583 3.948a.507.507 0 0 0-.302.819l7.32 8.883a1 1 0 0 0 1.185.204C12.735 20.405 16 16.792 16 15"}]],pr=[["path",{d:"M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z"}],["circle",{cx:"13.5",cy:"6.5",r:".5",fill:"currentColor"}],["circle",{cx:"17.5",cy:"10.5",r:".5",fill:"currentColor"}],["circle",{cx:"6.5",cy:"12.5",r:".5",fill:"currentColor"}],["circle",{cx:"8.5",cy:"7.5",r:".5",fill:"currentColor"}]],ir=[["path",{d:"M11.25 17.25h1.5L12 18z"}],["path",{d:"m15 12 2 2"}],["path",{d:"M18 6.5a.5.5 0 0 0-.5-.5"}],["path",{d:"M20.69 9.67a4.5 4.5 0 1 0-7.04-5.5 8.35 8.35 0 0 0-3.3 0 4.5 4.5 0 1 0-7.04 5.5C2.49 11.2 2 12.88 2 14.5 2 19.47 6.48 22 12 22s10-2.53 10-7.5c0-1.62-.48-3.3-1.3-4.83"}],["path",{d:"M6 6.5a.495.495 0 0 1 .5-.5"}],["path",{d:"m9 12-2 2"}]],nr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 15h18"}],["path",{d:"m15 8-3 3-3-3"}]],T2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M14 15h1"}],["path",{d:"M19 15h2"}],["path",{d:"M3 15h2"}],["path",{d:"M9 15h1"}]],lr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 15h18"}],["path",{d:"m9 10 3-3 3 3"}]],er=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 15h18"}]],q2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}],["path",{d:"m16 15-3-3 3-3"}]],U2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 14v1"}],["path",{d:"M9 19v2"}],["path",{d:"M9 3v2"}],["path",{d:"M9 9v1"}]],O2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}],["path",{d:"m14 9 3 3-3 3"}]],rr=[["path",{d:"M15 10V9"}],["path",{d:"M15 15v-1"}],["path",{d:"M15 21v-2"}],["path",{d:"M15 5V3"}],["path",{d:"M9 10V9"}],["path",{d:"M9 15v-1"}],["path",{d:"M9 21v-2"}],["path",{d:"M9 5V3"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],Z2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}]],or=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M15 3v18"}],["path",{d:"m8 9 3 3-3 3"}]],G2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M15 14v1"}],["path",{d:"M15 19v2"}],["path",{d:"M15 3v2"}],["path",{d:"M15 9v1"}]],vr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M15 3v18"}],["path",{d:"m10 15-3-3 3-3"}]],$r=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M15 3v18"}]],mr=[["path",{d:"M14 15h1"}],["path",{d:"M14 9h1"}],["path",{d:"M19 15h2"}],["path",{d:"M19 9h2"}],["path",{d:"M3 15h2"}],["path",{d:"M3 9h2"}],["path",{d:"M9 15h1"}],["path",{d:"M9 9h1"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],yr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"m9 16 3-3 3 3"}]],W2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M14 9h1"}],["path",{d:"M19 9h2"}],["path",{d:"M3 9h2"}],["path",{d:"M9 9h1"}]],sr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"m15 14-3 3-3-3"}]],gr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}],["path",{d:"M9 15h12"}]],Cr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}]],ur=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 15h12"}],["path",{d:"M15 3v18"}]],I2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"M9 21V9"}]],Hr=[["path",{d:"m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"}]],Ar=[["path",{d:"M11 15h2"}],["path",{d:"M12 12v3"}],["path",{d:"M12 19v3"}],["path",{d:"M15.282 19a1 1 0 0 0 .948-.68l2.37-6.988a7 7 0 1 0-13.2 0l2.37 6.988a1 1 0 0 0 .948.68z"}],["path",{d:"M9 9a3 3 0 1 1 6 0"}]],wr=[["path",{d:"M8 21s-4-3-4-9 4-9 4-9"}],["path",{d:"M16 3s4 3 4 9-4 9-4 9"}]],Vr=[["path",{d:"M5.8 11.3 2 22l10.7-3.79"}],["path",{d:"M4 3h.01"}],["path",{d:"M22 8h.01"}],["path",{d:"M15 2h.01"}],["path",{d:"M22 20h.01"}],["path",{d:"m22 2-2.24.75a2.9 2.9 0 0 0-1.96 3.12c.1.86-.57 1.63-1.45 1.63h-.38c-.86 0-1.6.6-1.76 1.44L14 10"}],["path",{d:"m22 13-.82-.33c-.86-.34-1.82.2-1.98 1.11c-.11.7-.72 1.22-1.43 1.22H17"}],["path",{d:"m11 2 .33.82c.34.86-.2 1.82-1.11 1.98C9.52 4.9 9 5.52 9 6.23V7"}],["path",{d:"M11 13c1.93 1.93 2.83 4.17 2 5-.83.83-3.07-.07-5-2-1.93-1.93-2.83-4.17-2-5 .83-.83 3.07.07 5 2Z"}]],Sr=[["rect",{x:"14",y:"3",width:"5",height:"18",rx:"1"}],["rect",{x:"5",y:"3",width:"5",height:"18",rx:"1"}]],Lr=[["circle",{cx:"11",cy:"4",r:"2"}],["circle",{cx:"18",cy:"8",r:"2"}],["circle",{cx:"20",cy:"16",r:"2"}],["path",{d:"M9 10a5 5 0 0 1 5 5v3.5a3.5 3.5 0 0 1-6.84 1.045Q6.52 17.48 4.46 16.84A3.5 3.5 0 0 1 5.5 10Z"}]],fr=[["rect",{width:"14",height:"20",x:"5",y:"2",rx:"2"}],["path",{d:"M15 14h.01"}],["path",{d:"M9 6h6"}],["path",{d:"M9 10h6"}]],E2=[["path",{d:"M13 21h8"}],["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}]],kr=[["path",{d:"m10 10-6.157 6.162a2 2 0 0 0-.5.833l-1.322 4.36a.5.5 0 0 0 .622.624l4.358-1.323a2 2 0 0 0 .83-.5L14 13.982"}],["path",{d:"m12.829 7.172 4.359-4.346a1 1 0 1 1 3.986 3.986l-4.353 4.353"}],["path",{d:"m2 2 20 20"}]],Pr=[["path",{d:"M15.707 21.293a1 1 0 0 1-1.414 0l-1.586-1.586a1 1 0 0 1 0-1.414l5.586-5.586a1 1 0 0 1 1.414 0l1.586 1.586a1 1 0 0 1 0 1.414z"}],["path",{d:"m18 13-1.375-6.874a1 1 0 0 0-.746-.776L3.235 2.028a1 1 0 0 0-1.207 1.207L5.35 15.879a1 1 0 0 0 .776.746L13 18"}],["path",{d:"m2.3 2.3 7.286 7.286"}],["circle",{cx:"11",cy:"11",r:"2"}]],X2=[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}]],Br=[["path",{d:"M13 21h8"}],["path",{d:"m15 5 4 4"}],["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}]],zr=[["path",{d:"m10 10-6.157 6.162a2 2 0 0 0-.5.833l-1.322 4.36a.5.5 0 0 0 .622.624l4.358-1.323a2 2 0 0 0 .83-.5L14 13.982"}],["path",{d:"m12.829 7.172 4.359-4.346a1 1 0 1 1 3.986 3.986l-4.353 4.353"}],["path",{d:"m15 5 4 4"}],["path",{d:"m2 2 20 20"}]],Fr=[["path",{d:"M13 7 8.7 2.7a2.41 2.41 0 0 0-3.4 0L2.7 5.3a2.41 2.41 0 0 0 0 3.4L7 13"}],["path",{d:"m8 6 2-2"}],["path",{d:"m18 16 2-2"}],["path",{d:"m17 11 4.3 4.3c.94.94.94 2.46 0 3.4l-2.6 2.6c-.94.94-2.46.94-3.4 0L11 17"}],["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}],["path",{d:"m15 5 4 4"}]],Dr=[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}],["path",{d:"m15 5 4 4"}]],br=[["path",{d:"M10.83 2.38a2 2 0 0 1 2.34 0l8 5.74a2 2 0 0 1 .73 2.25l-3.04 9.26a2 2 0 0 1-1.9 1.37H7.04a2 2 0 0 1-1.9-1.37L2.1 10.37a2 2 0 0 1 .73-2.25z"}]],Rr=[["line",{x1:"19",x2:"5",y1:"5",y2:"19"}],["circle",{cx:"6.5",cy:"6.5",r:"2.5"}],["circle",{cx:"17.5",cy:"17.5",r:"2.5"}]],Tr=[["circle",{cx:"12",cy:"5",r:"1"}],["path",{d:"m9 20 3-6 3 6"}],["path",{d:"m6 8 6 2 6-2"}],["path",{d:"M12 10v4"}]],qr=[["path",{d:"M20 11H4"}],["path",{d:"M20 7H4"}],["path",{d:"M7 21V4a1 1 0 0 1 1-1h4a1 1 0 0 1 0 12H7"}]],Ur=[["path",{d:"M14 6h8"}],["path",{d:"m18 2 4 4-4 4"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Or=[["path",{d:"M13 2a9 9 0 0 1 9 9"}],["path",{d:"M13 6a5 5 0 0 1 5 5"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Zr=[["path",{d:"M16 2v6h6"}],["path",{d:"m22 2-6 6"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Gr=[["path",{d:"m16 2 6 6"}],["path",{d:"m22 2-6 6"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Wr=[["path",{d:"M10.1 13.9a14 14 0 0 0 3.732 2.668 1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2 18 18 0 0 1-12.728-5.272"}],["path",{d:"M22 2 2 22"}],["path",{d:"M4.76 13.582A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 .244.473"}]],Ir=[["path",{d:"m16 8 6-6"}],["path",{d:"M22 8V2h-6"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Er=[["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Xr=[["line",{x1:"9",x2:"9",y1:"4",y2:"20"}],["path",{d:"M4 7c0-1.7 1.3-3 3-3h13"}],["path",{d:"M18 20c-1.7 0-3-1.3-3-3V4"}]],jr=[["path",{d:"M18.5 8c-1.4 0-2.6-.8-3.2-2A6.87 6.87 0 0 0 2 9v11a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-8.5C22 9.6 20.4 8 18.5 8"}],["path",{d:"M2 14h20"}],["path",{d:"M6 14v4"}],["path",{d:"M10 14v4"}],["path",{d:"M14 14v4"}],["path",{d:"M18 14v4"}]],Nr=[["path",{d:"m14 13-8.381 8.38a1 1 0 0 1-3.001-3L11 9.999"}],["path",{d:"M15.973 4.027A13 13 0 0 0 5.902 2.373c-1.398.342-1.092 2.158.277 2.601a19.9 19.9 0 0 1 5.822 3.024"}],["path",{d:"M16.001 11.999a19.9 19.9 0 0 1 3.024 5.824c.444 1.369 2.26 1.676 2.603.278A13 13 0 0 0 20 8.069"}],["path",{d:"M18.352 3.352a1.205 1.205 0 0 0-1.704 0l-5.296 5.296a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l5.296-5.296a1.205 1.205 0 0 0 0-1.704z"}]],Kr=[["path",{d:"M21 9V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h4"}],["rect",{width:"10",height:"7",x:"12",y:"13",rx:"2"}]],Qr=[["path",{d:"M2 10h6V4"}],["path",{d:"m2 4 6 6"}],["path",{d:"M21 10V7a2 2 0 0 0-2-2h-7"}],["path",{d:"M3 14v2a2 2 0 0 0 2 2h3"}],["rect",{x:"12",y:"14",width:"10",height:"7",rx:"1"}]],Jr=[["path",{d:"M11 17h3v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3a3.16 3.16 0 0 0 2-2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-1a5 5 0 0 0-2-4V3a4 4 0 0 0-3.2 1.6l-.3.4H11a6 6 0 0 0-6 6v1a5 5 0 0 0 2 4v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z"}],["path",{d:"M16 10h.01"}],["path",{d:"M2 8v1a2 2 0 0 0 2 2h1"}]],Yr=[["path",{d:"M14 3v11"}],["path",{d:"M14 9h-3a3 3 0 0 1 0-6h9"}],["path",{d:"M18 3v11"}],["path",{d:"M22 18H2l4-4"}],["path",{d:"m6 22-4-4"}]],_r=[["path",{d:"M10 3v11"}],["path",{d:"M10 9H7a1 1 0 0 1 0-6h8"}],["path",{d:"M14 3v11"}],["path",{d:"m18 14 4 4H2"}],["path",{d:"m22 18-4 4"}]],xr=[["path",{d:"M13 4v16"}],["path",{d:"M17 4v16"}],["path",{d:"M19 4H9.5a4.5 4.5 0 0 0 0 9H13"}]],ao=[["path",{d:"M18 11h-4a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h4"}],["path",{d:"M6 7v13a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7"}],["rect",{width:"16",height:"5",x:"4",y:"2",rx:"1"}]],to=[["path",{d:"m10.5 20.5 10-10a4.95 4.95 0 1 0-7-7l-10 10a4.95 4.95 0 1 0 7 7Z"}],["path",{d:"m8.5 8.5 7 7"}]],ho=[["path",{d:"M12 17v5"}],["path",{d:"M15 9.34V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H7.89"}],["path",{d:"m2 2 20 20"}],["path",{d:"M9 9v1.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h11"}]],co=[["path",{d:"M12 17v5"}],["path",{d:"M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"}]],Mo=[["path",{d:"m12 9-8.414 8.414A2 2 0 0 0 3 18.828v1.344a2 2 0 0 1-.586 1.414A2 2 0 0 1 3.828 21h1.344a2 2 0 0 0 1.414-.586L15 12"}],["path",{d:"m18 9 .4.4a1 1 0 1 1-3 3l-3.8-3.8a1 1 0 1 1 3-3l.4.4 3.4-3.4a1 1 0 1 1 3 3z"}],["path",{d:"m2 22 .414-.414"}]],po=[["path",{d:"m12 14-1 1"}],["path",{d:"m13.75 18.25-1.25 1.42"}],["path",{d:"M17.775 5.654a15.68 15.68 0 0 0-12.121 12.12"}],["path",{d:"M18.8 9.3a1 1 0 0 0 2.1 7.7"}],["path",{d:"M21.964 20.732a1 1 0 0 1-1.232 1.232l-18-5a1 1 0 0 1-.695-1.232A19.68 19.68 0 0 1 15.732 2.037a1 1 0 0 1 1.232.695z"}]],io=[["path",{d:"M2 22h20"}],["path",{d:"M3.77 10.77 2 9l2-4.5 1.1.55c.55.28.9.84.9 1.45s.35 1.17.9 1.45L8 8.5l3-6 1.05.53a2 2 0 0 1 1.09 1.52l.72 5.4a2 2 0 0 0 1.09 1.52l4.4 2.2c.42.22.78.55 1.01.96l.6 1.03c.49.88-.06 1.98-1.06 2.1l-1.18.15c-.47.06-.95-.02-1.37-.24L4.29 11.15a2 2 0 0 1-.52-.38Z"}]],no=[["path",{d:"M2 22h20"}],["path",{d:"M6.36 17.4 4 17l-2-4 1.1-.55a2 2 0 0 1 1.8 0l.17.1a2 2 0 0 0 1.8 0L8 12 5 6l.9-.45a2 2 0 0 1 2.09.2l4.02 3a2 2 0 0 0 2.1.2l4.19-2.06a2.41 2.41 0 0 1 1.73-.17L21 7a1.4 1.4 0 0 1 .87 1.99l-.38.76c-.23.46-.6.84-1.07 1.08L7.58 17.2a2 2 0 0 1-1.22.18Z"}]],lo=[["path",{d:"M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z"}]],eo=[["path",{d:"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"}]],ro=[["path",{d:"M9 2v6"}],["path",{d:"M15 2v6"}],["path",{d:"M12 17v5"}],["path",{d:"M5 8h14"}],["path",{d:"M6 11V8h12v3a6 6 0 1 1-12 0Z"}]],j2=[["path",{d:"M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"}],["path",{d:"m2 22 3-3"}],["path",{d:"M7.5 13.5 10 11"}],["path",{d:"M10.5 16.5 13 14"}],["path",{d:"m18 3-4 4h6l-4 4"}]],oo=[["path",{d:"M12 22v-5"}],["path",{d:"M15 8V2"}],["path",{d:"M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"}],["path",{d:"M9 8V2"}]],vo=[["path",{d:"M5 12h14"}],["path",{d:"M12 5v14"}]],$o=[["path",{d:"M3 2v1c0 1 2 1 2 2S3 6 3 7s2 1 2 2-2 1-2 2 2 1 2 2"}],["path",{d:"M18 6h.01"}],["path",{d:"M6 18h.01"}],["path",{d:"M20.83 8.83a4 4 0 0 0-5.66-5.66l-12 12a4 4 0 1 0 5.66 5.66Z"}],["path",{d:"M18 11.66V22a4 4 0 0 0 4-4V6"}]],mo=[["path",{d:"M20 3a2 2 0 0 1 2 2v6a1 1 0 0 1-20 0V5a2 2 0 0 1 2-2z"}],["path",{d:"m8 10 4 4 4-4"}]],yo=[["path",{d:"M13 17a1 1 0 1 0-2 0l.5 4.5a0.5 0.5 0 0 0 1 0z",fill:"currentColor"}],["path",{d:"M16.85 18.58a9 9 0 1 0-9.7 0"}],["path",{d:"M8 14a5 5 0 1 1 8 0"}],["circle",{cx:"12",cy:"11",r:"1",fill:"currentColor"}]],so=[["path",{d:"M10 4.5V4a2 2 0 0 0-2.41-1.957"}],["path",{d:"M13.9 8.4a2 2 0 0 0-1.26-1.295"}],["path",{d:"M21.7 16.2A8 8 0 0 0 22 14v-3a2 2 0 1 0-4 0v-1a2 2 0 0 0-3.63-1.158"}],["path",{d:"m7 15-1.8-1.8a2 2 0 0 0-2.79 2.86L6 19.7a7.74 7.74 0 0 0 6 2.3h2a8 8 0 0 0 5.657-2.343"}],["path",{d:"M6 6v8"}],["path",{d:"m2 2 20 20"}]],go=[["path",{d:"M22 14a8 8 0 0 1-8 8"}],["path",{d:"M18 11v-1a2 2 0 0 0-2-2a2 2 0 0 0-2 2"}],["path",{d:"M14 10V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v1"}],["path",{d:"M10 9.5V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v10"}],["path",{d:"M18 11a2 2 0 1 1 4 0v3a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"}]],Co=[["path",{d:"M18 8a2 2 0 0 0 0-4 2 2 0 0 0-4 0 2 2 0 0 0-4 0 2 2 0 0 0-4 0 2 2 0 0 0 0 4"}],["path",{d:"M10 22 9 8"}],["path",{d:"m14 22 1-14"}],["path",{d:"M20 8c.5 0 .9.4.8 1l-2.6 12c-.1.5-.7 1-1.2 1H7c-.6 0-1.1-.4-1.2-1L3.2 9c-.1-.6.3-1 .8-1Z"}]],uo=[["path",{d:"M18.6 14.4c.8-.8.8-2 0-2.8l-8.1-8.1a4.95 4.95 0 1 0-7.1 7.1l8.1 8.1c.9.7 2.1.7 2.9-.1Z"}],["path",{d:"m22 22-5.5-5.5"}]],Ho=[["path",{d:"M18 7c0-5.333-8-5.333-8 0"}],["path",{d:"M10 7v14"}],["path",{d:"M6 21h12"}],["path",{d:"M6 13h10"}]],Ao=[["path",{d:"M18.36 6.64A9 9 0 0 1 20.77 15"}],["path",{d:"M6.16 6.16a9 9 0 1 0 12.68 12.68"}],["path",{d:"M12 2v4"}],["path",{d:"m2 2 20 20"}]],wo=[["path",{d:"M12 2v10"}],["path",{d:"M18.4 6.6a9 9 0 1 1-12.77.04"}]],Vo=[["path",{d:"M2 3h20"}],["path",{d:"M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3"}],["path",{d:"m7 21 5-5 5 5"}]],So=[["path",{d:"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"}],["path",{d:"M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"}],["rect",{x:"6",y:"14",width:"12",height:"8",rx:"1"}]],Lo=[["path",{d:"M13.5 22H7a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v.5"}],["path",{d:"m16 19 2 2 4-4"}],["path",{d:"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v2"}],["path",{d:"M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"}]],fo=[["path",{d:"M5 7 3 5"}],["path",{d:"M9 6V3"}],["path",{d:"m13 7 2-2"}],["circle",{cx:"9",cy:"13",r:"3"}],["path",{d:"M11.83 12H20a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h2.17"}],["path",{d:"M16 16h2"}]],ko=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M12 9v11"}],["path",{d:"M2 9h13a2 2 0 0 1 2 2v9"}]],Po=[["path",{d:"M15.39 4.39a1 1 0 0 0 1.68-.474 2.5 2.5 0 1 1 3.014 3.015 1 1 0 0 0-.474 1.68l1.683 1.682a2.414 2.414 0 0 1 0 3.414L19.61 15.39a1 1 0 0 1-1.68-.474 2.5 2.5 0 1 0-3.014 3.015 1 1 0 0 1 .474 1.68l-1.683 1.682a2.414 2.414 0 0 1-3.414 0L8.61 19.61a1 1 0 0 0-1.68.474 2.5 2.5 0 1 1-3.014-3.015 1 1 0 0 0 .474-1.68l-1.683-1.682a2.414 2.414 0 0 1 0-3.414L4.39 8.61a1 1 0 0 1 1.68.474 2.5 2.5 0 1 0 3.014-3.015 1 1 0 0 1-.474-1.68l1.683-1.682a2.414 2.414 0 0 1 3.414 0z"}]],Bo=[["path",{d:"M2.5 16.88a1 1 0 0 1-.32-1.43l9-13.02a1 1 0 0 1 1.64 0l9 13.01a1 1 0 0 1-.32 1.44l-8.51 4.86a2 2 0 0 1-1.98 0Z"}],["path",{d:"M12 2v20"}]],zo=[["rect",{width:"5",height:"5",x:"3",y:"3",rx:"1"}],["rect",{width:"5",height:"5",x:"16",y:"3",rx:"1"}],["rect",{width:"5",height:"5",x:"3",y:"16",rx:"1"}],["path",{d:"M21 16h-3a2 2 0 0 0-2 2v3"}],["path",{d:"M21 21v.01"}],["path",{d:"M12 7v3a2 2 0 0 1-2 2H7"}],["path",{d:"M3 12h.01"}],["path",{d:"M12 3h.01"}],["path",{d:"M12 16v.01"}],["path",{d:"M16 12h1"}],["path",{d:"M21 12v.01"}],["path",{d:"M12 21v-1"}]],Fo=[["path",{d:"M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"}],["path",{d:"M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"}]],Do=[["path",{d:"M13 16a3 3 0 0 1 2.24 5"}],["path",{d:"M18 12h.01"}],["path",{d:"M18 21h-8a4 4 0 0 1-4-4 7 7 0 0 1 7-7h.2L9.6 6.4a1 1 0 1 1 2.8-2.8L15.8 7h.2c3.3 0 6 2.7 6 6v1a2 2 0 0 1-2 2h-1a3 3 0 0 0-3 3"}],["path",{d:"M20 8.54V4a2 2 0 1 0-4 0v3"}],["path",{d:"M7.612 12.524a3 3 0 1 0-1.6 4.3"}]],bo=[["path",{d:"M19.07 4.93A10 10 0 0 0 6.99 3.34"}],["path",{d:"M4 6h.01"}],["path",{d:"M2.29 9.62A10 10 0 1 0 21.31 8.35"}],["path",{d:"M16.24 7.76A6 6 0 1 0 8.23 16.67"}],["path",{d:"M12 18h.01"}],["path",{d:"M17.99 11.66A6 6 0 0 1 15.77 16.67"}],["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"m13.41 10.59 5.66-5.66"}]],Ro=[["path",{d:"M12 12h.01"}],["path",{d:"M14 15.4641a4 4 0 0 1-4 0L7.52786 19.74597 A 1 1 0 0 0 7.99303 21.16211 10 10 0 0 0 16.00697 21.16211 1 1 0 0 0 16.47214 19.74597z"}],["path",{d:"M16 12a4 4 0 0 0-2-3.464l2.472-4.282a1 1 0 0 1 1.46-.305 10 10 0 0 1 4.006 6.94A1 1 0 0 1 21 12z"}],["path",{d:"M8 12a4 4 0 0 1 2-3.464L7.528 4.254a1 1 0 0 0-1.46-.305 10 10 0 0 0-4.006 6.94A1 1 0 0 0 3 12z"}]],To=[["path",{d:"M3 12h3.28a1 1 0 0 1 .948.684l2.298 7.934a.5.5 0 0 0 .96-.044L13.82 4.771A1 1 0 0 1 14.792 4H21"}]],qo=[["path",{d:"M5 16v2"}],["path",{d:"M19 16v2"}],["rect",{width:"20",height:"8",x:"2",y:"8",rx:"2"}],["path",{d:"M18 12h.01"}]],Uo=[["path",{d:"M4.9 16.1C1 12.2 1 5.8 4.9 1.9"}],["path",{d:"M7.8 4.7a6.14 6.14 0 0 0-.8 7.5"}],["circle",{cx:"12",cy:"9",r:"2"}],["path",{d:"M16.2 4.8c2 2 2.26 5.11.8 7.47"}],["path",{d:"M19.1 1.9a9.96 9.96 0 0 1 0 14.1"}],["path",{d:"M9.5 18h5"}],["path",{d:"m8 22 4-11 4 11"}]],Oo=[["path",{d:"M16.247 7.761a6 6 0 0 1 0 8.478"}],["path",{d:"M19.075 4.933a10 10 0 0 1 0 14.134"}],["path",{d:"M4.925 19.067a10 10 0 0 1 0-14.134"}],["path",{d:"M7.753 16.239a6 6 0 0 1 0-8.478"}],["circle",{cx:"12",cy:"12",r:"2"}]],Zo=[["path",{d:"M20.34 17.52a10 10 0 1 0-2.82 2.82"}],["circle",{cx:"19",cy:"19",r:"2"}],["path",{d:"m13.41 13.41 4.18 4.18"}],["circle",{cx:"12",cy:"12",r:"2"}]],Go=[["path",{d:"M5 15h14"}],["path",{d:"M5 9h14"}],["path",{d:"m14 20-5-5 6-6-5-5"}]],Wo=[["path",{d:"M22 17a10 10 0 0 0-20 0"}],["path",{d:"M6 17a6 6 0 0 1 12 0"}],["path",{d:"M10 17a2 2 0 0 1 4 0"}]],Io=[["path",{d:"M13 22H4a2 2 0 0 1 0-4h12"}],["path",{d:"M13.236 18a3 3 0 0 0-2.2-5"}],["path",{d:"M16 9h.01"}],["path",{d:"M16.82 3.94a3 3 0 1 1 3.237 4.868l1.815 2.587a1.5 1.5 0 0 1-1.5 2.1l-2.872-.453a3 3 0 0 0-3.5 3"}],["path",{d:"M17 4.988a3 3 0 1 0-5.2 2.052A7 7 0 0 0 4 14.015 4 4 0 0 0 8 18"}]],Eo=[["rect",{width:"12",height:"20",x:"6",y:"2",rx:"2"}],["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}]],Xo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M12 6.5v11"}],["path",{d:"M15 9.4a4 4 0 1 0 0 5.2"}]],jo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M8 12h5"}],["path",{d:"M16 9.5a4 4 0 1 0 0 5.2"}]],No=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M8 7h8"}],["path",{d:"M12 17.5 8 15h1a4 4 0 0 0 0-8"}],["path",{d:"M8 11h8"}]],Ko=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"m12 10 3-3"}],["path",{d:"m9 7 3 3v7.5"}],["path",{d:"M9 11h6"}],["path",{d:"M9 15h6"}]],Qo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M8 13h5"}],["path",{d:"M10 17V9.5a2.5 2.5 0 0 1 5 0"}],["path",{d:"M8 17h7"}]],Jo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M8 15h5"}],["path",{d:"M8 11h5a2 2 0 1 0 0-4h-3v10"}]],Yo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M10 17V7h5"}],["path",{d:"M10 11h4"}],["path",{d:"M8 15h5"}]],_o=[["path",{d:"M13 16H8"}],["path",{d:"M14 8H8"}],["path",{d:"M16 12H8"}],["path",{d:"M4 3a1 1 0 0 1 1-1 1.3 1.3 0 0 1 .7.2l.933.6a1.3 1.3 0 0 0 1.4 0l.934-.6a1.3 1.3 0 0 1 1.4 0l.933.6a1.3 1.3 0 0 0 1.4 0l.933-.6a1.3 1.3 0 0 1 1.4 0l.934.6a1.3 1.3 0 0 0 1.4 0l.933-.6A1.3 1.3 0 0 1 19 2a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1 1.3 1.3 0 0 1-.7-.2l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.934.6a1.3 1.3 0 0 1-1.4 0l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-1.4 0l-.934-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-.7.2 1 1 0 0 1-1-1z"}]],xo=[["path",{d:"M10 6.5v11a5.5 5.5 0 0 0 5.5-5.5"}],["path",{d:"m14 8-6 3"}],["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1z"}]],av=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"}],["path",{d:"M12 17.5v-11"}]],tv=[["path",{d:"M14 4v16H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z"}],["circle",{cx:"14",cy:"12",r:"8"}]],N2=[["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}],["path",{d:"M12 12h.01"}],["path",{d:"M17 12h.01"}],["path",{d:"M7 12h.01"}]],hv=[["path",{d:"M20 6a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-4a2 2 0 0 1-1.6-.8l-1.6-2.13a1 1 0 0 0-1.6 0L9.6 17.2A2 2 0 0 1 8 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"}]],dv=[["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}]],cv=[["rect",{width:"12",height:"20",x:"6",y:"2",rx:"2"}]],Mv=[["path",{d:"M7 19H4.815a1.83 1.83 0 0 1-1.57-.881 1.785 1.785 0 0 1-.004-1.784L7.196 9.5"}],["path",{d:"M11 19h8.203a1.83 1.83 0 0 0 1.556-.89 1.784 1.784 0 0 0 0-1.775l-1.226-2.12"}],["path",{d:"m14 16-3 3 3 3"}],["path",{d:"M8.293 13.596 7.196 9.5 3.1 10.598"}],["path",{d:"m9.344 5.811 1.093-1.892A1.83 1.83 0 0 1 11.985 3a1.784 1.784 0 0 1 1.546.888l3.943 6.843"}],["path",{d:"m13.378 9.633 4.096 1.098 1.097-4.096"}]],pv=[["path",{d:"m15 14 5-5-5-5"}],["path",{d:"M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13"}]],iv=[["circle",{cx:"12",cy:"17",r:"1"}],["path",{d:"M21 7v6h-6"}],["path",{d:"M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"}]],nv=[["path",{d:"M21 7v6h-6"}],["path",{d:"M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"}]],lv=[["path",{d:"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}],["path",{d:"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"}],["path",{d:"M16 16h5v5"}],["circle",{cx:"12",cy:"12",r:"1"}]],ev=[["path",{d:"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}],["path",{d:"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"}],["path",{d:"M16 16h5v5"}]],rv=[["path",{d:"M21 8L18.74 5.74A9.75 9.75 0 0 0 12 3C11 3 10.03 3.16 9.13 3.47"}],["path",{d:"M8 16H3v5"}],["path",{d:"M3 12C3 9.51 4 7.26 5.64 5.64"}],["path",{d:"m3 16 2.26 2.26A9.75 9.75 0 0 0 12 21c2.49 0 4.74-1 6.36-2.64"}],["path",{d:"M21 12c0 1-.16 1.97-.47 2.87"}],["path",{d:"M21 3v5h-5"}],["path",{d:"M22 22 2 2"}]],ov=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"}],["path",{d:"M21 3v5h-5"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"}],["path",{d:"M8 16H3v5"}]],vv=[["path",{d:"M5 6a4 4 0 0 1 4-4h6a4 4 0 0 1 4 4v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6Z"}],["path",{d:"M5 10h14"}],["path",{d:"M15 7v6"}]],$v=[["path",{d:"M17 3v10"}],["path",{d:"m12.67 5.5 8.66 5"}],["path",{d:"m12.67 10.5 8.66-5"}],["path",{d:"M9 17a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2z"}]],mv=[["path",{d:"M4 7V4h16v3"}],["path",{d:"M5 20h6"}],["path",{d:"M13 4 8 20"}],["path",{d:"m15 15 5 5"}],["path",{d:"m20 15-5 5"}]],yv=[["path",{d:"m17 2 4 4-4 4"}],["path",{d:"M3 11v-1a4 4 0 0 1 4-4h14"}],["path",{d:"m7 22-4-4 4-4"}],["path",{d:"M21 13v1a4 4 0 0 1-4 4H3"}],["path",{d:"M11 10h1v4"}]],sv=[["path",{d:"m2 9 3-3 3 3"}],["path",{d:"M13 18H7a2 2 0 0 1-2-2V6"}],["path",{d:"m22 15-3 3-3-3"}],["path",{d:"M11 6h6a2 2 0 0 1 2 2v10"}]],gv=[["path",{d:"m17 2 4 4-4 4"}],["path",{d:"M3 11v-1a4 4 0 0 1 4-4h14"}],["path",{d:"m7 22-4-4 4-4"}],["path",{d:"M21 13v1a4 4 0 0 1-4 4H3"}]],Cv=[["path",{d:"M14 14a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1"}],["path",{d:"M14 4a1 1 0 0 1 1-1"}],["path",{d:"M15 10a1 1 0 0 1-1-1"}],["path",{d:"M19 14a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1"}],["path",{d:"M21 4a1 1 0 0 0-1-1"}],["path",{d:"M21 9a1 1 0 0 1-1 1"}],["path",{d:"m3 7 3 3 3-3"}],["path",{d:"M6 10V5a2 2 0 0 1 2-2h2"}],["rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}]],uv=[["path",{d:"M14 4a1 1 0 0 1 1-1"}],["path",{d:"M15 10a1 1 0 0 1-1-1"}],["path",{d:"M21 4a1 1 0 0 0-1-1"}],["path",{d:"M21 9a1 1 0 0 1-1 1"}],["path",{d:"m3 7 3 3 3-3"}],["path",{d:"M6 10V5a2 2 0 0 1 2-2h2"}],["rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}]],Hv=[["path",{d:"m12 17-5-5 5-5"}],["path",{d:"M22 18v-2a4 4 0 0 0-4-4H7"}],["path",{d:"m7 17-5-5 5-5"}]],Av=[["path",{d:"M20 18v-2a4 4 0 0 0-4-4H4"}],["path",{d:"m9 17-5-5 5-5"}]],wv=[["path",{d:"M12 6a2 2 0 0 0-3.414-1.414l-6 6a2 2 0 0 0 0 2.828l6 6A2 2 0 0 0 12 18z"}],["path",{d:"M22 6a2 2 0 0 0-3.414-1.414l-6 6a2 2 0 0 0 0 2.828l6 6A2 2 0 0 0 22 18z"}]],Vv=[["path",{d:"M12 11.22C11 9.997 10 9 10 8a2 2 0 0 1 4 0c0 1-.998 2.002-2.01 3.22"}],["path",{d:"m12 18 2.57-3.5"}],["path",{d:"M6.243 9.016a7 7 0 0 1 11.507-.009"}],["path",{d:"M9.35 14.53 12 11.22"}],["path",{d:"M9.35 14.53C7.728 12.246 6 10.221 6 7a6 5 0 0 1 12 0c-.005 3.22-1.778 5.235-3.43 7.5l3.557 4.527a1 1 0 0 1-.203 1.43l-1.894 1.36a1 1 0 0 1-1.384-.215L12 18l-2.679 3.593a1 1 0 0 1-1.39.213l-1.865-1.353a1 1 0 0 1-.203-1.422z"}]],Sv=[["path",{d:"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"}],["path",{d:"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"}],["path",{d:"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"}],["path",{d:"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"}]],Lv=[["polyline",{points:"3.5 2 6.5 12.5 18 12.5"}],["line",{x1:"9.5",x2:"5.5",y1:"12.5",y2:"20"}],["line",{x1:"15",x2:"18.5",y1:"12.5",y2:"20"}],["path",{d:"M2.75 18a13 13 0 0 0 18.5 0"}]],fv=[["path",{d:"M6 19V5"}],["path",{d:"M10 19V6.8"}],["path",{d:"M14 19v-7.8"}],["path",{d:"M18 5v4"}],["path",{d:"M18 19v-6"}],["path",{d:"M22 19V9"}],["path",{d:"M2 19V9a4 4 0 0 1 4-4c2 0 4 1.33 6 4s4 4 6 4a4 4 0 1 0-3-6.65"}]],kv=[["path",{d:"M17 10h-1a4 4 0 1 1 4-4v.534"}],["path",{d:"M17 6h1a4 4 0 0 1 1.42 7.74l-2.29.87a6 6 0 0 1-5.339-10.68l2.069-1.31"}],["path",{d:"M4.5 17c2.8-.5 4.4 0 5.5.8s1.8 2.2 2.3 3.7c-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2"}],["path",{d:"M9.77 12C4 15 2 22 2 22"}],["circle",{cx:"17",cy:"8",r:"2"}]],K2=[["path",{d:"M16.466 7.5C15.643 4.237 13.952 2 12 2 9.239 2 7 6.477 7 12s2.239 10 5 10c.342 0 .677-.069 1-.2"}],["path",{d:"m15.194 13.707 3.814 1.86-1.86 3.814"}],["path",{d:"M19 15.57c-1.804.885-4.274 1.43-7 1.43-5.523 0-10-2.239-10-5s4.477-5 10-5c4.838 0 8.873 1.718 9.8 4"}]],Pv=[["path",{d:"m14.5 9.5 1 1"}],["path",{d:"m15.5 8.5-4 4"}],["path",{d:"M3 12a9 9 0 1 0 9-9 9.74 9.74 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}],["circle",{cx:"10",cy:"14",r:"2"}]],Bv=[["path",{d:"M20 9V7a2 2 0 0 0-2-2h-6"}],["path",{d:"m15 2-3 3 3 3"}],["path",{d:"M20 13v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2"}]],zv=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}]],Fv=[["path",{d:"M12 5H6a2 2 0 0 0-2 2v3"}],["path",{d:"m9 8 3-3-3-3"}],["path",{d:"M4 14v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"}]],Dv=[["path",{d:"M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"}],["path",{d:"M21 3v5h-5"}]],bv=[["circle",{cx:"6",cy:"19",r:"3"}],["path",{d:"M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15"}],["circle",{cx:"18",cy:"5",r:"3"}]],Rv=[["circle",{cx:"6",cy:"19",r:"3"}],["path",{d:"M9 19h8.5c.4 0 .9-.1 1.3-.2"}],["path",{d:"M5.2 5.2A3.5 3.53 0 0 0 6.5 12H12"}],["path",{d:"m2 2 20 20"}],["path",{d:"M21 15.3a3.5 3.5 0 0 0-3.3-3.3"}],["path",{d:"M15 5h-4.3"}],["circle",{cx:"18",cy:"5",r:"3"}]],Tv=[["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2"}],["path",{d:"M6.01 18H6"}],["path",{d:"M10.01 18H10"}],["path",{d:"M15 10v4"}],["path",{d:"M17.84 7.17a4 4 0 0 0-5.66 0"}],["path",{d:"M20.66 4.34a8 8 0 0 0-11.31 0"}]],Q2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 12h18"}]],J2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M21 9H3"}],["path",{d:"M21 15H3"}]],qv=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M21 7.5H3"}],["path",{d:"M21 12H3"}],["path",{d:"M21 16.5H3"}]],Uv=[["path",{d:"M4 11a9 9 0 0 1 9 9"}],["path",{d:"M4 4a16 16 0 0 1 16 16"}],["circle",{cx:"5",cy:"19",r:"1"}]],Ov=[["path",{d:"M10 15v-3"}],["path",{d:"M14 15v-3"}],["path",{d:"M18 15v-3"}],["path",{d:"M2 8V4"}],["path",{d:"M22 6H2"}],["path",{d:"M22 8V4"}],["path",{d:"M6 15v-3"}],["rect",{x:"2",y:"12",width:"20",height:"8",rx:"2"}]],Zv=[["path",{d:"M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.41 2.41 0 0 1 0-3.4l2.6-2.6a2.41 2.41 0 0 1 3.4 0Z"}],["path",{d:"m14.5 12.5 2-2"}],["path",{d:"m11.5 9.5 2-2"}],["path",{d:"m8.5 6.5 2-2"}],["path",{d:"m17.5 15.5 2-2"}]],Gv=[["path",{d:"M6 11h8a4 4 0 0 0 0-8H9v18"}],["path",{d:"M6 15h8"}]],Wv=[["path",{d:"M10 2v15"}],["path",{d:"M7 22a4 4 0 0 1-4-4 1 1 0 0 1 1-1h16a1 1 0 0 1 1 1 4 4 0 0 1-4 4z"}],["path",{d:"M9.159 2.46a1 1 0 0 1 1.521-.193l9.977 8.98A1 1 0 0 1 20 13H4a1 1 0 0 1-.824-1.567z"}]],Iv=[["path",{d:"M7 21h10"}],["path",{d:"M12 21a9 9 0 0 0 9-9H3a9 9 0 0 0 9 9Z"}],["path",{d:"M11.38 12a2.4 2.4 0 0 1-.4-4.77 2.4 2.4 0 0 1 3.2-2.77 2.4 2.4 0 0 1 3.47-.63 2.4 2.4 0 0 1 3.37 3.37 2.4 2.4 0 0 1-1.1 3.7 2.51 2.51 0 0 1 .03 1.1"}],["path",{d:"m13 12 4-4"}],["path",{d:"M10.9 7.25A3.99 3.99 0 0 0 4 10c0 .73.2 1.41.54 2"}]],Ev=[["path",{d:"m2.37 11.223 8.372-6.777a2 2 0 0 1 2.516 0l8.371 6.777"}],["path",{d:"M21 15a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-5.25"}],["path",{d:"M3 15a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h9"}],["path",{d:"m6.67 15 6.13 4.6a2 2 0 0 0 2.8-.4l3.15-4.2"}],["rect",{width:"20",height:"4",x:"2",y:"11",rx:"1"}]],Xv=[["path",{d:"M4 10a7.31 7.31 0 0 0 10 10Z"}],["path",{d:"m9 15 3-3"}],["path",{d:"M17 13a6 6 0 0 0-6-6"}],["path",{d:"M21 13A10 10 0 0 0 11 3"}]],jv=[["path",{d:"m13.5 6.5-3.148-3.148a1.205 1.205 0 0 0-1.704 0L6.352 5.648a1.205 1.205 0 0 0 0 1.704L9.5 10.5"}],["path",{d:"M16.5 7.5 19 5"}],["path",{d:"m17.5 10.5 3.148 3.148a1.205 1.205 0 0 1 0 1.704l-2.296 2.296a1.205 1.205 0 0 1-1.704 0L13.5 14.5"}],["path",{d:"M9 21a6 6 0 0 0-6-6"}],["path",{d:"M9.352 10.648a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l4.296-4.296a1.205 1.205 0 0 0 0-1.704l-2.296-2.296a1.205 1.205 0 0 0-1.704 0z"}]],Nv=[["path",{d:"m20 19.5-5.5 1.2"}],["path",{d:"M14.5 4v11.22a1 1 0 0 0 1.242.97L20 15.2"}],["path",{d:"m2.978 19.351 5.549-1.363A2 2 0 0 0 10 16V2"}],["path",{d:"M20 10 4 13.5"}]],Kv=[["path",{d:"M10 2v3a1 1 0 0 0 1 1h5"}],["path",{d:"M18 18v-6a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6"}],["path",{d:"M18 22H4a2 2 0 0 1-2-2V6"}],["path",{d:"M8 18a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9.172a2 2 0 0 1 1.414.586l2.828 2.828A2 2 0 0 1 22 6.828V16a2 2 0 0 1-2.01 2z"}]],Qv=[["path",{d:"M13 13H8a1 1 0 0 0-1 1v7"}],["path",{d:"M14 8h1"}],["path",{d:"M17 21v-4"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20.41 20.41A2 2 0 0 1 19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 .59-1.41"}],["path",{d:"M29.5 11.5s5 5 4 5"}],["path",{d:"M9 3h6.2a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V15"}]],Jv=[["path",{d:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"}],["path",{d:"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"}],["path",{d:"M7 3v4a1 1 0 0 0 1 1h7"}]],Y2=[["path",{d:"M5 7v11a1 1 0 0 0 1 1h11"}],["path",{d:"M5.293 18.707 11 13"}],["circle",{cx:"19",cy:"19",r:"2"}],["circle",{cx:"5",cy:"5",r:"2"}]],Yv=[["path",{d:"M12 3v18"}],["path",{d:"m19 8 3 8a5 5 0 0 1-6 0zV7"}],["path",{d:"M3 7h1a17 17 0 0 0 8-2 17 17 0 0 0 8 2h1"}],["path",{d:"m5 8 3 8a5 5 0 0 1-6 0zV7"}],["path",{d:"M7 21h10"}]],_v=[["path",{d:"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}],["path",{d:"M14 15H9v-5"}],["path",{d:"M16 3h5v5"}],["path",{d:"M21 3 9 15"}]],xv=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["circle",{cx:"12",cy:"12",r:"1"}],["path",{d:"M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"}]],a$=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M8 7v10"}],["path",{d:"M12 7v10"}],["path",{d:"M17 7v10"}]],t$=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M8 14s1.5 2 4 2 4-2 4-2"}],["path",{d:"M9 9h.01"}],["path",{d:"M15 9h.01"}]],h$=[["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M7.828 13.07A3 3 0 0 1 12 8.764a3 3 0 0 1 4.172 4.306l-3.447 3.62a1 1 0 0 1-1.449 0z"}]],d$=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M7 12h10"}]],c$=[["path",{d:"M17 12v4a1 1 0 0 1-1 1h-4"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M17 8V7"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M7 17h.01"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["rect",{x:"7",y:"7",width:"5",height:"5",rx:"1"}]],M$=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"m16 16-1.9-1.9"}]],p$=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M7 8h8"}],["path",{d:"M7 12h10"}],["path",{d:"M7 16h6"}]],i$=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}]],n$=[["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"M18 5v16"}],["path",{d:"m4 6 7.106-3.79a2 2 0 0 1 1.788 0L20 6"}],["path",{d:"m6 11-3.52 2.147a1 1 0 0 0-.48.854V19a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5a1 1 0 0 0-.48-.853L18 11"}],["path",{d:"M6 5v16"}],["circle",{cx:"12",cy:"9",r:"2"}]],l$=[["path",{d:"M5.42 9.42 8 12"}],["circle",{cx:"4",cy:"8",r:"2"}],["path",{d:"m14 6-8.58 8.58"}],["circle",{cx:"4",cy:"16",r:"2"}],["path",{d:"M10.8 14.8 14 18"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}]],e$=[["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M8.12 8.12 12 12"}],["path",{d:"M20 4 8.12 15.88"}],["circle",{cx:"6",cy:"18",r:"3"}],["path",{d:"M14.8 14.8 20 20"}]],r$=[["path",{d:"M21 4h-3.5l2 11.05"}],["path",{d:"M6.95 17h5.142c.523 0 .95-.406 1.063-.916a6.5 6.5 0 0 1 5.345-5.009"}],["circle",{cx:"19.5",cy:"17.5",r:"2.5"}],["circle",{cx:"4.5",cy:"17.5",r:"2.5"}]],o$=[["path",{d:"M13 3H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-3"}],["path",{d:"M8 21h8"}],["path",{d:"M12 17v4"}],["path",{d:"m22 3-5 5"}],["path",{d:"m17 3 5 5"}]],v$=[["path",{d:"M13 3H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-3"}],["path",{d:"M8 21h8"}],["path",{d:"M12 17v4"}],["path",{d:"m17 8 5-5"}],["path",{d:"M17 3h5v5"}]],$$=[["path",{d:"M15 12h-5"}],["path",{d:"M15 8h-5"}],["path",{d:"M19 17V5a2 2 0 0 0-2-2H4"}],["path",{d:"M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"}]],m$=[["path",{d:"M19 17V5a2 2 0 0 0-2-2H4"}],["path",{d:"M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"}]],y$=[["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}],["path",{d:"M11 7v4"}],["path",{d:"M11 15h.01"}]],s$=[["path",{d:"m13 13.5 2-2.5-2-2.5"}],["path",{d:"m21 21-4.3-4.3"}],["path",{d:"M9 8.5 7 11l2 2.5"}],["circle",{cx:"11",cy:"11",r:"8"}]],g$=[["path",{d:"m8 11 2 2 4-4"}],["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}]],C$=[["path",{d:"m13.5 8.5-5 5"}],["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}]],u$=[["path",{d:"m13.5 8.5-5 5"}],["path",{d:"m8.5 8.5 5 5"}],["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}]],H$=[["path",{d:"m21 21-4.34-4.34"}],["circle",{cx:"11",cy:"11",r:"8"}]],_2=[["path",{d:"M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z"}],["path",{d:"M6 12h16"}]],A$=[["path",{d:"M16 5a4 3 0 0 0-8 0c0 4 8 3 8 7a4 3 0 0 1-8 0"}],["path",{d:"M8 19a4 3 0 0 0 8 0c0-4-8-3-8-7a4 3 0 0 1 8 0"}]],w$=[["rect",{x:"14",y:"14",width:"8",height:"8",rx:"2"}],["rect",{x:"2",y:"2",width:"8",height:"8",rx:"2"}],["path",{d:"M7 14v1a2 2 0 0 0 2 2h1"}],["path",{d:"M14 7h1a2 2 0 0 1 2 2v1"}]],V$=[["path",{d:"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"}],["path",{d:"m21.854 2.147-10.94 10.939"}]],S$=[["path",{d:"m16 16-4 4-4-4"}],["path",{d:"M3 12h18"}],["path",{d:"m8 8 4-4 4 4"}]],L$=[["path",{d:"M12 3v18"}],["path",{d:"m16 16 4-4-4-4"}],["path",{d:"m8 8-4 4 4 4"}]],f$=[["path",{d:"m10.852 14.772-.383.923"}],["path",{d:"M13.148 14.772a3 3 0 1 0-2.296-5.544l-.383-.923"}],["path",{d:"m13.148 9.228.383-.923"}],["path",{d:"m13.53 15.696-.382-.924a3 3 0 1 1-2.296-5.544"}],["path",{d:"m14.772 10.852.923-.383"}],["path",{d:"m14.772 13.148.923.383"}],["path",{d:"M4.5 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-.5"}],["path",{d:"M4.5 14H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-.5"}],["path",{d:"M6 18h.01"}],["path",{d:"M6 6h.01"}],["path",{d:"m9.228 10.852-.923-.383"}],["path",{d:"m9.228 13.148-.923.383"}]],k$=[["path",{d:"M6 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-2"}],["path",{d:"M6 14H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-2"}],["path",{d:"M6 6h.01"}],["path",{d:"M6 18h.01"}],["path",{d:"m13 6-4 6h6l-4 6"}]],P$=[["path",{d:"M7 2h13a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-5"}],["path",{d:"M10 10 2.5 2.5C2 2 2 2.5 2 5v3a2 2 0 0 0 2 2h6z"}],["path",{d:"M22 17v-1a2 2 0 0 0-2-2h-1"}],["path",{d:"M4 14a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16.5l1-.5.5.5-8-8H4z"}],["path",{d:"M6 18h.01"}],["path",{d:"m2 2 20 20"}]],B$=[["rect",{width:"20",height:"8",x:"2",y:"2",rx:"2",ry:"2"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2",ry:"2"}],["line",{x1:"6",x2:"6.01",y1:"6",y2:"6"}],["line",{x1:"6",x2:"6.01",y1:"18",y2:"18"}]],z$=[["path",{d:"M14 17H5"}],["path",{d:"M19 7h-9"}],["circle",{cx:"17",cy:"17",r:"3"}],["circle",{cx:"7",cy:"7",r:"3"}]],F$=[["path",{d:"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"}],["circle",{cx:"12",cy:"12",r:"3"}]],D$=[["path",{d:"M8.3 10a.7.7 0 0 1-.626-1.079L11.4 3a.7.7 0 0 1 1.198-.043L16.3 8.9a.7.7 0 0 1-.572 1.1Z"}],["rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}],["circle",{cx:"17.5",cy:"17.5",r:"3.5"}]],b$=[["circle",{cx:"18",cy:"5",r:"3"}],["circle",{cx:"6",cy:"12",r:"3"}],["circle",{cx:"18",cy:"19",r:"3"}],["line",{x1:"8.59",x2:"15.42",y1:"13.51",y2:"17.49"}],["line",{x1:"15.41",x2:"8.59",y1:"6.51",y2:"10.49"}]],R$=[["path",{d:"M12 2v13"}],["path",{d:"m16 6-4-4-4 4"}],["path",{d:"M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"}]],T$=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["line",{x1:"3",x2:"21",y1:"9",y2:"9"}],["line",{x1:"3",x2:"21",y1:"15",y2:"15"}],["line",{x1:"9",x2:"9",y1:"9",y2:"21"}],["line",{x1:"15",x2:"15",y1:"9",y2:"21"}]],q$=[["path",{d:"M14 11a2 2 0 1 1-4 0 4 4 0 0 1 8 0 6 6 0 0 1-12 0 8 8 0 0 1 16 0 10 10 0 1 1-20 0 11.93 11.93 0 0 1 2.42-7.22 2 2 0 1 1 3.16 2.44"}]],U$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M12 8v4"}],["path",{d:"M12 16h.01"}]],O$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"m4.243 5.21 14.39 12.472"}]],Z$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"m9 12 2 2 4-4"}]],G$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M8 12h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M16 12h.01"}]],W$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M12 22V2"}]],I$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M9 12h6"}]],E$=[["path",{d:"m2 2 20 20"}],["path",{d:"M5 5a1 1 0 0 0-1 1v7c0 5 3.5 7.5 7.67 8.94a1 1 0 0 0 .67.01c2.35-.82 4.48-1.97 5.9-3.71"}],["path",{d:"M9.309 3.652A12.252 12.252 0 0 0 11.24 2.28a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1v7a9.784 9.784 0 0 1-.08 1.264"}]],X$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M9 12h6"}],["path",{d:"M12 9v6"}]],x2=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M9.1 9a3 3 0 0 1 5.82 1c0 2-3 3-3 3"}],["path",{d:"M12 17h.01"}]],j$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M6.376 18.91a6 6 0 0 1 11.249.003"}],["circle",{cx:"12",cy:"11",r:"4"}]],a0=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"m14.5 9.5-5 5"}],["path",{d:"m9.5 9.5 5 5"}]],N$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}]],K$=[["circle",{cx:"12",cy:"12",r:"8"}],["path",{d:"M12 2v7.5"}],["path",{d:"m19 5-5.23 5.23"}],["path",{d:"M22 12h-7.5"}],["path",{d:"m19 19-5.23-5.23"}],["path",{d:"M12 14.5V22"}],["path",{d:"M10.23 13.77 5 19"}],["path",{d:"M9.5 12H2"}],["path",{d:"M10.23 10.23 5 5"}],["circle",{cx:"12",cy:"12",r:"2.5"}]],Q$=[["path",{d:"M12 10.189V14"}],["path",{d:"M12 2v3"}],["path",{d:"M19 13V7a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v6"}],["path",{d:"M19.38 20A11.6 11.6 0 0 0 21 14l-8.188-3.639a2 2 0 0 0-1.624 0L3 14a11.6 11.6 0 0 0 2.81 7.76"}],["path",{d:"M2 21c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1s1.2 1 2.5 1c2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}]],J$=[["path",{d:"M20.38 3.46 16 2a4 4 0 0 1-8 0L3.62 3.46a2 2 0 0 0-1.34 2.23l.58 3.47a1 1 0 0 0 .99.84H6v10c0 1.1.9 2 2 2h8a2 2 0 0 0 2-2V10h2.15a1 1 0 0 0 .99-.84l.58-3.47a2 2 0 0 0-1.34-2.23z"}]],Y$=[["path",{d:"M16 10a4 4 0 0 1-8 0"}],["path",{d:"M3.103 6.034h17.794"}],["path",{d:"M3.4 5.467a2 2 0 0 0-.4 1.2V20a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.667a2 2 0 0 0-.4-1.2l-2-2.667A2 2 0 0 0 17 2H7a2 2 0 0 0-1.6.8z"}]],_$=[["path",{d:"m15 11-1 9"}],["path",{d:"m19 11-4-7"}],["path",{d:"M2 11h20"}],["path",{d:"m3.5 11 1.6 7.4a2 2 0 0 0 2 1.6h9.8a2 2 0 0 0 2-1.6l1.7-7.4"}],["path",{d:"M4.5 15.5h15"}],["path",{d:"m5 11 4-7"}],["path",{d:"m9 11 1 9"}]],x$=[["circle",{cx:"8",cy:"21",r:"1"}],["circle",{cx:"19",cy:"21",r:"1"}],["path",{d:"M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"}]],am=[["path",{d:"M21.56 4.56a1.5 1.5 0 0 1 0 2.122l-.47.47a3 3 0 0 1-4.212-.03 3 3 0 0 1 0-4.243l.44-.44a1.5 1.5 0 0 1 2.121 0z"}],["path",{d:"M3 22a1 1 0 0 1-1-1v-3.586a1 1 0 0 1 .293-.707l3.355-3.355a1.205 1.205 0 0 1 1.704 0l3.296 3.296a1.205 1.205 0 0 1 0 1.704l-3.355 3.355a1 1 0 0 1-.707.293z"}],["path",{d:"m9 15 7.879-7.878"}]],tm=[["path",{d:"m4 4 2.5 2.5"}],["path",{d:"M13.5 6.5a4.95 4.95 0 0 0-7 7"}],["path",{d:"M15 5 5 15"}],["path",{d:"M14 17v.01"}],["path",{d:"M10 16v.01"}],["path",{d:"M13 13v.01"}],["path",{d:"M16 10v.01"}],["path",{d:"M11 20v.01"}],["path",{d:"M17 14v.01"}],["path",{d:"M20 11v.01"}]],hm=[["path",{d:"M4 13V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v5"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M10 22v-5"}],["path",{d:"M14 19v-2"}],["path",{d:"M18 20v-3"}],["path",{d:"M2 13h20"}],["path",{d:"M6 20v-3"}]],dm=[["path",{d:"M11 12h.01"}],["path",{d:"M13 22c.5-.5 1.12-1 2.5-1-1.38 0-2-.5-2.5-1"}],["path",{d:"M14 2a3.28 3.28 0 0 1-3.227 1.798l-6.17-.561A2.387 2.387 0 1 0 4.387 8H15.5a1 1 0 0 1 0 13 1 1 0 0 0 0-5H12a7 7 0 0 1-7-7V8"}],["path",{d:"M14 8a8.5 8.5 0 0 1 0 8"}],["path",{d:"M16 16c2 0 4.5-4 4-6"}]],cm=[["path",{d:"m15 15 6 6m-6-6v4.8m0-4.8h4.8"}],["path",{d:"M9 19.8V15m0 0H4.2M9 15l-6 6"}],["path",{d:"M15 4.2V9m0 0h4.8M15 9l6-6"}],["path",{d:"M9 4.2V9m0 0H4.2M9 9 3 3"}]],Mm=[["path",{d:"M12 22v-5.172a2 2 0 0 0-.586-1.414L9.5 13.5"}],["path",{d:"M14.5 14.5 12 17"}],["path",{d:"M17 8.8A6 6 0 0 1 13.8 20H10A6.5 6.5 0 0 1 7 8a5 5 0 0 1 10 0z"}]],pm=[["path",{d:"m18 14 4 4-4 4"}],["path",{d:"m18 2 4 4-4 4"}],["path",{d:"M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22"}],["path",{d:"M2 6h1.972a4 4 0 0 1 3.6 2.2"}],["path",{d:"M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45"}]],im=[["path",{d:"M2 20h.01"}],["path",{d:"M7 20v-4"}],["path",{d:"M12 20v-8"}],["path",{d:"M17 20V8"}]],nm=[["path",{d:"M18 7V5a1 1 0 0 0-1-1H6.5a.5.5 0 0 0-.4.8l4.5 6a2 2 0 0 1 0 2.4l-4.5 6a.5.5 0 0 0 .4.8H17a1 1 0 0 0 1-1v-2"}]],lm=[["path",{d:"M2 20h.01"}],["path",{d:"M7 20v-4"}]],em=[["path",{d:"M2 20h.01"}],["path",{d:"M7 20v-4"}],["path",{d:"M12 20v-8"}]],rm=[["path",{d:"M2 20h.01"}]],om=[["path",{d:"M2 20h.01"}],["path",{d:"M7 20v-4"}],["path",{d:"M12 20v-8"}],["path",{d:"M17 20V8"}],["path",{d:"M22 4v16"}]],vm=[["path",{d:"m21 17-2.156-1.868A.5.5 0 0 0 18 15.5v.5a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1c0-2.545-3.991-3.97-8.5-4a1 1 0 0 0 0 5c4.153 0 4.745-11.295 5.708-13.5a2.5 2.5 0 1 1 3.31 3.284"}],["path",{d:"M3 21h18"}]],$m=[["path",{d:"M10 9H4L2 7l2-2h6"}],["path",{d:"M14 5h6l2 2-2 2h-6"}],["path",{d:"M10 22V4a2 2 0 1 1 4 0v18"}],["path",{d:"M8 22h8"}]],mm=[["path",{d:"M12 13v8"}],["path",{d:"M12 3v3"}],["path",{d:"M18 6a2 2 0 0 1 1.387.56l2.307 2.22a1 1 0 0 1 0 1.44l-2.307 2.22A2 2 0 0 1 18 13H6a2 2 0 0 1-1.387-.56l-2.306-2.22a1 1 0 0 1 0-1.44l2.306-2.22A2 2 0 0 1 6 6z"}]],ym=[["path",{d:"M7 18v-6a5 5 0 1 1 10 0v6"}],["path",{d:"M5 21a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-1a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2z"}],["path",{d:"M21 12h1"}],["path",{d:"M18.5 4.5 18 5"}],["path",{d:"M2 12h1"}],["path",{d:"M12 2v1"}],["path",{d:"m4.929 4.929.707.707"}],["path",{d:"M12 12v6"}]],sm=[["path",{d:"M17.971 4.285A2 2 0 0 1 21 6v12a2 2 0 0 1-3.029 1.715l-9.997-5.998a2 2 0 0 1-.003-3.432z"}],["path",{d:"M3 20V4"}]],gm=[["path",{d:"m12.5 17-.5-1-.5 1h1z"}],["path",{d:"M15 22a1 1 0 0 0 1-1v-1a2 2 0 0 0 1.56-3.25 8 8 0 1 0-11.12 0A2 2 0 0 0 8 20v1a1 1 0 0 0 1 1z"}],["circle",{cx:"15",cy:"12",r:"1"}],["circle",{cx:"9",cy:"12",r:"1"}]],Cm=[["path",{d:"M21 4v16"}],["path",{d:"M6.029 4.285A2 2 0 0 0 3 6v12a2 2 0 0 0 3.029 1.715l9.997-5.998a2 2 0 0 0 .003-3.432z"}]],um=[["rect",{width:"3",height:"8",x:"13",y:"2",rx:"1.5"}],["path",{d:"M19 8.5V10h1.5A1.5 1.5 0 1 0 19 8.5"}],["rect",{width:"3",height:"8",x:"8",y:"14",rx:"1.5"}],["path",{d:"M5 15.5V14H3.5A1.5 1.5 0 1 0 5 15.5"}],["rect",{width:"8",height:"3",x:"14",y:"13",rx:"1.5"}],["path",{d:"M15.5 19H14v1.5a1.5 1.5 0 1 0 1.5-1.5"}],["rect",{width:"8",height:"3",x:"2",y:"8",rx:"1.5"}],["path",{d:"M8.5 5H10V3.5A1.5 1.5 0 1 0 8.5 5"}]],Hm=[["path",{d:"M22 2 2 22"}]],Am=[["path",{d:"M11 16.586V19a1 1 0 0 1-1 1H2L18.37 3.63a1 1 0 1 1 3 3l-9.663 9.663a1 1 0 0 1-1.414 0L8 14"}]],wm=[["path",{d:"M10 5H3"}],["path",{d:"M12 19H3"}],["path",{d:"M14 3v4"}],["path",{d:"M16 17v4"}],["path",{d:"M21 12h-9"}],["path",{d:"M21 19h-5"}],["path",{d:"M21 5h-7"}],["path",{d:"M8 10v4"}],["path",{d:"M8 12H3"}]],t0=[["path",{d:"M10 8h4"}],["path",{d:"M12 21v-9"}],["path",{d:"M12 8V3"}],["path",{d:"M17 16h4"}],["path",{d:"M19 12V3"}],["path",{d:"M19 21v-5"}],["path",{d:"M3 14h4"}],["path",{d:"M5 10V3"}],["path",{d:"M5 21v-7"}]],Vm=[["rect",{width:"14",height:"20",x:"5",y:"2",rx:"2",ry:"2"}],["path",{d:"M12.667 8 10 12h4l-2.667 4"}]],Sm=[["rect",{width:"7",height:"12",x:"2",y:"6",rx:"1"}],["path",{d:"M13 8.32a7.43 7.43 0 0 1 0 7.36"}],["path",{d:"M16.46 6.21a11.76 11.76 0 0 1 0 11.58"}],["path",{d:"M19.91 4.1a15.91 15.91 0 0 1 .01 15.8"}]],Lm=[["rect",{width:"14",height:"20",x:"5",y:"2",rx:"2",ry:"2"}],["path",{d:"M12 18h.01"}]],fm=[["path",{d:"M22 11v1a10 10 0 1 1-9-10"}],["path",{d:"M8 14s1.5 2 4 2 4-2 4-2"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}],["path",{d:"M16 5h6"}],["path",{d:"M19 2v6"}]],km=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M8 14s1.5 2 4 2 4-2 4-2"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}]],Pm=[["path",{d:"M2 13a6 6 0 1 0 12 0 4 4 0 1 0-8 0 2 2 0 0 0 4 0"}],["circle",{cx:"10",cy:"13",r:"8"}],["path",{d:"M2 21h12c4.4 0 8-3.6 8-8V7a2 2 0 1 0-4 0v6"}],["path",{d:"M18 3 19.1 5.2"}],["path",{d:"M22 3 20.9 5.2"}]],Bm=[["path",{d:"m10 20-1.25-2.5L6 18"}],["path",{d:"M10 4 8.75 6.5 6 6"}],["path",{d:"m14 20 1.25-2.5L18 18"}],["path",{d:"m14 4 1.25 2.5L18 6"}],["path",{d:"m17 21-3-6h-4"}],["path",{d:"m17 3-3 6 1.5 3"}],["path",{d:"M2 12h6.5L10 9"}],["path",{d:"m20 10-1.5 2 1.5 2"}],["path",{d:"M22 12h-6.5L14 15"}],["path",{d:"m4 10 1.5 2L4 14"}],["path",{d:"m7 21 3-6-1.5-3"}],["path",{d:"m7 3 3 6h4"}]],zm=[["path",{d:"M10.5 2v4"}],["path",{d:"M14 2H7a2 2 0 0 0-2 2"}],["path",{d:"M19.29 14.76A6.67 6.67 0 0 1 17 11a6.6 6.6 0 0 1-2.29 3.76c-1.15.92-1.71 2.04-1.71 3.19 0 2.22 1.8 4.05 4 4.05s4-1.83 4-4.05c0-1.16-.57-2.26-1.71-3.19"}],["path",{d:"M9.607 21H6a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h7V7a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3"}]],Fm=[["path",{d:"M20 9V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v3"}],["path",{d:"M2 16a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5a2 2 0 0 0-4 0v1.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V11a2 2 0 0 0-4 0z"}],["path",{d:"M4 18v2"}],["path",{d:"M20 18v2"}],["path",{d:"M12 4v9"}]],Dm=[["path",{d:"M11 2h2"}],["path",{d:"m14.28 14-4.56 8"}],["path",{d:"m21 22-1.558-4H4.558"}],["path",{d:"M3 10v2"}],["path",{d:"M6.245 15.04A2 2 0 0 1 8 14h12a1 1 0 0 1 .864 1.505l-3.11 5.457A2 2 0 0 1 16 22H4a1 1 0 0 1-.863-1.506z"}],["path",{d:"M7 2a4 4 0 0 1-4 4"}],["path",{d:"m8.66 7.66 1.41 1.41"}]],bm=[["path",{d:"M12 21a9 9 0 0 0 9-9H3a9 9 0 0 0 9 9Z"}],["path",{d:"M7 21h10"}],["path",{d:"M19.5 12 22 6"}],["path",{d:"M16.25 3c.27.1.8.53.75 1.36-.06.83-.93 1.2-1 2.02-.05.78.34 1.24.73 1.62"}],["path",{d:"M11.25 3c.27.1.8.53.74 1.36-.05.83-.93 1.2-.98 2.02-.06.78.33 1.24.72 1.62"}],["path",{d:"M6.25 3c.27.1.8.53.75 1.36-.06.83-.93 1.2-1 2.02-.05.78.34 1.24.74 1.62"}]],Rm=[["path",{d:"M22 17v1c0 .5-.5 1-1 1H3c-.5 0-1-.5-1-1v-1"}]],Tm=[["path",{d:"M12 18v4"}],["path",{d:"M2 14.499a5.5 5.5 0 0 0 9.591 3.675.6.6 0 0 1 .818.001A5.5 5.5 0 0 0 22 14.5c0-2.29-1.5-4-3-5.5l-5.492-5.312a2 2 0 0 0-3-.02L5 8.999c-1.5 1.5-3 3.2-3 5.5"}]],qm=[["path",{d:"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"}]],h0=[["path",{d:"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"}],["path",{d:"M20 2v4"}],["path",{d:"M22 4h-4"}],["circle",{cx:"4",cy:"20",r:"2"}]],Um=[["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["path",{d:"M12 6h.01"}],["circle",{cx:"12",cy:"14",r:"4"}],["path",{d:"M12 14h.01"}]],Om=[["path",{d:"M8.8 20v-4.1l1.9.2a2.3 2.3 0 0 0 2.164-2.1V8.3A5.37 5.37 0 0 0 2 8.25c0 2.8.656 3.054 1 4.55a5.77 5.77 0 0 1 .029 2.758L2 20"}],["path",{d:"M19.8 17.8a7.5 7.5 0 0 0 .003-10.603"}],["path",{d:"M17 15a3.5 3.5 0 0 0-.025-4.975"}]],Zm=[["path",{d:"m6 16 6-12 6 12"}],["path",{d:"M8 12h8"}],["path",{d:"M4 21c1.1 0 1.1-1 2.3-1s1.1 1 2.3 1c1.1 0 1.1-1 2.3-1 1.1 0 1.1 1 2.3 1 1.1 0 1.1-1 2.3-1 1.1 0 1.1 1 2.3 1 1.1 0 1.1-1 2.3-1"}]],Gm=[["path",{d:"m6 16 6-12 6 12"}],["path",{d:"M8 12h8"}],["path",{d:"m16 20 2 2 4-4"}]],Wm=[["path",{d:"M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"}],["path",{d:"M5 17A12 12 0 0 1 17 5"}],["circle",{cx:"19",cy:"5",r:"2"}],["circle",{cx:"5",cy:"19",r:"2"}]],Im=[["circle",{cx:"19",cy:"5",r:"2"}],["circle",{cx:"5",cy:"19",r:"2"}],["path",{d:"M5 17A12 12 0 0 1 17 5"}]],Em=[["path",{d:"M16 3h5v5"}],["path",{d:"M8 3H3v5"}],["path",{d:"M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3"}],["path",{d:"m15 9 6-6"}]],Xm=[["path",{d:"M17 13.44 4.442 17.082A2 2 0 0 0 4.982 21H19a2 2 0 0 0 .558-3.921l-1.115-.32A2 2 0 0 1 17 14.837V7.66"}],["path",{d:"m7 10.56 12.558-3.642A2 2 0 0 0 19.018 3H5a2 2 0 0 0-.558 3.921l1.115.32A2 2 0 0 1 7 9.163v7.178"}]],jm=[["path",{d:"M15.295 19.562 16 22"}],["path",{d:"m17 16 3.758 2.098"}],["path",{d:"m19 12.5 3.026-.598"}],["path",{d:"M7.61 6.3a3 3 0 0 0-3.92 1.3l-1.38 2.79a3 3 0 0 0 1.3 3.91l6.89 3.597a1 1 0 0 0 1.342-.447l3.106-6.211a1 1 0 0 0-.447-1.341z"}],["path",{d:"M8 9V2"}]],Nm=[["path",{d:"M3 3h.01"}],["path",{d:"M7 5h.01"}],["path",{d:"M11 7h.01"}],["path",{d:"M3 7h.01"}],["path",{d:"M7 9h.01"}],["path",{d:"M3 11h.01"}],["rect",{width:"4",height:"4",x:"15",y:"5"}],["path",{d:"m19 9 2 2v10c0 .6-.4 1-1 1h-6c-.6 0-1-.4-1-1V11l2-2"}],["path",{d:"m13 14 8-2"}],["path",{d:"m13 19 8-2"}]],Km=[["path",{d:"M14 9.536V7a4 4 0 0 1 4-4h1.5a.5.5 0 0 1 .5.5V5a4 4 0 0 1-4 4 4 4 0 0 0-4 4c0 2 1 3 1 5a5 5 0 0 1-1 3"}],["path",{d:"M4 9a5 5 0 0 1 8 4 5 5 0 0 1-8-4"}],["path",{d:"M5 21h14"}]],d0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M17 12h-2l-2 5-2-10-2 5H7"}]],c0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m16 8-8 8"}],["path",{d:"M16 16H8V8"}]],M0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m8 8 8 8"}],["path",{d:"M16 8v8H8"}]],p0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 8v8"}],["path",{d:"m8 12 4 4 4-4"}]],i0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m12 8-4 4 4 4"}],["path",{d:"M16 12H8"}]],n0=[["path",{d:"M13 21h6a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6"}],["path",{d:"m3 21 9-9"}],["path",{d:"M9 21H3v-6"}]],l0=[["path",{d:"M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"}],["path",{d:"m21 21-9-9"}],["path",{d:"M21 15v6h-6"}]],e0=[["path",{d:"M13 3h6a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-6"}],["path",{d:"m3 3 9 9"}],["path",{d:"M3 9V3h6"}]],r0=[["path",{d:"M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6"}],["path",{d:"m21 3-9 9"}],["path",{d:"M15 3h6v6"}]],o0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 12h8"}],["path",{d:"m12 16 4-4-4-4"}]],v0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 16V8h8"}],["path",{d:"M16 16 8 8"}]],$0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 8h8v8"}],["path",{d:"m8 16 8-8"}]],m0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m16 12-4-4-4 4"}],["path",{d:"M12 16V8"}]],y0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 8v8"}],["path",{d:"m8.5 14 7-4"}],["path",{d:"m8.5 10 7 4"}]],s0=[["line",{x1:"5",y1:"3",x2:"19",y2:"3"}],["line",{x1:"3",y1:"5",x2:"3",y2:"19"}],["line",{x1:"21",y1:"5",x2:"21",y2:"19"}],["line",{x1:"9",y1:"21",x2:"10",y2:"21"}],["line",{x1:"14",y1:"21",x2:"15",y2:"21"}],["path",{d:"M 3 5 A2 2 0 0 1 5 3"}],["path",{d:"M 19 3 A2 2 0 0 1 21 5"}],["path",{d:"M 5 21 A2 2 0 0 1 3 19"}],["path",{d:"M 21 19 A2 2 0 0 1 19 21"}],["circle",{cx:"8.5",cy:"8.5",r:"1.5"}],["line",{x1:"9.56066",y1:"9.56066",x2:"12",y2:"12"}],["line",{x1:"17",y1:"17",x2:"14.82",y2:"14.82"}],["circle",{cx:"8.5",cy:"15.5",r:"1.5"}],["line",{x1:"9.56066",y1:"14.43934",x2:"17",y2:"7"}]],m=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 8h7"}],["path",{d:"M8 12h6"}],["path",{d:"M11 16h5"}]],g0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m9 12 2 2 4-4"}]],C0=[["path",{d:"M21 10.656V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h12.344"}],["path",{d:"m9 11 3 3L22 4"}]],u0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m16 10-4 4-4-4"}]],H0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m14 16-4-4 4-4"}]],A0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m10 8 4 4-4 4"}]],w0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m8 14 4-4 4 4"}]],V0=[["path",{d:"m10 9-3 3 3 3"}],["path",{d:"m14 15 3-3-3-3"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],Qm=[["path",{d:"M10 9.5 8 12l2 2.5"}],["path",{d:"M14 21h1"}],["path",{d:"m14 9.5 2 2.5-2 2.5"}],["path",{d:"M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2"}],["path",{d:"M9 21h1"}]],Jm=[["path",{d:"M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2"}],["path",{d:"M9 21h1"}],["path",{d:"M14 21h1"}]],S0=[["path",{d:"M8 7v7"}],["path",{d:"M12 7v4"}],["path",{d:"M16 7v9"}],["path",{d:"M5 3a2 2 0 0 0-2 2"}],["path",{d:"M9 3h1"}],["path",{d:"M14 3h1"}],["path",{d:"M19 3a2 2 0 0 1 2 2"}],["path",{d:"M21 9v1"}],["path",{d:"M21 14v1"}],["path",{d:"M21 19a2 2 0 0 1-2 2"}],["path",{d:"M14 21h1"}],["path",{d:"M9 21h1"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M3 14v1"}],["path",{d:"M3 9v1"}]],L0=[["path",{d:"M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"}],["path",{d:"M5 3a2 2 0 0 0-2 2"}],["path",{d:"M19 3a2 2 0 0 1 2 2"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M9 3h1"}],["path",{d:"M9 21h2"}],["path",{d:"M14 3h1"}],["path",{d:"M3 9v1"}],["path",{d:"M21 9v2"}],["path",{d:"M3 14v1"}]],Ym=[["path",{d:"M14 21h1"}],["path",{d:"M21 14v1"}],["path",{d:"M21 19a2 2 0 0 1-2 2"}],["path",{d:"M21 9v1"}],["path",{d:"M3 14v1"}],["path",{d:"M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2"}],["path",{d:"M3 9v1"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M9 21h1"}]],f0=[["path",{d:"M5 3a2 2 0 0 0-2 2"}],["path",{d:"M19 3a2 2 0 0 1 2 2"}],["path",{d:"M21 19a2 2 0 0 1-2 2"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M9 3h1"}],["path",{d:"M9 21h1"}],["path",{d:"M14 3h1"}],["path",{d:"M14 21h1"}],["path",{d:"M3 9v1"}],["path",{d:"M21 9v1"}],["path",{d:"M3 14v1"}],["path",{d:"M21 14v1"}]],k0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"16",y2:"16"}],["line",{x1:"12",x2:"12",y1:"8",y2:"8"}]],P0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"12",cy:"12",r:"1"}]],B0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 10h10"}],["path",{d:"M7 14h10"}]],z0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"}],["path",{d:"M9 11.2h5.7"}]],F0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 7v7"}],["path",{d:"M12 7v4"}],["path",{d:"M16 7v9"}]],D0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 7v10"}],["path",{d:"M11 7v10"}],["path",{d:"m15 7 2 10"}]],b0=[["path",{d:"M8 16V8.5a.5.5 0 0 1 .9-.3l2.7 3.599a.5.5 0 0 0 .8 0l2.7-3.6a.5.5 0 0 1 .9.3V16"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],R0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 8h10"}],["path",{d:"M7 12h10"}],["path",{d:"M7 16h10"}]],T0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 12h8"}]],q0=[["path",{d:"M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"}],["path",{d:"M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"}]],U0=[["path",{d:"M3.6 3.6A2 2 0 0 1 5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-.59 1.41"}],["path",{d:"M3 8.7V19a2 2 0 0 0 2 2h10.3"}],["path",{d:"m2 2 20 20"}],["path",{d:"M13 13a3 3 0 1 0 0-6H9v2"}],["path",{d:"M9 17v-2.3"}]],O0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 17V7h4a3 3 0 0 1 0 6H9"}]],i=[["path",{d:"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}],["path",{d:"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"}]],_m=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["line",{x1:"10",x2:"10",y1:"15",y2:"9"}],["line",{x1:"14",x2:"14",y1:"15",y2:"9"}]],Z0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m15 9-6 6"}],["path",{d:"M9 9h.01"}],["path",{d:"M15 15h.01"}]],G0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 7h10"}],["path",{d:"M10 7v10"}],["path",{d:"M16 17a2 2 0 0 1-2-2V7"}]],W0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 12H9.5a2.5 2.5 0 0 1 0-5H17"}],["path",{d:"M12 7v10"}],["path",{d:"M16 7v10"}]],I0=[["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}],["path",{d:"M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z"}]],E0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 12h8"}],["path",{d:"M12 8v8"}]],X0=[["path",{d:"M12 7v4"}],["path",{d:"M7.998 9.003a5 5 0 1 0 8-.005"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],xm=[["path",{d:"M7 12h2l2 5 2-10h4"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],j0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"8.5",cy:"8.5",r:"1.5"}],["line",{x1:"9.56066",y1:"9.56066",x2:"12",y2:"12"}],["line",{x1:"17",y1:"17",x2:"14.82",y2:"14.82"}],["circle",{cx:"8.5",cy:"15.5",r:"1.5"}],["line",{x1:"9.56066",y1:"14.43934",x2:"17",y2:"7"}]],ay=[["path",{d:"M21 11a8 8 0 0 0-8-8"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"}]],N0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M16 8.9V7H8l4 5-4 5h8v-1.9"}]],K0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["line",{x1:"9",x2:"15",y1:"15",y2:"9"}]],Q0=[["path",{d:"M8 19H5c-1 0-2-1-2-2V7c0-1 1-2 2-2h3"}],["path",{d:"M16 5h3c1 0 2 1 2 2v10c0 1-1 2-2 2h-3"}],["line",{x1:"12",x2:"12",y1:"4",y2:"20"}]],J0=[["path",{d:"M5 8V5c0-1 1-2 2-2h10c1 0 2 1 2 2v3"}],["path",{d:"M19 16v3c0 1-1 2-2 2H7c-1 0-2-1-2-2v-3"}],["line",{x1:"4",x2:"20",y1:"12",y2:"12"}]],ty=[["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}],["rect",{x:"8",y:"8",width:"8",height:"8",rx:"1"}]],hy=[["path",{d:"M4 10c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2"}],["path",{d:"M10 16c-1.1 0-2-.9-2-2v-4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2"}],["rect",{width:"8",height:"8",x:"14",y:"14",rx:"2"}]],dy=[["path",{d:"M11.035 7.69a1 1 0 0 1 1.909.024l.737 1.452a1 1 0 0 0 .737.535l1.634.256a1 1 0 0 1 .588 1.806l-1.172 1.168a1 1 0 0 0-.282.866l.259 1.613a1 1 0 0 1-1.541 1.134l-1.465-.75a1 1 0 0 0-.912 0l-1.465.75a1 1 0 0 1-1.539-1.133l.258-1.613a1 1 0 0 0-.282-.866l-1.156-1.153a1 1 0 0 1 .572-1.822l1.633-.256a1 1 0 0 0 .737-.535z"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],cy=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["rect",{x:"9",y:"9",width:"6",height:"6",rx:"1"}]],Y0=[["path",{d:"m7 11 2-2-2-2"}],["path",{d:"M11 13h4"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}]],_0=[["path",{d:"M18 21a6 6 0 0 0-12 0"}],["circle",{cx:"12",cy:"11",r:"4"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}]],x0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M7 21v-2a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2"}]],aa=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"m15 9-6 6"}],["path",{d:"m9 9 6 6"}]],My=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}]],py=[["path",{d:"M16 12v2a2 2 0 0 1-2 2H9a1 1 0 0 0-1 1v3a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2h0"}],["path",{d:"M4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v3a1 1 0 0 1-1 1h-5a2 2 0 0 0-2 2v2"}]],iy=[["path",{d:"M10 22a2 2 0 0 1-2-2"}],["path",{d:"M14 2a2 2 0 0 1 2 2"}],["path",{d:"M16 22h-2"}],["path",{d:"M2 10V8"}],["path",{d:"M2 4a2 2 0 0 1 2-2"}],["path",{d:"M20 8a2 2 0 0 1 2 2"}],["path",{d:"M22 14v2"}],["path",{d:"M22 20a2 2 0 0 1-2 2"}],["path",{d:"M4 16a2 2 0 0 1-2-2"}],["path",{d:"M8 10a2 2 0 0 1 2-2h5a1 1 0 0 1 1 1v5a2 2 0 0 1-2 2H9a1 1 0 0 1-1-1z"}],["path",{d:"M8 2h2"}]],ny=[["path",{d:"M10 22a2 2 0 0 1-2-2"}],["path",{d:"M16 22h-2"}],["path",{d:"M16 4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h3a1 1 0 0 0 1-1v-5a2 2 0 0 1 2-2h5a1 1 0 0 0 1-1z"}],["path",{d:"M20 8a2 2 0 0 1 2 2"}],["path",{d:"M22 14v2"}],["path",{d:"M22 20a2 2 0 0 1-2 2"}]],ly=[["path",{d:"M13.77 3.043a34 34 0 0 0-3.54 0"}],["path",{d:"M13.771 20.956a33 33 0 0 1-3.541.001"}],["path",{d:"M20.18 17.74c-.51 1.15-1.29 1.93-2.439 2.44"}],["path",{d:"M20.18 6.259c-.51-1.148-1.291-1.929-2.44-2.438"}],["path",{d:"M20.957 10.23a33 33 0 0 1 0 3.54"}],["path",{d:"M3.043 10.23a34 34 0 0 0 .001 3.541"}],["path",{d:"M6.26 20.179c-1.15-.508-1.93-1.29-2.44-2.438"}],["path",{d:"M6.26 3.82c-1.149.51-1.93 1.291-2.44 2.44"}]],ey=[["path",{d:"M4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v3a1 1 0 0 0 1 1h3a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2v-3a1 1 0 0 0-1-1z"}]],ry=[["path",{d:"M12 3c7.2 0 9 1.8 9 9s-1.8 9-9 9-9-1.8-9-9 1.8-9 9-9"}]],oy=[["path",{d:"M15.236 22a3 3 0 0 0-2.2-5"}],["path",{d:"M16 20a3 3 0 0 1 3-3h1a2 2 0 0 0 2-2v-2a4 4 0 0 0-4-4V4"}],["path",{d:"M18 13h.01"}],["path",{d:"M18 6a4 4 0 0 0-4 4 7 7 0 0 0-7 7c0-5 4-5 4-10.5a4.5 4.5 0 1 0-9 0 2.5 2.5 0 0 0 5 0C7 10 3 11 3 17c0 2.8 2.2 5 5 5h10"}]],vy=[["path",{d:"M14 13V8.5C14 7 15 7 15 5a3 3 0 0 0-6 0c0 2 1 2 1 3.5V13"}],["path",{d:"M20 15.5a2.5 2.5 0 0 0-2.5-2.5h-11A2.5 2.5 0 0 0 4 15.5V17a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1z"}],["path",{d:"M5 22h14"}]],$y=[["path",{d:"M8.34 8.34 2 9.27l5 4.87L5.82 21 12 17.77 18.18 21l-.59-3.43"}],["path",{d:"M18.42 12.76 22 9.27l-6.91-1L12 2l-1.44 2.91"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],my=[["path",{d:"M12 18.338a2.1 2.1 0 0 0-.987.244L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.12 2.12 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.12 2.12 0 0 0 1.597-1.16l2.309-4.679A.53.53 0 0 1 12 2"}]],yy=[["path",{d:"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"}]],sy=[["path",{d:"M13.971 4.285A2 2 0 0 1 17 6v12a2 2 0 0 1-3.029 1.715l-9.997-5.998a2 2 0 0 1-.003-3.432z"}],["path",{d:"M21 20V4"}]],gy=[["path",{d:"M10.029 4.285A2 2 0 0 0 7 6v12a2 2 0 0 0 3.029 1.715l9.997-5.998a2 2 0 0 0 .003-3.432z"}],["path",{d:"M3 4v16"}]],Cy=[["path",{d:"M11 2v2"}],["path",{d:"M5 2v2"}],["path",{d:"M5 3H4a2 2 0 0 0-2 2v4a6 6 0 0 0 12 0V5a2 2 0 0 0-2-2h-1"}],["path",{d:"M8 15a6 6 0 0 0 12 0v-3"}],["circle",{cx:"20",cy:"10",r:"2"}]],uy=[["path",{d:"M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z"}],["path",{d:"M15 3v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 13h.01"}],["path",{d:"M16 13h.01"}],["path",{d:"M10 16s.8 1 2 1c1.3 0 2-1 2-1"}]],Hy=[["path",{d:"M11.264 2.205A4 4 0 0 0 6.42 4.211l-4 8a4 4 0 0 0 1.359 5.117l6 4a4 4 0 0 0 4.438 0l6-4a4 4 0 0 0 1.576-4.592l-2-6a4 4 0 0 0-2.53-2.53z"}],["path",{d:"M11.99 22 14 12l7.822 3.184"}],["path",{d:"M14 12 8.47 2.302"}]],Ay=[["path",{d:"M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z"}],["path",{d:"M15 3v5a1 1 0 0 0 1 1h5"}]],wy=[["path",{d:"M15 21v-5a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v5"}],["path",{d:"M17.774 10.31a1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.451 0 1.12 1.12 0 0 0-1.548 0 2.5 2.5 0 0 1-3.452 0 1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.77-3.248l2.889-4.184A2 2 0 0 1 7 2h10a2 2 0 0 1 1.653.873l2.895 4.192a2.5 2.5 0 0 1-3.774 3.244"}],["path",{d:"M4 10.95V19a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8.05"}]],Vy=[["rect",{width:"20",height:"6",x:"2",y:"4",rx:"2"}],["rect",{width:"20",height:"6",x:"2",y:"14",rx:"2"}]],Sy=[["rect",{width:"6",height:"20",x:"4",y:"2",rx:"2"}],["rect",{width:"6",height:"20",x:"14",y:"2",rx:"2"}]],Ly=[["path",{d:"M16 4H9a3 3 0 0 0-2.83 4"}],["path",{d:"M14 12a4 4 0 0 1 0 8H6"}],["line",{x1:"4",x2:"20",y1:"12",y2:"12"}]],fy=[["path",{d:"m4 5 8 8"}],["path",{d:"m12 5-8 8"}],["path",{d:"M20 19h-4c0-1.5.44-2 1.5-2.5S20 15.33 20 14c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07"}]],ky=[["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M12 4h.01"}],["path",{d:"M20 12h.01"}],["path",{d:"M12 20h.01"}],["path",{d:"M4 12h.01"}],["path",{d:"M17.657 6.343h.01"}],["path",{d:"M17.657 17.657h.01"}],["path",{d:"M6.343 17.657h.01"}],["path",{d:"M6.343 6.343h.01"}]],Py=[["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M12 3v1"}],["path",{d:"M12 20v1"}],["path",{d:"M3 12h1"}],["path",{d:"M20 12h1"}],["path",{d:"m18.364 5.636-.707.707"}],["path",{d:"m6.343 17.657-.707.707"}],["path",{d:"m5.636 5.636.707.707"}],["path",{d:"m17.657 17.657.707.707"}]],By=[["path",{d:"M12 2v2"}],["path",{d:"M14.837 16.385a6 6 0 1 1-7.223-7.222c.624-.147.97.66.715 1.248a4 4 0 0 0 5.26 5.259c.589-.255 1.396.09 1.248.715"}],["path",{d:"M16 12a4 4 0 0 0-4-4"}],["path",{d:"m19 5-1.256 1.256"}],["path",{d:"M20 12h2"}]],zy=[["path",{d:"M10 21v-1"}],["path",{d:"M10 4V3"}],["path",{d:"M10 9a3 3 0 0 0 0 6"}],["path",{d:"m14 20 1.25-2.5L18 18"}],["path",{d:"m14 4 1.25 2.5L18 6"}],["path",{d:"m17 21-3-6 1.5-3H22"}],["path",{d:"m17 3-3 6 1.5 3"}],["path",{d:"M2 12h1"}],["path",{d:"m20 10-1.5 2 1.5 2"}],["path",{d:"m3.64 18.36.7-.7"}],["path",{d:"m4.34 6.34-.7-.7"}]],Fy=[["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M12 2v2"}],["path",{d:"M12 20v2"}],["path",{d:"m4.93 4.93 1.41 1.41"}],["path",{d:"m17.66 17.66 1.41 1.41"}],["path",{d:"M2 12h2"}],["path",{d:"M20 12h2"}],["path",{d:"m6.34 17.66-1.41 1.41"}],["path",{d:"m19.07 4.93-1.41 1.41"}]],Dy=[["path",{d:"M12 2v8"}],["path",{d:"m4.93 10.93 1.41 1.41"}],["path",{d:"M2 18h2"}],["path",{d:"M20 18h2"}],["path",{d:"m19.07 10.93-1.41 1.41"}],["path",{d:"M22 22H2"}],["path",{d:"m8 6 4-4 4 4"}],["path",{d:"M16 18a4 4 0 0 0-8 0"}]],by=[["path",{d:"M12 10V2"}],["path",{d:"m4.93 10.93 1.41 1.41"}],["path",{d:"M2 18h2"}],["path",{d:"M20 18h2"}],["path",{d:"m19.07 10.93-1.41 1.41"}],["path",{d:"M22 22H2"}],["path",{d:"m16 6-4 4-4-4"}],["path",{d:"M16 18a4 4 0 0 0-8 0"}]],Ry=[["path",{d:"m4 19 8-8"}],["path",{d:"m12 19-8-8"}],["path",{d:"M20 12h-4c0-1.5.442-2 1.5-2.5S20 8.334 20 7.002c0-.472-.17-.93-.484-1.29a2.105 2.105 0 0 0-2.617-.436c-.42.239-.738.614-.899 1.06"}]],Ty=[["path",{d:"M11 17a4 4 0 0 1-8 0V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2Z"}],["path",{d:"M16.7 13H19a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H7"}],["path",{d:"M 7 17h.01"}],["path",{d:"m11 8 2.3-2.3a2.4 2.4 0 0 1 3.404.004L18.6 7.6a2.4 2.4 0 0 1 .026 3.434L9.9 19.8"}]],qy=[["path",{d:"M10 21V3h8"}],["path",{d:"M6 16h9"}],["path",{d:"M10 9.5h7"}]],Uy=[["path",{d:"M11 19H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5"}],["path",{d:"M13 5h7a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-5"}],["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"m18 22-3-3 3-3"}],["path",{d:"m6 2 3 3-3 3"}]],Oy=[["path",{d:"m11 19-6-6"}],["path",{d:"m5 21-2-2"}],["path",{d:"m8 16-4 4"}],["path",{d:"M9.5 17.5 21 6V3h-3L6.5 14.5"}]],Zy=[["polyline",{points:"14.5 17.5 3 6 3 3 6 3 17.5 14.5"}],["line",{x1:"13",x2:"19",y1:"19",y2:"13"}],["line",{x1:"16",x2:"20",y1:"16",y2:"20"}],["line",{x1:"19",x2:"21",y1:"21",y2:"19"}],["polyline",{points:"14.5 6.5 18 3 21 3 21 6 17.5 9.5"}],["line",{x1:"5",x2:"9",y1:"14",y2:"18"}],["line",{x1:"7",x2:"4",y1:"17",y2:"20"}],["line",{x1:"3",x2:"5",y1:"19",y2:"21"}]],Gy=[["path",{d:"m18 2 4 4"}],["path",{d:"m17 7 3-3"}],["path",{d:"M19 9 8.7 19.3c-1 1-2.5 1-3.4 0l-.6-.6c-1-1-1-2.5 0-3.4L15 5"}],["path",{d:"m9 11 4 4"}],["path",{d:"m5 19-3 3"}],["path",{d:"m14 4 6 6"}]],Wy=[["path",{d:"M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"}]],Iy=[["path",{d:"M12 21v-6"}],["path",{d:"M12 9V3"}],["path",{d:"M3 15h18"}],["path",{d:"M3 9h18"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}]],Ey=[["path",{d:"M12 15V9"}],["path",{d:"M3 15h18"}],["path",{d:"M3 9h18"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}]],Xy=[["path",{d:"M16 5H3"}],["path",{d:"M16 12H3"}],["path",{d:"M16 19H3"}],["path",{d:"M21 5h.01"}],["path",{d:"M21 12h.01"}],["path",{d:"M21 19h.01"}]],jy=[["path",{d:"M14 14v2"}],["path",{d:"M14 20v2"}],["path",{d:"M14 2v2"}],["path",{d:"M14 8v2"}],["path",{d:"M2 15h8"}],["path",{d:"M2 3h6a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H2"}],["path",{d:"M2 9h8"}],["path",{d:"M22 15h-4"}],["path",{d:"M22 3h-2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h2"}],["path",{d:"M22 9h-4"}],["path",{d:"M5 3v18"}]],Ny=[["path",{d:"M15 3v18"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M21 9H3"}],["path",{d:"M21 15H3"}]],Ky=[["path",{d:"M14 10h2"}],["path",{d:"M15 22v-8"}],["path",{d:"M15 2v4"}],["path",{d:"M2 10h2"}],["path",{d:"M20 10h2"}],["path",{d:"M3 19h18"}],["path",{d:"M3 22v-6a2 2 135 0 1 2-2h14a2 2 45 0 1 2 2v6"}],["path",{d:"M3 2v2a2 2 45 0 0 2 2h14a2 2 135 0 0 2-2V2"}],["path",{d:"M8 10h2"}],["path",{d:"M9 22v-8"}],["path",{d:"M9 2v4"}]],Qy=[["path",{d:"M12 3v18"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"M3 15h18"}]],Jy=[["rect",{width:"10",height:"14",x:"3",y:"8",rx:"2"}],["path",{d:"M5 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2h-2.4"}],["path",{d:"M8 18h.01"}]],Yy=[["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2",ry:"2"}],["line",{x1:"12",x2:"12.01",y1:"18",y2:"18"}]],_y=[["circle",{cx:"7",cy:"7",r:"5"}],["circle",{cx:"17",cy:"17",r:"5"}],["path",{d:"M12 17h10"}],["path",{d:"m3.46 10.54 7.08-7.08"}]],xy=[["path",{d:"M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"}],["circle",{cx:"7.5",cy:"7.5",r:".5",fill:"currentColor"}]],as=[["path",{d:"M13.172 2a2 2 0 0 1 1.414.586l6.71 6.71a2.4 2.4 0 0 1 0 3.408l-4.592 4.592a2.4 2.4 0 0 1-3.408 0l-6.71-6.71A2 2 0 0 1 6 9.172V3a1 1 0 0 1 1-1z"}],["path",{d:"M2 7v6.172a2 2 0 0 0 .586 1.414l6.71 6.71a2.4 2.4 0 0 0 3.191.193"}],["circle",{cx:"10.5",cy:"6.5",r:".5",fill:"currentColor"}]],ts=[["path",{d:"M4 4v16"}]],hs=[["path",{d:"M4 4v16"}],["path",{d:"M9 4v16"}]],ds=[["path",{d:"M4 4v16"}],["path",{d:"M9 4v16"}],["path",{d:"M14 4v16"}]],cs=[["path",{d:"M4 4v16"}],["path",{d:"M9 4v16"}],["path",{d:"M14 4v16"}],["path",{d:"M19 4v16"}]],Ms=[["path",{d:"M4 4v16"}],["path",{d:"M9 4v16"}],["path",{d:"M14 4v16"}],["path",{d:"M19 4v16"}],["path",{d:"M22 6 2 18"}]],ps=[["circle",{cx:"17",cy:"4",r:"2"}],["path",{d:"M15.59 5.41 5.41 15.59"}],["circle",{cx:"4",cy:"17",r:"2"}],["path",{d:"M12 22s-4-9-1.5-11.5S22 12 22 12"}]],is=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"6"}],["circle",{cx:"12",cy:"12",r:"2"}]],ns=[["path",{d:"m10.065 12.493-6.18 1.318a.934.934 0 0 1-1.108-.702l-.537-2.15a1.07 1.07 0 0 1 .691-1.265l13.504-4.44"}],["path",{d:"m13.56 11.747 4.332-.924"}],["path",{d:"m16 21-3.105-6.21"}],["path",{d:"M16.485 5.94a2 2 0 0 1 1.455-2.425l1.09-.272a1 1 0 0 1 1.212.727l1.515 6.06a1 1 0 0 1-.727 1.213l-1.09.272a2 2 0 0 1-2.425-1.455z"}],["path",{d:"m6.158 8.633 1.114 4.456"}],["path",{d:"m8 21 3.105-6.21"}],["circle",{cx:"12",cy:"13",r:"2"}]],ls=[["circle",{cx:"4",cy:"4",r:"2"}],["path",{d:"m14 5 3-3 3 3"}],["path",{d:"m14 10 3-3 3 3"}],["path",{d:"M17 14V2"}],["path",{d:"M17 14H7l-5 8h20Z"}],["path",{d:"M8 14v8"}],["path",{d:"m9 14 5 8"}]],es=[["path",{d:"M3.5 21 14 3"}],["path",{d:"M20.5 21 10 3"}],["path",{d:"M15.5 21 12 15l-3.5 6"}],["path",{d:"M2 21h20"}]],ta=[["path",{d:"M21 7 6.82 21.18a2.83 2.83 0 0 1-3.99-.01a2.83 2.83 0 0 1 0-4L17 3"}],["path",{d:"m16 2 6 6"}],["path",{d:"M12 16H4"}]],rs=[["path",{d:"M12 19h8"}],["path",{d:"m4 17 6-6-6-6"}]],os=[["path",{d:"M14.5 2v17.5c0 1.4-1.1 2.5-2.5 2.5c-1.4 0-2.5-1.1-2.5-2.5V2"}],["path",{d:"M8.5 2h7"}],["path",{d:"M14.5 16h-5"}]],vs=[["path",{d:"M9 2v17.5A2.5 2.5 0 0 1 6.5 22A2.5 2.5 0 0 1 4 19.5V2"}],["path",{d:"M20 2v17.5a2.5 2.5 0 0 1-2.5 2.5a2.5 2.5 0 0 1-2.5-2.5V2"}],["path",{d:"M3 2h7"}],["path",{d:"M14 2h7"}],["path",{d:"M9 16H4"}],["path",{d:"M20 16h-5"}]],ha=[["path",{d:"M21 5H3"}],["path",{d:"M17 12H7"}],["path",{d:"M19 19H5"}]],da=[["path",{d:"M21 5H3"}],["path",{d:"M21 12H9"}],["path",{d:"M21 19H7"}]],ca=[["path",{d:"M3 5h18"}],["path",{d:"M3 12h18"}],["path",{d:"M3 19h18"}]],y=[["path",{d:"M21 5H3"}],["path",{d:"M15 12H3"}],["path",{d:"M17 19H3"}]],$s=[["path",{d:"M12 20h-1a2 2 0 0 1-2-2 2 2 0 0 1-2 2H6"}],["path",{d:"M13 8h7a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-7"}],["path",{d:"M5 16H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h1"}],["path",{d:"M6 4h1a2 2 0 0 1 2 2 2 2 0 0 1 2-2h1"}],["path",{d:"M9 6v12"}]],ms=[["path",{d:"M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1"}],["path",{d:"M7 22h1a4 4 0 0 0 4-4v-1"}],["path",{d:"M7 2h1a4 4 0 0 1 4 4v1"}]],ys=[["path",{d:"M17 5H3"}],["path",{d:"M21 12H8"}],["path",{d:"M21 19H8"}],["path",{d:"M3 12v7"}]],Ma=[["path",{d:"M15 5h6"}],["path",{d:"M15 12h6"}],["path",{d:"M3 19h18"}],["path",{d:"m3 12 3.553-7.724a.5.5 0 0 1 .894 0L11 12"}],["path",{d:"M3.92 10h6.16"}]],ss=[["path",{d:"M21 5H3"}],["path",{d:"M10 12H3"}],["path",{d:"M10 19H3"}],["circle",{cx:"17",cy:"15",r:"3"}],["path",{d:"m21 19-1.9-1.9"}]],pa=[["path",{d:"M14 21h1"}],["path",{d:"M14 3h1"}],["path",{d:"M19 3a2 2 0 0 1 2 2"}],["path",{d:"M21 14v1"}],["path",{d:"M21 19a2 2 0 0 1-2 2"}],["path",{d:"M21 9v1"}],["path",{d:"M3 14v1"}],["path",{d:"M3 9v1"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M5 3a2 2 0 0 0-2 2"}],["path",{d:"M7 12h10"}],["path",{d:"M7 16h6"}],["path",{d:"M7 8h8"}],["path",{d:"M9 21h1"}],["path",{d:"M9 3h1"}]],gs=[["path",{d:"M2 10s3-3 3-8"}],["path",{d:"M22 10s-3-3-3-8"}],["path",{d:"M10 2c0 4.4-3.6 8-8 8"}],["path",{d:"M14 2c0 4.4 3.6 8 8 8"}],["path",{d:"M2 10s2 2 2 5"}],["path",{d:"M22 10s-2 2-2 5"}],["path",{d:"M8 15h8"}],["path",{d:"M2 22v-1a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1"}],["path",{d:"M14 22v-1a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1"}]],ia=[["path",{d:"m16 16-3 3 3 3"}],["path",{d:"M3 12h14.5a1 1 0 0 1 0 7H13"}],["path",{d:"M3 19h6"}],["path",{d:"M3 5h18"}]],Cs=[["path",{d:"m10 20-1.25-2.5L6 18"}],["path",{d:"M10 4 8.75 6.5 6 6"}],["path",{d:"M10.585 15H10"}],["path",{d:"M2 12h6.5L10 9"}],["path",{d:"M20 14.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0z"}],["path",{d:"m4 10 1.5 2L4 14"}],["path",{d:"m7 21 3-6-1.5-3"}],["path",{d:"m7 3 3 6h2"}]],us=[["path",{d:"M12 2v2"}],["path",{d:"M12 8a4 4 0 0 0-1.645 7.647"}],["path",{d:"M2 12h2"}],["path",{d:"M20 14.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0z"}],["path",{d:"m4.93 4.93 1.41 1.41"}],["path",{d:"m6.34 17.66-1.41 1.41"}]],Hs=[["path",{d:"M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"}]],As=[["path",{d:"M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"}],["path",{d:"M17 14V2"}]],ws=[["path",{d:"M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"}],["path",{d:"M7 10v12"}]],Vs=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"m9 12 2 2 4-4"}]],Ss=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"M9 12h6"}]],Ls=[["path",{d:"M2 9a3 3 0 1 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 1 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"M9 9h.01"}],["path",{d:"m15 9-6 6"}],["path",{d:"M15 15h.01"}]],fs=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"M9 12h6"}],["path",{d:"M12 9v6"}]],ks=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"m9.5 14.5 5-5"}]],Ps=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"m9.5 14.5 5-5"}],["path",{d:"m9.5 9.5 5 5"}]],Bs=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"M13 5v2"}],["path",{d:"M13 17v2"}],["path",{d:"M13 11v2"}]],zs=[["path",{d:"M10.5 17h1.227a2 2 0 0 0 1.345-.52L18 12"}],["path",{d:"m12 13.5 3.75.5"}],["path",{d:"m3.173 8.18 11-5a2 2 0 0 1 2.647.993L18.56 8"}],["path",{d:"M6 10V8"}],["path",{d:"M6 14v1"}],["path",{d:"M6 19v2"}],["rect",{x:"2",y:"8",width:"20",height:"13",rx:"2"}]],Fs=[["path",{d:"m3.173 8.18 11-5a2 2 0 0 1 2.647.993L18.56 8"}],["path",{d:"M6 10V8"}],["path",{d:"M6 14v1"}],["path",{d:"M6 19v2"}],["rect",{x:"2",y:"8",width:"20",height:"13",rx:"2"}]],Ds=[["path",{d:"M10 2h4"}],["path",{d:"M12 14v-4"}],["path",{d:"M4 13a8 8 0 0 1 8-7 8 8 0 1 1-5.3 14L4 17.6"}],["path",{d:"M9 17H4v5"}]],bs=[["path",{d:"M10 2h4"}],["path",{d:"M4.6 11a8 8 0 0 0 1.7 8.7 8 8 0 0 0 8.7 1.7"}],["path",{d:"M7.4 7.4a8 8 0 0 1 10.3 1 8 8 0 0 1 .9 10.2"}],["path",{d:"m2 2 20 20"}],["path",{d:"M12 12v-2"}]],Rs=[["line",{x1:"10",x2:"14",y1:"2",y2:"2"}],["line",{x1:"12",x2:"15",y1:"14",y2:"11"}],["circle",{cx:"12",cy:"14",r:"8"}]],Ts=[["circle",{cx:"9",cy:"12",r:"3"}],["rect",{width:"20",height:"14",x:"2",y:"5",rx:"7"}]],qs=[["circle",{cx:"15",cy:"12",r:"3"}],["rect",{width:"20",height:"14",x:"2",y:"5",rx:"7"}]],Us=[["path",{d:"M7 12h13a1 1 0 0 1 1 1 5 5 0 0 1-5 5h-.598a.5.5 0 0 0-.424.765l1.544 2.47a.5.5 0 0 1-.424.765H5.402a.5.5 0 0 1-.424-.765L7 18"}],["path",{d:"M8 18a5 5 0 0 1-5-5V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8"}]],Os=[["path",{d:"M10 15h4"}],["path",{d:"m14.817 10.995-.971-1.45 1.034-1.232a2 2 0 0 0-2.025-3.238l-1.82.364L9.91 3.885a2 2 0 0 0-3.625.748L6.141 6.55l-1.725.426a2 2 0 0 0-.19 3.756l.657.27"}],["path",{d:"m18.822 10.995 2.26-5.38a1 1 0 0 0-.557-1.318L16.954 2.9a1 1 0 0 0-1.281.533l-.924 2.122"}],["path",{d:"M4 12.006A1 1 0 0 1 4.994 11H19a1 1 0 0 1 1 1v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"}]],Zs=[["path",{d:"M16 12v4"}],["path",{d:"M16 6a2 2 0 0 1 1.414.586l4 4A2 2 0 0 1 22 12v7a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 .586-1.414l4-4A2 2 0 0 1 8 6z"}],["path",{d:"M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"}],["path",{d:"M2 14h20"}],["path",{d:"M8 12v4"}]],Gs=[["path",{d:"M21 4H3"}],["path",{d:"M18 8H6"}],["path",{d:"M19 12H9"}],["path",{d:"M16 16h-6"}],["path",{d:"M11 20H9"}]],Ws=[["ellipse",{cx:"12",cy:"11",rx:"3",ry:"2"}],["ellipse",{cx:"12",cy:"12.5",rx:"10",ry:"8.5"}]],Is=[["path",{d:"M12 20v-6"}],["path",{d:"M19.656 14H22"}],["path",{d:"M2 14h12"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20 20H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"}],["path",{d:"M9.656 4H20a2 2 0 0 1 2 2v10.344"}]],Es=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M2 14h20"}],["path",{d:"M12 20v-6"}]],Xs=[["path",{d:"M18.2 12.27 20 6H4l1.8 6.27a1 1 0 0 0 .95.73h10.5a1 1 0 0 0 .96-.73Z"}],["path",{d:"M8 13v9"}],["path",{d:"M16 22v-9"}],["path",{d:"m9 6 1 7"}],["path",{d:"m15 6-1 7"}],["path",{d:"M12 6V2"}],["path",{d:"M13 2h-2"}]],js=[["rect",{width:"18",height:"12",x:"3",y:"8",rx:"1"}],["path",{d:"M10 8V5c0-.6-.4-1-1-1H6a1 1 0 0 0-1 1v3"}],["path",{d:"M19 8V5c0-.6-.4-1-1-1h-3a1 1 0 0 0-1 1v3"}]],Ns=[["path",{d:"M16.05 10.966a5 2.5 0 0 1-8.1 0"}],["path",{d:"m16.923 14.049 4.48 2.04a1 1 0 0 1 .001 1.831l-8.574 3.9a2 2 0 0 1-1.66 0l-8.574-3.91a1 1 0 0 1 0-1.83l4.484-2.04"}],["path",{d:"M16.949 14.14a5 2.5 0 1 1-9.9 0L10.063 3.5a2 2 0 0 1 3.874 0z"}],["path",{d:"M9.194 6.57a5 2.5 0 0 0 5.61 0"}]],Ks=[["path",{d:"m10 11 11 .9a1 1 0 0 1 .8 1.1l-.665 4.158a1 1 0 0 1-.988.842H20"}],["path",{d:"M16 18h-5"}],["path",{d:"M18 5a1 1 0 0 0-1 1v5.573"}],["path",{d:"M3 4h8.129a1 1 0 0 1 .99.863L13 11.246"}],["path",{d:"M4 11V4"}],["path",{d:"M7 15h.01"}],["path",{d:"M8 10.1V4"}],["circle",{cx:"18",cy:"18",r:"2"}],["circle",{cx:"7",cy:"15",r:"5"}]],Qs=[["path",{d:"M2 22V12a10 10 0 1 1 20 0v10"}],["path",{d:"M15 6.8v1.4a3 2.8 0 1 1-6 0V6.8"}],["path",{d:"M10 15h.01"}],["path",{d:"M14 15h.01"}],["path",{d:"M10 19a4 4 0 0 1-4-4v-3a6 6 0 1 1 12 0v3a4 4 0 0 1-4 4Z"}],["path",{d:"m9 19-2 3"}],["path",{d:"m15 19 2 3"}]],Js=[["path",{d:"M8 3.1V7a4 4 0 0 0 8 0V3.1"}],["path",{d:"m9 15-1-1"}],["path",{d:"m15 15 1-1"}],["path",{d:"M9 19c-2.8 0-5-2.2-5-5v-4a8 8 0 0 1 16 0v4c0 2.8-2.2 5-5 5Z"}],["path",{d:"m8 19-2 3"}],["path",{d:"m16 19 2 3"}]],Ys=[["path",{d:"M2 17 17 2"}],["path",{d:"m2 14 8 8"}],["path",{d:"m5 11 8 8"}],["path",{d:"m8 8 8 8"}],["path",{d:"m11 5 8 8"}],["path",{d:"m14 2 8 8"}],["path",{d:"M7 22 22 7"}]],na=[["rect",{width:"16",height:"16",x:"4",y:"3",rx:"2"}],["path",{d:"M4 11h16"}],["path",{d:"M12 3v8"}],["path",{d:"m8 19-2 3"}],["path",{d:"m18 22-2-3"}],["path",{d:"M8 15h.01"}],["path",{d:"M16 15h.01"}]],_s=[["path",{d:"M12 16v6"}],["path",{d:"M14 20h-4"}],["path",{d:"M18 2h4v4"}],["path",{d:"m2 2 7.17 7.17"}],["path",{d:"M2 5.355V2h3.357"}],["path",{d:"m22 2-7.17 7.17"}],["path",{d:"M8 5 5 8"}],["circle",{cx:"12",cy:"12",r:"4"}]],xs=[["path",{d:"M10 11v6"}],["path",{d:"M14 11v6"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"}],["path",{d:"M3 6h18"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"}]],ag=[["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"}],["path",{d:"M3 6h18"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"}]],tg=[["path",{d:"M8 19a4 4 0 0 1-2.24-7.32A3.5 3.5 0 0 1 9 6.03V6a3 3 0 1 1 6 0v.04a3.5 3.5 0 0 1 3.24 5.65A4 4 0 0 1 16 19Z"}],["path",{d:"M12 19v3"}]],la=[["path",{d:"M13 8c0-2.76-2.46-5-5.5-5S2 5.24 2 8h2l1-1 1 1h4"}],["path",{d:"M13 7.14A5.82 5.82 0 0 1 16.5 6c3.04 0 5.5 2.24 5.5 5h-3l-1-1-1 1h-3"}],["path",{d:"M5.89 9.71c-2.15 2.15-2.3 5.47-.35 7.43l4.24-4.25.7-.7.71-.71 2.12-2.12c-1.95-1.96-5.27-1.8-7.42.35"}],["path",{d:"M11 15.5c.5 2.5-.17 4.5-1 6.5h4c2-5.5-.5-12-1-14"}]],hg=[["path",{d:"m17 14 3 3.3a1 1 0 0 1-.7 1.7H4.7a1 1 0 0 1-.7-1.7L7 14h-.3a1 1 0 0 1-.7-1.7L9 9h-.2A1 1 0 0 1 8 7.3L12 3l4 4.3a1 1 0 0 1-.8 1.7H15l3 3.3a1 1 0 0 1-.7 1.7H17Z"}],["path",{d:"M12 22v-3"}]],dg=[["path",{d:"M10 10v.2A3 3 0 0 1 8.9 16H5a3 3 0 0 1-1-5.8V10a3 3 0 0 1 6 0Z"}],["path",{d:"M7 16v6"}],["path",{d:"M13 19v3"}],["path",{d:"M12 19h8.3a1 1 0 0 0 .7-1.7L18 14h.3a1 1 0 0 0 .7-1.7L16 9h.2a1 1 0 0 0 .8-1.7L13 3l-1.4 1.5"}]],cg=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["rect",{width:"3",height:"9",x:"7",y:"7"}],["rect",{width:"3",height:"5",x:"14",y:"7"}]],Mg=[["path",{d:"M16 17h6v-6"}],["path",{d:"m22 17-8.5-8.5-5 5L2 7"}]],pg=[["path",{d:"M14.828 14.828 21 21"}],["path",{d:"M21 16v5h-5"}],["path",{d:"m21 3-9 9-4-4-6 6"}],["path",{d:"M21 8V3h-5"}]],ig=[["path",{d:"M16 7h6v6"}],["path",{d:"m22 7-8.5 8.5-5-5L2 17"}]],ea=[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"}],["path",{d:"M12 9v4"}],["path",{d:"M12 17h.01"}]],ng=[["path",{d:"M10.17 4.193a2 2 0 0 1 3.666.013"}],["path",{d:"M14 21h2"}],["path",{d:"m15.874 7.743 1 1.732"}],["path",{d:"m18.849 12.952 1 1.732"}],["path",{d:"M21.824 18.18a2 2 0 0 1-1.835 2.824"}],["path",{d:"M4.024 21a2 2 0 0 1-1.839-2.839"}],["path",{d:"m5.136 12.952-1 1.732"}],["path",{d:"M8 21h2"}],["path",{d:"m8.102 7.743-1 1.732"}]],lg=[["path",{d:"M22 18a2 2 0 0 1-2 2H3c-1.1 0-1.3-.6-.4-1.3L20.4 4.3c.9-.7 1.6-.4 1.6.7Z"}]],eg=[["path",{d:"M13.73 4a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"}]],rg=[["path",{d:"M10 14.66v1.626a2 2 0 0 1-.976 1.696A5 5 0 0 0 7 21.978"}],["path",{d:"M14 14.66v1.626a2 2 0 0 0 .976 1.696A5 5 0 0 1 17 21.978"}],["path",{d:"M18 9h1.5a1 1 0 0 0 0-5H18"}],["path",{d:"M4 22h16"}],["path",{d:"M6 9a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1z"}],["path",{d:"M6 9H4.5a1 1 0 0 1 0-5H6"}]],og=[["path",{d:"M14 19V7a2 2 0 0 0-2-2H9"}],["path",{d:"M15 19H9"}],["path",{d:"M19 19h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62L18.3 9.38a1 1 0 0 0-.78-.38H14"}],["path",{d:"M2 13v5a1 1 0 0 0 1 1h2"}],["path",{d:"M4 3 2.15 5.15a.495.495 0 0 0 .35.86h2.15a.47.47 0 0 1 .35.86L3 9.02"}],["circle",{cx:"17",cy:"19",r:"2"}],["circle",{cx:"7",cy:"19",r:"2"}]],vg=[["path",{d:"M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"}],["path",{d:"M15 18H9"}],["path",{d:"M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14"}],["circle",{cx:"17",cy:"18",r:"2"}],["circle",{cx:"7",cy:"18",r:"2"}]],$g=[["path",{d:"M15 4 5 9"}],["path",{d:"m15 8.5-10 5"}],["path",{d:"M18 12a9 9 0 0 1-9 9V3"}]],mg=[["path",{d:"M10 12.01h.01"}],["path",{d:"M18 8v4a8 8 0 0 1-1.07 4"}],["circle",{cx:"10",cy:"12",r:"4"}],["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2"}]],yg=[["path",{d:"m12 10 2 4v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3a8 8 0 1 0-16 0v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3l2-4h4Z"}],["path",{d:"M4.82 7.9 8 10"}],["path",{d:"M15.18 7.9 12 10"}],["path",{d:"M16.93 10H20a2 2 0 0 1 0 4H2"}]],sg=[["path",{d:"M15.033 9.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56V7.648a.645.645 0 0 1 .967-.56z"}],["path",{d:"M7 21h10"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}]],ra=[["path",{d:"M7 21h10"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}]],gg=[["path",{d:"m17 2-5 5-5-5"}],["rect",{width:"20",height:"15",x:"2",y:"7",rx:"2"}]],Cg=[["path",{d:"M21 2H3v16h5v4l4-4h5l4-4V2zm-10 9V7m5 4V7"}]],ug=[["path",{d:"M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"}]],Hg=[["path",{d:"M14 16.5a.5.5 0 0 0 .5.5h.5a2 2 0 0 1 0 4H9a2 2 0 0 1 0-4h.5a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5V8a2 2 0 0 1-4 0V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v3a2 2 0 0 1-4 0v-.5a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5Z"}]],Ag=[["path",{d:"M12 4v16"}],["path",{d:"M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2"}],["path",{d:"M9 20h6"}]],wg=[["path",{d:"M12 13v7a2 2 0 0 0 4 0"}],["path",{d:"M12 2v2"}],["path",{d:"M18.656 13h2.336a1 1 0 0 0 .97-1.274 10.284 10.284 0 0 0-12.07-7.51"}],["path",{d:"m2 2 20 20"}],["path",{d:"M5.961 5.957a10.28 10.28 0 0 0-3.922 5.769A1 1 0 0 0 3 13h10"}]],Vg=[["path",{d:"M12 13v7a2 2 0 0 0 4 0"}],["path",{d:"M12 2v2"}],["path",{d:"M20.992 13a1 1 0 0 0 .97-1.274 10.284 10.284 0 0 0-19.923 0A1 1 0 0 0 3 13z"}]],Sg=[["path",{d:"M6 4v6a6 6 0 0 0 12 0V4"}],["line",{x1:"4",x2:"20",y1:"20",y2:"20"}]],Lg=[["path",{d:"M9 14 4 9l5-5"}],["path",{d:"M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"}]],fg=[["path",{d:"M21 17a9 9 0 0 0-15-6.7L3 13"}],["path",{d:"M3 7v6h6"}],["circle",{cx:"12",cy:"17",r:"1"}]],kg=[["path",{d:"M3 7v6h6"}],["path",{d:"M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"}]],Pg=[["path",{d:"M16 12h6"}],["path",{d:"M8 12H2"}],["path",{d:"M12 2v2"}],["path",{d:"M12 8v2"}],["path",{d:"M12 14v2"}],["path",{d:"M12 20v2"}],["path",{d:"m19 15 3-3-3-3"}],["path",{d:"m5 9-3 3 3 3"}]],Bg=[["path",{d:"M12 22v-6"}],["path",{d:"M12 8V2"}],["path",{d:"M4 12H2"}],["path",{d:"M10 12H8"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}],["path",{d:"m15 19-3 3-3-3"}],["path",{d:"m15 5-3-3-3 3"}]],zg=[["rect",{width:"8",height:"6",x:"5",y:"4",rx:"1"}],["rect",{width:"8",height:"6",x:"11",y:"14",rx:"1"}]],oa=[["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"M18 12h.01"}],["path",{d:"M18 16h.01"}],["path",{d:"M22 7a1 1 0 0 0-1-1h-2a2 2 0 0 1-1.143-.359L13.143 2.36a2 2 0 0 0-2.286-.001L6.143 5.64A2 2 0 0 1 5 6H3a1 1 0 0 0-1 1v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2z"}],["path",{d:"M6 12h.01"}],["path",{d:"M6 16h.01"}],["circle",{cx:"12",cy:"10",r:"2"}]],Fg=[["path",{d:"M15 7h2a5 5 0 0 1 0 10h-2m-6 0H7A5 5 0 0 1 7 7h2"}]],Dg=[["path",{d:"m18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71"}],["path",{d:"m5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71"}],["line",{x1:"8",x2:"8",y1:"2",y2:"5"}],["line",{x1:"2",x2:"5",y1:"8",y2:"8"}],["line",{x1:"16",x2:"16",y1:"19",y2:"22"}],["line",{x1:"19",x2:"22",y1:"16",y2:"16"}]],bg=[["path",{d:"m19 5 3-3"}],["path",{d:"m2 22 3-3"}],["path",{d:"M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"}],["path",{d:"M7.5 13.5 10 11"}],["path",{d:"M10.5 16.5 13 14"}],["path",{d:"m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z"}]],Rg=[["circle",{cx:"10",cy:"7",r:"1"}],["circle",{cx:"4",cy:"20",r:"1"}],["path",{d:"M4.7 19.3 19 5"}],["path",{d:"m21 3-3 1 2 2Z"}],["path",{d:"M9.26 7.68 5 12l2 5"}],["path",{d:"m10 14 5 2 3.5-3.5"}],["path",{d:"m18 12 1-1 1 1-1 1Z"}]],Tg=[["path",{d:"M12 3v12"}],["path",{d:"m17 8-5-5-5 5"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}]],qg=[["path",{d:"m16 11 2 2 4-4"}],["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["circle",{cx:"9",cy:"7",r:"4"}]],Ug=[["path",{d:"M10 15H6a4 4 0 0 0-4 4v2"}],["path",{d:"m14.305 16.53.923-.382"}],["path",{d:"m15.228 13.852-.923-.383"}],["path",{d:"m16.852 12.228-.383-.923"}],["path",{d:"m16.852 17.772-.383.924"}],["path",{d:"m19.148 12.228.383-.923"}],["path",{d:"m19.53 18.696-.382-.924"}],["path",{d:"m20.772 13.852.924-.383"}],["path",{d:"m20.772 16.148.924.383"}],["circle",{cx:"18",cy:"15",r:"3"}],["circle",{cx:"9",cy:"7",r:"4"}]],Og=[["circle",{cx:"10",cy:"7",r:"4"}],["path",{d:"M10.3 15H7a4 4 0 0 0-4 4v2"}],["path",{d:"M15 15.5V14a2 2 0 0 1 4 0v1.5"}],["rect",{width:"8",height:"5",x:"13",y:"16",rx:".899"}]],Zg=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["circle",{cx:"9",cy:"7",r:"4"}],["line",{x1:"22",x2:"16",y1:"11",y2:"11"}]],Gg=[["path",{d:"M11.5 15H7a4 4 0 0 0-4 4v2"}],["path",{d:"M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["circle",{cx:"10",cy:"7",r:"4"}]],Wg=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["circle",{cx:"9",cy:"7",r:"4"}],["line",{x1:"19",x2:"19",y1:"8",y2:"14"}],["line",{x1:"22",x2:"16",y1:"11",y2:"11"}]],va=[["path",{d:"M2 21a8 8 0 0 1 13.292-6"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"m16 19 2 2 4-4"}]],$a=[["path",{d:"m14.305 19.53.923-.382"}],["path",{d:"m15.228 16.852-.923-.383"}],["path",{d:"m16.852 15.228-.383-.923"}],["path",{d:"m16.852 20.772-.383.924"}],["path",{d:"m19.148 15.228.383-.923"}],["path",{d:"m19.53 21.696-.382-.924"}],["path",{d:"M2 21a8 8 0 0 1 10.434-7.62"}],["path",{d:"m20.772 16.852.924-.383"}],["path",{d:"m20.772 19.148.924.383"}],["circle",{cx:"10",cy:"8",r:"5"}],["circle",{cx:"18",cy:"18",r:"3"}]],ma=[["path",{d:"M2 21a8 8 0 0 1 13.292-6"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"M22 19h-6"}]],Ig=[["path",{d:"M2 21a8 8 0 0 1 10.821-7.487"}],["path",{d:"M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["circle",{cx:"10",cy:"8",r:"5"}]],ya=[["path",{d:"M2 21a8 8 0 0 1 13.292-6"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"M19 16v6"}],["path",{d:"M22 19h-6"}]],Eg=[["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"M2 21a8 8 0 0 1 10.434-7.62"}],["circle",{cx:"18",cy:"18",r:"3"}],["path",{d:"m22 22-1.9-1.9"}]],sa=[["circle",{cx:"12",cy:"8",r:"5"}],["path",{d:"M20 21a8 8 0 0 0-16 0"}]],ga=[["path",{d:"M2 21a8 8 0 0 1 11.873-7"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"m17 17 5 5"}],["path",{d:"m22 17-5 5"}]],Xg=[["circle",{cx:"10",cy:"7",r:"4"}],["path",{d:"M10.3 15H7a4 4 0 0 0-4 4v2"}],["circle",{cx:"17",cy:"17",r:"3"}],["path",{d:"m21 21-1.9-1.9"}]],jg=[["path",{d:"M16.051 12.616a1 1 0 0 1 1.909.024l.737 1.452a1 1 0 0 0 .737.535l1.634.256a1 1 0 0 1 .588 1.806l-1.172 1.168a1 1 0 0 0-.282.866l.259 1.613a1 1 0 0 1-1.541 1.134l-1.465-.75a1 1 0 0 0-.912 0l-1.465.75a1 1 0 0 1-1.539-1.133l.258-1.613a1 1 0 0 0-.282-.866l-1.156-1.153a1 1 0 0 1 .572-1.822l1.633-.256a1 1 0 0 0 .737-.535z"}],["path",{d:"M8 15H7a4 4 0 0 0-4 4v2"}],["circle",{cx:"10",cy:"7",r:"4"}]],Ng=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["circle",{cx:"9",cy:"7",r:"4"}],["line",{x1:"17",x2:"22",y1:"8",y2:"13"}],["line",{x1:"22",x2:"17",y1:"8",y2:"13"}]],Kg=[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"}],["circle",{cx:"12",cy:"7",r:"4"}]],Ca=[["path",{d:"M18 21a8 8 0 0 0-16 0"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3"}]],Qg=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["path",{d:"M16 3.128a4 4 0 0 1 0 7.744"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87"}],["circle",{cx:"9",cy:"7",r:"4"}]],ua=[["path",{d:"m16 2-2.3 2.3a3 3 0 0 0 0 4.2l1.8 1.8a3 3 0 0 0 4.2 0L22 8"}],["path",{d:"M15 15 3.3 3.3a4.2 4.2 0 0 0 0 6l7.3 7.3c.7.7 2 .7 2.8 0L15 15Zm0 0 7 7"}],["path",{d:"m2.1 21.8 6.4-6.3"}],["path",{d:"m19 5-7 7"}]],Ha=[["path",{d:"M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2"}],["path",{d:"M7 2v20"}],["path",{d:"M21 15V2a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7"}]],Jg=[["path",{d:"M13 6v5a1 1 0 0 0 1 1h6.102a1 1 0 0 1 .712.298l.898.91a1 1 0 0 1 .288.702V17a1 1 0 0 1-1 1h-3"}],["path",{d:"M5 18H3a1 1 0 0 1-1-1V8a2 2 0 0 1 2-2h12c1.1 0 2.1.8 2.4 1.8l1.176 4.2"}],["path",{d:"M9 18h5"}],["circle",{cx:"16",cy:"18",r:"2"}],["circle",{cx:"7",cy:"18",r:"2"}]],Yg=[["path",{d:"M12 2v20"}],["path",{d:"M2 5h20"}],["path",{d:"M3 3v2"}],["path",{d:"M7 3v2"}],["path",{d:"M17 3v2"}],["path",{d:"M21 3v2"}],["path",{d:"m19 5-7 7-7-7"}]],_g=[["path",{d:"M8 21s-4-3-4-9 4-9 4-9"}],["path",{d:"M16 3s4 3 4 9-4 9-4 9"}],["line",{x1:"15",x2:"9",y1:"9",y2:"15"}],["line",{x1:"9",x2:"15",y1:"9",y2:"15"}]],xg=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"7.5",cy:"7.5",r:".5",fill:"currentColor"}],["path",{d:"m7.9 7.9 2.7 2.7"}],["circle",{cx:"16.5",cy:"7.5",r:".5",fill:"currentColor"}],["path",{d:"m13.4 10.6 2.7-2.7"}],["circle",{cx:"7.5",cy:"16.5",r:".5",fill:"currentColor"}],["path",{d:"m7.9 16.1 2.7-2.7"}],["circle",{cx:"16.5",cy:"16.5",r:".5",fill:"currentColor"}],["path",{d:"m13.4 13.4 2.7 2.7"}],["circle",{cx:"12",cy:"12",r:"2"}]],aC=[["path",{d:"M19.5 7a24 24 0 0 1 0 10"}],["path",{d:"M4.5 7a24 24 0 0 0 0 10"}],["path",{d:"M7 19.5a24 24 0 0 0 10 0"}],["path",{d:"M7 4.5a24 24 0 0 1 10 0"}],["rect",{x:"17",y:"17",width:"5",height:"5",rx:"1"}],["rect",{x:"17",y:"2",width:"5",height:"5",rx:"1"}],["rect",{x:"2",y:"17",width:"5",height:"5",rx:"1"}],["rect",{x:"2",y:"2",width:"5",height:"5",rx:"1"}]],tC=[["path",{d:"M16 8q6 0 6-6-6 0-6 6"}],["path",{d:"M17.41 3.59a10 10 0 1 0 3 3"}],["path",{d:"M2 2a26.6 26.6 0 0 1 10 20c.9-6.82 1.5-9.5 4-14"}]],hC=[["path",{d:"M18 11c-1.5 0-2.5.5-3 2"}],["path",{d:"M4 6a2 2 0 0 0-2 2v4a5 5 0 0 0 5 5 8 8 0 0 1 5 2 8 8 0 0 1 5-2 5 5 0 0 0 5-5V8a2 2 0 0 0-2-2h-3a8 8 0 0 0-5 2 8 8 0 0 0-5-2z"}],["path",{d:"M6 11c1.5 0 2.5.5 3 2"}]],dC=[["path",{d:"M12 15v7"}],["path",{d:"M9 19h6"}],["circle",{cx:"12",cy:"9",r:"6"}]],cC=[["path",{d:"M10 20h4"}],["path",{d:"M12 16v6"}],["path",{d:"M17 2h4v4"}],["path",{d:"m21 2-5.46 5.46"}],["circle",{cx:"12",cy:"11",r:"5"}]],MC=[["path",{d:"m2 8 2 2-2 2 2 2-2 2"}],["path",{d:"m22 8-2 2 2 2-2 2 2 2"}],["path",{d:"M8 8v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2"}],["path",{d:"M16 10.34V6c0-.55-.45-1-1-1h-4.34"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],pC=[["path",{d:"m2 8 2 2-2 2 2 2-2 2"}],["path",{d:"m22 8-2 2 2 2-2 2 2 2"}],["rect",{width:"8",height:"14",x:"8",y:"5",rx:"1"}]],iC=[["path",{d:"M10.66 6H14a2 2 0 0 1 2 2v2.5l5.248-3.062A.5.5 0 0 1 22 7.87v8.196"}],["path",{d:"M16 16a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2"}],["path",{d:"m2 2 20 20"}]],nC=[["path",{d:"m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5"}],["rect",{x:"2",y:"6",width:"14",height:"12",rx:"2"}]],lC=[["circle",{cx:"6",cy:"12",r:"4"}],["circle",{cx:"18",cy:"12",r:"4"}],["line",{x1:"6",x2:"18",y1:"16",y2:"16"}]],eC=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M2 8h20"}],["circle",{cx:"8",cy:"14",r:"2"}],["path",{d:"M8 12h8"}],["circle",{cx:"16",cy:"14",r:"2"}]],rC=[["path",{d:"M21 17v2a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M21 7V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2"}],["circle",{cx:"12",cy:"12",r:"1"}],["path",{d:"M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"}]],oC=[["path",{d:"M11.1 7.1a16.55 16.55 0 0 1 10.9 4"}],["path",{d:"M12 12a12.6 12.6 0 0 1-8.7 5"}],["path",{d:"M16.8 13.6a16.55 16.55 0 0 1-9 7.5"}],["path",{d:"M20.7 17a12.8 12.8 0 0 0-8.7-5 13.3 13.3 0 0 1 0-10"}],["path",{d:"M6.3 3.8a16.55 16.55 0 0 0 1.9 11.5"}],["circle",{cx:"12",cy:"12",r:"10"}]],vC=[["path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"}],["path",{d:"M16 9a5 5 0 0 1 0 6"}]],$C=[["path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"}],["path",{d:"M16 9a5 5 0 0 1 0 6"}],["path",{d:"M19.364 18.364a9 9 0 0 0 0-12.728"}]],mC=[["path",{d:"M16 9a5 5 0 0 1 .95 2.293"}],["path",{d:"M19.364 5.636a9 9 0 0 1 1.889 9.96"}],["path",{d:"m2 2 20 20"}],["path",{d:"m7 7-.587.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298V11"}],["path",{d:"M9.828 4.172A.686.686 0 0 1 11 4.657v.686"}]],yC=[["path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"}],["line",{x1:"22",x2:"16",y1:"9",y2:"15"}],["line",{x1:"16",x2:"22",y1:"9",y2:"15"}]],sC=[["path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"}]],gC=[["path",{d:"m9 12 2 2 4-4"}],["path",{d:"M5 7c0-1.1.9-2 2-2h10a2 2 0 0 1 2 2v12H5V7Z"}],["path",{d:"M22 19H2"}]],Aa=[["path",{d:"M17 14h.01"}],["path",{d:"M7 7h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14"}]],CC=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2"}],["path",{d:"M3 11h3c.8 0 1.6.3 2.1.9l1.1.9c1.6 1.6 4.1 1.6 5.7 0l1.1-.9c.5-.5 1.3-.9 2.1-.9H21"}]],uC=[["path",{d:"M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1"}],["path",{d:"M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"}]],HC=[["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}],["path",{d:"m9 17 6.1-6.1a2 2 0 0 1 2.81.01L22 15"}],["circle",{cx:"8",cy:"9",r:"2"}],["rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}]],wa=[["path",{d:"m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72"}],["path",{d:"m14 7 3 3"}],["path",{d:"M5 6v4"}],["path",{d:"M19 14v4"}],["path",{d:"M10 2v2"}],["path",{d:"M7 8H3"}],["path",{d:"M21 16h-4"}],["path",{d:"M11 3H9"}]],AC=[["path",{d:"M15 4V2"}],["path",{d:"M15 16v-2"}],["path",{d:"M8 9h2"}],["path",{d:"M20 9h2"}],["path",{d:"M17.8 11.8 19 13"}],["path",{d:"M15 9h.01"}],["path",{d:"M17.8 6.2 19 5"}],["path",{d:"m3 21 9-9"}],["path",{d:"M12.2 6.2 11 5"}]],wC=[["path",{d:"M18 21V10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v11"}],["path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 1.132-1.803l7.95-3.974a2 2 0 0 1 1.837 0l7.948 3.974A2 2 0 0 1 22 8z"}],["path",{d:"M6 13h12"}],["path",{d:"M6 17h12"}]],VC=[["path",{d:"M3 6h3"}],["path",{d:"M17 6h.01"}],["rect",{width:"18",height:"20",x:"3",y:"2",rx:"2"}],["circle",{cx:"12",cy:"13",r:"5"}],["path",{d:"M12 18a2.5 2.5 0 0 0 0-5 2.5 2.5 0 0 1 0-5"}]],SC=[["path",{d:"M12 10v2.2l1.6 1"}],["path",{d:"m16.13 7.66-.81-4.05a2 2 0 0 0-2-1.61h-2.68a2 2 0 0 0-2 1.61l-.78 4.05"}],["path",{d:"m7.88 16.36.8 4a2 2 0 0 0 2 1.61h2.72a2 2 0 0 0 2-1.61l.81-4.05"}],["circle",{cx:"12",cy:"12",r:"6"}]],LC=[["path",{d:"M12 10L12 2"}],["path",{d:"M16 6L12 10L8 6"}],["path",{d:"M2 15C2.6 15.5 3.2 16 4.5 16C7 16 7 14 9.5 14C12.1 14 11.9 16 14.5 16C17 16 17 14 19.5 14C20.8 14 21.4 14.5 22 15"}],["path",{d:"M2 21C2.6 21.5 3.2 22 4.5 22C7 22 7 20 9.5 20C12.1 20 11.9 22 14.5 22C17 22 17 20 19.5 20C20.8 20 21.4 20.5 22 21"}]],fC=[["path",{d:"M12 2v8"}],["path",{d:"M2 15c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M2 21c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"m8 6 4-4 4 4"}]],kC=[["path",{d:"M19 5a2 2 0 0 0-2 2v11"}],["path",{d:"M2 18c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M7 13h10"}],["path",{d:"M7 9h10"}],["path",{d:"M9 5a2 2 0 0 0-2 2v11"}]],PC=[["path",{d:"M2 6c.6.5 1.2 1 2.5 1C7 7 7 5 9.5 5c2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M2 12c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M2 18c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}]],BC=[["circle",{cx:"12",cy:"4.5",r:"2.5"}],["path",{d:"m10.2 6.3-3.9 3.9"}],["circle",{cx:"4.5",cy:"12",r:"2.5"}],["path",{d:"M7 12h10"}],["circle",{cx:"19.5",cy:"12",r:"2.5"}],["path",{d:"m13.8 17.7 3.9-3.9"}],["circle",{cx:"12",cy:"19.5",r:"2.5"}]],zC=[["circle",{cx:"12",cy:"10",r:"8"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M7 22h10"}],["path",{d:"M12 22v-4"}]],FC=[["path",{d:"M17 17h-5c-1.09-.02-1.94.92-2.5 1.9A3 3 0 1 1 2.57 15"}],["path",{d:"M9 3.4a4 4 0 0 1 6.52.66"}],["path",{d:"m6 17 3.1-5.8a2.5 2.5 0 0 0 .057-2.05"}],["path",{d:"M20.3 20.3a4 4 0 0 1-2.3.7"}],["path",{d:"M18.6 13a4 4 0 0 1 3.357 3.414"}],["path",{d:"m12 6 .6 1"}],["path",{d:"m2 2 20 20"}]],DC=[["path",{d:"M18 16.98h-5.99c-1.1 0-1.95.94-2.48 1.9A4 4 0 0 1 2 17c.01-.7.2-1.4.57-2"}],["path",{d:"m6 17 3.13-5.78c.53-.97.1-2.18-.5-3.1a4 4 0 1 1 6.89-4.06"}],["path",{d:"m12 6 3.13 5.73C15.66 12.7 16.9 13 18 13a4 4 0 0 1 0 8"}]],bC=[["path",{d:"M6.5 8a2 2 0 0 0-1.906 1.46L2.1 18.5A2 2 0 0 0 4 21h16a2 2 0 0 0 1.925-2.54L19.4 9.5A2 2 0 0 0 17.48 8z"}],["path",{d:"M7.999 15a2.5 2.5 0 0 1 4 0 2.5 2.5 0 0 0 4 0"}],["circle",{cx:"12",cy:"5",r:"3"}]],RC=[["circle",{cx:"12",cy:"5",r:"3"}],["path",{d:"M6.5 8a2 2 0 0 0-1.905 1.46L2.1 18.5A2 2 0 0 0 4 21h16a2 2 0 0 0 1.925-2.54L19.4 9.5A2 2 0 0 0 17.48 8Z"}]],TC=[["path",{d:"m2 22 10-10"}],["path",{d:"m16 8-1.17 1.17"}],["path",{d:"M3.47 12.53 5 11l1.53 1.53a3.5 3.5 0 0 1 0 4.94L5 19l-1.53-1.53a3.5 3.5 0 0 1 0-4.94Z"}],["path",{d:"m8 8-.53.53a3.5 3.5 0 0 0 0 4.94L9 15l1.53-1.53c.55-.55.88-1.25.98-1.97"}],["path",{d:"M10.91 5.26c.15-.26.34-.51.56-.73L13 3l1.53 1.53a3.5 3.5 0 0 1 .28 4.62"}],["path",{d:"M20 2h2v2a4 4 0 0 1-4 4h-2V6a4 4 0 0 1 4-4Z"}],["path",{d:"M11.47 17.47 13 19l-1.53 1.53a3.5 3.5 0 0 1-4.94 0L5 19l1.53-1.53a3.5 3.5 0 0 1 4.94 0Z"}],["path",{d:"m16 16-.53.53a3.5 3.5 0 0 1-4.94 0L9 15l1.53-1.53a3.49 3.49 0 0 1 1.97-.98"}],["path",{d:"M18.74 13.09c.26-.15.51-.34.73-.56L21 11l-1.53-1.53a3.5 3.5 0 0 0-4.62-.28"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],qC=[["path",{d:"M2 22 16 8"}],["path",{d:"M3.47 12.53 5 11l1.53 1.53a3.5 3.5 0 0 1 0 4.94L5 19l-1.53-1.53a3.5 3.5 0 0 1 0-4.94Z"}],["path",{d:"M7.47 8.53 9 7l1.53 1.53a3.5 3.5 0 0 1 0 4.94L9 15l-1.53-1.53a3.5 3.5 0 0 1 0-4.94Z"}],["path",{d:"M11.47 4.53 13 3l1.53 1.53a3.5 3.5 0 0 1 0 4.94L13 11l-1.53-1.53a3.5 3.5 0 0 1 0-4.94Z"}],["path",{d:"M20 2h2v2a4 4 0 0 1-4 4h-2V6a4 4 0 0 1 4-4Z"}],["path",{d:"M11.47 17.47 13 19l-1.53 1.53a3.5 3.5 0 0 1-4.94 0L5 19l1.53-1.53a3.5 3.5 0 0 1 4.94 0Z"}],["path",{d:"M15.47 13.47 17 15l-1.53 1.53a3.5 3.5 0 0 1-4.94 0L9 15l1.53-1.53a3.5 3.5 0 0 1 4.94 0Z"}],["path",{d:"M19.47 9.47 21 11l-1.53 1.53a3.5 3.5 0 0 1-4.94 0L13 11l1.53-1.53a3.5 3.5 0 0 1 4.94 0Z"}]],UC=[["circle",{cx:"7",cy:"12",r:"3"}],["path",{d:"M10 9v6"}],["circle",{cx:"17",cy:"12",r:"3"}],["path",{d:"M14 7v8"}],["path",{d:"M22 17v1c0 .5-.5 1-1 1H3c-.5 0-1-.5-1-1v-1"}]],OC=[["path",{d:"m14.305 19.53.923-.382"}],["path",{d:"m15.228 16.852-.923-.383"}],["path",{d:"m16.852 15.228-.383-.923"}],["path",{d:"m16.852 20.772-.383.924"}],["path",{d:"m19.148 15.228.383-.923"}],["path",{d:"m19.53 21.696-.382-.924"}],["path",{d:"M2 7.82a15 15 0 0 1 20 0"}],["path",{d:"m20.772 16.852.924-.383"}],["path",{d:"m20.772 19.148.924.383"}],["path",{d:"M5 11.858a10 10 0 0 1 11.5-1.785"}],["path",{d:"M8.5 15.429a5 5 0 0 1 2.413-1.31"}],["circle",{cx:"18",cy:"18",r:"3"}]],ZC=[["path",{d:"M12 20h.01"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0"}]],GC=[["path",{d:"M12 20h.01"}],["path",{d:"M5 12.859a10 10 0 0 1 14 0"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0"}]],WC=[["path",{d:"M12 20h.01"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0"}],["path",{d:"M5 12.859a10 10 0 0 1 5.17-2.69"}],["path",{d:"M19 12.859a10 10 0 0 0-2.007-1.523"}],["path",{d:"M2 8.82a15 15 0 0 1 4.177-2.643"}],["path",{d:"M22 8.82a15 15 0 0 0-11.288-3.764"}],["path",{d:"m2 2 20 20"}]],IC=[["path",{d:"M2 8.82a15 15 0 0 1 20 0"}],["path",{d:"M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["path",{d:"M5 12.859a10 10 0 0 1 10.5-2.222"}],["path",{d:"M8.5 16.429a5 5 0 0 1 3-1.406"}]],EC=[["path",{d:"M11.965 10.105v4L13.5 12.5a5 5 0 0 1 8 1.5"}],["path",{d:"M11.965 14.105h4"}],["path",{d:"M17.965 18.105h4L20.43 19.71a5 5 0 0 1-8-1.5"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0"}],["path",{d:"M21.965 22.105v-4"}],["path",{d:"M5 12.86a10 10 0 0 1 3-2.032"}],["path",{d:"M8.5 16.429h.01"}]],XC=[["path",{d:"M12 20h.01"}]],jC=[["path",{d:"M12 20h.01"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0"}],["path",{d:"M5 12.859a10 10 0 0 1 14 0"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0"}]],NC=[["path",{d:"M10 2v8"}],["path",{d:"M12.8 21.6A2 2 0 1 0 14 18H2"}],["path",{d:"M17.5 10a2.5 2.5 0 1 1 2 4H2"}],["path",{d:"m6 6 4 4 4-4"}]],KC=[["path",{d:"M12.8 19.6A2 2 0 1 0 14 16H2"}],["path",{d:"M17.5 8a2.5 2.5 0 1 1 2 4H2"}],["path",{d:"M9.8 4.4A2 2 0 1 1 11 8H2"}]],QC=[["path",{d:"M8 22h8"}],["path",{d:"M7 10h3m7 0h-1.343"}],["path",{d:"M12 15v7"}],["path",{d:"M7.307 7.307A12.33 12.33 0 0 0 7 10a5 5 0 0 0 7.391 4.391M8.638 2.981C8.75 2.668 8.872 2.34 9 2h6c1.5 4 2 6 2 8 0 .407-.05.809-.145 1.198"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],JC=[["path",{d:"M8 22h8"}],["path",{d:"M7 10h10"}],["path",{d:"M12 15v7"}],["path",{d:"M12 15a5 5 0 0 0 5-5c0-2-.5-4-2-8H9c-1.5 4-2 6-2 8a5 5 0 0 0 5 5Z"}]],YC=[["path",{d:"m19 12-1.5 3"}],["path",{d:"M19.63 18.81 22 20"}],["path",{d:"M6.47 8.23a1.68 1.68 0 0 1 2.44 1.93l-.64 2.08a6.76 6.76 0 0 0 10.16 7.67l.42-.27a1 1 0 1 0-2.73-4.21l-.42.27a1.76 1.76 0 0 1-2.63-1.99l.64-2.08A6.66 6.66 0 0 0 3.94 3.9l-.7.4a1 1 0 1 0 2.55 4.34z"}]],_C=[["rect",{width:"8",height:"8",x:"3",y:"3",rx:"2"}],["path",{d:"M7 11v4a2 2 0 0 0 2 2h4"}],["rect",{width:"8",height:"8",x:"13",y:"13",rx:"2"}]],xC=[["path",{d:"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z"}]],au=[["path",{d:"M18 6 6 18"}],["path",{d:"m6 6 12 12"}]],tu=[["path",{d:"M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17"}],["path",{d:"m10 15 5-3-5-3z"}]],hu=[["path",{d:"M10.513 4.856 13.12 2.17a.5.5 0 0 1 .86.46l-1.377 4.317"}],["path",{d:"M15.656 10H20a1 1 0 0 1 .78 1.63l-1.72 1.773"}],["path",{d:"M16.273 16.273 10.88 21.83a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14H4a1 1 0 0 1-.78-1.63l4.507-4.643"}],["path",{d:"m2 2 20 20"}]],du=[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"}]],cu=[["circle",{cx:"11",cy:"11",r:"8"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65"}],["line",{x1:"11",x2:"11",y1:"8",y2:"14"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11"}]],Mu=[["circle",{cx:"11",cy:"11",r:"8"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11"}]];var pu=Object.freeze({__proto__:null,AArrowDown:ka,AArrowUp:Pa,ALargeSmall:Ba,Accessibility:za,Activity:Fa,ActivitySquare:d0,AirVent:Da,Airplay:ba,AlarmCheck:s,AlarmClock:Ta,AlarmClockCheck:s,AlarmClockMinus:g,AlarmClockOff:Ra,AlarmClockPlus:C,AlarmMinus:g,AlarmPlus:C,AlarmSmoke:qa,Album:Ua,AlertCircle:K,AlertOctagon:F2,AlertTriangle:ea,AlignCenter:ha,AlignCenterHorizontal:Oa,AlignCenterVertical:Za,AlignEndHorizontal:Ga,AlignEndVertical:Wa,AlignHorizontalDistributeCenter:Ia,AlignHorizontalDistributeEnd:Xa,AlignHorizontalDistributeStart:Ea,AlignHorizontalJustifyCenter:ja,AlignHorizontalJustifyEnd:Na,AlignHorizontalJustifyStart:Ka,AlignHorizontalSpaceAround:Qa,AlignHorizontalSpaceBetween:Ja,AlignJustify:ca,AlignLeft:y,AlignRight:da,AlignStartHorizontal:Ya,AlignStartVertical:_a,AlignVerticalDistributeCenter:xa,AlignVerticalDistributeEnd:at,AlignVerticalDistributeStart:tt,AlignVerticalJustifyCenter:ht,AlignVerticalJustifyEnd:dt,AlignVerticalJustifyStart:ct,AlignVerticalSpaceAround:Mt,AlignVerticalSpaceBetween:pt,Ambulance:it,Ampersand:nt,Ampersands:lt,Amphora:et,Anchor:rt,Angry:ot,Annoyed:vt,Antenna:$t,Anvil:mt,Aperture:yt,AppWindow:gt,AppWindowMac:st,Apple:Ct,Archive:At,ArchiveRestore:ut,ArchiveX:Ht,AreaChart:b,Armchair:wt,ArrowBigDown:St,ArrowBigDownDash:Vt,ArrowBigLeft:ft,ArrowBigLeftDash:Lt,ArrowBigRight:Pt,ArrowBigRightDash:kt,ArrowBigUp:zt,ArrowBigUpDash:Bt,ArrowDown:Gt,ArrowDown01:Ft,ArrowDown10:Dt,ArrowDownAZ:u,ArrowDownAz:u,ArrowDownCircle:Q,ArrowDownFromLine:bt,ArrowDownLeft:Rt,ArrowDownLeftFromCircle:Y,ArrowDownLeftFromSquare:n0,ArrowDownLeftSquare:c0,ArrowDownNarrowWide:Ut,ArrowDownRight:Tt,ArrowDownRightFromCircle:_,ArrowDownRightFromSquare:l0,ArrowDownRightSquare:M0,ArrowDownSquare:p0,ArrowDownToDot:qt,ArrowDownToLine:Ot,ArrowDownUp:Zt,ArrowDownWideNarrow:H,ArrowDownZA:A,ArrowDownZa:A,ArrowLeft:Xt,ArrowLeftCircle:J,ArrowLeftFromLine:It,ArrowLeftRight:Wt,ArrowLeftSquare:i0,ArrowLeftToLine:Et,ArrowRight:Qt,ArrowRightCircle:t1,ArrowRightFromLine:jt,ArrowRightLeft:Nt,ArrowRightSquare:o0,ArrowRightToLine:Kt,ArrowUp:Mh,ArrowUp01:Jt,ArrowUp10:Yt,ArrowUpAZ:w,ArrowUpAz:w,ArrowUpCircle:h1,ArrowUpDown:_t,ArrowUpFromDot:xt,ArrowUpFromLine:ah,ArrowUpLeft:th,ArrowUpLeftFromCircle:x,ArrowUpLeftFromSquare:e0,ArrowUpLeftSquare:v0,ArrowUpNarrowWide:V,ArrowUpRight:hh,ArrowUpRightFromCircle:a1,ArrowUpRightFromSquare:r0,ArrowUpRightSquare:$0,ArrowUpSquare:m0,ArrowUpToLine:dh,ArrowUpWideNarrow:ch,ArrowUpZA:S,ArrowUpZa:S,ArrowsUpFromLine:ph,Asterisk:ih,AsteriskSquare:y0,AtSign:nh,Atom:lh,AudioLines:eh,AudioWaveform:oh,Award:rh,Axe:vh,Axis3D:L,Axis3d:L,Baby:$h,Backpack:mh,Badge:zh,BadgeAlert:yh,BadgeCent:sh,BadgeCheck:f,BadgeDollarSign:gh,BadgeEuro:Ch,BadgeHelp:k,BadgeIndianRupee:uh,BadgeInfo:Hh,BadgeJapaneseYen:Ah,BadgeMinus:wh,BadgePercent:Vh,BadgePlus:Sh,BadgePoundSterling:Lh,BadgeQuestionMark:k,BadgeRussianRuble:fh,BadgeSwissFranc:kh,BadgeTurkishLira:Ph,BadgeX:Bh,BaggageClaim:Fh,Balloon:Dh,Ban:bh,Banana:Rh,Bandage:Th,Banknote:Zh,BanknoteArrowDown:qh,BanknoteArrowUp:Uh,BanknoteX:Oh,BarChart:W,BarChart2:I,BarChart3:Z,BarChart4:O,BarChartBig:U,BarChartHorizontal:T,BarChartHorizontalBig:R,Barcode:Gh,Barrel:Wh,Baseline:Ih,Bath:Eh,Battery:Yh,BatteryCharging:Xh,BatteryFull:jh,BatteryLow:Nh,BatteryMedium:Qh,BatteryPlus:Kh,BatteryWarning:Jh,Beaker:_h,Bean:a5,BeanOff:xh,Bed:d5,BedDouble:t5,BedSingle:h5,Beef:c5,Beer:p5,BeerOff:M5,Bell:v5,BellDot:i5,BellElectric:n5,BellMinus:l5,BellOff:e5,BellPlus:r5,BellRing:o5,BetweenHorizonalEnd:P,BetweenHorizonalStart:B,BetweenHorizontalEnd:P,BetweenHorizontalStart:B,BetweenVerticalEnd:$5,BetweenVerticalStart:m5,BicepsFlexed:y5,Bike:s5,Binary:g5,Binoculars:u5,Biohazard:C5,Bird:A5,Birdhouse:H5,Bitcoin:w5,Blend:V5,Blinds:S5,Blocks:L5,Bluetooth:B5,BluetoothConnected:f5,BluetoothOff:k5,BluetoothSearching:P5,Bold:z5,Bolt:F5,Bomb:D5,Bone:b5,Book:M4,BookA:R5,BookAlert:T5,BookAudio:q5,BookCheck:U5,BookCopy:O5,BookDashed:z,BookDown:Z5,BookHeadphones:G5,BookHeart:W5,BookImage:E5,BookKey:I5,BookLock:X5,BookMarked:j5,BookMinus:N5,BookOpen:J5,BookOpenCheck:K5,BookOpenText:Q5,BookPlus:Y5,BookSearch:_5,BookTemplate:z,BookText:x5,BookType:t4,BookUp:a4,BookUp2:h4,BookUser:d4,BookX:c4,Bookmark:e4,BookmarkCheck:p4,BookmarkMinus:i4,BookmarkPlus:n4,BookmarkX:l4,BoomBox:r4,Bot:$4,BotMessageSquare:o4,BotOff:v4,BottleWine:m4,BowArrow:y4,Box:s4,BoxSelect:f0,Boxes:g4,Braces:F,Brackets:C4,Brain:A4,BrainCircuit:u4,BrainCog:H4,BrickWall:S4,BrickWallFire:w4,BrickWallShield:V4,Briefcase:P4,BriefcaseBusiness:L4,BriefcaseConveyorBelt:f4,BriefcaseMedical:k4,BringToFront:B4,Brush:F4,BrushCleaning:z4,Bubbles:D4,Bug:T4,BugOff:b4,BugPlay:R4,Building:U4,Building2:q4,Bus:Z4,BusFront:O4,Cable:W4,CableCar:G4,Cake:E4,CakeSlice:I4,Calculator:X4,Calendar:o3,Calendar1:j4,CalendarArrowDown:N4,CalendarArrowUp:K4,CalendarCheck:J4,CalendarCheck2:Q4,CalendarClock:_4,CalendarCog:Y4,CalendarDays:x4,CalendarFold:a3,CalendarHeart:t3,CalendarMinus:d3,CalendarMinus2:h3,CalendarOff:c3,CalendarPlus:p3,CalendarPlus2:M3,CalendarRange:i3,CalendarSearch:n3,CalendarSync:l3,CalendarX:r3,CalendarX2:e3,Calendars:v3,Camera:m3,CameraOff:$3,CandlestickChart:q,Candy:g3,CandyCane:y3,CandyOff:s3,Cannabis:u3,CannabisOff:C3,Captions:D,CaptionsOff:H3,Car:V3,CarFront:A3,CarTaxiFront:w3,Caravan:S3,CardSim:L3,Carrot:f3,CaseLower:k3,CaseSensitive:P3,CaseUpper:B3,CassetteTape:z3,Cast:F3,Castle:D3,Cat:b3,Cctv:R3,ChartArea:b,ChartBar:T,ChartBarBig:R,ChartBarDecreasing:T3,ChartBarIncreasing:q3,ChartBarStacked:U3,ChartCandlestick:q,ChartColumn:Z,ChartColumnBig:U,ChartColumnDecreasing:O3,ChartColumnIncreasing:O,ChartColumnStacked:Z3,ChartGantt:G3,ChartLine:G,ChartNetwork:W3,ChartNoAxesColumn:I,ChartNoAxesColumnDecreasing:I3,ChartNoAxesColumnIncreasing:W,ChartNoAxesCombined:E3,ChartNoAxesGantt:E,ChartPie:X,ChartScatter:j,ChartSpline:X3,Check:K3,CheckCheck:j3,CheckCircle:c1,CheckCircle2:d1,CheckLine:N3,CheckSquare:C0,CheckSquare2:g0,ChefHat:Q3,Cherry:J3,ChessBishop:Y3,ChessKing:_3,ChessKnight:x3,ChessPawn:ad,ChessQueen:td,ChessRook:cd,ChevronDown:hd,ChevronDownCircle:M1,ChevronDownSquare:u0,ChevronFirst:dd,ChevronLast:Md,ChevronLeft:pd,ChevronLeftCircle:p1,ChevronLeftSquare:H0,ChevronRight:id,ChevronRightCircle:i1,ChevronRightSquare:A0,ChevronUp:nd,ChevronUpCircle:n1,ChevronUpSquare:w0,ChevronsDown:ed,ChevronsDownUp:ld,ChevronsLeft:vd,ChevronsLeftRight:od,ChevronsLeftRightEllipsis:rd,ChevronsRight:md,ChevronsRightLeft:$d,ChevronsUp:sd,ChevronsUpDown:yd,Chrome:N,Chromium:N,Church:gd,Cigarette:ud,CigaretteOff:Cd,Circle:Rd,CircleAlert:K,CircleArrowDown:Q,CircleArrowLeft:J,CircleArrowOutDownLeft:Y,CircleArrowOutDownRight:_,CircleArrowOutUpLeft:x,CircleArrowOutUpRight:a1,CircleArrowRight:t1,CircleArrowUp:h1,CircleCheck:d1,CircleCheckBig:c1,CircleChevronDown:M1,CircleChevronLeft:p1,CircleChevronRight:i1,CircleChevronUp:n1,CircleDashed:Hd,CircleDivide:l1,CircleDollarSign:Ad,CircleDot:Vd,CircleDotDashed:wd,CircleEllipsis:Sd,CircleEqual:Ld,CircleFadingArrowUp:fd,CircleFadingPlus:kd,CircleGauge:e1,CircleHelp:l,CircleMinus:r1,CircleOff:Pd,CircleParking:v1,CircleParkingOff:o1,CirclePause:$1,CirclePercent:m1,CirclePile:Bd,CirclePlay:y1,CirclePlus:s1,CirclePoundSterling:zd,CirclePower:g1,CircleQuestionMark:l,CircleSlash:Fd,CircleSlash2:C1,CircleSlashed:C1,CircleSmall:Dd,CircleStar:bd,CircleStop:u1,CircleUser:A1,CircleUserRound:H1,CircleX:w1,CircuitBoard:Td,Citrus:qd,Clapperboard:Ud,Clipboard:Kd,ClipboardCheck:Od,ClipboardClock:Zd,ClipboardCopy:Gd,ClipboardEdit:S1,ClipboardList:Wd,ClipboardMinus:Id,ClipboardPaste:Ed,ClipboardPen:S1,ClipboardPenLine:V1,ClipboardPlus:Xd,ClipboardSignature:V1,ClipboardType:jd,ClipboardX:Nd,Clock:v8,Clock1:Qd,Clock10:Jd,Clock11:Yd,Clock12:_d,Clock2:xd,Clock3:a8,Clock4:t8,Clock5:h8,Clock6:d8,Clock7:c8,Clock8:M8,Clock9:p8,ClockAlert:i8,ClockArrowDown:n8,ClockArrowUp:l8,ClockCheck:e8,ClockFading:r8,ClockPlus:o8,ClosedCaption:$8,Cloud:F8,CloudAlert:m8,CloudBackup:y8,CloudCheck:s8,CloudCog:g8,CloudDownload:L1,CloudDrizzle:C8,CloudFog:u8,CloudHail:H8,CloudLightning:A8,CloudMoon:V8,CloudMoonRain:w8,CloudOff:L8,CloudRain:f8,CloudRainWind:S8,CloudSnow:k8,CloudSun:B8,CloudSunRain:P8,CloudSync:z8,CloudUpload:f1,Cloudy:D8,Clover:b8,Club:R8,Code:T8,Code2:k1,CodeSquare:V0,CodeXml:k1,Codepen:q8,Codesandbox:U8,Coffee:O8,Cog:G8,Coins:Z8,Columns:P1,Columns2:P1,Columns3:B1,Columns3Cog:e,Columns4:W8,ColumnsSettings:e,Combine:I8,Command:E8,Compass:X8,Component:j8,Computer:N8,ConciergeBell:K8,Cone:Q8,Construction:J8,Contact:Y8,Contact2:z1,ContactRound:z1,Container:_8,Contrast:x8,Cookie:a6,CookingPot:t6,Copy:p6,CopyCheck:h6,CopyMinus:d6,CopyPlus:c6,CopySlash:M6,CopyX:i6,Copyleft:n6,Copyright:l6,CornerDownLeft:r6,CornerDownRight:e6,CornerLeftDown:o6,CornerLeftUp:v6,CornerRightDown:$6,CornerRightUp:m6,CornerUpLeft:y6,CornerUpRight:s6,Cpu:g6,CreativeCommons:C6,CreditCard:u6,Croissant:H6,Crop:A6,Cross:w6,Crosshair:V6,Crown:S6,Cuboid:L6,CupSoda:f6,CurlyBraces:F,Currency:k6,Cylinder:P6,Dam:B6,Database:D6,DatabaseBackup:z6,DatabaseZap:F6,DecimalsArrowLeft:b6,DecimalsArrowRight:R6,Delete:T6,Dessert:q6,Diameter:U6,Diamond:W6,DiamondMinus:O6,DiamondPercent:F1,DiamondPlus:Z6,Dice1:G6,Dice2:I6,Dice3:E6,Dice4:N6,Dice5:X6,Dice6:j6,Dices:K6,Diff:Q6,Disc:x6,Disc2:J6,Disc3:Y6,DiscAlbum:_6,Divide:ac,DivideCircle:l1,DivideSquare:k0,Dna:dc,DnaOff:tc,Dock:hc,Dog:cc,DollarSign:Mc,Donut:pc,DoorClosed:nc,DoorClosedLocked:ic,DoorOpen:ec,Dot:lc,DotSquare:P0,Download:rc,DownloadCloud:L1,DraftingCompass:oc,Drama:vc,Dribbble:$c,Drill:mc,Drone:yc,Droplet:gc,DropletOff:sc,Droplets:Cc,Drum:uc,Drumstick:Hc,Dumbbell:Ac,Ear:Vc,EarOff:wc,Earth:D1,EarthLock:Sc,Eclipse:Lc,Edit:i,Edit2:X2,Edit3:E2,Egg:Pc,EggFried:fc,EggOff:kc,Ellipsis:R1,EllipsisVertical:b1,Equal:Dc,EqualApproximately:Bc,EqualNot:zc,EqualSquare:B0,Eraser:Fc,EthernetPort:bc,Euro:Rc,EvCharger:Tc,Expand:Uc,ExternalLink:qc,Eye:Gc,EyeClosed:Oc,EyeOff:Zc,Facebook:Ic,Factory:Wc,Fan:Ec,FastForward:Xc,Feather:jc,Fence:Nc,FerrisWheel:Kc,Figma:Qc,File:V7,FileArchive:Jc,FileAudio:r,FileAudio2:r,FileAxis3D:T1,FileAxis3d:T1,FileBadge:q1,FileBadge2:q1,FileBarChart:Z1,FileBarChart2:G1,FileBox:Yc,FileBraces:O1,FileBracesCorner:U1,FileChartColumn:G1,FileChartColumnIncreasing:Z1,FileChartLine:W1,FileChartPie:I1,FileCheck:_c,FileCheck2:E1,FileCheckCorner:E1,FileClock:xc,FileCode:a7,FileCode2:X1,FileCodeCorner:X1,FileCog:j1,FileCog2:j1,FileDiff:t7,FileDigit:h7,FileDown:d7,FileEdit:_1,FileExclamationPoint:N1,FileHeadphone:r,FileHeart:c7,FileImage:M7,FileInput:p7,FileJson:O1,FileJson2:U1,FileKey:K1,FileKey2:K1,FileLineChart:W1,FileLock:Q1,FileLock2:Q1,FileMinus:n7,FileMinus2:J1,FileMinusCorner:J1,FileMusic:i7,FileOutput:l7,FilePen:_1,FilePenLine:Y1,FilePieChart:I1,FilePlay:x1,FilePlus:e7,FilePlus2:t2,FilePlusCorner:t2,FileQuestion:a2,FileQuestionMark:a2,FileScan:r7,FileSearch:o7,FileSearch2:h2,FileSearchCorner:h2,FileSignal:d2,FileSignature:Y1,FileSliders:$7,FileSpreadsheet:v7,FileStack:m7,FileSymlink:y7,FileTerminal:s7,FileText:g7,FileType:C7,FileType2:c2,FileTypeCorner:c2,FileUp:u7,FileUser:H7,FileVideo:x1,FileVideo2:M2,FileVideoCamera:M2,FileVolume:A7,FileVolume2:d2,FileWarning:N1,FileX:w7,FileX2:p2,FileXCorner:p2,Files:S7,Film:L7,Filter:r2,FilterX:e2,Fingerprint:i2,FingerprintPattern:i2,FireExtinguisher:f7,Fish:B7,FishOff:k7,FishSymbol:P7,FishingHook:z7,Flag:T7,FlagOff:F7,FlagTriangleLeft:D7,FlagTriangleRight:b7,Flame:q7,FlameKindling:R7,Flashlight:O7,FlashlightOff:U7,FlaskConical:W7,FlaskConicalOff:Z7,FlaskRound:G7,FlipHorizontal:I7,FlipHorizontal2:E7,FlipVertical:j7,FlipVertical2:X7,Flower:K7,Flower2:N7,Focus:Q7,FoldHorizontal:J7,FoldVertical:Y7,Folder:SM,FolderArchive:_7,FolderCheck:x7,FolderClock:aM,FolderClosed:tM,FolderCode:hM,FolderCog:n2,FolderCog2:n2,FolderDot:dM,FolderDown:cM,FolderEdit:l2,FolderGit:pM,FolderGit2:MM,FolderHeart:iM,FolderInput:nM,FolderKanban:lM,FolderKey:eM,FolderLock:rM,FolderMinus:oM,FolderOpen:$M,FolderOpenDot:vM,FolderOutput:mM,FolderPen:l2,FolderPlus:yM,FolderRoot:sM,FolderSearch:CM,FolderSearch2:gM,FolderSymlink:uM,FolderSync:HM,FolderTree:AM,FolderUp:wM,FolderX:VM,Folders:LM,Footprints:fM,ForkKnife:Ha,ForkKnifeCrossed:ua,Forklift:kM,Form:PM,FormInput:N2,Forward:BM,Frame:zM,Framer:FM,Frown:DM,Fuel:bM,Fullscreen:RM,FunctionSquare:z0,Funnel:r2,FunnelPlus:TM,FunnelX:e2,GalleryHorizontal:UM,GalleryHorizontalEnd:qM,GalleryThumbnails:OM,GalleryVertical:GM,GalleryVerticalEnd:ZM,Gamepad:EM,Gamepad2:WM,GamepadDirectional:IM,GanttChart:E,GanttChartSquare:m,Gauge:XM,GaugeCircle:e1,Gavel:jM,Gem:NM,GeorgianLari:KM,Ghost:QM,Gift:JM,GitBranch:_M,GitBranchMinus:YM,GitBranchPlus:xM,GitCommit:o2,GitCommitHorizontal:o2,GitCommitVertical:a9,GitCompare:h9,GitCompareArrows:t9,GitFork:d9,GitGraph:c9,GitMerge:M9,GitPullRequest:r9,GitPullRequestArrow:p9,GitPullRequestClosed:i9,GitPullRequestCreate:l9,GitPullRequestCreateArrow:n9,GitPullRequestDraft:e9,Github:o9,Gitlab:v9,GlassWater:$9,Glasses:m9,Globe:s9,Globe2:D1,GlobeLock:y9,Goal:g9,Gpu:C9,Grab:s2,GraduationCap:u9,Grape:H9,Grid:o,Grid2X2:y2,Grid2X2Check:v2,Grid2X2Plus:$2,Grid2X2X:m2,Grid2x2:y2,Grid2x2Check:v2,Grid2x2Plus:$2,Grid2x2X:m2,Grid3X3:o,Grid3x2:A9,Grid3x3:o,Grip:S9,GripHorizontal:w9,GripVertical:V9,Group:L9,Guitar:k9,Ham:f9,Hamburger:P9,Hammer:B9,Hand:T9,HandCoins:z9,HandFist:F9,HandGrab:s2,HandHeart:D9,HandHelping:g2,HandMetal:b9,HandPlatter:R9,Handbag:q9,Handshake:U9,HardDrive:I9,HardDriveDownload:O9,HardDriveUpload:Z9,HardHat:G9,Hash:W9,HatGlasses:E9,Haze:X9,Hd:j9,HdmiPort:N9,Heading:ap,Heading1:K9,Heading2:J9,Heading3:Q9,Heading4:Y9,Heading5:_9,Heading6:x9,HeadphoneOff:tp,Headphones:hp,Headset:dp,Heart:rp,HeartCrack:cp,HeartHandshake:Mp,HeartMinus:pp,HeartOff:ip,HeartPlus:np,HeartPulse:lp,Heater:ep,Helicopter:op,HelpCircle:l,HelpingHand:g2,Hexagon:vp,Highlighter:$p,History:mp,Home:C2,Hop:yp,HopOff:sp,Hospital:gp,Hotel:Cp,Hourglass:up,House:C2,HouseHeart:Hp,HousePlug:Ap,HousePlus:wp,HouseWifi:Vp,IceCream:H2,IceCream2:u2,IceCreamBowl:u2,IceCreamCone:H2,IdCard:Lp,IdCardLanyard:Sp,Image:bp,ImageDown:fp,ImageMinus:kp,ImageOff:Pp,ImagePlay:zp,ImagePlus:Bp,ImageUp:Fp,ImageUpscale:Dp,Images:Rp,Import:Tp,Inbox:qp,Indent:$,IndentDecrease:v,IndentIncrease:$,IndianRupee:Up,Infinity:Op,Info:Zp,Inspect:q0,InspectionPanel:Gp,Instagram:Wp,Italic:Ip,IterationCcw:Ep,IterationCw:Xp,JapaneseYen:jp,Joystick:Np,Kanban:Kp,KanbanSquare:F0,KanbanSquareDashed:S0,Kayak:Qp,Key:_p,KeyRound:Jp,KeySquare:Yp,Keyboard:ti,KeyboardMusic:xp,KeyboardOff:ai,Lamp:ii,LampCeiling:hi,LampDesk:di,LampFloor:ci,LampWallDown:Mi,LampWallUp:pi,LandPlot:ni,Landmark:li,Languages:ei,Laptop:oi,Laptop2:A2,LaptopMinimal:A2,LaptopMinimalCheck:ri,Lasso:$i,LassoSelect:vi,Laugh:mi,Layers:w2,Layers2:yi,Layers3:w2,LayersPlus:si,Layout:I2,LayoutDashboard:gi,LayoutGrid:Ci,LayoutList:ui,LayoutPanelLeft:Hi,LayoutPanelTop:Ai,LayoutTemplate:wi,Leaf:Vi,LeafyGreen:Si,Lectern:Li,LetterText:Ma,Library:ki,LibraryBig:fi,LibrarySquare:D0,LifeBuoy:Pi,Ligature:Bi,Lightbulb:Fi,LightbulbOff:zi,LineChart:G,LineSquiggle:Di,Link:Ti,Link2:bi,Link2Off:Ri,Linkedin:qi,List:hn,ListCheck:Ui,ListChecks:Oi,ListChevronsDownUp:Zi,ListChevronsUpDown:Gi,ListCollapse:Wi,ListEnd:Ii,ListFilter:Xi,ListFilterPlus:Ei,ListIndentDecrease:v,ListIndentIncrease:$,ListMinus:ji,ListMusic:Ni,ListOrdered:Ki,ListPlus:Qi,ListRestart:Ji,ListStart:Yi,ListTodo:_i,ListTree:xi,ListVideo:an,ListX:tn,Loader:cn,Loader2:V2,LoaderCircle:V2,LoaderPinwheel:dn,Locate:nn,LocateFixed:Mn,LocateOff:pn,LocationEdit:k2,Lock:en,LockKeyhole:ln,LockKeyholeOpen:S2,LockOpen:L2,LogIn:rn,LogOut:on,Logs:vn,Lollipop:$n,Luggage:mn,MSquare:b0,Magnet:yn,Mail:Vn,MailCheck:sn,MailMinus:gn,MailOpen:Cn,MailPlus:un,MailQuestion:f2,MailQuestionMark:f2,MailSearch:Hn,MailWarning:An,MailX:wn,Mailbox:Sn,Mails:Ln,Map:Gn,MapMinus:fn,MapPin:Un,MapPinCheck:Pn,MapPinCheckInside:kn,MapPinHouse:Bn,MapPinMinus:Fn,MapPinMinusInside:zn,MapPinOff:Dn,MapPinPen:k2,MapPinPlus:Rn,MapPinPlusInside:bn,MapPinX:qn,MapPinXInside:Tn,MapPinned:On,MapPlus:Zn,Mars:In,MarsStroke:Wn,Martini:En,Maximize:jn,Maximize2:Xn,Medal:Qn,Megaphone:Kn,MegaphoneOff:Nn,Meh:Jn,MemoryStick:Yn,Menu:xn,MenuSquare:R0,Merge:_n,MessageCircle:ll,MessageCircleCode:al,MessageCircleDashed:tl,MessageCircleHeart:hl,MessageCircleMore:dl,MessageCircleOff:cl,MessageCirclePlus:Ml,MessageCircleQuestion:P2,MessageCircleQuestionMark:P2,MessageCircleReply:pl,MessageCircleWarning:il,MessageCircleX:nl,MessageSquare:Sl,MessageSquareCode:el,MessageSquareDashed:rl,MessageSquareDiff:ol,MessageSquareDot:vl,MessageSquareHeart:$l,MessageSquareLock:ml,MessageSquareMore:yl,MessageSquareOff:sl,MessageSquarePlus:gl,MessageSquareQuote:Cl,MessageSquareReply:ul,MessageSquareShare:Hl,MessageSquareText:Al,MessageSquareWarning:wl,MessageSquareX:Vl,MessagesSquare:Ll,Mic:kl,Mic2:B2,MicOff:fl,MicVocal:B2,Microchip:Pl,Microscope:Bl,Microwave:zl,Milestone:Fl,Milk:bl,MilkOff:Dl,Minimize:Tl,Minimize2:Rl,Minus:ql,MinusCircle:r1,MinusSquare:T0,Monitor:Yl,MonitorCheck:Ul,MonitorCloud:Ol,MonitorCog:Zl,MonitorDot:Gl,MonitorDown:Wl,MonitorOff:Il,MonitorPause:El,MonitorPlay:Xl,MonitorSmartphone:jl,MonitorSpeaker:Nl,MonitorStop:Kl,MonitorUp:Ql,MonitorX:Jl,Moon:ae,MoonStar:_l,MoreHorizontal:R1,MoreVertical:b1,Motorbike:xl,Mountain:he,MountainSnow:te,Mouse:le,MouseOff:de,MousePointer:ne,MousePointer2:Me,MousePointer2Off:ce,MousePointerBan:pe,MousePointerClick:ie,MousePointerSquareDashed:L0,Move:Ae,Move3D:z2,Move3d:z2,MoveDiagonal:re,MoveDiagonal2:ee,MoveDown:$e,MoveDownLeft:oe,MoveDownRight:ve,MoveHorizontal:ye,MoveLeft:me,MoveRight:se,MoveUp:ue,MoveUpLeft:ge,MoveUpRight:Ce,MoveVertical:He,Music:Le,Music2:we,Music3:Ve,Music4:Se,Navigation:Be,Navigation2:ke,Navigation2Off:fe,NavigationOff:Pe,Network:Fe,Newspaper:ze,Nfc:De,NonBinary:be,Notebook:Ue,NotebookPen:Re,NotebookTabs:Te,NotebookText:qe,NotepadText:Ze,NotepadTextDashed:Oe,Nut:We,NutOff:Ge,Octagon:Ee,OctagonAlert:F2,OctagonMinus:Ie,OctagonPause:D2,OctagonX:b2,Omega:Xe,Option:je,Orbit:Ne,Origami:Ke,Outdent:v,Package:hr,Package2:Qe,PackageCheck:Je,PackageMinus:Ye,PackageOpen:_e,PackagePlus:xe,PackageSearch:ar,PackageX:tr,PaintBucket:dr,PaintRoller:cr,Paintbrush:Mr,Paintbrush2:R2,PaintbrushVertical:R2,Palette:pr,Palmtree:la,Panda:ir,PanelBottom:er,PanelBottomClose:nr,PanelBottomDashed:T2,PanelBottomInactive:T2,PanelBottomOpen:lr,PanelLeft:Z2,PanelLeftClose:q2,PanelLeftDashed:U2,PanelLeftInactive:U2,PanelLeftOpen:O2,PanelLeftRightDashed:rr,PanelRight:$r,PanelRightClose:or,PanelRightDashed:G2,PanelRightInactive:G2,PanelRightOpen:vr,PanelTop:Cr,PanelTopBottomDashed:mr,PanelTopClose:yr,PanelTopDashed:W2,PanelTopInactive:W2,PanelTopOpen:sr,PanelsLeftBottom:gr,PanelsLeftRight:B1,PanelsRightBottom:ur,PanelsTopBottom:J2,PanelsTopLeft:I2,Paperclip:Hr,Parentheses:wr,ParkingCircle:v1,ParkingCircleOff:o1,ParkingMeter:Ar,ParkingSquare:O0,ParkingSquareOff:U0,PartyPopper:Vr,Pause:Sr,PauseCircle:$1,PauseOctagon:D2,PawPrint:Lr,PcCase:fr,Pen:X2,PenBox:i,PenLine:E2,PenOff:kr,PenSquare:i,PenTool:Pr,Pencil:Dr,PencilLine:Br,PencilOff:zr,PencilRuler:Fr,Pentagon:br,Percent:Rr,PercentCircle:m1,PercentDiamond:F1,PercentSquare:Z0,PersonStanding:Tr,PhilippinePeso:qr,Phone:Er,PhoneCall:Or,PhoneForwarded:Ur,PhoneIncoming:Zr,PhoneMissed:Gr,PhoneOff:Wr,PhoneOutgoing:Ir,Pi:Xr,PiSquare:G0,Piano:jr,Pickaxe:Nr,PictureInPicture:Qr,PictureInPicture2:Kr,PieChart:X,PiggyBank:Jr,Pilcrow:xr,PilcrowLeft:Yr,PilcrowRight:_r,PilcrowSquare:W0,Pill:to,PillBottle:ao,Pin:co,PinOff:ho,Pipette:Mo,Pizza:po,Plane:lo,PlaneLanding:io,PlaneTakeoff:no,Play:eo,PlayCircle:y1,PlaySquare:I0,Plug:oo,Plug2:ro,PlugZap:j2,PlugZap2:j2,Plus:vo,PlusCircle:s1,PlusSquare:E0,Pocket:mo,PocketKnife:$o,Podcast:yo,Pointer:go,PointerOff:so,Popcorn:Co,Popsicle:uo,PoundSterling:Ho,Power:wo,PowerCircle:g1,PowerOff:Ao,PowerSquare:X0,Presentation:Vo,Printer:So,PrinterCheck:Lo,Projector:fo,Proportions:ko,Puzzle:Po,Pyramid:Bo,QrCode:zo,Quote:Fo,Rabbit:Do,Radar:bo,Radiation:Ro,Radical:To,Radio:Oo,RadioReceiver:qo,RadioTower:Uo,Radius:Zo,RailSymbol:Go,Rainbow:Wo,Rat:Io,Ratio:Eo,Receipt:av,ReceiptCent:Xo,ReceiptEuro:jo,ReceiptIndianRupee:No,ReceiptJapaneseYen:Ko,ReceiptPoundSterling:Qo,ReceiptRussianRuble:Jo,ReceiptSwissFranc:Yo,ReceiptText:_o,ReceiptTurkishLira:xo,RectangleCircle:tv,RectangleEllipsis:N2,RectangleGoggles:hv,RectangleHorizontal:dv,RectangleVertical:cv,Recycle:Mv,Redo:nv,Redo2:pv,RedoDot:iv,RefreshCcw:ev,RefreshCcwDot:lv,RefreshCw:ov,RefreshCwOff:rv,Refrigerator:vv,Regex:$v,RemoveFormatting:mv,Repeat:gv,Repeat1:yv,Repeat2:sv,Replace:uv,ReplaceAll:Cv,Reply:Av,ReplyAll:Hv,Rewind:wv,Ribbon:Vv,Rocket:Sv,RockingChair:Lv,RollerCoaster:fv,Rose:kv,Rotate3D:K2,Rotate3d:K2,RotateCcw:zv,RotateCcwKey:Pv,RotateCcwSquare:Bv,RotateCw:Dv,RotateCwSquare:Fv,Route:bv,RouteOff:Rv,Router:Tv,Rows:Q2,Rows2:Q2,Rows3:J2,Rows4:qv,Rss:Uv,Ruler:Zv,RulerDimensionLine:Ov,RussianRuble:Gv,Sailboat:Wv,Salad:Iv,Sandwich:Ev,Satellite:jv,SatelliteDish:Xv,SaudiRiyal:Nv,Save:Jv,SaveAll:Kv,SaveOff:Qv,Scale:Yv,Scale3D:Y2,Scale3d:Y2,Scaling:_v,Scan:i$,ScanBarcode:a$,ScanEye:xv,ScanFace:t$,ScanHeart:h$,ScanLine:d$,ScanQrCode:c$,ScanSearch:M$,ScanText:p$,ScatterChart:j,School:n$,School2:oa,Scissors:e$,ScissorsLineDashed:l$,ScissorsSquare:j0,ScissorsSquareDashedBottom:s0,Scooter:r$,ScreenShare:v$,ScreenShareOff:o$,Scroll:m$,ScrollText:$$,Search:H$,SearchAlert:y$,SearchCheck:g$,SearchCode:s$,SearchSlash:C$,SearchX:u$,Section:A$,Send:V$,SendHorizonal:_2,SendHorizontal:_2,SendToBack:w$,SeparatorHorizontal:S$,SeparatorVertical:L$,Server:B$,ServerCog:f$,ServerCrash:k$,ServerOff:P$,Settings:F$,Settings2:z$,Shapes:D$,Share:R$,Share2:b$,Sheet:T$,Shell:q$,Shield:N$,ShieldAlert:U$,ShieldBan:O$,ShieldCheck:Z$,ShieldClose:a0,ShieldEllipsis:G$,ShieldHalf:W$,ShieldMinus:I$,ShieldOff:E$,ShieldPlus:X$,ShieldQuestion:x2,ShieldQuestionMark:x2,ShieldUser:j$,ShieldX:a0,Ship:Q$,ShipWheel:K$,Shirt:J$,ShoppingBag:Y$,ShoppingBasket:_$,ShoppingCart:x$,Shovel:am,ShowerHead:tm,Shredder:hm,Shrimp:dm,Shrink:cm,Shrub:Mm,Shuffle:pm,Sidebar:Z2,SidebarClose:q2,SidebarOpen:O2,Sigma:nm,SigmaSquare:N0,Signal:om,SignalHigh:im,SignalLow:lm,SignalMedium:em,SignalZero:rm,Signature:vm,Signpost:mm,SignpostBig:$m,Siren:ym,SkipBack:sm,SkipForward:Cm,Skull:gm,Slack:um,Slash:Hm,SlashSquare:K0,Slice:Am,Sliders:t0,SlidersHorizontal:wm,SlidersVertical:t0,Smartphone:Lm,SmartphoneCharging:Vm,SmartphoneNfc:Sm,Smile:km,SmilePlus:fm,Snail:Pm,Snowflake:Bm,SoapDispenserDroplet:zm,Sofa:Fm,SolarPanel:Dm,SortAsc:V,SortDesc:H,Soup:bm,Space:Rm,Spade:Tm,Sparkle:qm,Sparkles:h0,Speaker:Um,Speech:Om,SpellCheck:Gm,SpellCheck2:Zm,Spline:Im,SplinePointer:Wm,Split:Em,SplitSquareHorizontal:Q0,SplitSquareVertical:J0,Spool:Xm,Spotlight:jm,SprayCan:Nm,Sprout:Km,Square:My,SquareActivity:d0,SquareArrowDown:p0,SquareArrowDownLeft:c0,SquareArrowDownRight:M0,SquareArrowLeft:i0,SquareArrowOutDownLeft:n0,SquareArrowOutDownRight:l0,SquareArrowOutUpLeft:e0,SquareArrowOutUpRight:r0,SquareArrowRight:o0,SquareArrowUp:m0,SquareArrowUpLeft:v0,SquareArrowUpRight:$0,SquareAsterisk:y0,SquareBottomDashedScissors:s0,SquareChartGantt:m,SquareCheck:g0,SquareCheckBig:C0,SquareChevronDown:u0,SquareChevronLeft:H0,SquareChevronRight:A0,SquareChevronUp:w0,SquareCode:V0,SquareDashed:f0,SquareDashedBottom:Jm,SquareDashedBottomCode:Qm,SquareDashedKanban:S0,SquareDashedMousePointer:L0,SquareDashedTopSolid:Ym,SquareDivide:k0,SquareDot:P0,SquareEqual:B0,SquareFunction:z0,SquareGanttChart:m,SquareKanban:F0,SquareLibrary:D0,SquareM:b0,SquareMenu:R0,SquareMinus:T0,SquareMousePointer:q0,SquareParking:O0,SquareParkingOff:U0,SquarePause:_m,SquarePen:i,SquarePercent:Z0,SquarePi:G0,SquarePilcrow:W0,SquarePlay:I0,SquarePlus:E0,SquarePower:X0,SquareRadical:xm,SquareRoundCorner:ay,SquareScissors:j0,SquareSigma:N0,SquareSlash:K0,SquareSplitHorizontal:Q0,SquareSplitVertical:J0,SquareSquare:ty,SquareStack:hy,SquareStar:dy,SquareStop:cy,SquareTerminal:Y0,SquareUser:x0,SquareUserRound:_0,SquareX:aa,SquaresExclude:py,SquaresIntersect:iy,SquaresSubtract:ny,SquaresUnite:ey,Squircle:ry,SquircleDashed:ly,Squirrel:oy,Stamp:vy,Star:yy,StarHalf:my,StarOff:$y,Stars:h0,StepBack:sy,StepForward:gy,Stethoscope:Cy,Sticker:uy,StickyNote:Ay,Stone:Hy,StopCircle:u1,Store:wy,StretchHorizontal:Vy,StretchVertical:Sy,Strikethrough:Ly,Subscript:fy,Subtitles:D,Sun:Fy,SunDim:ky,SunMedium:Py,SunMoon:By,SunSnow:zy,Sunrise:Dy,Sunset:by,Superscript:Ry,SwatchBook:Ty,SwissFranc:qy,SwitchCamera:Uy,Sword:Oy,Swords:Zy,Syringe:Gy,Table:Qy,Table2:Wy,TableCellsMerge:Iy,TableCellsSplit:Ey,TableColumnsSplit:jy,TableConfig:e,TableOfContents:Xy,TableProperties:Ny,TableRowsSplit:Ky,Tablet:Yy,TabletSmartphone:Jy,Tablets:_y,Tag:xy,Tags:as,Tally1:ts,Tally2:hs,Tally3:ds,Tally4:cs,Tally5:Ms,Tangent:ps,Target:is,Telescope:ns,Tent:es,TentTree:ls,Terminal:rs,TerminalSquare:Y0,TestTube:os,TestTube2:ta,TestTubeDiagonal:ta,TestTubes:vs,Text:y,TextAlignCenter:ha,TextAlignEnd:da,TextAlignJustify:ca,TextAlignStart:y,TextCursor:ms,TextCursorInput:$s,TextInitial:Ma,TextQuote:ys,TextSearch:ss,TextSelect:pa,TextSelection:pa,TextWrap:ia,Theater:gs,Thermometer:Hs,ThermometerSnowflake:Cs,ThermometerSun:us,ThumbsDown:As,ThumbsUp:ws,Ticket:Bs,TicketCheck:Vs,TicketMinus:Ss,TicketPercent:Ls,TicketPlus:fs,TicketSlash:ks,TicketX:Ps,Tickets:Fs,TicketsPlane:zs,Timer:Rs,TimerOff:bs,TimerReset:Ds,ToggleLeft:Ts,ToggleRight:qs,Toilet:Us,ToolCase:Os,Toolbox:Zs,Tornado:Gs,Torus:Ws,Touchpad:Es,TouchpadOff:Is,TowerControl:Xs,ToyBrick:js,Tractor:Ks,TrafficCone:Ns,Train:na,TrainFront:Js,TrainFrontTunnel:Qs,TrainTrack:Ys,TramFront:na,Transgender:_s,Trash:ag,Trash2:xs,TreeDeciduous:tg,TreePalm:la,TreePine:hg,Trees:dg,Trello:cg,TrendingDown:Mg,TrendingUp:ig,TrendingUpDown:pg,Triangle:eg,TriangleAlert:ea,TriangleDashed:ng,TriangleRight:lg,Trophy:rg,Truck:vg,TruckElectric:og,TurkishLira:$g,Turntable:mg,Turtle:yg,Tv:gg,Tv2:ra,TvMinimal:ra,TvMinimalPlay:sg,Twitch:Cg,Twitter:ug,Type:Ag,TypeOutline:Hg,Umbrella:Vg,UmbrellaOff:wg,Underline:Sg,Undo:kg,Undo2:Lg,UndoDot:fg,UnfoldHorizontal:Pg,UnfoldVertical:Bg,Ungroup:zg,University:oa,Unlink:Dg,Unlink2:Fg,Unlock:L2,UnlockKeyhole:S2,Unplug:bg,Upload:Tg,UploadCloud:f1,Usb:Rg,User:Kg,User2:sa,UserCheck:qg,UserCheck2:va,UserCircle:A1,UserCircle2:H1,UserCog:Ug,UserCog2:$a,UserLock:Og,UserMinus:Zg,UserMinus2:ma,UserPen:Gg,UserPlus:Wg,UserPlus2:ya,UserRound:sa,UserRoundCheck:va,UserRoundCog:$a,UserRoundMinus:ma,UserRoundPen:Ig,UserRoundPlus:ya,UserRoundSearch:Eg,UserRoundX:ga,UserSearch:Xg,UserSquare:x0,UserSquare2:_0,UserStar:jg,UserX:Ng,UserX2:ga,Users:Qg,Users2:Ca,UsersRound:Ca,Utensils:Ha,UtensilsCrossed:ua,UtilityPole:Yg,Van:Jg,Variable:_g,Vault:xg,VectorSquare:aC,Vegan:tC,VenetianMask:hC,Venus:dC,VenusAndMars:cC,Verified:f,Vibrate:pC,VibrateOff:MC,Video:nC,VideoOff:iC,Videotape:eC,View:rC,Voicemail:lC,Volleyball:oC,Volume:sC,Volume1:vC,Volume2:$C,VolumeOff:mC,VolumeX:yC,Vote:gC,Wallet:uC,Wallet2:Aa,WalletCards:CC,WalletMinimal:Aa,Wallpaper:HC,Wand:AC,Wand2:wa,WandSparkles:wa,Warehouse:wC,WashingMachine:VC,Watch:SC,Waves:PC,WavesArrowDown:LC,WavesArrowUp:fC,WavesLadder:kC,Waypoints:BC,Webcam:zC,Webhook:DC,WebhookOff:FC,Weight:RC,WeightTilde:bC,Wheat:qC,WheatOff:TC,WholeWord:UC,Wifi:jC,WifiCog:OC,WifiHigh:GC,WifiLow:ZC,WifiOff:WC,WifiPen:IC,WifiSync:EC,WifiZero:XC,Wind:KC,WindArrowDown:NC,Wine:JC,WineOff:QC,Workflow:_C,Worm:YC,WrapText:ia,Wrench:xC,X:au,XCircle:w1,XOctagon:b2,XSquare:aa,Youtube:tu,Zap:du,ZapOff:hu,ZoomIn:cu,ZoomOut:Mu});const iu=({icons:t=pu,nameAttr:h="data-lucide",attrs:d={},root:c=document,inTemplates:M}={})=>{if(!Object.values(t).length)throw new Error(`Please provide an icons object. 9 + If you want to use all the icons you can import it like: 10 + \`import { createIcons, icons } from 'lucide'; 11 + lucide.createIcons({icons});\``);if(typeof c>"u")throw new Error("`createIcons()` only works in a browser environment.");if(Array.from(c.querySelectorAll(`[${h}]`)).forEach(p=>fa(p,{nameAttr:h,icons:t,attrs:d})),M&&Array.from(c.querySelectorAll("template")).forEach(p=>iu({icons:t,nameAttr:h,attrs:d,root:p.content,inTemplates:M})),h==="data-lucide"){const p=c.querySelectorAll("[icon-name]");p.length>0&&(console.warn("[Lucide] Some icons were found with the now deprecated icon-name attribute. These will still be replaced for backwards compatibility, but will no longer be supported in v1.0 and you should switch to data-lucide"),Array.from(p).forEach(Va=>fa(Va,{nameAttr:"icon-name",icons:t,attrs:d})))}};a.AArrowDown=ka,a.AArrowUp=Pa,a.ALargeSmall=Ba,a.Accessibility=za,a.Activity=Fa,a.ActivitySquare=d0,a.AirVent=Da,a.Airplay=ba,a.AlarmCheck=s,a.AlarmClock=Ta,a.AlarmClockCheck=s,a.AlarmClockMinus=g,a.AlarmClockOff=Ra,a.AlarmClockPlus=C,a.AlarmMinus=g,a.AlarmPlus=C,a.AlarmSmoke=qa,a.Album=Ua,a.AlertCircle=K,a.AlertOctagon=F2,a.AlertTriangle=ea,a.AlignCenter=ha,a.AlignCenterHorizontal=Oa,a.AlignCenterVertical=Za,a.AlignEndHorizontal=Ga,a.AlignEndVertical=Wa,a.AlignHorizontalDistributeCenter=Ia,a.AlignHorizontalDistributeEnd=Xa,a.AlignHorizontalDistributeStart=Ea,a.AlignHorizontalJustifyCenter=ja,a.AlignHorizontalJustifyEnd=Na,a.AlignHorizontalJustifyStart=Ka,a.AlignHorizontalSpaceAround=Qa,a.AlignHorizontalSpaceBetween=Ja,a.AlignJustify=ca,a.AlignLeft=y,a.AlignRight=da,a.AlignStartHorizontal=Ya,a.AlignStartVertical=_a,a.AlignVerticalDistributeCenter=xa,a.AlignVerticalDistributeEnd=at,a.AlignVerticalDistributeStart=tt,a.AlignVerticalJustifyCenter=ht,a.AlignVerticalJustifyEnd=dt,a.AlignVerticalJustifyStart=ct,a.AlignVerticalSpaceAround=Mt,a.AlignVerticalSpaceBetween=pt,a.Ambulance=it,a.Ampersand=nt,a.Ampersands=lt,a.Amphora=et,a.Anchor=rt,a.Angry=ot,a.Annoyed=vt,a.Antenna=$t,a.Anvil=mt,a.Aperture=yt,a.AppWindow=gt,a.AppWindowMac=st,a.Apple=Ct,a.Archive=At,a.ArchiveRestore=ut,a.ArchiveX=Ht,a.AreaChart=b,a.Armchair=wt,a.ArrowBigDown=St,a.ArrowBigDownDash=Vt,a.ArrowBigLeft=ft,a.ArrowBigLeftDash=Lt,a.ArrowBigRight=Pt,a.ArrowBigRightDash=kt,a.ArrowBigUp=zt,a.ArrowBigUpDash=Bt,a.ArrowDown=Gt,a.ArrowDown01=Ft,a.ArrowDown10=Dt,a.ArrowDownAZ=u,a.ArrowDownAz=u,a.ArrowDownCircle=Q,a.ArrowDownFromLine=bt,a.ArrowDownLeft=Rt,a.ArrowDownLeftFromCircle=Y,a.ArrowDownLeftFromSquare=n0,a.ArrowDownLeftSquare=c0,a.ArrowDownNarrowWide=Ut,a.ArrowDownRight=Tt,a.ArrowDownRightFromCircle=_,a.ArrowDownRightFromSquare=l0,a.ArrowDownRightSquare=M0,a.ArrowDownSquare=p0,a.ArrowDownToDot=qt,a.ArrowDownToLine=Ot,a.ArrowDownUp=Zt,a.ArrowDownWideNarrow=H,a.ArrowDownZA=A,a.ArrowDownZa=A,a.ArrowLeft=Xt,a.ArrowLeftCircle=J,a.ArrowLeftFromLine=It,a.ArrowLeftRight=Wt,a.ArrowLeftSquare=i0,a.ArrowLeftToLine=Et,a.ArrowRight=Qt,a.ArrowRightCircle=t1,a.ArrowRightFromLine=jt,a.ArrowRightLeft=Nt,a.ArrowRightSquare=o0,a.ArrowRightToLine=Kt,a.ArrowUp=Mh,a.ArrowUp01=Jt,a.ArrowUp10=Yt,a.ArrowUpAZ=w,a.ArrowUpAz=w,a.ArrowUpCircle=h1,a.ArrowUpDown=_t,a.ArrowUpFromDot=xt,a.ArrowUpFromLine=ah,a.ArrowUpLeft=th,a.ArrowUpLeftFromCircle=x,a.ArrowUpLeftFromSquare=e0,a.ArrowUpLeftSquare=v0,a.ArrowUpNarrowWide=V,a.ArrowUpRight=hh,a.ArrowUpRightFromCircle=a1,a.ArrowUpRightFromSquare=r0,a.ArrowUpRightSquare=$0,a.ArrowUpSquare=m0,a.ArrowUpToLine=dh,a.ArrowUpWideNarrow=ch,a.ArrowUpZA=S,a.ArrowUpZa=S,a.ArrowsUpFromLine=ph,a.Asterisk=ih,a.AsteriskSquare=y0,a.AtSign=nh,a.Atom=lh,a.AudioLines=eh,a.AudioWaveform=oh,a.Award=rh,a.Axe=vh,a.Axis3D=L,a.Axis3d=L,a.Baby=$h,a.Backpack=mh,a.Badge=zh,a.BadgeAlert=yh,a.BadgeCent=sh,a.BadgeCheck=f,a.BadgeDollarSign=gh,a.BadgeEuro=Ch,a.BadgeHelp=k,a.BadgeIndianRupee=uh,a.BadgeInfo=Hh,a.BadgeJapaneseYen=Ah,a.BadgeMinus=wh,a.BadgePercent=Vh,a.BadgePlus=Sh,a.BadgePoundSterling=Lh,a.BadgeQuestionMark=k,a.BadgeRussianRuble=fh,a.BadgeSwissFranc=kh,a.BadgeTurkishLira=Ph,a.BadgeX=Bh,a.BaggageClaim=Fh,a.Balloon=Dh,a.Ban=bh,a.Banana=Rh,a.Bandage=Th,a.Banknote=Zh,a.BanknoteArrowDown=qh,a.BanknoteArrowUp=Uh,a.BanknoteX=Oh,a.BarChart=W,a.BarChart2=I,a.BarChart3=Z,a.BarChart4=O,a.BarChartBig=U,a.BarChartHorizontal=T,a.BarChartHorizontalBig=R,a.Barcode=Gh,a.Barrel=Wh,a.Baseline=Ih,a.Bath=Eh,a.Battery=Yh,a.BatteryCharging=Xh,a.BatteryFull=jh,a.BatteryLow=Nh,a.BatteryMedium=Qh,a.BatteryPlus=Kh,a.BatteryWarning=Jh,a.Beaker=_h,a.Bean=a5,a.BeanOff=xh,a.Bed=d5,a.BedDouble=t5,a.BedSingle=h5,a.Beef=c5,a.Beer=p5,a.BeerOff=M5,a.Bell=v5,a.BellDot=i5,a.BellElectric=n5,a.BellMinus=l5,a.BellOff=e5,a.BellPlus=r5,a.BellRing=o5,a.BetweenHorizonalEnd=P,a.BetweenHorizonalStart=B,a.BetweenHorizontalEnd=P,a.BetweenHorizontalStart=B,a.BetweenVerticalEnd=$5,a.BetweenVerticalStart=m5,a.BicepsFlexed=y5,a.Bike=s5,a.Binary=g5,a.Binoculars=u5,a.Biohazard=C5,a.Bird=A5,a.Birdhouse=H5,a.Bitcoin=w5,a.Blend=V5,a.Blinds=S5,a.Blocks=L5,a.Bluetooth=B5,a.BluetoothConnected=f5,a.BluetoothOff=k5,a.BluetoothSearching=P5,a.Bold=z5,a.Bolt=F5,a.Bomb=D5,a.Bone=b5,a.Book=M4,a.BookA=R5,a.BookAlert=T5,a.BookAudio=q5,a.BookCheck=U5,a.BookCopy=O5,a.BookDashed=z,a.BookDown=Z5,a.BookHeadphones=G5,a.BookHeart=W5,a.BookImage=E5,a.BookKey=I5,a.BookLock=X5,a.BookMarked=j5,a.BookMinus=N5,a.BookOpen=J5,a.BookOpenCheck=K5,a.BookOpenText=Q5,a.BookPlus=Y5,a.BookSearch=_5,a.BookTemplate=z,a.BookText=x5,a.BookType=t4,a.BookUp=a4,a.BookUp2=h4,a.BookUser=d4,a.BookX=c4,a.Bookmark=e4,a.BookmarkCheck=p4,a.BookmarkMinus=i4,a.BookmarkPlus=n4,a.BookmarkX=l4,a.BoomBox=r4,a.Bot=$4,a.BotMessageSquare=o4,a.BotOff=v4,a.BottleWine=m4,a.BowArrow=y4,a.Box=s4,a.BoxSelect=f0,a.Boxes=g4,a.Braces=F,a.Brackets=C4,a.Brain=A4,a.BrainCircuit=u4,a.BrainCog=H4,a.BrickWall=S4,a.BrickWallFire=w4,a.BrickWallShield=V4,a.Briefcase=P4,a.BriefcaseBusiness=L4,a.BriefcaseConveyorBelt=f4,a.BriefcaseMedical=k4,a.BringToFront=B4,a.Brush=F4,a.BrushCleaning=z4,a.Bubbles=D4,a.Bug=T4,a.BugOff=b4,a.BugPlay=R4,a.Building=U4,a.Building2=q4,a.Bus=Z4,a.BusFront=O4,a.Cable=W4,a.CableCar=G4,a.Cake=E4,a.CakeSlice=I4,a.Calculator=X4,a.Calendar=o3,a.Calendar1=j4,a.CalendarArrowDown=N4,a.CalendarArrowUp=K4,a.CalendarCheck=J4,a.CalendarCheck2=Q4,a.CalendarClock=_4,a.CalendarCog=Y4,a.CalendarDays=x4,a.CalendarFold=a3,a.CalendarHeart=t3,a.CalendarMinus=d3,a.CalendarMinus2=h3,a.CalendarOff=c3,a.CalendarPlus=p3,a.CalendarPlus2=M3,a.CalendarRange=i3,a.CalendarSearch=n3,a.CalendarSync=l3,a.CalendarX=r3,a.CalendarX2=e3,a.Calendars=v3,a.Camera=m3,a.CameraOff=$3,a.CandlestickChart=q,a.Candy=g3,a.CandyCane=y3,a.CandyOff=s3,a.Cannabis=u3,a.CannabisOff=C3,a.Captions=D,a.CaptionsOff=H3,a.Car=V3,a.CarFront=A3,a.CarTaxiFront=w3,a.Caravan=S3,a.CardSim=L3,a.Carrot=f3,a.CaseLower=k3,a.CaseSensitive=P3,a.CaseUpper=B3,a.CassetteTape=z3,a.Cast=F3,a.Castle=D3,a.Cat=b3,a.Cctv=R3,a.ChartArea=b,a.ChartBar=T,a.ChartBarBig=R,a.ChartBarDecreasing=T3,a.ChartBarIncreasing=q3,a.ChartBarStacked=U3,a.ChartCandlestick=q,a.ChartColumn=Z,a.ChartColumnBig=U,a.ChartColumnDecreasing=O3,a.ChartColumnIncreasing=O,a.ChartColumnStacked=Z3,a.ChartGantt=G3,a.ChartLine=G,a.ChartNetwork=W3,a.ChartNoAxesColumn=I,a.ChartNoAxesColumnDecreasing=I3,a.ChartNoAxesColumnIncreasing=W,a.ChartNoAxesCombined=E3,a.ChartNoAxesGantt=E,a.ChartPie=X,a.ChartScatter=j,a.ChartSpline=X3,a.Check=K3,a.CheckCheck=j3,a.CheckCircle=c1,a.CheckCircle2=d1,a.CheckLine=N3,a.CheckSquare=C0,a.CheckSquare2=g0,a.ChefHat=Q3,a.Cherry=J3,a.ChessBishop=Y3,a.ChessKing=_3,a.ChessKnight=x3,a.ChessPawn=ad,a.ChessQueen=td,a.ChessRook=cd,a.ChevronDown=hd,a.ChevronDownCircle=M1,a.ChevronDownSquare=u0,a.ChevronFirst=dd,a.ChevronLast=Md,a.ChevronLeft=pd,a.ChevronLeftCircle=p1,a.ChevronLeftSquare=H0,a.ChevronRight=id,a.ChevronRightCircle=i1,a.ChevronRightSquare=A0,a.ChevronUp=nd,a.ChevronUpCircle=n1,a.ChevronUpSquare=w0,a.ChevronsDown=ed,a.ChevronsDownUp=ld,a.ChevronsLeft=vd,a.ChevronsLeftRight=od,a.ChevronsLeftRightEllipsis=rd,a.ChevronsRight=md,a.ChevronsRightLeft=$d,a.ChevronsUp=sd,a.ChevronsUpDown=yd,a.Chrome=N,a.Chromium=N,a.Church=gd,a.Cigarette=ud,a.CigaretteOff=Cd,a.Circle=Rd,a.CircleAlert=K,a.CircleArrowDown=Q,a.CircleArrowLeft=J,a.CircleArrowOutDownLeft=Y,a.CircleArrowOutDownRight=_,a.CircleArrowOutUpLeft=x,a.CircleArrowOutUpRight=a1,a.CircleArrowRight=t1,a.CircleArrowUp=h1,a.CircleCheck=d1,a.CircleCheckBig=c1,a.CircleChevronDown=M1,a.CircleChevronLeft=p1,a.CircleChevronRight=i1,a.CircleChevronUp=n1,a.CircleDashed=Hd,a.CircleDivide=l1,a.CircleDollarSign=Ad,a.CircleDot=Vd,a.CircleDotDashed=wd,a.CircleEllipsis=Sd,a.CircleEqual=Ld,a.CircleFadingArrowUp=fd,a.CircleFadingPlus=kd,a.CircleGauge=e1,a.CircleHelp=l,a.CircleMinus=r1,a.CircleOff=Pd,a.CircleParking=v1,a.CircleParkingOff=o1,a.CirclePause=$1,a.CirclePercent=m1,a.CirclePile=Bd,a.CirclePlay=y1,a.CirclePlus=s1,a.CirclePoundSterling=zd,a.CirclePower=g1,a.CircleQuestionMark=l,a.CircleSlash=Fd,a.CircleSlash2=C1,a.CircleSlashed=C1,a.CircleSmall=Dd,a.CircleStar=bd,a.CircleStop=u1,a.CircleUser=A1,a.CircleUserRound=H1,a.CircleX=w1,a.CircuitBoard=Td,a.Citrus=qd,a.Clapperboard=Ud,a.Clipboard=Kd,a.ClipboardCheck=Od,a.ClipboardClock=Zd,a.ClipboardCopy=Gd,a.ClipboardEdit=S1,a.ClipboardList=Wd,a.ClipboardMinus=Id,a.ClipboardPaste=Ed,a.ClipboardPen=S1,a.ClipboardPenLine=V1,a.ClipboardPlus=Xd,a.ClipboardSignature=V1,a.ClipboardType=jd,a.ClipboardX=Nd,a.Clock=v8,a.Clock1=Qd,a.Clock10=Jd,a.Clock11=Yd,a.Clock12=_d,a.Clock2=xd,a.Clock3=a8,a.Clock4=t8,a.Clock5=h8,a.Clock6=d8,a.Clock7=c8,a.Clock8=M8,a.Clock9=p8,a.ClockAlert=i8,a.ClockArrowDown=n8,a.ClockArrowUp=l8,a.ClockCheck=e8,a.ClockFading=r8,a.ClockPlus=o8,a.ClosedCaption=$8,a.Cloud=F8,a.CloudAlert=m8,a.CloudBackup=y8,a.CloudCheck=s8,a.CloudCog=g8,a.CloudDownload=L1,a.CloudDrizzle=C8,a.CloudFog=u8,a.CloudHail=H8,a.CloudLightning=A8,a.CloudMoon=V8,a.CloudMoonRain=w8,a.CloudOff=L8,a.CloudRain=f8,a.CloudRainWind=S8,a.CloudSnow=k8,a.CloudSun=B8,a.CloudSunRain=P8,a.CloudSync=z8,a.CloudUpload=f1,a.Cloudy=D8,a.Clover=b8,a.Club=R8,a.Code=T8,a.Code2=k1,a.CodeSquare=V0,a.CodeXml=k1,a.Codepen=q8,a.Codesandbox=U8,a.Coffee=O8,a.Cog=G8,a.Coins=Z8,a.Columns=P1,a.Columns2=P1,a.Columns3=B1,a.Columns3Cog=e,a.Columns4=W8,a.ColumnsSettings=e,a.Combine=I8,a.Command=E8,a.Compass=X8,a.Component=j8,a.Computer=N8,a.ConciergeBell=K8,a.Cone=Q8,a.Construction=J8,a.Contact=Y8,a.Contact2=z1,a.ContactRound=z1,a.Container=_8,a.Contrast=x8,a.Cookie=a6,a.CookingPot=t6,a.Copy=p6,a.CopyCheck=h6,a.CopyMinus=d6,a.CopyPlus=c6,a.CopySlash=M6,a.CopyX=i6,a.Copyleft=n6,a.Copyright=l6,a.CornerDownLeft=r6,a.CornerDownRight=e6,a.CornerLeftDown=o6,a.CornerLeftUp=v6,a.CornerRightDown=$6,a.CornerRightUp=m6,a.CornerUpLeft=y6,a.CornerUpRight=s6,a.Cpu=g6,a.CreativeCommons=C6,a.CreditCard=u6,a.Croissant=H6,a.Crop=A6,a.Cross=w6,a.Crosshair=V6,a.Crown=S6,a.Cuboid=L6,a.CupSoda=f6,a.CurlyBraces=F,a.Currency=k6,a.Cylinder=P6,a.Dam=B6,a.Database=D6,a.DatabaseBackup=z6,a.DatabaseZap=F6,a.DecimalsArrowLeft=b6,a.DecimalsArrowRight=R6,a.Delete=T6,a.Dessert=q6,a.Diameter=U6,a.Diamond=W6,a.DiamondMinus=O6,a.DiamondPercent=F1,a.DiamondPlus=Z6,a.Dice1=G6,a.Dice2=I6,a.Dice3=E6,a.Dice4=N6,a.Dice5=X6,a.Dice6=j6,a.Dices=K6,a.Diff=Q6,a.Disc=x6,a.Disc2=J6,a.Disc3=Y6,a.DiscAlbum=_6,a.Divide=ac,a.DivideCircle=l1,a.DivideSquare=k0,a.Dna=dc,a.DnaOff=tc,a.Dock=hc,a.Dog=cc,a.DollarSign=Mc,a.Donut=pc,a.DoorClosed=nc,a.DoorClosedLocked=ic,a.DoorOpen=ec,a.Dot=lc,a.DotSquare=P0,a.Download=rc,a.DownloadCloud=L1,a.DraftingCompass=oc,a.Drama=vc,a.Dribbble=$c,a.Drill=mc,a.Drone=yc,a.Droplet=gc,a.DropletOff=sc,a.Droplets=Cc,a.Drum=uc,a.Drumstick=Hc,a.Dumbbell=Ac,a.Ear=Vc,a.EarOff=wc,a.Earth=D1,a.EarthLock=Sc,a.Eclipse=Lc,a.Edit=i,a.Edit2=X2,a.Edit3=E2,a.Egg=Pc,a.EggFried=fc,a.EggOff=kc,a.Ellipsis=R1,a.EllipsisVertical=b1,a.Equal=Dc,a.EqualApproximately=Bc,a.EqualNot=zc,a.EqualSquare=B0,a.Eraser=Fc,a.EthernetPort=bc,a.Euro=Rc,a.EvCharger=Tc,a.Expand=Uc,a.ExternalLink=qc,a.Eye=Gc,a.EyeClosed=Oc,a.EyeOff=Zc,a.Facebook=Ic,a.Factory=Wc,a.Fan=Ec,a.FastForward=Xc,a.Feather=jc,a.Fence=Nc,a.FerrisWheel=Kc,a.Figma=Qc,a.File=V7,a.FileArchive=Jc,a.FileAudio=r,a.FileAudio2=r,a.FileAxis3D=T1,a.FileAxis3d=T1,a.FileBadge=q1,a.FileBadge2=q1,a.FileBarChart=Z1,a.FileBarChart2=G1,a.FileBox=Yc,a.FileBraces=O1,a.FileBracesCorner=U1,a.FileChartColumn=G1,a.FileChartColumnIncreasing=Z1,a.FileChartLine=W1,a.FileChartPie=I1,a.FileCheck=_c,a.FileCheck2=E1,a.FileCheckCorner=E1,a.FileClock=xc,a.FileCode=a7,a.FileCode2=X1,a.FileCodeCorner=X1,a.FileCog=j1,a.FileCog2=j1,a.FileDiff=t7,a.FileDigit=h7,a.FileDown=d7,a.FileEdit=_1,a.FileExclamationPoint=N1,a.FileHeadphone=r,a.FileHeart=c7,a.FileImage=M7,a.FileInput=p7,a.FileJson=O1,a.FileJson2=U1,a.FileKey=K1,a.FileKey2=K1,a.FileLineChart=W1,a.FileLock=Q1,a.FileLock2=Q1,a.FileMinus=n7,a.FileMinus2=J1,a.FileMinusCorner=J1,a.FileMusic=i7,a.FileOutput=l7,a.FilePen=_1,a.FilePenLine=Y1,a.FilePieChart=I1,a.FilePlay=x1,a.FilePlus=e7,a.FilePlus2=t2,a.FilePlusCorner=t2,a.FileQuestion=a2,a.FileQuestionMark=a2,a.FileScan=r7,a.FileSearch=o7,a.FileSearch2=h2,a.FileSearchCorner=h2,a.FileSignal=d2,a.FileSignature=Y1,a.FileSliders=$7,a.FileSpreadsheet=v7,a.FileStack=m7,a.FileSymlink=y7,a.FileTerminal=s7,a.FileText=g7,a.FileType=C7,a.FileType2=c2,a.FileTypeCorner=c2,a.FileUp=u7,a.FileUser=H7,a.FileVideo=x1,a.FileVideo2=M2,a.FileVideoCamera=M2,a.FileVolume=A7,a.FileVolume2=d2,a.FileWarning=N1,a.FileX=w7,a.FileX2=p2,a.FileXCorner=p2,a.Files=S7,a.Film=L7,a.Filter=r2,a.FilterX=e2,a.Fingerprint=i2,a.FingerprintPattern=i2,a.FireExtinguisher=f7,a.Fish=B7,a.FishOff=k7,a.FishSymbol=P7,a.FishingHook=z7,a.Flag=T7,a.FlagOff=F7,a.FlagTriangleLeft=D7,a.FlagTriangleRight=b7,a.Flame=q7,a.FlameKindling=R7,a.Flashlight=O7,a.FlashlightOff=U7,a.FlaskConical=W7,a.FlaskConicalOff=Z7,a.FlaskRound=G7,a.FlipHorizontal=I7,a.FlipHorizontal2=E7,a.FlipVertical=j7,a.FlipVertical2=X7,a.Flower=K7,a.Flower2=N7,a.Focus=Q7,a.FoldHorizontal=J7,a.FoldVertical=Y7,a.Folder=SM,a.FolderArchive=_7,a.FolderCheck=x7,a.FolderClock=aM,a.FolderClosed=tM,a.FolderCode=hM,a.FolderCog=n2,a.FolderCog2=n2,a.FolderDot=dM,a.FolderDown=cM,a.FolderEdit=l2,a.FolderGit=pM,a.FolderGit2=MM,a.FolderHeart=iM,a.FolderInput=nM,a.FolderKanban=lM,a.FolderKey=eM,a.FolderLock=rM,a.FolderMinus=oM,a.FolderOpen=$M,a.FolderOpenDot=vM,a.FolderOutput=mM,a.FolderPen=l2,a.FolderPlus=yM,a.FolderRoot=sM,a.FolderSearch=CM,a.FolderSearch2=gM,a.FolderSymlink=uM,a.FolderSync=HM,a.FolderTree=AM,a.FolderUp=wM,a.FolderX=VM,a.Folders=LM,a.Footprints=fM,a.ForkKnife=Ha,a.ForkKnifeCrossed=ua,a.Forklift=kM,a.Form=PM,a.FormInput=N2,a.Forward=BM,a.Frame=zM,a.Framer=FM,a.Frown=DM,a.Fuel=bM,a.Fullscreen=RM,a.FunctionSquare=z0,a.Funnel=r2,a.FunnelPlus=TM,a.FunnelX=e2,a.GalleryHorizontal=UM,a.GalleryHorizontalEnd=qM,a.GalleryThumbnails=OM,a.GalleryVertical=GM,a.GalleryVerticalEnd=ZM,a.Gamepad=EM,a.Gamepad2=WM,a.GamepadDirectional=IM,a.GanttChart=E,a.GanttChartSquare=m,a.Gauge=XM,a.GaugeCircle=e1,a.Gavel=jM,a.Gem=NM,a.GeorgianLari=KM,a.Ghost=QM,a.Gift=JM,a.GitBranch=_M,a.GitBranchMinus=YM,a.GitBranchPlus=xM,a.GitCommit=o2,a.GitCommitHorizontal=o2,a.GitCommitVertical=a9,a.GitCompare=h9,a.GitCompareArrows=t9,a.GitFork=d9,a.GitGraph=c9,a.GitMerge=M9,a.GitPullRequest=r9,a.GitPullRequestArrow=p9,a.GitPullRequestClosed=i9,a.GitPullRequestCreate=l9,a.GitPullRequestCreateArrow=n9,a.GitPullRequestDraft=e9,a.Github=o9,a.Gitlab=v9,a.GlassWater=$9,a.Glasses=m9,a.Globe=s9,a.Globe2=D1,a.GlobeLock=y9,a.Goal=g9,a.Gpu=C9,a.Grab=s2,a.GraduationCap=u9,a.Grape=H9,a.Grid=o,a.Grid2X2=y2,a.Grid2X2Check=v2,a.Grid2X2Plus=$2,a.Grid2X2X=m2,a.Grid2x2=y2,a.Grid2x2Check=v2,a.Grid2x2Plus=$2,a.Grid2x2X=m2,a.Grid3X3=o,a.Grid3x2=A9,a.Grid3x3=o,a.Grip=S9,a.GripHorizontal=w9,a.GripVertical=V9,a.Group=L9,a.Guitar=k9,a.Ham=f9,a.Hamburger=P9,a.Hammer=B9,a.Hand=T9,a.HandCoins=z9,a.HandFist=F9,a.HandGrab=s2,a.HandHeart=D9,a.HandHelping=g2,a.HandMetal=b9,a.HandPlatter=R9,a.Handbag=q9,a.Handshake=U9,a.HardDrive=I9,a.HardDriveDownload=O9,a.HardDriveUpload=Z9,a.HardHat=G9,a.Hash=W9,a.HatGlasses=E9,a.Haze=X9,a.Hd=j9,a.HdmiPort=N9,a.Heading=ap,a.Heading1=K9,a.Heading2=J9,a.Heading3=Q9,a.Heading4=Y9,a.Heading5=_9,a.Heading6=x9,a.HeadphoneOff=tp,a.Headphones=hp,a.Headset=dp,a.Heart=rp,a.HeartCrack=cp,a.HeartHandshake=Mp,a.HeartMinus=pp,a.HeartOff=ip,a.HeartPlus=np,a.HeartPulse=lp,a.Heater=ep,a.Helicopter=op,a.HelpCircle=l,a.HelpingHand=g2,a.Hexagon=vp,a.Highlighter=$p,a.History=mp,a.Home=C2,a.Hop=yp,a.HopOff=sp,a.Hospital=gp,a.Hotel=Cp,a.Hourglass=up,a.House=C2,a.HouseHeart=Hp,a.HousePlug=Ap,a.HousePlus=wp,a.HouseWifi=Vp,a.IceCream=H2,a.IceCream2=u2,a.IceCreamBowl=u2,a.IceCreamCone=H2,a.IdCard=Lp,a.IdCardLanyard=Sp,a.Image=bp,a.ImageDown=fp,a.ImageMinus=kp,a.ImageOff=Pp,a.ImagePlay=zp,a.ImagePlus=Bp,a.ImageUp=Fp,a.ImageUpscale=Dp,a.Images=Rp,a.Import=Tp,a.Inbox=qp,a.Indent=$,a.IndentDecrease=v,a.IndentIncrease=$,a.IndianRupee=Up,a.Infinity=Op,a.Info=Zp,a.Inspect=q0,a.InspectionPanel=Gp,a.Instagram=Wp,a.Italic=Ip,a.IterationCcw=Ep,a.IterationCw=Xp,a.JapaneseYen=jp,a.Joystick=Np,a.Kanban=Kp,a.KanbanSquare=F0,a.KanbanSquareDashed=S0,a.Kayak=Qp,a.Key=_p,a.KeyRound=Jp,a.KeySquare=Yp,a.Keyboard=ti,a.KeyboardMusic=xp,a.KeyboardOff=ai,a.Lamp=ii,a.LampCeiling=hi,a.LampDesk=di,a.LampFloor=ci,a.LampWallDown=Mi,a.LampWallUp=pi,a.LandPlot=ni,a.Landmark=li,a.Languages=ei,a.Laptop=oi,a.Laptop2=A2,a.LaptopMinimal=A2,a.LaptopMinimalCheck=ri,a.Lasso=$i,a.LassoSelect=vi,a.Laugh=mi,a.Layers=w2,a.Layers2=yi,a.Layers3=w2,a.LayersPlus=si,a.Layout=I2,a.LayoutDashboard=gi,a.LayoutGrid=Ci,a.LayoutList=ui,a.LayoutPanelLeft=Hi,a.LayoutPanelTop=Ai,a.LayoutTemplate=wi,a.Leaf=Vi,a.LeafyGreen=Si,a.Lectern=Li,a.LetterText=Ma,a.Library=ki,a.LibraryBig=fi,a.LibrarySquare=D0,a.LifeBuoy=Pi,a.Ligature=Bi,a.Lightbulb=Fi,a.LightbulbOff=zi,a.LineChart=G,a.LineSquiggle=Di,a.Link=Ti,a.Link2=bi,a.Link2Off=Ri,a.Linkedin=qi,a.List=hn,a.ListCheck=Ui,a.ListChecks=Oi,a.ListChevronsDownUp=Zi,a.ListChevronsUpDown=Gi,a.ListCollapse=Wi,a.ListEnd=Ii,a.ListFilter=Xi,a.ListFilterPlus=Ei,a.ListIndentDecrease=v,a.ListIndentIncrease=$,a.ListMinus=ji,a.ListMusic=Ni,a.ListOrdered=Ki,a.ListPlus=Qi,a.ListRestart=Ji,a.ListStart=Yi,a.ListTodo=_i,a.ListTree=xi,a.ListVideo=an,a.ListX=tn,a.Loader=cn,a.Loader2=V2,a.LoaderCircle=V2,a.LoaderPinwheel=dn,a.Locate=nn,a.LocateFixed=Mn,a.LocateOff=pn,a.LocationEdit=k2,a.Lock=en,a.LockKeyhole=ln,a.LockKeyholeOpen=S2,a.LockOpen=L2,a.LogIn=rn,a.LogOut=on,a.Logs=vn,a.Lollipop=$n,a.Luggage=mn,a.MSquare=b0,a.Magnet=yn,a.Mail=Vn,a.MailCheck=sn,a.MailMinus=gn,a.MailOpen=Cn,a.MailPlus=un,a.MailQuestion=f2,a.MailQuestionMark=f2,a.MailSearch=Hn,a.MailWarning=An,a.MailX=wn,a.Mailbox=Sn,a.Mails=Ln,a.Map=Gn,a.MapMinus=fn,a.MapPin=Un,a.MapPinCheck=Pn,a.MapPinCheckInside=kn,a.MapPinHouse=Bn,a.MapPinMinus=Fn,a.MapPinMinusInside=zn,a.MapPinOff=Dn,a.MapPinPen=k2,a.MapPinPlus=Rn,a.MapPinPlusInside=bn,a.MapPinX=qn,a.MapPinXInside=Tn,a.MapPinned=On,a.MapPlus=Zn,a.Mars=In,a.MarsStroke=Wn,a.Martini=En,a.Maximize=jn,a.Maximize2=Xn,a.Medal=Qn,a.Megaphone=Kn,a.MegaphoneOff=Nn,a.Meh=Jn,a.MemoryStick=Yn,a.Menu=xn,a.MenuSquare=R0,a.Merge=_n,a.MessageCircle=ll,a.MessageCircleCode=al,a.MessageCircleDashed=tl,a.MessageCircleHeart=hl,a.MessageCircleMore=dl,a.MessageCircleOff=cl,a.MessageCirclePlus=Ml,a.MessageCircleQuestion=P2,a.MessageCircleQuestionMark=P2,a.MessageCircleReply=pl,a.MessageCircleWarning=il,a.MessageCircleX=nl,a.MessageSquare=Sl,a.MessageSquareCode=el,a.MessageSquareDashed=rl,a.MessageSquareDiff=ol,a.MessageSquareDot=vl,a.MessageSquareHeart=$l,a.MessageSquareLock=ml,a.MessageSquareMore=yl,a.MessageSquareOff=sl,a.MessageSquarePlus=gl,a.MessageSquareQuote=Cl,a.MessageSquareReply=ul,a.MessageSquareShare=Hl,a.MessageSquareText=Al,a.MessageSquareWarning=wl,a.MessageSquareX=Vl,a.MessagesSquare=Ll,a.Mic=kl,a.Mic2=B2,a.MicOff=fl,a.MicVocal=B2,a.Microchip=Pl,a.Microscope=Bl,a.Microwave=zl,a.Milestone=Fl,a.Milk=bl,a.MilkOff=Dl,a.Minimize=Tl,a.Minimize2=Rl,a.Minus=ql,a.MinusCircle=r1,a.MinusSquare=T0,a.Monitor=Yl,a.MonitorCheck=Ul,a.MonitorCloud=Ol,a.MonitorCog=Zl,a.MonitorDot=Gl,a.MonitorDown=Wl,a.MonitorOff=Il,a.MonitorPause=El,a.MonitorPlay=Xl,a.MonitorSmartphone=jl,a.MonitorSpeaker=Nl,a.MonitorStop=Kl,a.MonitorUp=Ql,a.MonitorX=Jl,a.Moon=ae,a.MoonStar=_l,a.MoreHorizontal=R1,a.MoreVertical=b1,a.Motorbike=xl,a.Mountain=he,a.MountainSnow=te,a.Mouse=le,a.MouseOff=de,a.MousePointer=ne,a.MousePointer2=Me,a.MousePointer2Off=ce,a.MousePointerBan=pe,a.MousePointerClick=ie,a.MousePointerSquareDashed=L0,a.Move=Ae,a.Move3D=z2,a.Move3d=z2,a.MoveDiagonal=re,a.MoveDiagonal2=ee,a.MoveDown=$e,a.MoveDownLeft=oe,a.MoveDownRight=ve,a.MoveHorizontal=ye,a.MoveLeft=me,a.MoveRight=se,a.MoveUp=ue,a.MoveUpLeft=ge,a.MoveUpRight=Ce,a.MoveVertical=He,a.Music=Le,a.Music2=we,a.Music3=Ve,a.Music4=Se,a.Navigation=Be,a.Navigation2=ke,a.Navigation2Off=fe,a.NavigationOff=Pe,a.Network=Fe,a.Newspaper=ze,a.Nfc=De,a.NonBinary=be,a.Notebook=Ue,a.NotebookPen=Re,a.NotebookTabs=Te,a.NotebookText=qe,a.NotepadText=Ze,a.NotepadTextDashed=Oe,a.Nut=We,a.NutOff=Ge,a.Octagon=Ee,a.OctagonAlert=F2,a.OctagonMinus=Ie,a.OctagonPause=D2,a.OctagonX=b2,a.Omega=Xe,a.Option=je,a.Orbit=Ne,a.Origami=Ke,a.Outdent=v,a.Package=hr,a.Package2=Qe,a.PackageCheck=Je,a.PackageMinus=Ye,a.PackageOpen=_e,a.PackagePlus=xe,a.PackageSearch=ar,a.PackageX=tr,a.PaintBucket=dr,a.PaintRoller=cr,a.Paintbrush=Mr,a.Paintbrush2=R2,a.PaintbrushVertical=R2,a.Palette=pr,a.Palmtree=la,a.Panda=ir,a.PanelBottom=er,a.PanelBottomClose=nr,a.PanelBottomDashed=T2,a.PanelBottomInactive=T2,a.PanelBottomOpen=lr,a.PanelLeft=Z2,a.PanelLeftClose=q2,a.PanelLeftDashed=U2,a.PanelLeftInactive=U2,a.PanelLeftOpen=O2,a.PanelLeftRightDashed=rr,a.PanelRight=$r,a.PanelRightClose=or,a.PanelRightDashed=G2,a.PanelRightInactive=G2,a.PanelRightOpen=vr,a.PanelTop=Cr,a.PanelTopBottomDashed=mr,a.PanelTopClose=yr,a.PanelTopDashed=W2,a.PanelTopInactive=W2,a.PanelTopOpen=sr,a.PanelsLeftBottom=gr,a.PanelsLeftRight=B1,a.PanelsRightBottom=ur,a.PanelsTopBottom=J2,a.PanelsTopLeft=I2,a.Paperclip=Hr,a.Parentheses=wr,a.ParkingCircle=v1,a.ParkingCircleOff=o1,a.ParkingMeter=Ar,a.ParkingSquare=O0,a.ParkingSquareOff=U0,a.PartyPopper=Vr,a.Pause=Sr,a.PauseCircle=$1,a.PauseOctagon=D2,a.PawPrint=Lr,a.PcCase=fr,a.Pen=X2,a.PenBox=i,a.PenLine=E2,a.PenOff=kr,a.PenSquare=i,a.PenTool=Pr,a.Pencil=Dr,a.PencilLine=Br,a.PencilOff=zr,a.PencilRuler=Fr,a.Pentagon=br,a.Percent=Rr,a.PercentCircle=m1,a.PercentDiamond=F1,a.PercentSquare=Z0,a.PersonStanding=Tr,a.PhilippinePeso=qr,a.Phone=Er,a.PhoneCall=Or,a.PhoneForwarded=Ur,a.PhoneIncoming=Zr,a.PhoneMissed=Gr,a.PhoneOff=Wr,a.PhoneOutgoing=Ir,a.Pi=Xr,a.PiSquare=G0,a.Piano=jr,a.Pickaxe=Nr,a.PictureInPicture=Qr,a.PictureInPicture2=Kr,a.PieChart=X,a.PiggyBank=Jr,a.Pilcrow=xr,a.PilcrowLeft=Yr,a.PilcrowRight=_r,a.PilcrowSquare=W0,a.Pill=to,a.PillBottle=ao,a.Pin=co,a.PinOff=ho,a.Pipette=Mo,a.Pizza=po,a.Plane=lo,a.PlaneLanding=io,a.PlaneTakeoff=no,a.Play=eo,a.PlayCircle=y1,a.PlaySquare=I0,a.Plug=oo,a.Plug2=ro,a.PlugZap=j2,a.PlugZap2=j2,a.Plus=vo,a.PlusCircle=s1,a.PlusSquare=E0,a.Pocket=mo,a.PocketKnife=$o,a.Podcast=yo,a.Pointer=go,a.PointerOff=so,a.Popcorn=Co,a.Popsicle=uo,a.PoundSterling=Ho,a.Power=wo,a.PowerCircle=g1,a.PowerOff=Ao,a.PowerSquare=X0,a.Presentation=Vo,a.Printer=So,a.PrinterCheck=Lo,a.Projector=fo,a.Proportions=ko,a.Puzzle=Po,a.Pyramid=Bo,a.QrCode=zo,a.Quote=Fo,a.Rabbit=Do,a.Radar=bo,a.Radiation=Ro,a.Radical=To,a.Radio=Oo,a.RadioReceiver=qo,a.RadioTower=Uo,a.Radius=Zo,a.RailSymbol=Go,a.Rainbow=Wo,a.Rat=Io,a.Ratio=Eo,a.Receipt=av,a.ReceiptCent=Xo,a.ReceiptEuro=jo,a.ReceiptIndianRupee=No,a.ReceiptJapaneseYen=Ko,a.ReceiptPoundSterling=Qo,a.ReceiptRussianRuble=Jo,a.ReceiptSwissFranc=Yo,a.ReceiptText=_o,a.ReceiptTurkishLira=xo,a.RectangleCircle=tv,a.RectangleEllipsis=N2,a.RectangleGoggles=hv,a.RectangleHorizontal=dv,a.RectangleVertical=cv,a.Recycle=Mv,a.Redo=nv,a.Redo2=pv,a.RedoDot=iv,a.RefreshCcw=ev,a.RefreshCcwDot=lv,a.RefreshCw=ov,a.RefreshCwOff=rv,a.Refrigerator=vv,a.Regex=$v,a.RemoveFormatting=mv,a.Repeat=gv,a.Repeat1=yv,a.Repeat2=sv,a.Replace=uv,a.ReplaceAll=Cv,a.Reply=Av,a.ReplyAll=Hv,a.Rewind=wv,a.Ribbon=Vv,a.Rocket=Sv,a.RockingChair=Lv,a.RollerCoaster=fv,a.Rose=kv,a.Rotate3D=K2,a.Rotate3d=K2,a.RotateCcw=zv,a.RotateCcwKey=Pv,a.RotateCcwSquare=Bv,a.RotateCw=Dv,a.RotateCwSquare=Fv,a.Route=bv,a.RouteOff=Rv,a.Router=Tv,a.Rows=Q2,a.Rows2=Q2,a.Rows3=J2,a.Rows4=qv,a.Rss=Uv,a.Ruler=Zv,a.RulerDimensionLine=Ov,a.RussianRuble=Gv,a.Sailboat=Wv,a.Salad=Iv,a.Sandwich=Ev,a.Satellite=jv,a.SatelliteDish=Xv,a.SaudiRiyal=Nv,a.Save=Jv,a.SaveAll=Kv,a.SaveOff=Qv,a.Scale=Yv,a.Scale3D=Y2,a.Scale3d=Y2,a.Scaling=_v,a.Scan=i$,a.ScanBarcode=a$,a.ScanEye=xv,a.ScanFace=t$,a.ScanHeart=h$,a.ScanLine=d$,a.ScanQrCode=c$,a.ScanSearch=M$,a.ScanText=p$,a.ScatterChart=j,a.School=n$,a.School2=oa,a.Scissors=e$,a.ScissorsLineDashed=l$,a.ScissorsSquare=j0,a.ScissorsSquareDashedBottom=s0,a.Scooter=r$,a.ScreenShare=v$,a.ScreenShareOff=o$,a.Scroll=m$,a.ScrollText=$$,a.Search=H$,a.SearchAlert=y$,a.SearchCheck=g$,a.SearchCode=s$,a.SearchSlash=C$,a.SearchX=u$,a.Section=A$,a.Send=V$,a.SendHorizonal=_2,a.SendHorizontal=_2,a.SendToBack=w$,a.SeparatorHorizontal=S$,a.SeparatorVertical=L$,a.Server=B$,a.ServerCog=f$,a.ServerCrash=k$,a.ServerOff=P$,a.Settings=F$,a.Settings2=z$,a.Shapes=D$,a.Share=R$,a.Share2=b$,a.Sheet=T$,a.Shell=q$,a.Shield=N$,a.ShieldAlert=U$,a.ShieldBan=O$,a.ShieldCheck=Z$,a.ShieldClose=a0,a.ShieldEllipsis=G$,a.ShieldHalf=W$,a.ShieldMinus=I$,a.ShieldOff=E$,a.ShieldPlus=X$,a.ShieldQuestion=x2,a.ShieldQuestionMark=x2,a.ShieldUser=j$,a.ShieldX=a0,a.Ship=Q$,a.ShipWheel=K$,a.Shirt=J$,a.ShoppingBag=Y$,a.ShoppingBasket=_$,a.ShoppingCart=x$,a.Shovel=am,a.ShowerHead=tm,a.Shredder=hm,a.Shrimp=dm,a.Shrink=cm,a.Shrub=Mm,a.Shuffle=pm,a.Sidebar=Z2,a.SidebarClose=q2,a.SidebarOpen=O2,a.Sigma=nm,a.SigmaSquare=N0,a.Signal=om,a.SignalHigh=im,a.SignalLow=lm,a.SignalMedium=em,a.SignalZero=rm,a.Signature=vm,a.Signpost=mm,a.SignpostBig=$m,a.Siren=ym,a.SkipBack=sm,a.SkipForward=Cm,a.Skull=gm,a.Slack=um,a.Slash=Hm,a.SlashSquare=K0,a.Slice=Am,a.Sliders=t0,a.SlidersHorizontal=wm,a.SlidersVertical=t0,a.Smartphone=Lm,a.SmartphoneCharging=Vm,a.SmartphoneNfc=Sm,a.Smile=km,a.SmilePlus=fm,a.Snail=Pm,a.Snowflake=Bm,a.SoapDispenserDroplet=zm,a.Sofa=Fm,a.SolarPanel=Dm,a.SortAsc=V,a.SortDesc=H,a.Soup=bm,a.Space=Rm,a.Spade=Tm,a.Sparkle=qm,a.Sparkles=h0,a.Speaker=Um,a.Speech=Om,a.SpellCheck=Gm,a.SpellCheck2=Zm,a.Spline=Im,a.SplinePointer=Wm,a.Split=Em,a.SplitSquareHorizontal=Q0,a.SplitSquareVertical=J0,a.Spool=Xm,a.Spotlight=jm,a.SprayCan=Nm,a.Sprout=Km,a.Square=My,a.SquareActivity=d0,a.SquareArrowDown=p0,a.SquareArrowDownLeft=c0,a.SquareArrowDownRight=M0,a.SquareArrowLeft=i0,a.SquareArrowOutDownLeft=n0,a.SquareArrowOutDownRight=l0,a.SquareArrowOutUpLeft=e0,a.SquareArrowOutUpRight=r0,a.SquareArrowRight=o0,a.SquareArrowUp=m0,a.SquareArrowUpLeft=v0,a.SquareArrowUpRight=$0,a.SquareAsterisk=y0,a.SquareBottomDashedScissors=s0,a.SquareChartGantt=m,a.SquareCheck=g0,a.SquareCheckBig=C0,a.SquareChevronDown=u0,a.SquareChevronLeft=H0,a.SquareChevronRight=A0,a.SquareChevronUp=w0,a.SquareCode=V0,a.SquareDashed=f0,a.SquareDashedBottom=Jm,a.SquareDashedBottomCode=Qm,a.SquareDashedKanban=S0,a.SquareDashedMousePointer=L0,a.SquareDashedTopSolid=Ym,a.SquareDivide=k0,a.SquareDot=P0,a.SquareEqual=B0,a.SquareFunction=z0,a.SquareGanttChart=m,a.SquareKanban=F0,a.SquareLibrary=D0,a.SquareM=b0,a.SquareMenu=R0,a.SquareMinus=T0,a.SquareMousePointer=q0,a.SquareParking=O0,a.SquareParkingOff=U0,a.SquarePause=_m,a.SquarePen=i,a.SquarePercent=Z0,a.SquarePi=G0,a.SquarePilcrow=W0,a.SquarePlay=I0,a.SquarePlus=E0,a.SquarePower=X0,a.SquareRadical=xm,a.SquareRoundCorner=ay,a.SquareScissors=j0,a.SquareSigma=N0,a.SquareSlash=K0,a.SquareSplitHorizontal=Q0,a.SquareSplitVertical=J0,a.SquareSquare=ty,a.SquareStack=hy,a.SquareStar=dy,a.SquareStop=cy,a.SquareTerminal=Y0,a.SquareUser=x0,a.SquareUserRound=_0,a.SquareX=aa,a.SquaresExclude=py,a.SquaresIntersect=iy,a.SquaresSubtract=ny,a.SquaresUnite=ey,a.Squircle=ry,a.SquircleDashed=ly,a.Squirrel=oy,a.Stamp=vy,a.Star=yy,a.StarHalf=my,a.StarOff=$y,a.Stars=h0,a.StepBack=sy,a.StepForward=gy,a.Stethoscope=Cy,a.Sticker=uy,a.StickyNote=Ay,a.Stone=Hy,a.StopCircle=u1,a.Store=wy,a.StretchHorizontal=Vy,a.StretchVertical=Sy,a.Strikethrough=Ly,a.Subscript=fy,a.Subtitles=D,a.Sun=Fy,a.SunDim=ky,a.SunMedium=Py,a.SunMoon=By,a.SunSnow=zy,a.Sunrise=Dy,a.Sunset=by,a.Superscript=Ry,a.SwatchBook=Ty,a.SwissFranc=qy,a.SwitchCamera=Uy,a.Sword=Oy,a.Swords=Zy,a.Syringe=Gy,a.Table=Qy,a.Table2=Wy,a.TableCellsMerge=Iy,a.TableCellsSplit=Ey,a.TableColumnsSplit=jy,a.TableConfig=e,a.TableOfContents=Xy,a.TableProperties=Ny,a.TableRowsSplit=Ky,a.Tablet=Yy,a.TabletSmartphone=Jy,a.Tablets=_y,a.Tag=xy,a.Tags=as,a.Tally1=ts,a.Tally2=hs,a.Tally3=ds,a.Tally4=cs,a.Tally5=Ms,a.Tangent=ps,a.Target=is,a.Telescope=ns,a.Tent=es,a.TentTree=ls,a.Terminal=rs,a.TerminalSquare=Y0,a.TestTube=os,a.TestTube2=ta,a.TestTubeDiagonal=ta,a.TestTubes=vs,a.Text=y,a.TextAlignCenter=ha,a.TextAlignEnd=da,a.TextAlignJustify=ca,a.TextAlignStart=y,a.TextCursor=ms,a.TextCursorInput=$s,a.TextInitial=Ma,a.TextQuote=ys,a.TextSearch=ss,a.TextSelect=pa,a.TextSelection=pa,a.TextWrap=ia,a.Theater=gs,a.Thermometer=Hs,a.ThermometerSnowflake=Cs,a.ThermometerSun=us,a.ThumbsDown=As,a.ThumbsUp=ws,a.Ticket=Bs,a.TicketCheck=Vs,a.TicketMinus=Ss,a.TicketPercent=Ls,a.TicketPlus=fs,a.TicketSlash=ks,a.TicketX=Ps,a.Tickets=Fs,a.TicketsPlane=zs,a.Timer=Rs,a.TimerOff=bs,a.TimerReset=Ds,a.ToggleLeft=Ts,a.ToggleRight=qs,a.Toilet=Us,a.ToolCase=Os,a.Toolbox=Zs,a.Tornado=Gs,a.Torus=Ws,a.Touchpad=Es,a.TouchpadOff=Is,a.TowerControl=Xs,a.ToyBrick=js,a.Tractor=Ks,a.TrafficCone=Ns,a.Train=na,a.TrainFront=Js,a.TrainFrontTunnel=Qs,a.TrainTrack=Ys,a.TramFront=na,a.Transgender=_s,a.Trash=ag,a.Trash2=xs,a.TreeDeciduous=tg,a.TreePalm=la,a.TreePine=hg,a.Trees=dg,a.Trello=cg,a.TrendingDown=Mg,a.TrendingUp=ig,a.TrendingUpDown=pg,a.Triangle=eg,a.TriangleAlert=ea,a.TriangleDashed=ng,a.TriangleRight=lg,a.Trophy=rg,a.Truck=vg,a.TruckElectric=og,a.TurkishLira=$g,a.Turntable=mg,a.Turtle=yg,a.Tv=gg,a.Tv2=ra,a.TvMinimal=ra,a.TvMinimalPlay=sg,a.Twitch=Cg,a.Twitter=ug,a.Type=Ag,a.TypeOutline=Hg,a.Umbrella=Vg,a.UmbrellaOff=wg,a.Underline=Sg,a.Undo=kg,a.Undo2=Lg,a.UndoDot=fg,a.UnfoldHorizontal=Pg,a.UnfoldVertical=Bg,a.Ungroup=zg,a.University=oa,a.Unlink=Dg,a.Unlink2=Fg,a.Unlock=L2,a.UnlockKeyhole=S2,a.Unplug=bg,a.Upload=Tg,a.UploadCloud=f1,a.Usb=Rg,a.User=Kg,a.User2=sa,a.UserCheck=qg,a.UserCheck2=va,a.UserCircle=A1,a.UserCircle2=H1,a.UserCog=Ug,a.UserCog2=$a,a.UserLock=Og,a.UserMinus=Zg,a.UserMinus2=ma,a.UserPen=Gg,a.UserPlus=Wg,a.UserPlus2=ya,a.UserRound=sa,a.UserRoundCheck=va,a.UserRoundCog=$a,a.UserRoundMinus=ma,a.UserRoundPen=Ig,a.UserRoundPlus=ya,a.UserRoundSearch=Eg,a.UserRoundX=ga,a.UserSearch=Xg,a.UserSquare=x0,a.UserSquare2=_0,a.UserStar=jg,a.UserX=Ng,a.UserX2=ga,a.Users=Qg,a.Users2=Ca,a.UsersRound=Ca,a.Utensils=Ha,a.UtensilsCrossed=ua,a.UtilityPole=Yg,a.Van=Jg,a.Variable=_g,a.Vault=xg,a.VectorSquare=aC,a.Vegan=tC,a.VenetianMask=hC,a.Venus=dC,a.VenusAndMars=cC,a.Verified=f,a.Vibrate=pC,a.VibrateOff=MC,a.Video=nC,a.VideoOff=iC,a.Videotape=eC,a.View=rC,a.Voicemail=lC,a.Volleyball=oC,a.Volume=sC,a.Volume1=vC,a.Volume2=$C,a.VolumeOff=mC,a.VolumeX=yC,a.Vote=gC,a.Wallet=uC,a.Wallet2=Aa,a.WalletCards=CC,a.WalletMinimal=Aa,a.Wallpaper=HC,a.Wand=AC,a.Wand2=wa,a.WandSparkles=wa,a.Warehouse=wC,a.WashingMachine=VC,a.Watch=SC,a.Waves=PC,a.WavesArrowDown=LC,a.WavesArrowUp=fC,a.WavesLadder=kC,a.Waypoints=BC,a.Webcam=zC,a.Webhook=DC,a.WebhookOff=FC,a.Weight=RC,a.WeightTilde=bC,a.Wheat=qC,a.WheatOff=TC,a.WholeWord=UC,a.Wifi=jC,a.WifiCog=OC,a.WifiHigh=GC,a.WifiLow=ZC,a.WifiOff=WC,a.WifiPen=IC,a.WifiSync=EC,a.WifiZero=XC,a.Wind=KC,a.WindArrowDown=NC,a.Wine=JC,a.WineOff=QC,a.Workflow=_C,a.Worm=YC,a.WrapText=ia,a.Wrench=xC,a.X=au,a.XCircle=w1,a.XOctagon=b2,a.XSquare=aa,a.Youtube=tu,a.Zap=du,a.ZapOff=hu,a.ZoomIn=cu,a.ZoomOut=Mu,a.createElement=La,a.createIcons=iu,a.icons=pu})); 12 + //# sourceMappingURL=lucide.min.js.map
+18
pkg/hold/admin/templates/components/nav.html
··· 1 + {{define "nav"}} 2 + <nav class="nav"> 3 + <div class="nav-brand"> 4 + <a href="/admin">Hold Admin</a> 5 + </div> 6 + <ul class="nav-links"> 7 + <li><a href="/admin" class="{{if eq .ActivePage "dashboard"}}active{{end}}">Dashboard</a></li> 8 + <li><a href="/admin/crew" class="{{if eq .ActivePage "crew"}}active{{end}}">Crew</a></li> 9 + <li><a href="/admin/settings" class="{{if eq .ActivePage "settings"}}active{{end}}">Settings</a></li> 10 + </ul> 11 + {{if .User}} 12 + <div class="nav-user"> 13 + <span>{{.User.Handle}}</span> 14 + <a href="/admin/auth/logout" class="btn btn-sm">Logout</a> 15 + </div> 16 + {{end}} 17 + </nav> 18 + {{end}}
+81
pkg/hold/admin/templates/pages/crew.html
··· 1 + {{define "pages/crew.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>{{.Title}} - Hold Admin</title> 8 + <script src="/admin/static/js/htmx.min.js"></script> 9 + <script src="/admin/static/js/lucide.min.js"></script> 10 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 11 + </head> 12 + <body> 13 + {{template "nav" .}} 14 + 15 + <main class="container"> 16 + {{if .Flash}} 17 + <div class="flash flash-{{.Flash.Category}}">{{.Flash.Message}}</div> 18 + {{end}} 19 + 20 + <div class="page-header"> 21 + <h1>Crew Management</h1> 22 + <a href="/admin/crew/add" class="btn btn-primary"> 23 + <i data-lucide="user-plus"></i> 24 + Add Crew Member 25 + </a> 26 + </div> 27 + 28 + {{if .Crew}} 29 + <table class="table"> 30 + <thead> 31 + <tr> 32 + <th>Member</th> 33 + <th>Role</th> 34 + <th>Permissions</th> 35 + <th>Tier</th> 36 + <th>Usage</th> 37 + <th class="actions-header">Actions</th> 38 + </tr> 39 + </thead> 40 + <tbody id="crew-list"> 41 + {{range .Crew}} 42 + <tr id="crew-{{.RKey}}"> 43 + <td class="member-cell"> 44 + {{if .Handle}}<strong>{{.Handle}}</strong><br>{{end}} 45 + <code class="did-code">{{.DID}}</code> 46 + </td> 47 + <td>{{.Role}}</td> 48 + <td class="permissions-cell">{{range .Permissions}}<span class="badge">{{.}}</span>{{end}}</td> 49 + <td><span class="badge badge-tier">{{.Tier}}</span><br><small class="tier-limit">{{.TierLimit}}</small></td> 50 + <td> 51 + <div class="usage-cell"> 52 + <span>{{.UsageHuman}}</span> 53 + <div class="progress-bar"> 54 + <div class="progress-fill {{if gt .UsagePercent 90}}danger{{else if gt .UsagePercent 75}}warning{{end}}" style="width: {{.UsagePercent}}%"></div> 55 + </div> 56 + <small>{{.UsagePercent}}%</small> 57 + </div> 58 + </td> 59 + <td class="actions"> 60 + <a href="/admin/crew/{{.RKey}}" class="btn btn-icon" title="Edit"> 61 + <i data-lucide="pencil"></i> 62 + </a> 63 + <button class="btn btn-icon btn-danger" title="Delete" hx-post="/admin/crew/{{.RKey}}/delete" hx-confirm="Remove this crew member?" hx-target="#crew-{{.RKey}}" hx-swap="outerHTML"> 64 + <i data-lucide="trash-2"></i> 65 + </button> 66 + </td> 67 + </tr> 68 + {{end}} 69 + </tbody> 70 + </table> 71 + {{else}} 72 + <p class="empty">No crew members yet. <a href="/admin/crew/add">Add your first crew member</a>.</p> 73 + {{end}} 74 + </main> 75 + 76 + <footer class="footer"><p>Hold: <code>{{.HoldDID}}</code></p></footer> 77 + 78 + <script>lucide.createIcons();</script> 79 + </body> 80 + </html> 81 + {{end}}
+152
pkg/hold/admin/templates/pages/crew_add.html
··· 1 + {{define "pages/crew_add.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>{{.Title}} - Hold Admin</title> 8 + <script src="/admin/static/js/htmx.min.js"></script> 9 + <script src="/admin/static/js/lucide.min.js"></script> 10 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 11 + </head> 12 + <body> 13 + {{template "nav" .}} 14 + 15 + <main class="container"> 16 + {{if .Flash}} 17 + <div class="flash flash-{{.Flash.Category}}">{{.Flash.Message}}</div> 18 + {{end}} 19 + 20 + <div class="page-header"> 21 + <h1>Add Crew Member</h1> 22 + <a href="/admin/crew" class="btn"> 23 + <i data-lucide="arrow-left"></i> 24 + Back to Crew 25 + </a> 26 + </div> 27 + 28 + <form action="/admin/crew/add" method="POST" class="form"> 29 + <div class="form-group"> 30 + <label for="did">DID</label> 31 + <div class="input-with-lookup"> 32 + <input type="text" id="did" name="did" placeholder="did:plc:..." required> 33 + <button type="button" id="lookup-btn" class="btn btn-sm" title="Lookup handle"> 34 + <i data-lucide="search"></i> 35 + </button> 36 + </div> 37 + <small>The member's ATProto DID</small> 38 + <div id="handle-result" class="handle-lookup-result"></div> 39 + </div> 40 + 41 + <div class="form-group"> 42 + <label for="role">Role</label> 43 + <input type="text" id="role" name="role" placeholder="member" value="member"> 44 + <small>Optional role name (e.g., member, admin)</small> 45 + </div> 46 + 47 + <div class="form-group"> 48 + <label>Permissions</label> 49 + <div class="checkbox-group"> 50 + <label class="checkbox"> 51 + <input type="checkbox" name="perm_read" checked> 52 + <span>blob:read</span> 53 + <small>Can pull/download blobs</small> 54 + </label> 55 + <label class="checkbox"> 56 + <input type="checkbox" name="perm_write" checked> 57 + <span>blob:write</span> 58 + <small>Can push/upload blobs</small> 59 + </label> 60 + <label class="checkbox"> 61 + <input type="checkbox" name="perm_admin"> 62 + <span>crew:admin</span> 63 + <small>Can manage crew members</small> 64 + </label> 65 + </div> 66 + </div> 67 + 68 + {{if .Tiers}} 69 + <div class="form-group"> 70 + <label for="tier">Quota Tier</label> 71 + <select id="tier" name="tier"> 72 + {{range .Tiers}} 73 + <option value="{{.Key}}">{{.Name}} ({{.Limit}})</option> 74 + {{end}} 75 + </select> 76 + <small>Storage quota limit for this member</small> 77 + </div> 78 + {{end}} 79 + 80 + <div class="form-actions"> 81 + <button type="submit" class="btn btn-primary"> 82 + <i data-lucide="user-plus"></i> 83 + Add Crew Member 84 + </button> 85 + <a href="/admin/crew" class="btn">Cancel</a> 86 + </div> 87 + </form> 88 + </main> 89 + 90 + <footer class="footer"><p>Hold: <code>{{.HoldDID}}</code></p></footer> 91 + 92 + <script> 93 + lucide.createIcons(); 94 + 95 + // DID to handle lookup 96 + const didInput = document.getElementById('did'); 97 + const lookupBtn = document.getElementById('lookup-btn'); 98 + const handleResult = document.getElementById('handle-result'); 99 + 100 + async function lookupHandle() { 101 + const did = didInput.value.trim(); 102 + if (!did.startsWith('did:')) { 103 + handleResult.innerHTML = '<span class="error">Invalid DID format</span>'; 104 + return; 105 + } 106 + 107 + handleResult.innerHTML = '<span class="loading">Looking up...</span>'; 108 + 109 + try { 110 + // Use plc.directory for did:plc or did:web resolution 111 + let url; 112 + if (did.startsWith('did:plc:')) { 113 + url = `https://plc.directory/${did}`; 114 + } else if (did.startsWith('did:web:')) { 115 + const host = did.replace('did:web:', '').replace(/%3A/g, ':'); 116 + url = `https://${host}/.well-known/did.json`; 117 + } else { 118 + handleResult.innerHTML = '<span class="error">Unsupported DID method</span>'; 119 + return; 120 + } 121 + 122 + const resp = await fetch(url); 123 + if (!resp.ok) throw new Error('DID not found'); 124 + 125 + const doc = await resp.json(); 126 + // Look for handle in alsoKnownAs 127 + const aka = doc.alsoKnownAs || []; 128 + const handleUri = aka.find(u => u.startsWith('at://')); 129 + if (handleUri) { 130 + const handle = handleUri.replace('at://', ''); 131 + handleResult.innerHTML = `<span class="success"><i data-lucide="check-circle"></i> <strong>${handle}</strong></span>`; 132 + lucide.createIcons(); 133 + } else { 134 + handleResult.innerHTML = '<span class="warning">No handle found</span>'; 135 + } 136 + } catch (err) { 137 + handleResult.innerHTML = `<span class="error">Lookup failed: ${err.message}</span>`; 138 + } 139 + } 140 + 141 + lookupBtn.addEventListener('click', lookupHandle); 142 + 143 + // Auto-lookup on blur if DID looks valid 144 + didInput.addEventListener('blur', function() { 145 + if (this.value.startsWith('did:') && this.value.length > 10) { 146 + lookupHandle(); 147 + } 148 + }); 149 + </script> 150 + </body> 151 + </html> 152 + {{end}}
+89
pkg/hold/admin/templates/pages/crew_edit.html
··· 1 + {{define "pages/crew_edit.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>{{.Title}} - Hold Admin</title> 8 + <script src="/admin/static/js/htmx.min.js"></script> 9 + <script src="/admin/static/js/lucide.min.js"></script> 10 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 11 + </head> 12 + <body> 13 + {{template "nav" .}} 14 + 15 + <main class="container"> 16 + {{if .Flash}} 17 + <div class="flash flash-{{.Flash.Category}}">{{.Flash.Message}}</div> 18 + {{end}} 19 + 20 + <div class="page-header"> 21 + <h1>Edit Crew Member</h1> 22 + <a href="/admin/crew" class="btn"> 23 + <i data-lucide="arrow-left"></i> 24 + Back to Crew 25 + </a> 26 + </div> 27 + 28 + <div class="card"> 29 + <div class="card-header member-header"> 30 + <div class="member-info"> 31 + {{if .MemberHandle}}<strong id="member-handle">{{.MemberHandle}}</strong>{{end}} 32 + <code class="did-code">{{.Member.Member}}</code> 33 + </div> 34 + {{if .IsOwner}}<span class="badge badge-gold">Owner</span>{{end}} 35 + </div> 36 + 37 + <form action="/admin/crew/{{.RKey}}/update" method="POST" class="form"> 38 + <div class="form-group"> 39 + <label for="role">Role</label> 40 + <input type="text" id="role" name="role" value="{{.Member.Role}}" {{if .IsOwner}}disabled{{end}}> 41 + </div> 42 + 43 + <div class="form-group"> 44 + <label>Permissions</label> 45 + <div class="checkbox-group"> 46 + <label class="checkbox"> 47 + <input type="checkbox" name="perm_read" {{if contains .Member.Permissions "blob:read"}}checked{{end}} {{if .IsOwner}}disabled{{end}}> 48 + <span>blob:read</span> 49 + </label> 50 + <label class="checkbox"> 51 + <input type="checkbox" name="perm_write" {{if contains .Member.Permissions "blob:write"}}checked{{end}} {{if .IsOwner}}disabled{{end}}> 52 + <span>blob:write</span> 53 + </label> 54 + <label class="checkbox"> 55 + <input type="checkbox" name="perm_admin" {{if contains .Member.Permissions "crew:admin"}}checked{{end}} {{if .IsOwner}}disabled{{end}}> 56 + <span>crew:admin</span> 57 + </label> 58 + </div> 59 + </div> 60 + 61 + {{if .Tiers}} 62 + <div class="form-group"> 63 + <label for="tier">Quota Tier</label> 64 + <select id="tier" name="tier" {{if .IsOwner}}disabled{{end}}> 65 + {{range .Tiers}} 66 + <option value="{{.Key}}" {{if eq .Key $.Member.Tier}}selected{{end}}>{{.Name}} ({{.Limit}})</option> 67 + {{end}} 68 + </select> 69 + </div> 70 + {{end}} 71 + 72 + {{if .IsOwner}} 73 + <p class="note">Owner permissions cannot be modified.</p> 74 + {{else}} 75 + <div class="form-actions"> 76 + <button type="submit" class="btn btn-primary">Save Changes</button> 77 + <a href="/admin/crew" class="btn">Cancel</a> 78 + </div> 79 + {{end}} 80 + </form> 81 + </div> 82 + </main> 83 + 84 + <footer class="footer"><p>Hold: <code>{{.HoldDID}}</code></p></footer> 85 + 86 + <script>lucide.createIcons();</script> 87 + </body> 88 + </html> 89 + {{end}}
+58
pkg/hold/admin/templates/pages/dashboard.html
··· 1 + {{define "pages/dashboard.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>{{.Title}} - Hold Admin</title> 8 + <script src="/admin/static/js/htmx.min.js"></script> 9 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 10 + </head> 11 + <body> 12 + {{template "nav" .}} 13 + 14 + <main class="container"> 15 + {{if .Flash}} 16 + <div class="flash flash-{{.Flash.Category}}">{{.Flash.Message}}</div> 17 + {{end}} 18 + 19 + <h1>Dashboard</h1> 20 + 21 + <div class="stats-grid"> 22 + <div class="stat-card"> 23 + <h3>Crew Members</h3> 24 + <p class="stat-value">{{.Stats.TotalCrewMembers}}</p> 25 + </div> 26 + <div class="stat-card" hx-get="/admin/api/stats" hx-trigger="load" hx-swap="innerHTML"> 27 + <p class="loading">Loading storage stats...</p> 28 + </div> 29 + </div> 30 + 31 + <section class="section"> 32 + <h2>Tier Distribution</h2> 33 + {{if .Stats.TierDistribution}} 34 + <div class="tier-chart"> 35 + {{range $tier, $count := .Stats.TierDistribution}} 36 + <div class="tier-bar"> 37 + <span class="tier-name">{{$tier}}</span> 38 + <span class="tier-count">{{$count}} members</span> 39 + </div> 40 + {{end}} 41 + </div> 42 + {{else}} 43 + <p class="empty">No crew members yet.</p> 44 + {{end}} 45 + </section> 46 + 47 + <section class="section"> 48 + <h2>Top Users by Storage</h2> 49 + <div hx-get="/admin/api/top-users?limit=10" hx-trigger="load" hx-swap="innerHTML"> 50 + <p class="loading">Loading top users...</p> 51 + </div> 52 + </section> 53 + </main> 54 + 55 + <footer class="footer"><p>Hold: <code>{{.HoldDID}}</code></p></footer> 56 + </body> 57 + </html> 58 + {{end}}
+24
pkg/hold/admin/templates/pages/error.html
··· 1 + {{define "pages/error.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>Error - Hold Admin</title> 8 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 9 + </head> 10 + <body> 11 + {{template "nav" .}} 12 + 13 + <main class="container"> 14 + <div class="error-page"> 15 + <h1>Error</h1> 16 + <p class="error-message">{{.Error}}</p> 17 + <a href="/admin" class="btn btn-primary">Back to Dashboard</a> 18 + </div> 19 + </main> 20 + 21 + <footer class="footer"><p>Hold: <code>{{.HoldDID}}</code></p></footer> 22 + </body> 23 + </html> 24 + {{end}}
+48
pkg/hold/admin/templates/pages/login.html
··· 1 + {{define "pages/login.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>Login - Hold Admin</title> 8 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 9 + </head> 10 + <body class="login-page"> 11 + <div class="login-container"> 12 + <div class="login-card"> 13 + <h1>Hold Admin</h1> 14 + <p class="login-subtitle">Sign in with your ATProto account</p> 15 + 16 + {{if .Error}} 17 + <div class="flash flash-error"> 18 + {{.Error}} 19 + </div> 20 + {{end}} 21 + 22 + <form action="/admin/auth/oauth/authorize" method="GET" class="login-form"> 23 + <input type="hidden" name="return_to" value="{{.ReturnTo}}"> 24 + 25 + <div class="form-group"> 26 + <label for="handle">Handle or DID</label> 27 + <input type="text" id="handle" name="handle" 28 + placeholder="alice.bsky.social" 29 + required autofocus> 30 + </div> 31 + 32 + <button type="submit" class="btn btn-primary btn-block"> 33 + Sign in 34 + </button> 35 + </form> 36 + 37 + <p class="login-note"> 38 + Only the hold owner can access the admin panel. 39 + </p> 40 + </div> 41 + 42 + <footer class="login-footer"> 43 + <p>Hold: <code>{{.HoldDID}}</code></p> 44 + </footer> 45 + </div> 46 + </body> 47 + </html> 48 + {{end}}
+91
pkg/hold/admin/templates/pages/settings.html
··· 1 + {{define "pages/settings.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>{{.Title}} - Hold Admin</title> 8 + <script src="/admin/static/js/htmx.min.js"></script> 9 + <script src="/admin/static/js/lucide.min.js"></script> 10 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 11 + </head> 12 + <body> 13 + {{template "nav" .}} 14 + 15 + <main class="container"> 16 + {{if .Flash}} 17 + <div class="flash flash-{{.Flash.Category}}">{{.Flash.Message}}</div> 18 + {{end}} 19 + 20 + <h1>Hold Settings</h1> 21 + 22 + <form action="/admin/settings/update" method="POST" class="form settings-form"> 23 + <section class="section"> 24 + <h2>Access Control</h2> 25 + 26 + <label class="toggle-setting"> 27 + <input type="checkbox" name="public" {{if .Settings.Public}}checked{{end}}> 28 + <span class="toggle-label"> 29 + <strong>Public Hold</strong> 30 + <small>Allow anonymous users to read blobs (no auth required for pulls)</small> 31 + </span> 32 + </label> 33 + 34 + <label class="toggle-setting"> 35 + <input type="checkbox" name="allow_all_crew" {{if .Settings.AllowAllCrew}}checked{{end}}> 36 + <span class="toggle-label"> 37 + <strong>Open Registration</strong> 38 + <small>Allow any authenticated user to join as crew via requestCrew</small> 39 + </span> 40 + </label> 41 + </section> 42 + 43 + <section class="section"> 44 + <h2>Integrations</h2> 45 + 46 + <label class="toggle-setting"> 47 + <input type="checkbox" name="enable_bluesky_posts" {{if .Settings.EnableBlueskyPosts}}checked{{end}}> 48 + <span class="toggle-label"> 49 + <strong>Bluesky Posts</strong> 50 + <small>Post to Bluesky when images are pushed to this hold</small> 51 + </span> 52 + </label> 53 + </section> 54 + 55 + <section class="section"> 56 + <h2>Hold Information</h2> 57 + <dl class="info-list"> 58 + <dt>Hold DID</dt> 59 + <dd><code>{{.Settings.HoldDID}}</code></dd> 60 + <dt>Owner</dt> 61 + <dd> 62 + {{if .Settings.OwnerHandle}}<strong>{{.Settings.OwnerHandle}}</strong><br>{{end}} 63 + <code class="did-code">{{.Settings.OwnerDID}}</code> 64 + </dd> 65 + <dt>Quotas</dt> 66 + <dd> 67 + {{if .Settings.QuotasEnabled}} 68 + <span class="badge badge-tier">Enabled</span> 69 + <small>({{.Settings.TierCount}} tiers, default: {{.Settings.DefaultTier}})</small> 70 + {{else}} 71 + <span class="badge">Disabled</span> 72 + {{end}} 73 + </dd> 74 + </dl> 75 + </section> 76 + 77 + <div class="form-actions"> 78 + <button type="submit" class="btn btn-primary"> 79 + <i data-lucide="save"></i> 80 + Save Settings 81 + </button> 82 + </div> 83 + </form> 84 + </main> 85 + 86 + <footer class="footer"><p>Hold: <code>{{.HoldDID}}</code></p></footer> 87 + 88 + <script>lucide.createIcons();</script> 89 + </body> 90 + </html> 91 + {{end}}
+27
pkg/hold/admin/templates/partials/top_users.html
··· 1 + {{define "partials/top_users.html"}} 2 + {{if .Users}} 3 + <table class="table"> 4 + <thead> 5 + <tr> 6 + <th>Member</th> 7 + <th>Usage</th> 8 + <th>Blobs</th> 9 + </tr> 10 + </thead> 11 + <tbody> 12 + {{range .Users}} 13 + <tr> 14 + <td class="member-cell"> 15 + {{if .Handle}}<strong>{{.Handle}}</strong><br>{{end}} 16 + <code class="did-code">{{.DID}}</code> 17 + </td> 18 + <td>{{.UsageHuman}}</td> 19 + <td>{{.BlobCount}}</td> 20 + </tr> 21 + {{end}} 22 + </tbody> 23 + </table> 24 + {{else}} 25 + <p class="empty">No usage data yet.</p> 26 + {{end}} 27 + {{end}}
+5
pkg/hold/admin/templates/partials/usage_stats.html
··· 1 + {{define "partials/usage_stats.html"}} 2 + <h3>Storage</h3> 3 + <p class="stat-value">{{.Stats.TotalHuman}}</p> 4 + <p class="stat-detail">{{.Stats.UniqueDigests}} unique blobs</p> 5 + {{end}}
+10
pkg/hold/config.go
··· 26 26 Server ServerConfig `yaml:"server"` 27 27 Registration RegistrationConfig `yaml:"registration"` 28 28 Database DatabaseConfig `yaml:"database"` 29 + Admin AdminConfig `yaml:"admin"` 30 + } 31 + 32 + // AdminConfig defines admin panel settings 33 + type AdminConfig struct { 34 + // Enabled controls whether the admin panel is accessible (from env: HOLD_ADMIN_ENABLED) 35 + Enabled bool `yaml:"enabled"` 29 36 } 30 37 31 38 // RegistrationConfig defines auto-registration settings ··· 137 144 if err != nil { 138 145 return nil, fmt.Errorf("failed to build storage config: %w", err) 139 146 } 147 + 148 + // Admin panel configuration 149 + cfg.Admin.Enabled = os.Getenv("HOLD_ADMIN_ENABLED") == "true" 140 150 141 151 return cfg, nil 142 152 }
-90
pkg/hold/oci/helpers_test.go
··· 5 5 ) 6 6 7 7 // Tests for helper functions 8 - 9 - func TestBlobPath_SHA256(t *testing.T) { 10 - tests := []struct { 11 - name string 12 - digest string 13 - expected string 14 - }{ 15 - { 16 - name: "standard sha256 digest", 17 - digest: "sha256:abc123def456", 18 - expected: "/docker/registry/v2/blobs/sha256/ab/abc123def456/data", 19 - }, 20 - { 21 - name: "short hash (less than 2 chars)", 22 - digest: "sha256:a", 23 - expected: "/docker/registry/v2/blobs/sha256/a/data", 24 - }, 25 - { 26 - name: "exactly 2 char hash", 27 - digest: "sha256:ab", 28 - expected: "/docker/registry/v2/blobs/sha256/ab/ab/data", 29 - }, 30 - } 31 - 32 - for _, tt := range tests { 33 - t.Run(tt.name, func(t *testing.T) { 34 - result := blobPath(tt.digest) 35 - if result != tt.expected { 36 - t.Errorf("Expected %s, got %s", tt.expected, result) 37 - } 38 - }) 39 - } 40 - } 41 - 42 - func TestBlobPath_TempUpload(t *testing.T) { 43 - tests := []struct { 44 - name string 45 - digest string 46 - expected string 47 - }{ 48 - { 49 - name: "temp upload path", 50 - digest: "uploads/temp-uuid-123", 51 - expected: "/docker/registry/v2/uploads/temp-uuid-123/data", 52 - }, 53 - { 54 - name: "temp upload with different uuid", 55 - digest: "uploads/temp-abc-def-456", 56 - expected: "/docker/registry/v2/uploads/temp-abc-def-456/data", 57 - }, 58 - } 59 - 60 - for _, tt := range tests { 61 - t.Run(tt.name, func(t *testing.T) { 62 - result := blobPath(tt.digest) 63 - if result != tt.expected { 64 - t.Errorf("Expected %s, got %s", tt.expected, result) 65 - } 66 - }) 67 - } 68 - } 69 - 70 - func TestBlobPath_MalformedDigest(t *testing.T) { 71 - tests := []struct { 72 - name string 73 - digest string 74 - expected string 75 - }{ 76 - { 77 - name: "no colon in digest", 78 - digest: "malformed-digest", 79 - expected: "/docker/registry/v2/blobs/malformed-digest/data", 80 - }, 81 - { 82 - name: "empty digest", 83 - digest: "", 84 - expected: "/docker/registry/v2/blobs//data", 85 - }, 86 - } 87 - 88 - for _, tt := range tests { 89 - t.Run(tt.name, func(t *testing.T) { 90 - result := blobPath(tt.digest) 91 - if result != tt.expected { 92 - t.Errorf("Expected %s, got %s", tt.expected, result) 93 - } 94 - }) 95 - } 96 - } 97 - 98 8 func TestNormalizeETag(t *testing.T) { 99 9 tests := []struct { 100 10 name string
+15 -43
pkg/hold/oci/multipart.go
··· 12 12 "time" 13 13 14 14 "atcr.io/pkg/atproto" 15 - "github.com/aws/aws-sdk-go/service/s3" 15 + "atcr.io/pkg/s3" 16 + awss3 "github.com/aws/aws-sdk-go/service/s3" 16 17 "github.com/google/uuid" 17 18 ) 18 19 ··· 237 238 if h.s3Service.Client == nil { 238 239 return "", S3Native, fmt.Errorf("S3 not configured") 239 240 } 240 - path := blobPath(digest) 241 + path := s3.BlobPath(digest) 241 242 s3Key := strings.TrimPrefix(path, "/") 242 243 if h.s3Service.PathPrefix != "" { 243 244 s3Key = h.s3Service.PathPrefix + "/" + s3Key 244 245 } 245 246 246 - result, err := h.s3Service.Client.CreateMultipartUploadWithContext(ctx, &s3.CreateMultipartUploadInput{ 247 + result, err := h.s3Service.Client.CreateMultipartUploadWithContext(ctx, &awss3.CreateMultipartUploadInput{ 247 248 Bucket: &h.s3Service.Bucket, 248 249 Key: &s3Key, 249 250 }) ··· 280 281 return nil, fmt.Errorf("S3 not configured") 281 282 } 282 283 283 - path := blobPath(session.Digest) 284 + path := s3.BlobPath(session.Digest) 284 285 s3Key := strings.TrimPrefix(path, "/") 285 286 if h.s3Service.PathPrefix != "" { 286 287 s3Key = h.s3Service.PathPrefix + "/" + s3Key 287 288 } 288 289 pnum := int64(partNumber) 289 - req, _ := h.s3Service.Client.UploadPartRequest(&s3.UploadPartInput{ 290 + req, _ := h.s3Service.Client.UploadPartRequest(&awss3.UploadPartInput{ 290 291 Bucket: &h.s3Service.Bucket, 291 292 Key: &s3Key, 292 293 UploadId: &session.S3UploadID, ··· 342 343 343 344 // Convert to S3 CompletedPart format 344 345 // IMPORTANT: S3 requires ETags to be quoted in the CompleteMultipartUpload XML 345 - s3Parts := make([]*s3.CompletedPart, len(parts)) 346 + s3Parts := make([]*awss3.CompletedPart, len(parts)) 346 347 for i, p := range parts { 347 348 etag := normalizeETag(p.ETag) 348 349 pnum := int64(p.PartNumber) 349 - s3Parts[i] = &s3.CompletedPart{ 350 + s3Parts[i] = &awss3.CompletedPart{ 350 351 PartNumber: &pnum, 351 352 ETag: &etag, 352 353 } 353 354 } 354 - sourcePath := blobPath(session.Digest) 355 + sourcePath := s3.BlobPath(session.Digest) 355 356 s3Key := strings.TrimPrefix(sourcePath, "/") 356 357 if h.s3Service.PathPrefix != "" { 357 358 s3Key = h.s3Service.PathPrefix + "/" + s3Key 358 359 } 359 360 360 - _, err = h.s3Service.Client.CompleteMultipartUploadWithContext(ctx, &s3.CompleteMultipartUploadInput{ 361 + _, err = h.s3Service.Client.CompleteMultipartUploadWithContext(ctx, &awss3.CompleteMultipartUploadInput{ 361 362 Bucket: &h.s3Service.Bucket, 362 363 Key: &s3Key, 363 364 UploadId: &session.S3UploadID, 364 - MultipartUpload: &s3.CompletedMultipartUpload{ 365 + MultipartUpload: &awss3.CompletedMultipartUpload{ 365 366 Parts: s3Parts, 366 367 }, 367 368 }) ··· 374 375 "parts", len(s3Parts)) 375 376 376 377 // Verify the blob exists at temp location before moving 377 - destPath := blobPath(finalDigest) 378 + destPath := s3.BlobPath(finalDigest) 378 379 slog.Debug("About to move blob", 379 380 "source", sourcePath, 380 381 "dest", destPath) ··· 412 413 } 413 414 414 415 // Write assembled blob to final digest location (not temp) 415 - path := blobPath(finalDigest) 416 + path := s3.BlobPath(finalDigest) 416 417 writer, err := h.driver.Writer(ctx, path, false) 417 418 if err != nil { 418 419 return fmt.Errorf("failed to create writer: %w", err) ··· 448 449 if h.s3Service.Client == nil { 449 450 return fmt.Errorf("S3 not configured") 450 451 } 451 - path := blobPath(session.Digest) 452 + path := s3.BlobPath(session.Digest) 452 453 s3Key := strings.TrimPrefix(path, "/") 453 454 if h.s3Service.PathPrefix != "" { 454 455 s3Key = h.s3Service.PathPrefix + "/" + s3Key 455 456 } 456 457 457 - _, err := h.s3Service.Client.AbortMultipartUploadWithContext(ctx, &s3.AbortMultipartUploadInput{ 458 + _, err := h.s3Service.Client.AbortMultipartUploadWithContext(ctx, &awss3.AbortMultipartUploadInput{ 458 459 Bucket: &h.s3Service.Bucket, 459 460 Key: &s3Key, 460 461 UploadId: &session.S3UploadID, ··· 499 500 // Add quotes 500 501 return fmt.Sprintf("\"%s\"", etag) 501 502 } 502 - 503 - // blobPath converts a digest (e.g., "sha256:abc123...") or temp path to a storage path 504 - // Distribution stores blobs as: /docker/registry/v2/blobs/{algorithm}/{xx}/{hash}/data 505 - // where xx is the first 2 characters of the hash for directory sharding 506 - // NOTE: Path must start with / for filesystem driver 507 - // This is used for OCI container layers (content-addressed, globally deduplicated) 508 - func blobPath(digest string) string { 509 - // Handle temp paths (start with uploads/temp-) 510 - if strings.HasPrefix(digest, "uploads/temp-") { 511 - return fmt.Sprintf("/docker/registry/v2/%s/data", digest) 512 - } 513 - 514 - // Split digest into algorithm and hash 515 - parts := strings.SplitN(digest, ":", 2) 516 - if len(parts) != 2 { 517 - // Fallback for malformed digest 518 - return fmt.Sprintf("/docker/registry/v2/blobs/%s/data", digest) 519 - } 520 - 521 - algorithm := parts[0] 522 - hash := parts[1] 523 - 524 - // Use first 2 characters for sharding 525 - if len(hash) < 2 { 526 - return fmt.Sprintf("/docker/registry/v2/blobs/%s/%s/data", algorithm, hash) 527 - } 528 - 529 - return fmt.Sprintf("/docker/registry/v2/blobs/%s/%s/%s/data", algorithm, hash[:2], hash) 530 - }
+43 -12
pkg/hold/oci/xrpc.go
··· 9 9 10 10 "atcr.io/pkg/atproto" 11 11 "atcr.io/pkg/hold/pds" 12 + "atcr.io/pkg/hold/quota" 12 13 "atcr.io/pkg/s3" 13 14 storagedriver "github.com/distribution/distribution/v3/registry/storage/driver" 14 15 "github.com/go-chi/chi/v5" ··· 23 24 pds *pds.HoldPDS 24 25 httpClient pds.HTTPClient 25 26 enableBlueskyPosts bool 27 + quotaMgr *quota.Manager // Quota manager for tier-based limits 26 28 } 27 29 28 30 // NewXRPCHandler creates a new OCI XRPC handler 29 - func NewXRPCHandler(holdPDS *pds.HoldPDS, s3Service s3.S3Service, driver storagedriver.StorageDriver, disablePresignedURLs bool, enableBlueskyPosts bool, httpClient pds.HTTPClient) *XRPCHandler { 31 + func NewXRPCHandler(holdPDS *pds.HoldPDS, s3Service s3.S3Service, driver storagedriver.StorageDriver, disablePresignedURLs bool, enableBlueskyPosts bool, httpClient pds.HTTPClient, quotaMgr *quota.Manager) *XRPCHandler { 30 32 return &XRPCHandler{ 31 33 driver: driver, 32 34 disablePresignedURLs: disablePresignedURLs, ··· 35 37 pds: holdPDS, 36 38 httpClient: httpClient, 37 39 enableBlueskyPosts: enableBlueskyPosts, 40 + quotaMgr: quotaMgr, 38 41 } 39 42 } 40 43 ··· 217 220 218 221 // Parse request 219 222 var req struct { 220 - Repository string `json:"repository"` 221 - Tag string `json:"tag"` 222 - UserDID string `json:"userDid"` 223 - UserHandle string `json:"userHandle"` 224 - Operation string `json:"operation"` // "push" or "pull", defaults to "push" for backward compatibility 225 - Manifest struct { 223 + Repository string `json:"repository"` 224 + Tag string `json:"tag"` 225 + UserDID string `json:"userDid"` 226 + ManifestDigest string `json:"manifestDigest"` // For building layer record AT-URIs 227 + Operation string `json:"operation"` // "push" or "pull", defaults to "push" for backward compatibility 228 + Manifest struct { 226 229 MediaType string `json:"mediaType"` 227 230 Config struct { 228 231 Digest string `json:"digest"` ··· 276 279 277 280 // Only create layer records and Bluesky posts for pushes 278 281 if operation == "push" { 282 + // Soft limit check: block if ALREADY over quota 283 + // (blobs already uploaded to S3 by this point, no sense rejecting) 284 + stats, err := h.pds.GetQuotaForUserWithTier(ctx, req.UserDID, h.quotaMgr) 285 + if err == nil && stats.Limit != nil && stats.TotalSize > *stats.Limit { 286 + slog.Warn("Quota exceeded for push", 287 + "userDid", req.UserDID, 288 + "currentUsage", stats.TotalSize, 289 + "limit", *stats.Limit, 290 + "repository", req.Repository, 291 + "tag", req.Tag, 292 + ) 293 + RespondError(w, http.StatusForbidden, fmt.Sprintf( 294 + "quota exceeded: current=%d bytes, limit=%d bytes. Delete images to free space.", 295 + stats.TotalSize, *stats.Limit, 296 + )) 297 + return 298 + } 299 + 279 300 // Check if manifest posts are enabled 280 301 // Read from captain record (which is synced with HOLD_BLUESKY_POSTS_ENABLED env var) 281 302 postsEnabled := false ··· 287 308 postsEnabled = h.enableBlueskyPosts 288 309 } 289 310 311 + // Build manifest AT-URI for layer records 312 + manifestURI := atproto.BuildManifestURI(req.UserDID, req.ManifestDigest) 313 + 290 314 // Create layer records for each blob 291 315 for _, layer := range req.Manifest.Layers { 292 316 record := atproto.NewLayerRecord( 293 317 layer.Digest, 294 318 layer.Size, 295 319 layer.MediaType, 296 - req.Repository, 297 320 req.UserDID, 298 - req.UserHandle, 321 + manifestURI, 299 322 ) 300 323 301 324 _, _, err := h.pds.CreateLayerRecord(ctx, record) ··· 327 350 } 328 351 } 329 352 330 - // Create Bluesky post if enabled 331 - if postsEnabled { 353 + // Create Bluesky post if enabled and tag is present 354 + // Skip posts for tagless pushes (e.g., buildx platform manifests pushed by digest) 355 + if postsEnabled && req.Tag != "" { 356 + // Resolve handle from DID (cached, 24-hour TTL) 357 + _, userHandle, _, resolveErr := atproto.ResolveIdentity(ctx, req.UserDID) 358 + if resolveErr != nil { 359 + slog.Warn("Failed to resolve handle for user", "did", req.UserDID, "error", resolveErr) 360 + userHandle = req.UserDID // Fallback to DID if resolution fails 361 + } 362 + 332 363 // Extract manifest digest from first layer (or use config digest as fallback) 333 364 manifestDigest := req.Manifest.Config.Digest 334 365 if len(req.Manifest.Layers) > 0 { ··· 340 371 h.driver, 341 372 req.Repository, 342 373 req.Tag, 343 - req.UserHandle, 374 + userHandle, 344 375 req.UserDID, 345 376 manifestDigest, 346 377 totalSize,
+1 -1
pkg/hold/oci/xrpc_test.go
··· 127 127 128 128 // Create OCI handler with buffered mode (no S3) 129 129 mockS3 := s3.S3Service{} 130 - handler := NewXRPCHandler(holdPDS, mockS3, driver, true, false, mockClient) 130 + handler := NewXRPCHandler(holdPDS, mockS3, driver, true, false, mockClient, nil) 131 131 132 132 return handler, ctx 133 133 }
+51
pkg/hold/pds/crew.go
··· 149 149 150 150 return nil 151 151 } 152 + 153 + // UpdateCrewMemberTier updates a crew member's tier 154 + // Since ATProto records are immutable, this finds the member's record by DID, 155 + // deletes it, and recreates it with the new tier value. 156 + func (p *HoldPDS) UpdateCrewMemberTier(ctx context.Context, memberDID, tier string) error { 157 + // Find the crew member's record by iterating over crew records 158 + members, err := p.ListCrewMembers(ctx) 159 + if err != nil { 160 + return fmt.Errorf("failed to list crew members: %w", err) 161 + } 162 + 163 + // Find the member with matching DID 164 + var targetMember *CrewMemberWithKey 165 + for _, m := range members { 166 + if m.Record.Member == memberDID { 167 + targetMember = m 168 + break 169 + } 170 + } 171 + 172 + if targetMember == nil { 173 + return fmt.Errorf("crew member not found: %s", memberDID) 174 + } 175 + 176 + // If tier is already the same, no update needed 177 + if targetMember.Record.Tier == tier { 178 + return nil 179 + } 180 + 181 + // Delete the old record 182 + if err := p.RemoveCrewMember(ctx, targetMember.Rkey); err != nil { 183 + return fmt.Errorf("failed to remove old crew record: %w", err) 184 + } 185 + 186 + // Create new record with updated tier 187 + newRecord := &atproto.CrewRecord{ 188 + Type: atproto.CrewCollection, 189 + Member: targetMember.Record.Member, 190 + Role: targetMember.Record.Role, 191 + Permissions: targetMember.Record.Permissions, 192 + Tier: tier, 193 + AddedAt: targetMember.Record.AddedAt, // Preserve original add time 194 + } 195 + 196 + _, _, err = p.repomgr.CreateRecord(ctx, p.uid, atproto.CrewCollection, newRecord) 197 + if err != nil { 198 + return fmt.Errorf("failed to create updated crew record: %w", err) 199 + } 200 + 201 + return nil 202 + }
+1 -1
pkg/hold/pds/events_test.go
··· 150 150 testCID, _ := cid.Decode("bafyreib2rxk3rkhh5ylyxj3x3gathxt3s32qvwj2lf3qg4kmzr6b7teqke") 151 151 152 152 // Broadcast 5 events (exceeds maxHistory of 3) 153 - for i := 0; i < 5; i++ { 153 + for range 5 { 154 154 event := &RepoEvent{ 155 155 NewRoot: testCID, 156 156 Rev: "test-rev",
+155
pkg/hold/pds/layer.go
··· 5 5 "fmt" 6 6 7 7 "atcr.io/pkg/atproto" 8 + "atcr.io/pkg/hold/quota" 9 + lexutil "github.com/bluesky-social/indigo/lex/util" 10 + "github.com/bluesky-social/indigo/repo" 8 11 ) 9 12 10 13 // CreateLayerRecord creates a new layer record in the hold's PDS ··· 57 60 // not for runtime queries 58 61 return nil, "", fmt.Errorf("ListLayerRecords not yet implemented") 59 62 } 63 + 64 + // QuotaStats represents storage quota information for a user 65 + type QuotaStats struct { 66 + UserDID string `json:"userDid"` 67 + UniqueBlobs int `json:"uniqueBlobs"` 68 + TotalSize int64 `json:"totalSize"` 69 + Limit *int64 `json:"limit,omitempty"` // nil = unlimited 70 + Tier string `json:"tier,omitempty"` // quota tier (e.g., 'deckhand', 'bosun', 'quartermaster') 71 + } 72 + 73 + // GetQuotaForUser calculates storage quota for a specific user 74 + // It iterates through all layer records, filters by userDid, deduplicates by digest, 75 + // and sums the sizes of unique blobs. 76 + func (p *HoldPDS) GetQuotaForUser(ctx context.Context, userDID string) (*QuotaStats, error) { 77 + if p.recordsIndex == nil { 78 + return nil, fmt.Errorf("records index not available") 79 + } 80 + 81 + // Get session for reading record data 82 + session, err := p.carstore.ReadOnlySession(p.uid) 83 + if err != nil { 84 + return nil, fmt.Errorf("failed to create session: %w", err) 85 + } 86 + 87 + head, err := p.carstore.GetUserRepoHead(ctx, p.uid) 88 + if err != nil { 89 + return nil, fmt.Errorf("failed to get repo head: %w", err) 90 + } 91 + 92 + if !head.Defined() { 93 + // Empty repo - return zero stats 94 + return &QuotaStats{UserDID: userDID}, nil 95 + } 96 + 97 + repoHandle, err := repo.OpenRepo(ctx, session, head) 98 + if err != nil { 99 + return nil, fmt.Errorf("failed to open repo: %w", err) 100 + } 101 + 102 + // Track unique digests and their sizes 103 + digestSizes := make(map[string]int64) 104 + 105 + // Iterate all layer records via the index 106 + cursor := "" 107 + batchSize := 1000 // Process in batches 108 + 109 + for { 110 + records, nextCursor, err := p.recordsIndex.ListRecords(atproto.LayerCollection, batchSize, cursor, true) 111 + if err != nil { 112 + return nil, fmt.Errorf("failed to list layer records: %w", err) 113 + } 114 + 115 + for _, rec := range records { 116 + // Construct record path and get the record data 117 + recordPath := rec.Collection + "/" + rec.Rkey 118 + 119 + _, recBytes, err := repoHandle.GetRecordBytes(ctx, recordPath) 120 + if err != nil { 121 + // Skip records we can't read 122 + continue 123 + } 124 + 125 + // Decode the layer record 126 + recordValue, err := lexutil.CborDecodeValue(*recBytes) 127 + if err != nil { 128 + continue 129 + } 130 + 131 + layerRecord, ok := recordValue.(*atproto.LayerRecord) 132 + if !ok { 133 + continue 134 + } 135 + 136 + // Filter by userDID 137 + if layerRecord.UserDID != userDID { 138 + continue 139 + } 140 + 141 + // Deduplicate by digest - keep the size (could be different pushes of same blob) 142 + // Store the size - we only count each unique digest once 143 + if _, exists := digestSizes[layerRecord.Digest]; !exists { 144 + digestSizes[layerRecord.Digest] = layerRecord.Size 145 + } 146 + } 147 + 148 + if nextCursor == "" { 149 + break 150 + } 151 + cursor = nextCursor 152 + } 153 + 154 + // Calculate totals 155 + var totalSize int64 156 + for _, size := range digestSizes { 157 + totalSize += size 158 + } 159 + 160 + return &QuotaStats{ 161 + UserDID: userDID, 162 + UniqueBlobs: len(digestSizes), 163 + TotalSize: totalSize, 164 + }, nil 165 + } 166 + 167 + // GetQuotaForUserWithTier calculates quota with tier-aware limits 168 + // It returns the base quota stats plus the tier limit and tier name. 169 + // Captain (owner) always has unlimited quota. 170 + func (p *HoldPDS) GetQuotaForUserWithTier(ctx context.Context, userDID string, quotaMgr *quota.Manager) (*QuotaStats, error) { 171 + // Get base stats 172 + stats, err := p.GetQuotaForUser(ctx, userDID) 173 + if err != nil { 174 + return nil, err 175 + } 176 + 177 + // If quota manager is nil or disabled, return unlimited 178 + if quotaMgr == nil || !quotaMgr.IsEnabled() { 179 + return stats, nil 180 + } 181 + 182 + // Check if user is captain (owner) - always unlimited 183 + _, captain, err := p.GetCaptainRecord(ctx) 184 + if err == nil && captain.Owner == userDID { 185 + stats.Tier = "owner" 186 + // Limit remains nil (unlimited) 187 + return stats, nil 188 + } 189 + 190 + // Get crew record to find tier 191 + crewTier := p.getCrewTier(ctx, userDID) 192 + 193 + // Resolve limit from quota manager 194 + stats.Limit = quotaMgr.GetTierLimit(crewTier) 195 + stats.Tier = quotaMgr.GetTierName(crewTier) 196 + 197 + return stats, nil 198 + } 199 + 200 + // getCrewTier returns the tier for a crew member, or empty string if not found 201 + func (p *HoldPDS) getCrewTier(ctx context.Context, userDID string) string { 202 + crewMembers, err := p.ListCrewMembers(ctx) 203 + if err != nil { 204 + return "" 205 + } 206 + 207 + for _, member := range crewMembers { 208 + if member.Record.Member == userDID { 209 + return member.Record.Tier 210 + } 211 + } 212 + 213 + return "" 214 + }
+498 -73
pkg/hold/pds/layer_test.go
··· 1 1 package pds 2 2 3 3 import ( 4 + "os" 5 + "path/filepath" 4 6 "testing" 5 7 6 8 "atcr.io/pkg/atproto" 9 + "atcr.io/pkg/hold/quota" 7 10 ) 8 11 9 12 func TestCreateLayerRecord(t *testing.T) { ··· 22 25 "sha256:abc123def456", 23 26 1048576, // 1 MB 24 27 "application/vnd.oci.image.layer.v1.tar+gzip", 25 - "myapp", 26 28 "did:plc:alice123", 27 - "alice.bsky.social", 29 + "at://did:plc:alice123/io.atcr.manifest/abc123def456", 28 30 ), 29 31 wantErr: false, 30 32 }, ··· 34 36 "sha256:fedcba987654", 35 37 1073741824, // 1 GB 36 38 "application/vnd.docker.image.rootfs.diff.tar.gzip", 37 - "debian", 38 39 "did:plc:bob456", 39 - "bob.example.com", 40 + "at://did:plc:bob456/io.atcr.manifest/fedcba987654", 40 41 ), 41 42 wantErr: false, 42 43 }, 43 44 { 44 45 name: "invalid record type", 45 46 record: &atproto.LayerRecord{ 46 - Type: "wrong.type", 47 - Digest: "sha256:abc123", 48 - Size: 1024, 49 - MediaType: "application/vnd.oci.image.layer.v1.tar", 50 - Repository: "test", 51 - UserDID: "did:plc:test", 52 - UserHandle: "test.example.com", 47 + Type: "wrong.type", 48 + Digest: "sha256:abc123", 49 + Size: 1024, 50 + MediaType: "application/vnd.oci.image.layer.v1.tar", 51 + Manifest: "at://did:plc:test/io.atcr.manifest/abc123", 52 + UserDID: "did:plc:test", 53 53 }, 54 54 wantErr: true, 55 55 errSubstr: "invalid record type", ··· 57 57 { 58 58 name: "missing digest", 59 59 record: &atproto.LayerRecord{ 60 - Type: atproto.LayerCollection, 61 - Digest: "", 62 - Size: 1024, 63 - MediaType: "application/vnd.oci.image.layer.v1.tar", 64 - Repository: "test", 65 - UserDID: "did:plc:test", 66 - UserHandle: "test.example.com", 60 + Type: atproto.LayerCollection, 61 + Digest: "", 62 + Size: 1024, 63 + MediaType: "application/vnd.oci.image.layer.v1.tar", 64 + Manifest: "at://did:plc:test/io.atcr.manifest/abc123", 65 + UserDID: "did:plc:test", 67 66 }, 68 67 wantErr: true, 69 68 errSubstr: "digest is required", ··· 71 70 { 72 71 name: "zero size", 73 72 record: &atproto.LayerRecord{ 74 - Type: atproto.LayerCollection, 75 - Digest: "sha256:abc123", 76 - Size: 0, 77 - MediaType: "application/vnd.oci.image.layer.v1.tar", 78 - Repository: "test", 79 - UserDID: "did:plc:test", 80 - UserHandle: "test.example.com", 73 + Type: atproto.LayerCollection, 74 + Digest: "sha256:abc123", 75 + Size: 0, 76 + MediaType: "application/vnd.oci.image.layer.v1.tar", 77 + Manifest: "at://did:plc:test/io.atcr.manifest/abc123", 78 + UserDID: "did:plc:test", 81 79 }, 82 80 wantErr: true, 83 81 errSubstr: "size must be positive", ··· 85 83 { 86 84 name: "negative size", 87 85 record: &atproto.LayerRecord{ 88 - Type: atproto.LayerCollection, 89 - Digest: "sha256:abc123", 90 - Size: -1, 91 - MediaType: "application/vnd.oci.image.layer.v1.tar", 92 - Repository: "test", 93 - UserDID: "did:plc:test", 94 - UserHandle: "test.example.com", 86 + Type: atproto.LayerCollection, 87 + Digest: "sha256:abc123", 88 + Size: -1, 89 + MediaType: "application/vnd.oci.image.layer.v1.tar", 90 + Manifest: "at://did:plc:test/io.atcr.manifest/abc123", 91 + UserDID: "did:plc:test", 95 92 }, 96 93 wantErr: true, 97 94 errSubstr: "size must be positive", ··· 134 131 func TestCreateLayerRecord_MultipleRecords(t *testing.T) { 135 132 // Test creating multiple layer records for the same manifest 136 133 pds, ctx := setupTestPDS(t) 134 + 135 + manifestURI := "at://did:plc:test123/io.atcr.manifest/manifestabc123" 137 136 138 137 layers := []struct { 139 138 digest string ··· 151 150 layer.digest, 152 151 layer.size, 153 152 "application/vnd.oci.image.layer.v1.tar+gzip", 154 - "multi-layer-app", 155 153 "did:plc:test123", 156 - "test.example.com", 154 + manifestURI, 157 155 ) 158 156 159 157 rkey, cid, err := pds.CreateLayerRecord(ctx, record) ··· 180 178 digest := "sha256:abc123def456" 181 179 size := int64(1048576) 182 180 mediaType := "application/vnd.oci.image.layer.v1.tar+gzip" 183 - repository := "myapp" 184 181 userDID := "did:plc:alice123" 185 - userHandle := "alice.bsky.social" 182 + manifestURI := "at://did:plc:alice123/io.atcr.manifest/abc123def456" 186 183 187 - record := atproto.NewLayerRecord(digest, size, mediaType, repository, userDID, userHandle) 184 + record := atproto.NewLayerRecord(digest, size, mediaType, userDID, manifestURI) 188 185 189 186 if record == nil { 190 187 t.Fatal("NewLayerRecord() returned nil") ··· 207 204 t.Errorf("MediaType = %q, want %q", record.MediaType, mediaType) 208 205 } 209 206 210 - if record.Repository != repository { 211 - t.Errorf("Repository = %q, want %q", record.Repository, repository) 207 + if record.Manifest != manifestURI { 208 + t.Errorf("Manifest = %q, want %q", record.Manifest, manifestURI) 212 209 } 213 210 214 211 if record.UserDID != userDID { 215 212 t.Errorf("UserDID = %q, want %q", record.UserDID, userDID) 216 - } 217 - 218 - if record.UserHandle != userHandle { 219 - t.Errorf("UserHandle = %q, want %q", record.UserHandle, userHandle) 220 213 } 221 214 222 215 if record.CreatedAt == "" { ··· 229 222 func TestLayerRecord_FieldValidation(t *testing.T) { 230 223 // Test various field values 231 224 tests := []struct { 232 - name string 233 - digest string 234 - size int64 235 - mediaType string 236 - repository string 237 - userDID string 238 - userHandle string 225 + name string 226 + digest string 227 + size int64 228 + mediaType string 229 + userDID string 230 + manifestURI string 239 231 }{ 240 232 { 241 - name: "typical OCI layer", 242 - digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", 243 - size: 12582912, // 12 MB 244 - mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", 245 - repository: "hsm-secrets-operator", 246 - userDID: "did:plc:evan123", 247 - userHandle: "evan.jarrett.net", 233 + name: "typical OCI layer", 234 + digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", 235 + size: 12582912, // 12 MB 236 + mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", 237 + userDID: "did:plc:evan123", 238 + manifestURI: "at://did:plc:evan123/io.atcr.manifest/abc123", 248 239 }, 249 240 { 250 - name: "Docker layer format", 251 - digest: "sha256:abc123", 252 - size: 1024, 253 - mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", 254 - repository: "nginx", 255 - userDID: "did:plc:user456", 256 - userHandle: "user.example.com", 241 + name: "Docker layer format", 242 + digest: "sha256:abc123", 243 + size: 1024, 244 + mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", 245 + userDID: "did:plc:user456", 246 + manifestURI: "at://did:plc:user456/io.atcr.manifest/def456", 257 247 }, 258 248 { 259 - name: "uncompressed layer", 260 - digest: "sha256:def456", 261 - size: 2048, 262 - mediaType: "application/vnd.oci.image.layer.v1.tar", 263 - repository: "alpine", 264 - userDID: "did:plc:user789", 265 - userHandle: "user.bsky.social", 249 + name: "uncompressed layer", 250 + digest: "sha256:def456", 251 + size: 2048, 252 + mediaType: "application/vnd.oci.image.layer.v1.tar", 253 + userDID: "did:plc:user789", 254 + manifestURI: "at://did:plc:user789/io.atcr.manifest/ghi789", 266 255 }, 267 256 } 268 257 ··· 272 261 tt.digest, 273 262 tt.size, 274 263 tt.mediaType, 275 - tt.repository, 276 264 tt.userDID, 277 - tt.userHandle, 265 + tt.manifestURI, 278 266 ) 279 267 280 268 if record == nil { ··· 289 277 if record.Digest != tt.digest { 290 278 t.Errorf("Digest = %q, want %q", record.Digest, tt.digest) 291 279 } 280 + 281 + if record.Manifest != tt.manifestURI { 282 + t.Errorf("Manifest = %q, want %q", record.Manifest, tt.manifestURI) 283 + } 292 284 }) 293 285 } 294 286 } 287 + 288 + // setupTestPDSWithIndex creates a PDS with file-based database (enables RecordsIndex) 289 + // and bootstraps it with the given owner. Required for quota tests. 290 + func setupTestPDSWithIndex(t *testing.T, ownerDID string) (*HoldPDS, func()) { 291 + t.Helper() 292 + 293 + ctx := sharedCtx 294 + tmpDir := t.TempDir() 295 + 296 + // Use file-based database to enable RecordsIndex 297 + dbPath := filepath.Join(tmpDir, "pds.db") 298 + keyPath := filepath.Join(tmpDir, "signing-key") 299 + 300 + // Copy shared signing key 301 + if err := os.WriteFile(keyPath, sharedTestKey, 0600); err != nil { 302 + t.Fatalf("Failed to copy shared signing key: %v", err) 303 + } 304 + 305 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 306 + if err != nil { 307 + t.Fatalf("Failed to create test PDS: %v", err) 308 + } 309 + 310 + // Bootstrap with owner 311 + if err := pds.Bootstrap(ctx, nil, ownerDID, true, false, ""); err != nil { 312 + t.Fatalf("Failed to bootstrap PDS: %v", err) 313 + } 314 + 315 + // Wire up records indexing 316 + indexingHandler := pds.CreateRecordsIndexEventHandler(nil) 317 + pds.RepomgrRef().SetEventHandler(indexingHandler, true) 318 + 319 + // Backfill index from MST 320 + if err := pds.BackfillRecordsIndex(ctx); err != nil { 321 + t.Fatalf("Failed to backfill records index: %v", err) 322 + } 323 + 324 + cleanup := func() { 325 + pds.Close() 326 + } 327 + 328 + return pds, cleanup 329 + } 330 + 331 + // addCrewMemberWithTier adds a crew member with a specific tier 332 + func addCrewMemberWithTier(t *testing.T, pds *HoldPDS, memberDID, role string, permissions []string, tier string) { 333 + t.Helper() 334 + 335 + crewRecord := &atproto.CrewRecord{ 336 + Type: atproto.CrewCollection, 337 + Member: memberDID, 338 + Role: role, 339 + Permissions: permissions, 340 + Tier: tier, 341 + AddedAt: "2026-01-04T12:00:00Z", 342 + } 343 + 344 + _, _, err := pds.repomgr.CreateRecord(sharedCtx, pds.uid, atproto.CrewCollection, crewRecord) 345 + if err != nil { 346 + t.Fatalf("Failed to add crew member with tier: %v", err) 347 + } 348 + } 349 + 350 + func TestGetQuotaForUserWithTier_OwnerUnlimited(t *testing.T) { 351 + ownerDID := "did:plc:owner123" 352 + pds, cleanup := setupTestPDSWithIndex(t, ownerDID) 353 + defer cleanup() 354 + 355 + ctx := sharedCtx 356 + 357 + // Create quota manager with config 358 + tmpDir := t.TempDir() 359 + configPath := filepath.Join(tmpDir, "quotas.yaml") 360 + configContent := ` 361 + tiers: 362 + deckhand: 363 + quota: 5GB 364 + bosun: 365 + quota: 50GB 366 + 367 + defaults: 368 + new_crew_tier: deckhand 369 + ` 370 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 371 + t.Fatalf("Failed to write quota config: %v", err) 372 + } 373 + 374 + quotaMgr, err := quota.NewManager(configPath) 375 + if err != nil { 376 + t.Fatalf("Failed to create quota manager: %v", err) 377 + } 378 + 379 + // Create layer records for owner 380 + for i := range 3 { 381 + record := atproto.NewLayerRecord( 382 + "sha256:owner"+string(rune('a'+i)), 383 + 1024*1024*100, // 100MB each 384 + "application/vnd.oci.image.layer.v1.tar+gzip", 385 + ownerDID, 386 + "at://"+ownerDID+"/io.atcr.manifest/test123", 387 + ) 388 + if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil { 389 + t.Fatalf("Failed to create layer record: %v", err) 390 + } 391 + } 392 + 393 + // Get quota for owner 394 + stats, err := pds.GetQuotaForUserWithTier(ctx, ownerDID, quotaMgr) 395 + if err != nil { 396 + t.Fatalf("GetQuotaForUserWithTier failed: %v", err) 397 + } 398 + 399 + // Owner should have unlimited quota (nil limit) 400 + if stats.Limit != nil { 401 + t.Errorf("Expected nil limit for owner, got %d", *stats.Limit) 402 + } 403 + 404 + // Tier should be "owner" 405 + if stats.Tier != "owner" { 406 + t.Errorf("Expected tier 'owner', got %q", stats.Tier) 407 + } 408 + 409 + // Should have 3 unique blobs 410 + if stats.UniqueBlobs != 3 { 411 + t.Errorf("Expected 3 unique blobs, got %d", stats.UniqueBlobs) 412 + } 413 + 414 + // Total size should be 300MB 415 + expectedSize := int64(3 * 100 * 1024 * 1024) 416 + if stats.TotalSize != expectedSize { 417 + t.Errorf("Expected total size %d, got %d", expectedSize, stats.TotalSize) 418 + } 419 + 420 + t.Logf("Owner quota stats: %+v", stats) 421 + } 422 + 423 + func TestGetQuotaForUserWithTier_CrewWithDefaultTier(t *testing.T) { 424 + ownerDID := "did:plc:owner456" 425 + crewDID := "did:plc:crew123" 426 + pds, cleanup := setupTestPDSWithIndex(t, ownerDID) 427 + defer cleanup() 428 + 429 + ctx := sharedCtx 430 + 431 + // Create quota manager 432 + tmpDir := t.TempDir() 433 + configPath := filepath.Join(tmpDir, "quotas.yaml") 434 + configContent := ` 435 + tiers: 436 + deckhand: 437 + quota: 5GB 438 + bosun: 439 + quota: 50GB 440 + 441 + defaults: 442 + new_crew_tier: deckhand 443 + ` 444 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 445 + t.Fatalf("Failed to write quota config: %v", err) 446 + } 447 + 448 + quotaMgr, err := quota.NewManager(configPath) 449 + if err != nil { 450 + t.Fatalf("Failed to create quota manager: %v", err) 451 + } 452 + 453 + // Add crew member with no tier (should use default) 454 + addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "") 455 + 456 + // Create layer records for crew member 457 + for i := range 2 { 458 + record := atproto.NewLayerRecord( 459 + "sha256:crew"+string(rune('a'+i)), 460 + 1024*1024*50, // 50MB each 461 + "application/vnd.oci.image.layer.v1.tar+gzip", 462 + crewDID, 463 + "at://"+crewDID+"/io.atcr.manifest/test456", 464 + ) 465 + if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil { 466 + t.Fatalf("Failed to create layer record: %v", err) 467 + } 468 + } 469 + 470 + // Get quota for crew member 471 + stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, quotaMgr) 472 + if err != nil { 473 + t.Fatalf("GetQuotaForUserWithTier failed: %v", err) 474 + } 475 + 476 + // Should have 5GB limit (deckhand tier) 477 + expectedLimit := int64(5 * 1024 * 1024 * 1024) 478 + if stats.Limit == nil { 479 + t.Fatal("Expected non-nil limit for crew member") 480 + } 481 + if *stats.Limit != expectedLimit { 482 + t.Errorf("Expected limit %d, got %d", expectedLimit, *stats.Limit) 483 + } 484 + 485 + // Tier should be "deckhand" 486 + if stats.Tier != "deckhand" { 487 + t.Errorf("Expected tier 'deckhand', got %q", stats.Tier) 488 + } 489 + 490 + // Should have 2 unique blobs 491 + if stats.UniqueBlobs != 2 { 492 + t.Errorf("Expected 2 unique blobs, got %d", stats.UniqueBlobs) 493 + } 494 + 495 + t.Logf("Crew (deckhand tier) quota stats: %+v", stats) 496 + } 497 + 498 + func TestGetQuotaForUserWithTier_CrewWithExplicitTier(t *testing.T) { 499 + ownerDID := "did:plc:owner789" 500 + crewDID := "did:plc:bosuncrew456" 501 + pds, cleanup := setupTestPDSWithIndex(t, ownerDID) 502 + defer cleanup() 503 + 504 + ctx := sharedCtx 505 + 506 + // Create quota manager 507 + tmpDir := t.TempDir() 508 + configPath := filepath.Join(tmpDir, "quotas.yaml") 509 + configContent := ` 510 + tiers: 511 + deckhand: 512 + quota: 5GB 513 + bosun: 514 + quota: 50GB 515 + 516 + defaults: 517 + new_crew_tier: deckhand 518 + ` 519 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 520 + t.Fatalf("Failed to write quota config: %v", err) 521 + } 522 + 523 + quotaMgr, err := quota.NewManager(configPath) 524 + if err != nil { 525 + t.Fatalf("Failed to create quota manager: %v", err) 526 + } 527 + 528 + // Add crew member with explicit "bosun" tier 529 + addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "bosun") 530 + 531 + // Create layer records for crew member 532 + record := atproto.NewLayerRecord( 533 + "sha256:bosunlayer1", 534 + 1024*1024*1024, // 1GB 535 + "application/vnd.oci.image.layer.v1.tar+gzip", 536 + crewDID, 537 + "at://"+crewDID+"/io.atcr.manifest/test789", 538 + ) 539 + if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil { 540 + t.Fatalf("Failed to create layer record: %v", err) 541 + } 542 + 543 + // Get quota for crew member 544 + stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, quotaMgr) 545 + if err != nil { 546 + t.Fatalf("GetQuotaForUserWithTier failed: %v", err) 547 + } 548 + 549 + // Should have 50GB limit (bosun tier) 550 + expectedLimit := int64(50 * 1024 * 1024 * 1024) 551 + if stats.Limit == nil { 552 + t.Fatal("Expected non-nil limit for crew member") 553 + } 554 + if *stats.Limit != expectedLimit { 555 + t.Errorf("Expected limit %d, got %d", expectedLimit, *stats.Limit) 556 + } 557 + 558 + // Tier should be "bosun" 559 + if stats.Tier != "bosun" { 560 + t.Errorf("Expected tier 'bosun', got %q", stats.Tier) 561 + } 562 + 563 + t.Logf("Crew (bosun tier) quota stats: %+v", stats) 564 + } 565 + 566 + func TestGetQuotaForUserWithTier_NoQuotaManager(t *testing.T) { 567 + ownerDID := "did:plc:ownerabc" 568 + crewDID := "did:plc:crewabc" 569 + pds, cleanup := setupTestPDSWithIndex(t, ownerDID) 570 + defer cleanup() 571 + 572 + ctx := sharedCtx 573 + 574 + // Add crew member 575 + addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "deckhand") 576 + 577 + // Create layer record 578 + record := atproto.NewLayerRecord( 579 + "sha256:noquotalayer1", 580 + 1024*1024*100, 581 + "application/vnd.oci.image.layer.v1.tar+gzip", 582 + crewDID, 583 + "at://"+crewDID+"/io.atcr.manifest/testabc", 584 + ) 585 + if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil { 586 + t.Fatalf("Failed to create layer record: %v", err) 587 + } 588 + 589 + // Get quota with nil quota manager (no enforcement) 590 + stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, nil) 591 + if err != nil { 592 + t.Fatalf("GetQuotaForUserWithTier failed: %v", err) 593 + } 594 + 595 + // Should have nil limit (unlimited) 596 + if stats.Limit != nil { 597 + t.Errorf("Expected nil limit when quota manager is nil, got %d", *stats.Limit) 598 + } 599 + 600 + // Tier should be empty 601 + if stats.Tier != "" { 602 + t.Errorf("Expected empty tier, got %q", stats.Tier) 603 + } 604 + 605 + t.Logf("No quota manager stats: %+v", stats) 606 + } 607 + 608 + func TestGetQuotaForUserWithTier_DisabledQuotas(t *testing.T) { 609 + ownerDID := "did:plc:ownerdef" 610 + crewDID := "did:plc:crewdef" 611 + pds, cleanup := setupTestPDSWithIndex(t, ownerDID) 612 + defer cleanup() 613 + 614 + ctx := sharedCtx 615 + 616 + // Create quota manager with nonexistent config (disabled) 617 + quotaMgr, err := quota.NewManager("/nonexistent/quotas.yaml") 618 + if err != nil { 619 + t.Fatalf("Failed to create quota manager: %v", err) 620 + } 621 + 622 + if quotaMgr.IsEnabled() { 623 + t.Fatal("Expected quotas to be disabled") 624 + } 625 + 626 + // Add crew member 627 + addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "bosun") 628 + 629 + // Create layer record 630 + record := atproto.NewLayerRecord( 631 + "sha256:disabledlayer1", 632 + 1024*1024*100, 633 + "application/vnd.oci.image.layer.v1.tar+gzip", 634 + crewDID, 635 + "at://"+crewDID+"/io.atcr.manifest/testdef", 636 + ) 637 + if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil { 638 + t.Fatalf("Failed to create layer record: %v", err) 639 + } 640 + 641 + // Get quota with disabled quota manager 642 + stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, quotaMgr) 643 + if err != nil { 644 + t.Fatalf("GetQuotaForUserWithTier failed: %v", err) 645 + } 646 + 647 + // Should have nil limit (unlimited when quotas disabled) 648 + if stats.Limit != nil { 649 + t.Errorf("Expected nil limit when quotas disabled, got %d", *stats.Limit) 650 + } 651 + 652 + t.Logf("Disabled quotas stats: %+v", stats) 653 + } 654 + 655 + func TestGetQuotaForUserWithTier_DeduplicatesBlobs(t *testing.T) { 656 + ownerDID := "did:plc:ownerghi" 657 + crewDID := "did:plc:crewghi" 658 + pds, cleanup := setupTestPDSWithIndex(t, ownerDID) 659 + defer cleanup() 660 + 661 + ctx := sharedCtx 662 + 663 + // Create quota manager 664 + tmpDir := t.TempDir() 665 + configPath := filepath.Join(tmpDir, "quotas.yaml") 666 + configContent := ` 667 + tiers: 668 + deckhand: 669 + quota: 5GB 670 + 671 + defaults: 672 + new_crew_tier: deckhand 673 + ` 674 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 675 + t.Fatalf("Failed to write quota config: %v", err) 676 + } 677 + 678 + quotaMgr, err := quota.NewManager(configPath) 679 + if err != nil { 680 + t.Fatalf("Failed to create quota manager: %v", err) 681 + } 682 + 683 + // Add crew member 684 + addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "") 685 + 686 + // Create multiple layer records with same digest (should be deduplicated) 687 + digest := "sha256:duplicatelayer" 688 + for i := range 5 { 689 + record := atproto.NewLayerRecord( 690 + digest, 691 + 1024*1024*100, // 100MB 692 + "application/vnd.oci.image.layer.v1.tar+gzip", 693 + crewDID, 694 + "at://"+crewDID+"/io.atcr.manifest/manifest"+string(rune('a'+i)), 695 + ) 696 + if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil { 697 + t.Fatalf("Failed to create layer record %d: %v", i, err) 698 + } 699 + } 700 + 701 + // Get quota 702 + stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, quotaMgr) 703 + if err != nil { 704 + t.Fatalf("GetQuotaForUserWithTier failed: %v", err) 705 + } 706 + 707 + // Should have 1 unique blob (deduplicated) 708 + if stats.UniqueBlobs != 1 { 709 + t.Errorf("Expected 1 unique blob (deduplicated), got %d", stats.UniqueBlobs) 710 + } 711 + 712 + // Total size should be 100MB (not 500MB) 713 + expectedSize := int64(100 * 1024 * 1024) 714 + if stats.TotalSize != expectedSize { 715 + t.Errorf("Expected total size %d, got %d", expectedSize, stats.TotalSize) 716 + } 717 + 718 + t.Logf("Deduplicated quota stats: %+v", stats) 719 + }
+3 -23
pkg/hold/pds/records_test.go
··· 1 1 package pds 2 2 3 3 import ( 4 - "context" 5 4 "os" 6 5 "path/filepath" 7 6 "testing" 8 7 9 - "github.com/bluesky-social/indigo/repo" 10 8 _ "github.com/mattn/go-sqlite3" 11 9 ) 12 10 ··· 324 322 defer ri.Close() 325 323 326 324 // Add 5 records 327 - for i := 0; i < 5; i++ { 325 + for i := range 5 { 328 326 rkey := string(rune('a' + i)) 329 327 if err := ri.IndexRecord("io.atcr.hold.crew", rkey, "cid-"+rkey); err != nil { 330 328 t.Fatalf("IndexRecord() error = %v", err) ··· 475 473 defer ri.Close() 476 474 477 475 // Add records to two collections 478 - for i := 0; i < 3; i++ { 476 + for i := range 3 { 479 477 ri.IndexRecord("io.atcr.hold.crew", string(rune('a'+i)), "cid1") 480 478 } 481 - for i := 0; i < 5; i++ { 479 + for i := range 5 { 482 480 ri.IndexRecord("io.atcr.hold.captain", string(rune('a'+i)), "cid2") 483 481 } 484 482 ··· 607 605 t.Errorf("Expected captain count 1 after deleting crew, got %d", count) 608 606 } 609 607 } 610 - 611 - // mockRepo is a minimal mock for testing backfill 612 - // Note: Full backfill testing requires integration tests with real repo 613 - type mockRepo struct { 614 - records map[string]string // key -> cid 615 - } 616 - 617 - func (m *mockRepo) ForEach(ctx context.Context, prefix string, fn func(string, interface{}) error) error { 618 - for k, v := range m.records { 619 - if err := fn(k, v); err != nil { 620 - if err == repo.ErrDoneIterating { 621 - return nil 622 - } 623 - return err 624 - } 625 - } 626 - return nil 627 - }
+1 -2
pkg/hold/pds/server.go
··· 103 103 // Uses same database as carstore for simplicity 104 104 var recordsIndex *RecordsIndex 105 105 if dbPath != ":memory:" { 106 - recordsDbPath := dbPath + "/db.sqlite3" 107 - recordsIndex, err = NewRecordsIndex(recordsDbPath) 106 + recordsIndex, err = NewRecordsIndex(dbPath + "/db.sqlite3") 108 107 if err != nil { 109 108 return nil, fmt.Errorf("failed to create records index: %w", err) 110 109 }
+3 -3
pkg/hold/pds/status_test.go
··· 55 55 } 56 56 57 57 // Create handler for XRPC endpoints 58 - handler := NewXRPCHandler(holdPDS, s3.S3Service{}, nil, nil, &mockPDSClient{}) 58 + handler := NewXRPCHandler(holdPDS, s3.S3Service{}, nil, nil, &mockPDSClient{}, nil) 59 59 60 60 // Helper function to list posts via XRPC 61 61 listPosts := func() ([]map[string]any, error) { ··· 232 232 } 233 233 234 234 func findSubstring(s, substr string) bool { 235 - for i := 0; i <= len(s)-len(substr); i++ { 235 + for i := range len(s) - len(substr) + 1 { 236 236 if s[i:i+len(substr)] == substr { 237 237 return true 238 238 } ··· 283 283 } 284 284 285 285 // Create shared handler 286 - sharedHandler = NewXRPCHandler(sharedPDS, s3.S3Service{}, nil, nil, &mockPDSClient{}) 286 + sharedHandler = NewXRPCHandler(sharedPDS, s3.S3Service{}, nil, nil, &mockPDSClient{}, nil) 287 287 288 288 // Run tests 289 289 code := m.Run()
+39 -4
pkg/hold/pds/xrpc.go
··· 7 7 "fmt" 8 8 9 9 "atcr.io/pkg/atproto" 10 + "atcr.io/pkg/hold/quota" 10 11 "atcr.io/pkg/s3" 11 12 "github.com/bluesky-social/indigo/api/bsky" 12 13 lexutil "github.com/bluesky-social/indigo/lex/util" ··· 46 47 s3Service s3.S3Service 47 48 storageDriver driver.StorageDriver 48 49 broadcaster *EventBroadcaster 49 - httpClient HTTPClient // For testing - allows injecting mock HTTP client 50 + httpClient HTTPClient // For testing - allows injecting mock HTTP client 51 + quotaMgr *quota.Manager // Quota manager for tier-based limits 50 52 } 51 53 52 54 // PartInfo represents a completed part in a multipart upload ··· 64 66 } 65 67 66 68 // NewXRPCHandler creates a new XRPC handler 67 - func NewXRPCHandler(pds *HoldPDS, s3Service s3.S3Service, storageDriver driver.StorageDriver, broadcaster *EventBroadcaster, httpClient HTTPClient) *XRPCHandler { 69 + func NewXRPCHandler(pds *HoldPDS, s3Service s3.S3Service, storageDriver driver.StorageDriver, broadcaster *EventBroadcaster, httpClient HTTPClient, quotaMgr *quota.Manager) *XRPCHandler { 68 70 return &XRPCHandler{ 69 71 pds: pds, 70 72 s3Service: s3Service, 71 73 storageDriver: storageDriver, 72 74 broadcaster: broadcaster, 73 75 httpClient: httpClient, 76 + quotaMgr: quotaMgr, 74 77 } 75 78 } 76 79 ··· 192 195 r.Use(h.requireAuth) 193 196 r.Post(atproto.HoldRequestCrew, h.HandleRequestCrew) 194 197 }) 198 + 199 + // Public quota endpoint (no auth - quota is per-user, just needs userDid param) 200 + r.Get(atproto.HoldGetQuota, h.HandleGetQuota) 195 201 } 196 202 197 203 // HandleHealth returns health check information ··· 211 217 hostname := h.pds.PublicURL 212 218 hostname = strings.TrimPrefix(hostname, "http://") 213 219 hostname = strings.TrimPrefix(hostname, "https://") 214 - hostname = strings.Split(hostname, "/")[0] // Remove path 215 - hostname = strings.Split(hostname, ":")[0] // Remove port 220 + hostname, _, _ = strings.Cut(hostname, "/") // Remove path 221 + hostname, _, _ = strings.Cut(hostname, ":") // Remove port 216 222 217 223 response := map[string]any{ 218 224 "did": h.pds.DID(), ··· 1513 1519 // Clients should use multipart upload flow via com.atproto.repo.uploadBlob 1514 1520 return "" 1515 1521 } 1522 + 1523 + // HandleGetQuota returns storage quota information for a user 1524 + // This calculates the total unique blob storage used by a specific user 1525 + // by iterating layer records and deduplicating by digest. 1526 + // Also returns tier-aware quota limits if quotas.yaml is configured. 1527 + func (h *XRPCHandler) HandleGetQuota(w http.ResponseWriter, r *http.Request) { 1528 + userDID := r.URL.Query().Get("userDid") 1529 + if userDID == "" { 1530 + http.Error(w, "missing required parameter: userDid", http.StatusBadRequest) 1531 + return 1532 + } 1533 + 1534 + // Validate DID format 1535 + if !atproto.IsDID(userDID) { 1536 + http.Error(w, "invalid userDid format", http.StatusBadRequest) 1537 + return 1538 + } 1539 + 1540 + // Get quota stats with tier-aware limits 1541 + stats, err := h.pds.GetQuotaForUserWithTier(r.Context(), userDID, h.quotaMgr) 1542 + if err != nil { 1543 + slog.Error("Failed to get quota", "userDid", userDID, "error", err) 1544 + http.Error(w, fmt.Sprintf("failed to get quota: %v", err), http.StatusInternalServerError) 1545 + return 1546 + } 1547 + 1548 + w.Header().Set("Content-Type", "application/json") 1549 + json.NewEncoder(w).Encode(stats) 1550 + }
+10 -10
pkg/hold/pds/xrpc_test.go
··· 76 76 mockS3 := s3.S3Service{} 77 77 78 78 // Create XRPC handler with mock HTTP client 79 - handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient) 79 + handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient, nil) 80 80 81 81 return handler, ctx 82 82 } ··· 143 143 mockS3 := s3.S3Service{} 144 144 145 145 // Create XRPC handler with mock HTTP client 146 - handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient) 146 + handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient, nil) 147 147 148 148 return handler, ctx 149 149 } ··· 609 609 610 610 // Note: Bootstrap already added 1 crew member 611 611 // Add 4 more for a total of 5 612 - for i := 0; i < 4; i++ { 612 + for i := range 4 { 613 613 _, err := handler.pds.AddCrewMember(ctx, "did:plc:member"+string(rune(i+'0')), "reader", []string{"blob:read"}) 614 614 if err != nil { 615 615 t.Fatalf("Failed to add crew member: %v", err) ··· 673 673 holdDID := "did:web:hold.example.com" 674 674 675 675 // Add crew members 676 - for i := 0; i < 3; i++ { 676 + for i := range 3 { 677 677 _, err := handler.pds.AddCrewMember(ctx, "did:plc:member"+string(rune(i+'0')), "reader", []string{"blob:read"}) 678 678 if err != nil { 679 679 t.Fatalf("Failed to add crew member: %v", err) ··· 753 753 pds, ctx := setupTestPDS(t) // Don't bootstrap - no records created yet 754 754 mockClient := &mockPDSClient{} 755 755 mockS3 := s3.S3Service{} 756 - handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient) 756 + handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient, nil) 757 757 758 758 // Initialize repo manually (setupTestPDS doesn't call Bootstrap, so no crew members) 759 759 err := pds.repomgr.InitNewActor(ctx, pds.uid, "", pds.did, "", "", "") ··· 888 888 holdDID := "did:web:hold.example.com" 889 889 890 890 // Add 4 more crew members for total of 5 891 - for i := 0; i < 4; i++ { 891 + for i := range 4 { 892 892 _, err := handler.pds.AddCrewMember(ctx, fmt.Sprintf("did:plc:member%d", i), "reader", []string{"blob:read"}) 893 893 if err != nil { 894 894 t.Fatalf("Failed to add crew member: %v", err) ··· 968 968 holdDID := "did:web:hold.example.com" 969 969 970 970 // Add crew members 971 - for i := 0; i < 3; i++ { 971 + for i := range 3 { 972 972 _, err := handler.pds.AddCrewMember(ctx, fmt.Sprintf("did:plc:member%d", i), "reader", []string{"blob:read"}) 973 973 if err != nil { 974 974 t.Fatalf("Failed to add crew member: %v", err) ··· 1231 1231 pds, ctx := setupTestPDS(t) // Don't bootstrap 1232 1232 mockClient := &mockPDSClient{} 1233 1233 mockS3 := s3.S3Service{} 1234 - handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient) 1234 + handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient, nil) 1235 1235 1236 1236 // setupTestPDS creates the PDS/database but doesn't initialize the repo 1237 1237 // Check if implementation returns repos before initialization ··· 1317 1317 pds, ctx := setupTestPDS(t) // Don't bootstrap 1318 1318 mockClient := &mockPDSClient{} 1319 1319 mockS3 := s3.S3Service{} 1320 - handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient) 1320 + handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient, nil) 1321 1321 holdDID := "did:web:hold.example.com" 1322 1322 1323 1323 // Initialize repo but don't add any records ··· 2014 2014 mockClient := &mockPDSClient{} 2015 2015 2016 2016 // Create XRPC handler with mock s3 service and real filesystem driver 2017 - handler := NewXRPCHandler(pds, mockS3Svc.toS3Service(), driver, nil, mockClient) 2017 + handler := NewXRPCHandler(pds, mockS3Svc.toS3Service(), driver, nil, mockClient, nil) 2018 2018 2019 2019 return handler, mockS3Svc, ctx 2020 2020 }
+221
pkg/hold/quota/config.go
··· 1 + // Package quota provides storage quota management for hold services. 2 + package quota 3 + 4 + import ( 5 + "fmt" 6 + "os" 7 + "regexp" 8 + "strconv" 9 + "strings" 10 + 11 + "go.yaml.in/yaml/v4" 12 + ) 13 + 14 + // Config represents the quotas.yaml configuration 15 + type Config struct { 16 + Tiers map[string]TierConfig `yaml:"tiers"` 17 + Defaults DefaultsConfig `yaml:"defaults"` 18 + } 19 + 20 + // TierConfig represents a single tier's configuration 21 + type TierConfig struct { 22 + Quota string `yaml:"quota"` // Human-readable size: "5GB", "50GB", etc. 23 + } 24 + 25 + // DefaultsConfig represents default settings 26 + type DefaultsConfig struct { 27 + NewCrewTier string `yaml:"new_crew_tier"` 28 + } 29 + 30 + // Manager manages quota configuration and tier resolution 31 + type Manager struct { 32 + config *Config 33 + tiers map[string]int64 // resolved tier name -> bytes 34 + } 35 + 36 + // NewManager creates a quota manager, loading config from file if present 37 + func NewManager(configPath string) (*Manager, error) { 38 + m := &Manager{ 39 + tiers: make(map[string]int64), 40 + } 41 + 42 + // Try to load config file 43 + data, err := os.ReadFile(configPath) 44 + if err != nil { 45 + if os.IsNotExist(err) { 46 + // No config file = no quotas enforced 47 + return m, nil 48 + } 49 + return nil, fmt.Errorf("failed to read quota config: %w", err) 50 + } 51 + 52 + var cfg Config 53 + if err := yaml.Unmarshal(data, &cfg); err != nil { 54 + return nil, fmt.Errorf("failed to parse quota config: %w", err) 55 + } 56 + 57 + m.config = &cfg 58 + 59 + // Parse and resolve all tiers 60 + for name, tier := range cfg.Tiers { 61 + bytes, err := ParseHumanBytes(tier.Quota) 62 + if err != nil { 63 + return nil, fmt.Errorf("invalid quota for tier %q: %w", name, err) 64 + } 65 + m.tiers[name] = bytes 66 + } 67 + 68 + return m, nil 69 + } 70 + 71 + // IsEnabled returns true if quotas are being enforced 72 + func (m *Manager) IsEnabled() bool { 73 + return m.config != nil 74 + } 75 + 76 + // GetTierLimit resolves the quota limit for a tier key 77 + // Returns nil for unlimited (captain, no config, or tier not found with no default) 78 + // 79 + // Resolution order: 80 + // 1. If quotas disabled โ†’ nil (unlimited) 81 + // 2. If tierKey provided and found โ†’ return that tier's limit 82 + // 3. If tierKey not found or empty โ†’ use defaults.new_crew_tier 83 + // 4. If default tier not found โ†’ nil (unlimited) 84 + func (m *Manager) GetTierLimit(tierKey string) *int64 { 85 + if !m.IsEnabled() { 86 + return nil 87 + } 88 + 89 + // Try the provided tier key first 90 + if tierKey != "" { 91 + if limit, ok := m.tiers[tierKey]; ok { 92 + return &limit 93 + } 94 + } 95 + 96 + // Fall back to default tier 97 + if m.config.Defaults.NewCrewTier != "" { 98 + if limit, ok := m.tiers[m.config.Defaults.NewCrewTier]; ok { 99 + return &limit 100 + } 101 + } 102 + 103 + // No valid tier found - unlimited 104 + return nil 105 + } 106 + 107 + // GetTierName resolves the tier name for a tier key 108 + // Returns the actual tier name being used (after fallback resolution) 109 + func (m *Manager) GetTierName(tierKey string) string { 110 + if !m.IsEnabled() { 111 + return "" 112 + } 113 + 114 + // Try the provided tier key first 115 + if tierKey != "" { 116 + if _, ok := m.tiers[tierKey]; ok { 117 + return tierKey 118 + } 119 + } 120 + 121 + // Fall back to default tier 122 + if m.config.Defaults.NewCrewTier != "" { 123 + if _, ok := m.tiers[m.config.Defaults.NewCrewTier]; ok { 124 + return m.config.Defaults.NewCrewTier 125 + } 126 + } 127 + 128 + return "" 129 + } 130 + 131 + // GetDefaultTier returns the default tier name for new crew members 132 + func (m *Manager) GetDefaultTier() string { 133 + if m.config == nil { 134 + return "" 135 + } 136 + return m.config.Defaults.NewCrewTier 137 + } 138 + 139 + // TierCount returns the number of configured tiers 140 + func (m *Manager) TierCount() int { 141 + return len(m.tiers) 142 + } 143 + 144 + // TierInfo represents tier information for listing 145 + type TierInfo struct { 146 + Key string 147 + Limit *int64 148 + } 149 + 150 + // ListTiers returns all configured tiers with their limits 151 + func (m *Manager) ListTiers() []TierInfo { 152 + if !m.IsEnabled() { 153 + return nil 154 + } 155 + 156 + tiers := make([]TierInfo, 0, len(m.tiers)) 157 + for key, limit := range m.tiers { 158 + limitCopy := limit // Create copy to take address of 159 + tiers = append(tiers, TierInfo{ 160 + Key: key, 161 + Limit: &limitCopy, 162 + }) 163 + } 164 + return tiers 165 + } 166 + 167 + // ParseHumanBytes parses human-readable byte sizes like "5GB", "100MB", "1.5TB" 168 + func ParseHumanBytes(s string) (int64, error) { 169 + s = strings.TrimSpace(strings.ToUpper(s)) 170 + if s == "" { 171 + return 0, fmt.Errorf("empty size string") 172 + } 173 + 174 + // Match number (with optional decimal) followed by optional unit 175 + re := regexp.MustCompile(`^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB|PB)?$`) 176 + matches := re.FindStringSubmatch(s) 177 + if matches == nil { 178 + return 0, fmt.Errorf("invalid size format: %s", s) 179 + } 180 + 181 + value, err := strconv.ParseFloat(matches[1], 64) 182 + if err != nil { 183 + return 0, fmt.Errorf("invalid number: %w", err) 184 + } 185 + 186 + unit := matches[2] 187 + if unit == "" { 188 + unit = "B" 189 + } 190 + 191 + multipliers := map[string]float64{ 192 + "B": 1, 193 + "KB": 1024, 194 + "MB": 1024 * 1024, 195 + "GB": 1024 * 1024 * 1024, 196 + "TB": 1024 * 1024 * 1024 * 1024, 197 + "PB": 1024 * 1024 * 1024 * 1024 * 1024, 198 + } 199 + 200 + mult, ok := multipliers[unit] 201 + if !ok { 202 + return 0, fmt.Errorf("unknown unit: %s", unit) 203 + } 204 + 205 + return int64(value * mult), nil 206 + } 207 + 208 + // FormatHumanBytes formats bytes as a human-readable string 209 + func FormatHumanBytes(bytes int64) string { 210 + const unit = 1024 211 + if bytes < unit { 212 + return fmt.Sprintf("%d B", bytes) 213 + } 214 + div, exp := int64(unit), 0 215 + for n := bytes / unit; n >= unit; n /= unit { 216 + div *= unit 217 + exp++ 218 + } 219 + units := []string{"KB", "MB", "GB", "TB", "PB"} 220 + return fmt.Sprintf("%.1f %s", float64(bytes)/float64(div), units[exp]) 221 + }
+275
pkg/hold/quota/config_test.go
··· 1 + package quota 2 + 3 + import ( 4 + "os" 5 + "path/filepath" 6 + "testing" 7 + ) 8 + 9 + func TestParseHumanBytes(t *testing.T) { 10 + tests := []struct { 11 + input string 12 + expected int64 13 + wantErr bool 14 + }{ 15 + // Basic units 16 + {"1024", 1024, false}, 17 + {"1024B", 1024, false}, 18 + {"1KB", 1024, false}, 19 + {"1MB", 1024 * 1024, false}, 20 + {"1GB", 1024 * 1024 * 1024, false}, 21 + {"1TB", 1024 * 1024 * 1024 * 1024, false}, 22 + {"1PB", 1024 * 1024 * 1024 * 1024 * 1024, false}, 23 + 24 + // Common sizes 25 + {"5GB", 5 * 1024 * 1024 * 1024, false}, 26 + {"50GB", 50 * 1024 * 1024 * 1024, false}, 27 + {"100GB", 100 * 1024 * 1024 * 1024, false}, 28 + {"500MB", 500 * 1024 * 1024, false}, 29 + 30 + // Case insensitive 31 + {"5gb", 5 * 1024 * 1024 * 1024, false}, 32 + {"5Gb", 5 * 1024 * 1024 * 1024, false}, 33 + 34 + // With whitespace 35 + {" 5GB ", 5 * 1024 * 1024 * 1024, false}, 36 + 37 + // Decimals 38 + {"1.5GB", int64(1.5 * 1024 * 1024 * 1024), false}, 39 + {"2.5TB", int64(2.5 * 1024 * 1024 * 1024 * 1024), false}, 40 + 41 + // Errors 42 + {"", 0, true}, 43 + {"invalid", 0, true}, 44 + {"GB", 0, true}, 45 + {"-5GB", 0, true}, 46 + } 47 + 48 + for _, tt := range tests { 49 + t.Run(tt.input, func(t *testing.T) { 50 + result, err := ParseHumanBytes(tt.input) 51 + if tt.wantErr { 52 + if err == nil { 53 + t.Errorf("expected error for input %q", tt.input) 54 + } 55 + return 56 + } 57 + if err != nil { 58 + t.Errorf("unexpected error: %v", err) 59 + return 60 + } 61 + if result != tt.expected { 62 + t.Errorf("got %d, want %d", result, tt.expected) 63 + } 64 + }) 65 + } 66 + } 67 + 68 + func TestFormatHumanBytes(t *testing.T) { 69 + tests := []struct { 70 + bytes int64 71 + expected string 72 + }{ 73 + {0, "0 B"}, 74 + {512, "512 B"}, 75 + {1024, "1.0 KB"}, 76 + {1024 * 1024, "1.0 MB"}, 77 + {1024 * 1024 * 1024, "1.0 GB"}, 78 + {5 * 1024 * 1024 * 1024, "5.0 GB"}, 79 + {1024 * 1024 * 1024 * 1024, "1.0 TB"}, 80 + } 81 + 82 + for _, tt := range tests { 83 + t.Run(tt.expected, func(t *testing.T) { 84 + result := FormatHumanBytes(tt.bytes) 85 + if result != tt.expected { 86 + t.Errorf("got %q, want %q", result, tt.expected) 87 + } 88 + }) 89 + } 90 + } 91 + 92 + func TestNewManager_NoConfigFile(t *testing.T) { 93 + m, err := NewManager("/nonexistent/quotas.yaml") 94 + if err != nil { 95 + t.Fatalf("expected no error for missing file, got: %v", err) 96 + } 97 + if m.IsEnabled() { 98 + t.Error("expected quotas to be disabled when file missing") 99 + } 100 + if m.GetTierLimit("anything") != nil { 101 + t.Error("expected nil limit when quotas disabled") 102 + } 103 + } 104 + 105 + func TestNewManager_ValidConfig(t *testing.T) { 106 + tmpDir := t.TempDir() 107 + configPath := filepath.Join(tmpDir, "quotas.yaml") 108 + 109 + configContent := ` 110 + tiers: 111 + deckhand: 112 + quota: 5GB 113 + bosun: 114 + quota: 50GB 115 + quartermaster: 116 + quota: 100GB 117 + 118 + defaults: 119 + new_crew_tier: deckhand 120 + ` 121 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 122 + t.Fatalf("failed to write config: %v", err) 123 + } 124 + 125 + m, err := NewManager(configPath) 126 + if err != nil { 127 + t.Fatalf("failed to load config: %v", err) 128 + } 129 + 130 + if !m.IsEnabled() { 131 + t.Error("expected quotas to be enabled") 132 + } 133 + 134 + if m.TierCount() != 3 { 135 + t.Errorf("expected 3 tiers, got %d", m.TierCount()) 136 + } 137 + 138 + // Test default tier (empty string) 139 + limit := m.GetTierLimit("") 140 + if limit == nil { 141 + t.Fatal("expected non-nil limit for default tier") 142 + } 143 + if *limit != 5*1024*1024*1024 { 144 + t.Errorf("expected 5GB limit for default, got %d", *limit) 145 + } 146 + 147 + // Test explicit tier 148 + limit = m.GetTierLimit("bosun") 149 + if limit == nil { 150 + t.Fatal("expected non-nil limit for bosun") 151 + } 152 + if *limit != 50*1024*1024*1024 { 153 + t.Errorf("expected 50GB limit for bosun, got %d", *limit) 154 + } 155 + 156 + // Test tier name resolution 157 + if m.GetTierName("") != "deckhand" { 158 + t.Errorf("expected tier name 'deckhand' for empty key, got %q", m.GetTierName("")) 159 + } 160 + if m.GetTierName("bosun") != "bosun" { 161 + t.Errorf("expected tier name 'bosun', got %q", m.GetTierName("bosun")) 162 + } 163 + } 164 + 165 + func TestNewManager_FallbackToDefault(t *testing.T) { 166 + tmpDir := t.TempDir() 167 + configPath := filepath.Join(tmpDir, "quotas.yaml") 168 + 169 + configContent := ` 170 + tiers: 171 + deckhand: 172 + quota: 5GB 173 + quartermaster: 174 + quota: 50GB 175 + 176 + defaults: 177 + new_crew_tier: deckhand 178 + ` 179 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 180 + t.Fatalf("failed to write config: %v", err) 181 + } 182 + 183 + m, err := NewManager(configPath) 184 + if err != nil { 185 + t.Fatalf("failed to load config: %v", err) 186 + } 187 + 188 + // Unknown tier should fall back to default 189 + limit := m.GetTierLimit("unknown_tier") 190 + if limit == nil { 191 + t.Fatal("expected fallback to default tier") 192 + } 193 + if *limit != 5*1024*1024*1024 { 194 + t.Errorf("expected 5GB limit from default fallback, got %d", *limit) 195 + } 196 + 197 + // Tier name should also fall back 198 + if m.GetTierName("unknown_tier") != "deckhand" { 199 + t.Errorf("expected tier name 'deckhand' for unknown tier, got %q", m.GetTierName("unknown_tier")) 200 + } 201 + } 202 + 203 + func TestNewManager_InvalidConfig(t *testing.T) { 204 + tmpDir := t.TempDir() 205 + configPath := filepath.Join(tmpDir, "quotas.yaml") 206 + 207 + // Invalid YAML 208 + if err := os.WriteFile(configPath, []byte("invalid: [yaml"), 0644); err != nil { 209 + t.Fatalf("failed to write config: %v", err) 210 + } 211 + 212 + _, err := NewManager(configPath) 213 + if err == nil { 214 + t.Error("expected error for invalid YAML") 215 + } 216 + } 217 + 218 + func TestNewManager_InvalidQuotaSize(t *testing.T) { 219 + tmpDir := t.TempDir() 220 + configPath := filepath.Join(tmpDir, "quotas.yaml") 221 + 222 + configContent := ` 223 + tiers: 224 + deckhand: 225 + quota: invalid_size 226 + 227 + defaults: 228 + new_crew_tier: deckhand 229 + ` 230 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 231 + t.Fatalf("failed to write config: %v", err) 232 + } 233 + 234 + _, err := NewManager(configPath) 235 + if err == nil { 236 + t.Error("expected error for invalid quota size") 237 + } 238 + } 239 + 240 + func TestNewManager_NoDefaultTier(t *testing.T) { 241 + tmpDir := t.TempDir() 242 + configPath := filepath.Join(tmpDir, "quotas.yaml") 243 + 244 + configContent := ` 245 + tiers: 246 + quartermaster: 247 + quota: 50GB 248 + 249 + defaults: 250 + new_crew_tier: nonexistent 251 + ` 252 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 253 + t.Fatalf("failed to write config: %v", err) 254 + } 255 + 256 + m, err := NewManager(configPath) 257 + if err != nil { 258 + t.Fatalf("failed to load config: %v", err) 259 + } 260 + 261 + // Empty tier key with nonexistent default should return nil (unlimited) 262 + limit := m.GetTierLimit("") 263 + if limit != nil { 264 + t.Error("expected nil limit when default tier doesn't exist") 265 + } 266 + 267 + // Explicit tier should still work 268 + limit = m.GetTierLimit("quartermaster") 269 + if limit == nil { 270 + t.Fatal("expected non-nil limit for quartermaster tier") 271 + } 272 + if *limit != 50*1024*1024*1024 { 273 + t.Errorf("expected 50GB limit for quartermaster, got %d", *limit) 274 + } 275 + }
+2 -2
pkg/logging/logger_test.go
··· 366 366 defer slog.SetDefault(originalLogger) 367 367 368 368 b.ResetTimer() 369 - for i := 0; i < b.N; i++ { 369 + for range b.N { 370 370 InitLogger("info") 371 371 } 372 372 } ··· 376 376 defer slog.SetDefault(originalLogger) 377 377 378 378 b.ResetTimer() 379 - for i := 0; i < b.N; i++ { 379 + for range b.N { 380 380 cleanup := SetupTestLogger() 381 381 cleanup() 382 382 }
+35
quotas.yaml.example
··· 1 + # ATCR Hold Service Quota Configuration 2 + # Copy this file to quotas.yaml to enable quota enforcement. 3 + # If quotas.yaml doesn't exist, quotas are disabled (unlimited for all users). 4 + 5 + # Tiers define quota levels using nautical crew ranks. 6 + # Each tier has a quota limit specified in human-readable format. 7 + # Supported units: B, KB, MB, GB, TB, PB (case-insensitive) 8 + tiers: 9 + # Entry-level crew - suitable for new or casual users 10 + deckhand: 11 + quota: 5GB 12 + 13 + # Mid-level crew - for regular contributors 14 + bosun: 15 + quota: 50GB 16 + 17 + # Senior crew - for power users or trusted contributors 18 + quartermaster: 19 + quota: 100GB 20 + 21 + # You can add custom tiers with any name: 22 + # unlimited_crew: 23 + # quota: 1TB 24 + 25 + defaults: 26 + # Default tier assigned to new crew members who don't have an explicit tier. 27 + # This tier must exist in the tiers section above. 28 + new_crew_tier: deckhand 29 + 30 + # Notes: 31 + # - The hold captain (owner) always has unlimited quota regardless of tiers. 32 + # - Crew members can be assigned a specific tier in their crew record. 33 + # - If a crew member's tier doesn't exist in config, they fall back to the default. 34 + # - Quota is calculated per-user by summing unique blob sizes (deduplicated). 35 + # - Quota is checked when pushing manifests (after blobs are already uploaded).