furu 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- furu/__init__.py +82 -0
- furu/adapters/__init__.py +3 -0
- furu/adapters/submitit.py +195 -0
- furu/config.py +98 -0
- furu/core/__init__.py +4 -0
- furu/core/furu.py +999 -0
- furu/core/list.py +123 -0
- furu/dashboard/__init__.py +9 -0
- furu/dashboard/__main__.py +7 -0
- furu/dashboard/api/__init__.py +7 -0
- furu/dashboard/api/models.py +170 -0
- furu/dashboard/api/routes.py +135 -0
- furu/dashboard/frontend/dist/assets/index-CbdDfSOZ.css +1 -0
- furu/dashboard/frontend/dist/assets/index-DDv_TYB_.js +67 -0
- furu/dashboard/frontend/dist/favicon.svg +10 -0
- furu/dashboard/frontend/dist/index.html +22 -0
- furu/dashboard/main.py +134 -0
- furu/dashboard/scanner.py +931 -0
- furu/errors.py +76 -0
- furu/migrate.py +48 -0
- furu/migration.py +926 -0
- furu/runtime/__init__.py +27 -0
- furu/runtime/env.py +8 -0
- furu/runtime/logging.py +301 -0
- furu/runtime/tracebacks.py +64 -0
- furu/serialization/__init__.py +20 -0
- furu/serialization/migrations.py +246 -0
- furu/serialization/serializer.py +233 -0
- furu/storage/__init__.py +32 -0
- furu/storage/metadata.py +282 -0
- furu/storage/migration.py +81 -0
- furu/storage/state.py +1107 -0
- furu-0.0.1.dist-info/METADATA +502 -0
- furu-0.0.1.dist-info/RECORD +36 -0
- furu-0.0.1.dist-info/WHEEL +4 -0
- furu-0.0.1.dist-info/entry_points.txt +2 -0
furu/core/list.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Generator,
|
|
3
|
+
Generic,
|
|
4
|
+
Iterator,
|
|
5
|
+
Literal,
|
|
6
|
+
TypeVar,
|
|
7
|
+
cast,
|
|
8
|
+
overload,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from .furu import Furu
|
|
12
|
+
|
|
13
|
+
_H = TypeVar("_H", bound=Furu, covariant=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _FuruListMeta(type):
|
|
17
|
+
"""Metaclass that provides collection methods for FuruList subclasses."""
|
|
18
|
+
|
|
19
|
+
def _entries(cls: "type[FuruList[_H]]") -> list[_H]:
|
|
20
|
+
"""Collect all Furu instances from class attributes."""
|
|
21
|
+
items: list[_H] = []
|
|
22
|
+
seen: set[str] = set()
|
|
23
|
+
|
|
24
|
+
def maybe_add(obj: object) -> None:
|
|
25
|
+
if not isinstance(obj, Furu):
|
|
26
|
+
raise TypeError(f"{obj!r} is not a Furu instance")
|
|
27
|
+
|
|
28
|
+
digest = obj._furu_hash
|
|
29
|
+
if digest not in seen:
|
|
30
|
+
seen.add(digest)
|
|
31
|
+
items.append(cast(_H, obj))
|
|
32
|
+
|
|
33
|
+
for name, value in cls.__dict__.items():
|
|
34
|
+
if name.startswith("_") or callable(value):
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
if isinstance(value, dict):
|
|
38
|
+
for v in value.values():
|
|
39
|
+
maybe_add(v)
|
|
40
|
+
elif isinstance(value, list):
|
|
41
|
+
for v in value:
|
|
42
|
+
maybe_add(v)
|
|
43
|
+
else:
|
|
44
|
+
maybe_add(value)
|
|
45
|
+
|
|
46
|
+
return items
|
|
47
|
+
|
|
48
|
+
def __iter__(cls: "type[FuruList[_H]]") -> Iterator[_H]:
|
|
49
|
+
"""Iterate over all Furu instances."""
|
|
50
|
+
return iter(cls._entries())
|
|
51
|
+
|
|
52
|
+
def all(cls: "type[FuruList[_H]]") -> list[_H]:
|
|
53
|
+
"""Get all Furu instances as a list."""
|
|
54
|
+
return cls._entries()
|
|
55
|
+
|
|
56
|
+
def items_iter(
|
|
57
|
+
cls: "type[FuruList[_H]]",
|
|
58
|
+
) -> Generator[tuple[str, _H], None, None]:
|
|
59
|
+
"""Iterate over (name, instance) pairs."""
|
|
60
|
+
for name, value in cls.__dict__.items():
|
|
61
|
+
if name.startswith("_") or callable(value):
|
|
62
|
+
continue
|
|
63
|
+
if not isinstance(value, dict):
|
|
64
|
+
yield name, cast(_H, value)
|
|
65
|
+
|
|
66
|
+
def items(cls: "type[FuruList[_H]]") -> list[tuple[str, _H]]:
|
|
67
|
+
"""Get all (name, instance) pairs as a list."""
|
|
68
|
+
return list(cls.items_iter())
|
|
69
|
+
|
|
70
|
+
@overload
|
|
71
|
+
def by_name(
|
|
72
|
+
cls: "type[FuruList[_H]]", name: str, *, strict: Literal[True] = True
|
|
73
|
+
) -> _H: ...
|
|
74
|
+
|
|
75
|
+
@overload
|
|
76
|
+
def by_name(
|
|
77
|
+
cls: "type[FuruList[_H]]", name: str, *, strict: Literal[False]
|
|
78
|
+
) -> _H | None: ...
|
|
79
|
+
|
|
80
|
+
def by_name(cls: "type[FuruList[_H]]", name: str, *, strict: bool = True):
|
|
81
|
+
"""Get Furu instance by name."""
|
|
82
|
+
attr = cls.__dict__.get(name)
|
|
83
|
+
if attr and not callable(attr) and not name.startswith("_"):
|
|
84
|
+
return cast(_H, attr)
|
|
85
|
+
|
|
86
|
+
# Check nested dicts
|
|
87
|
+
for value in cls.__dict__.values():
|
|
88
|
+
if isinstance(value, dict) and name in value:
|
|
89
|
+
return cast(_H, value[name])
|
|
90
|
+
|
|
91
|
+
if strict:
|
|
92
|
+
raise KeyError(f"{cls.__name__} has no entry named '{name}'")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class FuruList(Generic[_H], metaclass=_FuruListMeta):
|
|
97
|
+
"""
|
|
98
|
+
Base class for typed Furu collections.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
class MyComputation(Furu[str]):
|
|
102
|
+
value: int
|
|
103
|
+
|
|
104
|
+
def _create(self) -> str:
|
|
105
|
+
result = f"Result: {self.value}"
|
|
106
|
+
(self.furu_dir / "result.txt").write_text(result)
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
def _load(self) -> str:
|
|
110
|
+
return (self.furu_dir / "result.txt").read_text()
|
|
111
|
+
|
|
112
|
+
class MyExperiments(FuruList[MyComputation]):
|
|
113
|
+
exp1 = MyComputation(value=1)
|
|
114
|
+
exp2 = MyComputation(value=2)
|
|
115
|
+
exp3 = MyComputation(value=3)
|
|
116
|
+
|
|
117
|
+
# Use the collection
|
|
118
|
+
for exp in MyExperiments:
|
|
119
|
+
result = exp.load_or_create()
|
|
120
|
+
print(result)
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
pass
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Pydantic models for the Dashboard API."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from ...storage import StateAttempt
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Type alias for JSON-serializable data from Pydantic model_dump(mode="json").
|
|
11
|
+
# This is the output of serializing Furu state/metadata models to JSON format.
|
|
12
|
+
JsonDict = dict[str, Any]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HealthCheck(BaseModel):
|
|
16
|
+
"""Health check response."""
|
|
17
|
+
|
|
18
|
+
status: str
|
|
19
|
+
version: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ExperimentSummary(BaseModel):
|
|
23
|
+
"""Summary of an experiment for list views."""
|
|
24
|
+
|
|
25
|
+
namespace: str
|
|
26
|
+
furu_hash: str
|
|
27
|
+
class_name: str
|
|
28
|
+
result_status: str
|
|
29
|
+
attempt_status: str | None = None
|
|
30
|
+
attempt_number: int | None = None
|
|
31
|
+
updated_at: str | None = None
|
|
32
|
+
started_at: str | None = None
|
|
33
|
+
# Additional fields for filtering
|
|
34
|
+
backend: str | None = None
|
|
35
|
+
hostname: str | None = None
|
|
36
|
+
user: str | None = None
|
|
37
|
+
# Migration metadata
|
|
38
|
+
migration_kind: str | None = None
|
|
39
|
+
migration_policy: str | None = None
|
|
40
|
+
migrated_at: str | None = None
|
|
41
|
+
overwritten_at: str | None = None
|
|
42
|
+
migration_origin: str | None = None
|
|
43
|
+
migration_note: str | None = None
|
|
44
|
+
from_namespace: str | None = None
|
|
45
|
+
from_hash: str | None = None
|
|
46
|
+
to_namespace: str | None = None
|
|
47
|
+
to_hash: str | None = None
|
|
48
|
+
original_result_status: str | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ExperimentDetail(ExperimentSummary):
|
|
52
|
+
"""Detailed experiment information."""
|
|
53
|
+
|
|
54
|
+
directory: str
|
|
55
|
+
state: JsonDict
|
|
56
|
+
metadata: JsonDict | None = None
|
|
57
|
+
attempt: StateAttempt | None = None
|
|
58
|
+
original_namespace: str | None = None
|
|
59
|
+
original_hash: str | None = None
|
|
60
|
+
alias_namespaces: list[str] | None = None
|
|
61
|
+
alias_hashes: list[str] | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ExperimentList(BaseModel):
|
|
65
|
+
"""List of experiments with total count."""
|
|
66
|
+
|
|
67
|
+
experiments: list[ExperimentSummary]
|
|
68
|
+
total: int
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class StatusCount(BaseModel):
|
|
72
|
+
"""Count of experiments by status."""
|
|
73
|
+
|
|
74
|
+
status: str
|
|
75
|
+
count: int
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DashboardStats(BaseModel):
|
|
79
|
+
"""Aggregate dashboard statistics."""
|
|
80
|
+
|
|
81
|
+
total: int
|
|
82
|
+
by_result_status: list[StatusCount]
|
|
83
|
+
by_attempt_status: list[StatusCount]
|
|
84
|
+
running_count: int
|
|
85
|
+
queued_count: int
|
|
86
|
+
failed_count: int
|
|
87
|
+
success_count: int
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# DAG Models
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DAGExperiment(BaseModel):
|
|
94
|
+
"""An experiment instance within a DAG node (grouped by class)."""
|
|
95
|
+
|
|
96
|
+
namespace: str
|
|
97
|
+
furu_hash: str
|
|
98
|
+
result_status: str
|
|
99
|
+
attempt_status: str | None = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class DAGNode(BaseModel):
|
|
103
|
+
"""A node in the experiment DAG representing a class type.
|
|
104
|
+
|
|
105
|
+
Multiple experiments of the same class are grouped into one node.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
id: str # Unique node identifier (class name)
|
|
109
|
+
class_name: str # Short class name (e.g., "TrainModel")
|
|
110
|
+
full_class_name: str # Full qualified class name from __class__
|
|
111
|
+
experiments: list[DAGExperiment] # All experiments of this class
|
|
112
|
+
# Counts by status for quick access
|
|
113
|
+
total_count: int
|
|
114
|
+
success_count: int
|
|
115
|
+
failed_count: int
|
|
116
|
+
running_count: int
|
|
117
|
+
# Parent class for subclass relationships
|
|
118
|
+
parent_class: str | None = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class DAGEdge(BaseModel):
|
|
122
|
+
"""An edge in the experiment DAG representing a dependency."""
|
|
123
|
+
|
|
124
|
+
source: str # Source node id (parent/upstream)
|
|
125
|
+
target: str # Target node id (child/downstream)
|
|
126
|
+
field_name: str # The field name that creates this dependency
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ExperimentDAG(BaseModel):
|
|
130
|
+
"""Complete DAG structure for visualization."""
|
|
131
|
+
|
|
132
|
+
nodes: list[DAGNode]
|
|
133
|
+
edges: list[DAGEdge]
|
|
134
|
+
# Summary statistics
|
|
135
|
+
total_nodes: int
|
|
136
|
+
total_edges: int
|
|
137
|
+
total_experiments: int
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Parent/Child relationship models
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ParentExperiment(BaseModel):
|
|
144
|
+
"""A parent experiment that this experiment depends on."""
|
|
145
|
+
|
|
146
|
+
field_name: str # The field name that references this parent
|
|
147
|
+
class_name: str # Short class name
|
|
148
|
+
full_class_name: str # Full qualified class name
|
|
149
|
+
namespace: str | None = None # Namespace if the experiment exists
|
|
150
|
+
furu_hash: str | None = None # Hash if the experiment exists
|
|
151
|
+
result_status: str | None = None # Status if the experiment exists
|
|
152
|
+
config: dict[str, Any] | None = None # Parent's config for identification
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ChildExperiment(BaseModel):
|
|
156
|
+
"""A child experiment that depends on this experiment."""
|
|
157
|
+
|
|
158
|
+
field_name: str # The field name through which this experiment is referenced
|
|
159
|
+
class_name: str # Short class name of the child
|
|
160
|
+
full_class_name: str # Full qualified class name of the child
|
|
161
|
+
namespace: str # Namespace of the child experiment
|
|
162
|
+
furu_hash: str # Hash of the child experiment
|
|
163
|
+
result_status: str # Status of the child experiment
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ExperimentRelationships(BaseModel):
|
|
167
|
+
"""Parent and child relationships for an experiment."""
|
|
168
|
+
|
|
169
|
+
parents: list[ParentExperiment]
|
|
170
|
+
children: list[ChildExperiment]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""API route definitions for the Furu Dashboard."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
4
|
+
|
|
5
|
+
from .. import __version__
|
|
6
|
+
from ..scanner import (
|
|
7
|
+
scan_experiments,
|
|
8
|
+
get_experiment_detail,
|
|
9
|
+
get_stats,
|
|
10
|
+
get_experiment_dag,
|
|
11
|
+
get_experiment_relationships,
|
|
12
|
+
)
|
|
13
|
+
from .models import (
|
|
14
|
+
DashboardStats,
|
|
15
|
+
ExperimentDAG,
|
|
16
|
+
ExperimentDetail,
|
|
17
|
+
ExperimentList,
|
|
18
|
+
ExperimentRelationships,
|
|
19
|
+
HealthCheck,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
router = APIRouter(prefix="/api", tags=["api"])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("/health", response_model=HealthCheck)
|
|
26
|
+
async def health_check() -> HealthCheck:
|
|
27
|
+
"""Health check endpoint."""
|
|
28
|
+
return HealthCheck(status="healthy", version=__version__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.get("/experiments", response_model=ExperimentList)
|
|
32
|
+
async def list_experiments(
|
|
33
|
+
result_status: str | None = Query(None, description="Filter by result status"),
|
|
34
|
+
attempt_status: str | None = Query(None, description="Filter by attempt status"),
|
|
35
|
+
namespace: str | None = Query(None, description="Filter by namespace prefix"),
|
|
36
|
+
backend: str | None = Query(
|
|
37
|
+
None, description="Filter by backend (local, submitit)"
|
|
38
|
+
),
|
|
39
|
+
hostname: str | None = Query(None, description="Filter by hostname"),
|
|
40
|
+
user: str | None = Query(None, description="Filter by user"),
|
|
41
|
+
started_after: str | None = Query(
|
|
42
|
+
None, description="Filter experiments started after this ISO datetime"
|
|
43
|
+
),
|
|
44
|
+
started_before: str | None = Query(
|
|
45
|
+
None, description="Filter experiments started before this ISO datetime"
|
|
46
|
+
),
|
|
47
|
+
updated_after: str | None = Query(
|
|
48
|
+
None, description="Filter experiments updated after this ISO datetime"
|
|
49
|
+
),
|
|
50
|
+
updated_before: str | None = Query(
|
|
51
|
+
None, description="Filter experiments updated before this ISO datetime"
|
|
52
|
+
),
|
|
53
|
+
config_filter: str | None = Query(
|
|
54
|
+
None, description="Filter by config field (format: field.path=value)"
|
|
55
|
+
),
|
|
56
|
+
migration_kind: str | None = Query(None, description="Filter by migration kind"),
|
|
57
|
+
migration_policy: str | None = Query(
|
|
58
|
+
None, description="Filter by migration policy"
|
|
59
|
+
),
|
|
60
|
+
view: str = Query("resolved", description="View mode: resolved or original"),
|
|
61
|
+
limit: int = Query(100, ge=1, le=1000, description="Maximum number of results"),
|
|
62
|
+
offset: int = Query(0, ge=0, description="Offset for pagination"),
|
|
63
|
+
) -> ExperimentList:
|
|
64
|
+
"""List all experiments with optional filtering."""
|
|
65
|
+
experiments = scan_experiments(
|
|
66
|
+
result_status=result_status,
|
|
67
|
+
attempt_status=attempt_status,
|
|
68
|
+
namespace_prefix=namespace,
|
|
69
|
+
backend=backend,
|
|
70
|
+
hostname=hostname,
|
|
71
|
+
user=user,
|
|
72
|
+
started_after=started_after,
|
|
73
|
+
started_before=started_before,
|
|
74
|
+
updated_after=updated_after,
|
|
75
|
+
updated_before=updated_before,
|
|
76
|
+
config_filter=config_filter,
|
|
77
|
+
migration_kind=migration_kind,
|
|
78
|
+
migration_policy=migration_policy,
|
|
79
|
+
view=view,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Apply pagination
|
|
83
|
+
total = len(experiments)
|
|
84
|
+
experiments = experiments[offset : offset + limit]
|
|
85
|
+
|
|
86
|
+
return ExperimentList(experiments=experiments, total=total)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@router.get(
|
|
90
|
+
"/experiments/{namespace:path}/{furu_hash}/relationships",
|
|
91
|
+
response_model=ExperimentRelationships,
|
|
92
|
+
)
|
|
93
|
+
async def get_experiment_relationships_route(
|
|
94
|
+
namespace: str,
|
|
95
|
+
furu_hash: str,
|
|
96
|
+
view: str = Query("resolved", description="View mode: resolved or original"),
|
|
97
|
+
) -> ExperimentRelationships:
|
|
98
|
+
"""Get parent and child relationships for a specific experiment."""
|
|
99
|
+
relationships = get_experiment_relationships(namespace, furu_hash, view=view)
|
|
100
|
+
if relationships is None:
|
|
101
|
+
raise HTTPException(status_code=404, detail="Experiment not found")
|
|
102
|
+
return relationships
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@router.get(
|
|
106
|
+
"/experiments/{namespace:path}/{furu_hash}", response_model=ExperimentDetail
|
|
107
|
+
)
|
|
108
|
+
async def get_experiment(
|
|
109
|
+
namespace: str,
|
|
110
|
+
furu_hash: str,
|
|
111
|
+
view: str = Query("resolved", description="View mode: resolved or original"),
|
|
112
|
+
) -> ExperimentDetail:
|
|
113
|
+
"""Get detailed information about a specific experiment."""
|
|
114
|
+
experiment = get_experiment_detail(namespace, furu_hash, view=view)
|
|
115
|
+
if experiment is None:
|
|
116
|
+
raise HTTPException(status_code=404, detail="Experiment not found")
|
|
117
|
+
return experiment
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@router.get("/stats", response_model=DashboardStats)
|
|
121
|
+
async def dashboard_stats() -> DashboardStats:
|
|
122
|
+
"""Get aggregate statistics for the dashboard."""
|
|
123
|
+
return get_stats()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@router.get("/dag", response_model=ExperimentDAG)
|
|
127
|
+
async def experiment_dag() -> ExperimentDAG:
|
|
128
|
+
"""Get the experiment dependency DAG.
|
|
129
|
+
|
|
130
|
+
Returns a graph structure where:
|
|
131
|
+
- Nodes represent experiment classes (e.g., TrainModel, PrepareDataset)
|
|
132
|
+
- Multiple experiments of the same class are grouped into a single node
|
|
133
|
+
- Edges represent dependencies between classes
|
|
134
|
+
"""
|
|
135
|
+
return get_experiment_dag()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:DM Sans,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 222.2 47.4% 11.2%;--card: 0 0% 100%;--card-foreground: 222.2 47.4% 11.2%;--popover: 0 0% 100%;--popover-foreground: 222.2 47.4% 11.2%;--primary: 142.1 76.2% 36.3%;--primary-foreground: 355.7 100% 97.3%;--secondary: 210 40% 96.1%;--secondary-foreground: 222.2 47.4% 11.2%;--muted: 210 40% 96.1%;--muted-foreground: 215.4 16.3% 46.9%;--accent: 210 40% 96.1%;--accent-foreground: 222.2 47.4% 11.2%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 210 40% 98%;--border: 214.3 31.8% 91.4%;--input: 214.3 31.8% 91.4%;--ring: 142.1 76.2% 36.3%;--radius: .75rem}.dark{--background: 222.2 47.4% 6%;--foreground: 210 40% 98%;--card: 222.2 47.4% 9%;--card-foreground: 210 40% 98%;--popover: 222.2 47.4% 10%;--popover-foreground: 210 40% 98%;--primary: 142.1 70% 45%;--primary-foreground: 144.9 80.4% 10%;--secondary: 217.2 32.6% 17.5%;--secondary-foreground: 210 40% 98%;--muted: 217.2 32.6% 17.5%;--muted-foreground: 215 20.2% 65.1%;--accent: 217.2 32.6% 17.5%;--accent-foreground: 210 40% 98%;--destructive: 0 62.8% 40.6%;--destructive-foreground: 210 40% 98%;--border: 217.2 32.6% 17.5%;--input: 217.2 32.6% 17.5%;--ring: 142.1 70% 45%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.relative{position:relative}.m-4{margin:1rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-4{margin-left:1rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.\!h-3{height:.75rem!important}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-2{height:.5rem}.h-4{height:1rem}.h-7{height:1.75rem}.h-9{height:2.25rem}.h-\[600px\]{height:600px}.max-h-40{max-height:10rem}.max-h-96{max-height:24rem}.min-h-screen{min-height:100vh}.\!w-3{width:.75rem!important}.w-1\.5{width:.375rem}.w-10{width:2.5rem}.w-2{width:.5rem}.w-4{width:1rem}.w-7{width:1.75rem}.w-80{width:20rem}.w-full{width:100%}.min-w-\[180px\]{min-width:180px}.min-w-fit{min-width:-moz-fit-content;min-width:fit-content}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[150px\]{max-width:150px}.max-w-md{max-width:28rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.caption-bottom{caption-side:bottom}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(0px * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px * var(--tw-space-y-reverse))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-500\/30{border-color:#f59e0b4d}.border-amber-500\/40{border-color:#f59e0b66}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-blue-500\/30{border-color:#3b82f64d}.border-blue-500\/40{border-color:#3b82f666}.border-border{border-color:hsl(var(--border))}.border-destructive\/40{border-color:hsl(var(--destructive) / .4)}.border-emerald-500\/30{border-color:#10b9814d}.border-emerald-500\/40{border-color:#10b98166}.border-green-500{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.border-input{border-color:hsl(var(--input))}.border-muted{border-color:hsl(var(--muted))}.border-primary{border-color:hsl(var(--primary))}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.border-red-500\/30{border-color:#ef44444d}.border-red-500\/40{border-color:#ef444466}.border-transparent{border-color:transparent}.\!bg-muted-foreground{background-color:hsl(var(--muted-foreground))!important}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-amber-500\/15{background-color:#f59e0b26}.bg-background{background-color:hsl(var(--background))}.bg-blue-300{--tw-bg-opacity: 1;background-color:rgb(147 197 253 / var(--tw-bg-opacity, 1))}.bg-blue-500\/15{background-color:#3b82f626}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-emerald-300{--tw-bg-opacity: 1;background-color:rgb(110 231 183 / var(--tw-bg-opacity, 1))}.bg-emerald-500\/15{background-color:#10b98126}.bg-muted{background-color:hsl(var(--muted))}.bg-muted\/40{background-color:hsl(var(--muted) / .4)}.bg-muted\/50{background-color:hsl(var(--muted) / .5)}.bg-primary{background-color:hsl(var(--primary))}.bg-red-500\/15{background-color:#ef444426}.bg-secondary{background-color:hsl(var(--secondary))}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pl-6{padding-left:1.5rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.align-middle{vertical-align:middle}.font-mono{font-family:JetBrains Mono,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-5xl{font-size:3rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.italic{font-style:italic}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.text-amber-300{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-cyan-400{--tw-text-opacity: 1;color:rgb(34 211 238 / var(--tw-text-opacity, 1))}.text-destructive{color:hsl(var(--destructive))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-emerald-300{--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity, 1))}.text-foreground{color:hsl(var(--foreground))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.no-underline{text-decoration-line:none}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-primary\/20{--tw-ring-color: hsl(var(--primary) / .2)}.ring-offset-background{--tw-ring-offset-color: hsl(var(--background))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background-color:hsl(var(--muted))}::-webkit-scrollbar-thumb{border-radius:.25rem;background-color:hsl(var(--border))}::-webkit-scrollbar-thumb:hover{background-color:hsl(var(--muted-foreground) / .4)}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-muted:hover{background-color:hsl(var(--muted))}.hover\:bg-muted\/50:hover{background-color:hsl(var(--muted) / .5)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-foreground:hover{color:hsl(var(--foreground))}.hover\:text-primary:hover{color:hsl(var(--primary))}.hover\:underline:hover{text-decoration-line:underline}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:hsl(var(--muted))}@media (min-width: 768px){.md\:col-span-2{grid-column:span 2 / span 2}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.\[\&\.active\]\:bg-accent.active{background-color:hsl(var(--accent))}.\[\&\.active\]\:text-primary.active{color:hsl(var(--primary))}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:0}.\[\&\>tr\]\:last\:border-b-0:last-child>tr{border-bottom-width:0px}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-width:0px}.\[\&_tr\]\:border-b tr{border-bottom-width:1px}
|