+25
.air.hold.toml
+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
+1
.gitignore
+7
-2
.golangci.yml
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+1
-1
docs/INTEGRATION_STRATEGY.md
+354
-1068
docs/QUOTAS.md
+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
+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
+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
+1
-1
examples/plugins/ratify-verifier/README.md
+2
-2
examples/plugins/ratify-verifier/verifier.go.temp
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
-13
pkg/appview/holdhealth/worker_test.go
+1
-1
pkg/appview/jetstream/processor_test.go
+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
+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
+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
+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
+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
-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
+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
+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
+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
+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
+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
-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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
-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
+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
+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
+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
+2
-1
pkg/auth/oauth/client_test.go
+2
-11
pkg/auth/oauth/server_test.go
+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
+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
pkg/auth/token/claims.go
+15
-2
pkg/auth/token/handler.go
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
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
+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
+81
pkg/hold/admin/templates/pages/crew.html
+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
+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
+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
+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
+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
+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
+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
+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
+5
pkg/hold/admin/templates/partials/usage_stats.html
+10
pkg/hold/config.go
+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
-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
+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
+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
+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
+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
+1
-1
pkg/hold/pds/events_test.go
+155
pkg/hold/pds/layer.go
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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).