leadger 0.1.0__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.
leadger/server.py ADDED
@@ -0,0 +1,221 @@
1
+ """Leadger FastAPI application factory + REST API.
2
+
3
+ Endpoints (all under /api, registered BEFORE the static mount at "/"):
4
+ GET /api/state?period=... tasks in the period + today's metrics
5
+ POST /api/tasks create task (parses inline syntax in title)
6
+ PATCH /api/tasks/{id} edit title/target/tags/status/recur
7
+ GET /api/history?from&to daily series for charts
8
+ GET /api/insights personal correction factor (30-day window)
9
+ GET /calendar.ics read-only iCalendar export of open tasks
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from datetime import date, timedelta
15
+ from pathlib import Path
16
+ from typing import Literal, Optional
17
+
18
+ from fastapi import FastAPI, HTTPException, Query
19
+ from fastapi.middleware.cors import CORSMiddleware
20
+ from fastapi.staticfiles import StaticFiles
21
+ from pydantic import BaseModel, Field
22
+ from starlette.exceptions import HTTPException as StarletteHTTPException
23
+ from starlette.responses import Response
24
+ from starlette.types import Scope
25
+
26
+ from .core import InvalidTransitionError, Storage, TaskStatus
27
+ from .core.ics import tasks_to_ics
28
+ from .core.parse import parse_inline
29
+ from .core.periods import period_range
30
+ from .core.storage import DayMetrics
31
+ from .core.tasks import Task
32
+
33
+ STATIC_DIR = Path(__file__).parent / "static"
34
+
35
+ INSIGHTS_WINDOW_DAYS = 30
36
+
37
+ class SPAStaticFiles(StaticFiles):
38
+ """StaticFiles that falls back to index.html for client-side routes (/history)."""
39
+
40
+ async def get_response(self, path: str, scope: Scope) -> Response:
41
+ try:
42
+ return await super().get_response(path, scope)
43
+ except StarletteHTTPException as exc:
44
+ if exc.status_code == 404:
45
+ return await super().get_response("index.html", scope)
46
+ raise
47
+
48
+
49
+ PeriodName = Literal["day", "week", "month", "quarter", "semester", "year"]
50
+ StatusName = Literal["todo", "done", "paused", "cancelled"]
51
+ RecurName = Literal["dia", "seg", "ter", "qua", "qui", "sex", "sab", "dom"]
52
+
53
+
54
+ class TaskCreateBody(BaseModel):
55
+ title: str = Field(min_length=1)
56
+ target: Optional[date] = None
57
+ tags: list[str] = Field(default_factory=list)
58
+
59
+
60
+ class TaskPatchBody(BaseModel):
61
+ title: Optional[str] = Field(default=None, min_length=1)
62
+ target: Optional[date] = None
63
+ tags: Optional[list[str]] = None
64
+ status: Optional[StatusName] = None
65
+ recur: Optional[RecurName] = None # only applied when the field is in the body
66
+
67
+
68
+ def task_to_dict(task: Task) -> dict:
69
+ return {
70
+ "id": task.id,
71
+ "title": task.title,
72
+ "status": task.status.value,
73
+ "target": task.target.isoformat(),
74
+ "created": task.created.isoformat(),
75
+ "completed": task.completed.isoformat() if task.completed else None,
76
+ "tags": task.tags,
77
+ "migrations": task.migrations,
78
+ "recur": task.recur,
79
+ }
80
+
81
+
82
+ def metrics_to_dict(metrics: DayMetrics) -> dict:
83
+ return {
84
+ "date": metrics.date.isoformat(),
85
+ "goal": metrics.goal,
86
+ "done": metrics.done,
87
+ "snapshot_done": metrics.snapshot_done,
88
+ "extras_done": metrics.extras_done,
89
+ "extras_total": metrics.extras_total,
90
+ "pct": metrics.pct, # None on bonus days (goal=0 with deliveries) -> UI shows infinity
91
+ "bonus_day": metrics.bonus_day,
92
+ "over_delivery": metrics.over_delivery,
93
+ }
94
+
95
+
96
+ def create_app(data_path: Path) -> FastAPI:
97
+ """Build the Leadger FastAPI app serving `data_path` (leadger.yaml)."""
98
+ app = FastAPI(title="Leadger")
99
+ app.state.data_path = data_path
100
+ storage = Storage(data_path)
101
+ app.state.storage = storage
102
+
103
+ # Vite dev server (development); in production the build is served by this app
104
+ app.add_middleware(
105
+ CORSMiddleware,
106
+ allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
107
+ allow_methods=["*"],
108
+ allow_headers=["*"],
109
+ )
110
+
111
+ @app.get("/api/state")
112
+ def get_state(period: PeriodName = "day") -> dict:
113
+ metrics = storage.get_day_metrics() # runs the lazy snapshot + rollover
114
+ start, end = period_range(period, metrics.date)
115
+ tasks = [task for task in storage.all_tasks() if start <= task.target <= end]
116
+ tasks.sort(key=lambda task: (task.target, task.created.isoformat()))
117
+ return {
118
+ "today": metrics_to_dict(metrics),
119
+ "period": {"name": period, "start": start.isoformat(), "end": end.isoformat()},
120
+ "tasks": [task_to_dict(task) for task in tasks],
121
+ }
122
+
123
+ @app.post("/api/tasks", status_code=201)
124
+ def create_task(body: TaskCreateBody) -> dict:
125
+ parsed = parse_inline(body.title, storage.today())
126
+ if not parsed.title:
127
+ raise HTTPException(status_code=422, detail="Tarefa sem titulo")
128
+ tags = parsed.tags + [tag for tag in body.tags if tag not in parsed.tags]
129
+ task = storage.add_task(
130
+ parsed.title,
131
+ target=body.target or parsed.target,
132
+ tags=tags,
133
+ recur=parsed.recur,
134
+ )
135
+ return task_to_dict(task)
136
+
137
+ @app.patch("/api/tasks/{task_id}")
138
+ def patch_task(task_id: str, body: TaskPatchBody) -> dict:
139
+ extra: dict = {}
140
+ if "recur" in body.model_fields_set: # explicit null ends the series
141
+ extra["recur"] = body.recur
142
+ try:
143
+ task = storage.update_task(
144
+ task_id,
145
+ title=body.title,
146
+ target=body.target,
147
+ tags=body.tags,
148
+ status=TaskStatus(body.status) if body.status else None,
149
+ **extra,
150
+ )
151
+ except KeyError:
152
+ raise HTTPException(status_code=404, detail=f"Tarefa nao encontrada: {task_id}")
153
+ except InvalidTransitionError as exc:
154
+ raise HTTPException(status_code=409, detail=str(exc))
155
+ return task_to_dict(task)
156
+
157
+ @app.delete("/api/tasks/{task_id}", status_code=204)
158
+ def delete_task(task_id: str) -> Response:
159
+ try:
160
+ storage.delete_task(task_id)
161
+ except KeyError:
162
+ raise HTTPException(status_code=404, detail=f"Tarefa nao encontrada: {task_id}")
163
+ return Response(status_code=204)
164
+
165
+ @app.get("/api/history")
166
+ def get_history(
167
+ from_: Optional[date] = Query(default=None, alias="from"),
168
+ to: Optional[date] = None,
169
+ ) -> dict:
170
+ storage.ensure_day()
171
+ end = to or storage.today()
172
+ start = from_ or end - timedelta(days=29)
173
+ days = []
174
+ for day in storage.recorded_days():
175
+ if not start <= day <= end:
176
+ continue
177
+ m = storage.get_day_metrics(day)
178
+ days.append(
179
+ {
180
+ "date": day.isoformat(),
181
+ "goal": m.goal,
182
+ "done": m.done,
183
+ "extras_done": m.extras_done,
184
+ "pct": m.pct,
185
+ "bonus_day": m.bonus_day,
186
+ }
187
+ )
188
+ return {"from": start.isoformat(), "to": end.isoformat(), "days": days}
189
+
190
+ @app.get("/calendar.ics")
191
+ def calendar_ics() -> Response:
192
+ """iCalendar export of open tasks — import it in your calendar app."""
193
+ storage.ensure_day()
194
+ content = tasks_to_ics(storage.all_tasks(), now=storage.now())
195
+ return Response(content, media_type="text/calendar; charset=utf-8")
196
+
197
+ @app.get("/api/insights")
198
+ def get_insights() -> dict:
199
+ storage.ensure_day()
200
+ today = storage.today()
201
+ start = today - timedelta(days=INSIGHTS_WINDOW_DAYS - 1)
202
+ records = [
203
+ storage.get_day_metrics(day)
204
+ for day in storage.recorded_days()
205
+ if start <= day <= today
206
+ ]
207
+ goal_sum = sum(m.goal for m in records)
208
+ done_sum = sum(m.done for m in records)
209
+ count = len(records)
210
+ return {
211
+ "window_days": INSIGHTS_WINDOW_DAYS,
212
+ "days_with_data": count,
213
+ "avg_planned": goal_sum / count if count else 0.0,
214
+ "avg_delivered": done_sum / count if count else 0.0,
215
+ # delivered / planned; None without enough data
216
+ "correction_factor": done_sum / goal_sum if goal_sum else None,
217
+ }
218
+
219
+ app.mount("/", SPAStaticFiles(directory=STATIC_DIR, html=True), name="static")
220
+
221
+ return app
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:"Inter", system-ui, -apple-system, "Segoe UI", sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-amber-500:oklch(76.9% .188 70.08);--color-black:#000;--spacing:.25rem;--container-md:28rem;--container-2xl:42rem;--container-3xl:48rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:"Inter", system-ui, -apple-system, "Segoe UI", sans-serif;--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.right-0{right:calc(var(--spacing) * 0)}.bottom-0{bottom:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.z-50{z-index:50}.z-\[60\]{z-index:60}.mx-auto{margin-inline:auto}.-mt-4{margin-top:calc(var(--spacing) * -4)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-\[18px\]{width:18px;height:18px}.h-3{height:calc(var(--spacing) * 3)}.h-auto{height:auto}.h-full{height:100%}.max-h-72{max-height:calc(var(--spacing) * 72)}.min-h-screen{min-height:100vh}.w-8{width:calc(var(--spacing) * 8)}.w-24{width:calc(var(--spacing) * 24)}.w-32{width:calc(var(--spacing) * 32)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-rotate-90{rotate:-90deg}.rotate-90{rotate:90deg}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-10{gap:calc(var(--spacing) * 10)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-accent,.border-accent\/50{border-color:var(--app-accent)}@supports (color:color-mix(in lab, red, red)){.border-accent\/50{border-color:color-mix(in oklab, var(--app-accent) 50%, transparent)}}.border-accent\/60{border-color:var(--app-accent)}@supports (color:color-mix(in lab, red, red)){.border-accent\/60{border-color:color-mix(in oklab, var(--app-accent) 60%, transparent)}}.border-amber-500\/40{border-color:#f99c0066}@supports (color:color-mix(in lab, red, red)){.border-amber-500\/40{border-color:color-mix(in oklab, var(--color-amber-500) 40%, transparent)}}.border-edge{border-color:var(--app-edge)}.border-muted,.border-muted\/70{border-color:var(--app-muted)}@supports (color:color-mix(in lab, red, red)){.border-muted\/70{border-color:color-mix(in oklab, var(--app-muted) 70%, transparent)}}.border-transparent{border-color:#0000}.bg-accent{background-color:var(--app-accent)}.bg-amber-500\/5{background-color:#f99c000d}@supports (color:color-mix(in lab, red, red)){.bg-amber-500\/5{background-color:color-mix(in oklab, var(--color-amber-500) 5%, transparent)}}.bg-bg,.bg-bg\/90{background-color:var(--app-bg)}@supports (color:color-mix(in lab, red, red)){.bg-bg\/90{background-color:color-mix(in oklab, var(--app-bg) 90%, transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-edge,.bg-edge\/60{background-color:var(--app-edge)}@supports (color:color-mix(in lab, red, red)){.bg-edge\/60{background-color:color-mix(in oklab, var(--app-edge) 60%, transparent)}}.bg-panel,.bg-panel\/60{background-color:var(--app-panel)}@supports (color:color-mix(in lab, red, red)){.bg-panel\/60{background-color:color-mix(in oklab, var(--app-panel) 60%, transparent)}}.bg-transparent{background-color:#0000}.p-1{padding:calc(var(--spacing) * 1)}.p-5{padding:calc(var(--spacing) * 5)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.pt-10{padding-top:calc(var(--spacing) * 10)}.pt-\[18vh\]{padding-top:18vh}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.pb-24{padding-bottom:calc(var(--spacing) * 24)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-sans{font-family:Inter,system-ui,-apple-system,Segoe UI,sans-serif}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.text-\[15px\]{font-size:15px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.text-accent{color:var(--app-accent)}.text-amber-500{color:var(--color-amber-500)}.text-bg{color:var(--app-bg)}.text-ink,.text-ink\/85{color:var(--app-ink)}@supports (color:color-mix(in lab, red, red)){.text-ink\/85{color:color-mix(in oklab, var(--app-ink) 85%, transparent)}}.text-ink\/90{color:var(--app-ink)}@supports (color:color-mix(in lab, red, red)){.text-ink\/90{color:color-mix(in oklab, var(--app-ink) 90%, transparent)}}.text-muted,.text-muted\/60{color:var(--app-muted)}@supports (color:color-mix(in lab, red, red)){.text-muted\/60{color:color-mix(in oklab, var(--app-muted) 60%, transparent)}}.text-muted\/70{color:var(--app-muted)}@supports (color:color-mix(in lab, red, red)){.text-muted\/70{color:color-mix(in oklab, var(--app-muted) 70%, transparent)}}.text-red-400{color:var(--color-red-400)}.text-transparent{color:#0000}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.line-through{text-decoration-line:line-through}.caret-accent{caret-color:var(--app-accent)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring,.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-accent\/50{--tw-ring-color:var(--app-accent)}@supports (color:color-mix(in lab, red, red)){.ring-accent\/50{--tw-ring-color:color-mix(in oklab, var(--app-accent) 50%, transparent)}}.ring-red-400\/60{--tw-ring-color:#ff656899}@supports (color:color-mix(in lab, red, red)){.ring-red-400\/60{--tw-ring-color:color-mix(in oklab, var(--color-red-400) 60%, transparent)}}.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,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}@media (hover:hover){.group-hover\:flex:is(:where(.group):hover *){display:flex}}.placeholder\:text-muted\/60::placeholder{color:var(--app-muted)}@supports (color:color-mix(in lab, red, red)){.placeholder\:text-muted\/60::placeholder{color:color-mix(in oklab, var(--app-muted) 60%, transparent)}}@media (hover:hover){.hover\:border-accent:hover{border-color:var(--app-accent)}.hover\:bg-panel\/70:hover{background-color:var(--app-panel)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-panel\/70:hover{background-color:color-mix(in oklab, var(--app-panel) 70%, transparent)}}.hover\:text-accent:hover{color:var(--app-accent)}.hover\:text-ink:hover{color:var(--app-ink)}.hover\:text-red-400:hover{color:var(--color-red-400)}}.focus\:border-accent\/60:focus{border-color:var(--app-accent)}@supports (color:color-mix(in lab, red, red)){.focus\:border-accent\/60:focus{border-color:color-mix(in oklab, var(--app-accent) 60%, transparent)}}@media (width>=40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}:root{--app-accent:#f5a524;--app-bg:#0e0e11;--app-panel:#16161a;--app-ink:#e6e6e6;--app-muted:#8b8b94;--app-edge:#26262c;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}:root.light{--app-accent:#c47d00;--app-bg:#fafafa;--app-panel:#fff;--app-ink:#1b1b1f;--app-muted:#71717a;--app-edge:#e4e4e7;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}body{background-color:var(--app-bg);color:var(--app-ink);font-family:var(--font-sans);font-variant-numeric:tabular-nums;margin:0;transition:background-color .2s ease-out,color .2s ease-out}@keyframes task-in{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}.task-in{animation:.18s ease-out task-in}@keyframes overlay-in{0%{opacity:0;transform:scale(.985)}to{opacity:1;transform:scale(1)}}.overlay-in{animation:.15s ease-out overlay-in}@keyframes welcome-fade{0%,88%{opacity:1}to{opacity:0}}.welcome-fade{animation:5s ease-out forwards welcome-fade}@keyframes welcome-letter-in{0%{opacity:0;filter:blur(6px);transform:translateY(16px)}to{opacity:1;filter:blur();transform:translateY(0)}}.welcome-letter{opacity:0;animation:.8s ease-out forwards welcome-letter-in}.welcome-tagline{opacity:0;animation:.9s ease-out 2.2s forwards welcome-letter-in}@keyframes welcome-ring-draw{to{stroke-dashoffset:0}}.welcome-ring{animation:2.4s ease-out .5s forwards welcome-ring-draw}@media (prefers-reduced-motion:reduce){.welcome-fade,.welcome-letter,.welcome-tagline,.welcome-ring{opacity:1;animation:none}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}