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/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,9 @@
1
+ """
2
+ Furu Dashboard: A web-based monitoring interface for Furu experiments.
3
+
4
+ Install with: uv add furu[dashboard]
5
+ Run with: furu-dashboard serve
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+
@@ -0,0 +1,7 @@
1
+ """Entry point for running the dashboard as a module: python -m furu.dashboard"""
2
+
3
+ from .main import cli
4
+
5
+ if __name__ == "__main__":
6
+ cli()
7
+
@@ -0,0 +1,7 @@
1
+ """Dashboard API module."""
2
+
3
+ from .routes import router
4
+
5
+ __all__ = ["router"]
6
+
7
+
@@ -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}