diff --git a/CLAUDE.md b/CLAUDE.md index 6c441de8..6729f74f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,6 +48,26 @@ go -C go vet ./... go -C go test ./... ``` +**The wasm host API** (`wasmruntime/host.go` + `host_wasm.go` / `host_native.go`) is how neutral +component code reaches the browser: element measurement (`Measure`, `Viewport`), imperative style +writes (`SetStyle` — positioning must NOT go through signals, which re-render the whole tree), +the post-commit hook (`AfterRender`, the only point at which a just-rendered element can be +measured), document/window listeners, timers, localStorage, and file download. It is **dual-build**: +real under `js && wasm`, no-op stubs natively — which is what lets `webui` call it unconditionally +and still SSR. Refs come from `vdom.Ref` + `vdom.WithRef`; `vdom.Portal` mounts children at +`document.body` (needed to escape `overflow:hidden` / `transform` ancestors). + +The reconciler is wasm-only and needs a DOM, so `go test ./...` cannot reach it. It has its own +harness — a minimal DOM under node: +``` +GOOS=js GOARCH=wasm go -C go test -exec="node testdata/domexec.js" ./wasmruntime +``` + +`webui` components that measure the page (Tooltip, Popover, Menu, Modal, DatePicker, Tutorial, +AutoTable) are **controllers**: create them once alongside your signals, never inside a render +closure. Floating panels share one positioning engine (`webui/position.go`, pure math, unit-tested +natively) driven by the `Floating` controller (`webui/floating.go`). + ### web/ - `kit/` — Solid.js `.tsx` component kit. Apps import components as `@ui/*`. diff --git a/go/bundler/tailwind.go b/go/bundler/tailwind.go index b4e94af2..8e37abd3 100644 --- a/go/bundler/tailwind.go +++ b/go/bundler/tailwind.go @@ -1420,6 +1420,14 @@ func twCompile(input, baseDir string, candidates []string) (string, int, error) if e != nil { return "", 0, e } + // The preflight is written against Tailwind's compile-time CSS functions — + // `font-family: --theme(--default-font-family, …)` and five more like it. They + // have to be resolved here, exactly as the theme's own declarations are above. + // Left in, `--theme(…)` reaches the browser verbatim, which cannot parse it and + // so DROPS THE WHOLE DECLARATION: html ends up with no font-family at all and + // falls back to the browser default, and no @theme override of --font-sans can + // ever take effect. + substituteFunctions(pfAst, ds) out = append(out, atRule("@layer", "base", pfAst...)) } diff --git a/go/bundler/tailwind_test.go b/go/bundler/tailwind_test.go index e2c107bd..554b24c9 100644 --- a/go/bundler/tailwind_test.go +++ b/go/bundler/tailwind_test.go @@ -215,3 +215,61 @@ func TestExtractCandidatesRegexLiterals(t *testing.T) { } } } + +// The preflight is written against Tailwind's compile-time CSS functions, e.g. +// `font-family: --theme(--default-font-family, …)`. If those are not resolved, they +// reach the browser verbatim; the browser cannot parse `--theme(…)` and DROPS THE +// WHOLE DECLARATION. The symptom is subtle and global: every page silently loses its +// font-family and falls back to the browser default, and no @theme override of +// --font-sans can ever take effect. That shipped for a while, so pin it. +func TestPreflightResolvesThemeFunctions(t *testing.T) { + css, _, err := twCompile(`@import "tailwindcss";`, ".", nil) + if err != nil { + t.Fatal(err) + } + + flat := strings.Join(strings.Fields(css), " ") + if strings.Contains(css, "--theme(") { + t.Error("the compiled CSS still contains an unresolved --theme(…); the browser will drop those declarations") + } + if !strings.Contains(flat, "font-family: var(--default-font-family)") { + t.Error("preflight did not resolve html's font-family to a var()") + } +} + +// And the whole point of resolving it: an app's @theme override of --font-sans must +// actually reach the page. +func TestThemeFontOverrideReachesHTML(t *testing.T) { + css, _, err := twCompile(` +@import "tailwindcss"; + +@font-face { + font-family: "Lora"; + src: url("/fonts/lora.woff2") format("woff2"); +} + +@theme { + --font-sans: "Lora", serif; +} +`, ".", nil) + if err != nil { + t.Fatal(err) + } + + // The chain the browser walks: html -> --default-font-family -> --font-sans. + flat := strings.Join(strings.Fields(css), " ") + for _, want := range []string{ + "font-family: var(--default-font-family)", + "--default-font-family: var(--font-sans)", + `--font-sans: "Lora", serif`, + } { + if !strings.Contains(flat, want) { + t.Errorf("missing %q — the font override does not reach the page", want) + } + } + // A vendored face must survive the build; dropping it would leave the family + // declared but never loaded. + if !strings.Contains(css, "@font-face") || !strings.Contains(css, "lora.woff2") { + t.Error("@font-face was dropped from the output") + } +} diff --git a/go/cmd/examples/go-wasm-web/app/kit.go b/go/cmd/examples/go-wasm-web/app/kit.go index 63b0bc14..ade59706 100644 --- a/go/cmd/examples/go-wasm-web/app/kit.go +++ b/go/cmd/examples/go-wasm-web/app/kit.go @@ -38,12 +38,17 @@ func KitPage(d Deps) func() *VNode { acc := NewSignal(0) notify := NewSignal(true) span := NewSignal("week") - modalOpen := NewSignal(false) - menuOpen := NewSignal(false) name := NewSignal("") email := NewSignal("") plan := NewSignal("pro") + // Floating components are CONTROLLERS: they own refs, timers and open state, so + // they are built once here — never inside the render closure below, which would + // rebuild them (and lose their state) on every frame. + modal := ui.NewModal(ui.ModalOptions{Size: ui.ModalMedium}) + menu := ui.NewMenu(ui.MenuOptions{Placement: ui.PlacementBottomStart}) + tip := ui.NewHoverTooltip(ui.PlacementTop, "") + return func() *VNode { return Div(Attr("class", "space-y-8"), Div( @@ -148,39 +153,45 @@ func KitPage(d Deps) func() *VNode { ), ), - kitSection("Overlays (interactive)", + kitSection("Overlays (measured, portaled)", row("flex flex-wrap items-center gap-4", - // Modal, toggled by a signal. - ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Open modal", OnClick: func() { modalOpen.Set(true) }}), - // Menu, toggled by a signal. - ui.Menu("relative inline-block", - ui.MenuTrigger(func() { menuOpen.Set(!menuOpen.Get()) }, "", - ui.Button(ui.ButtonProps{Color: ui.ButtonWhite, Text: "Menu ▾"})), - ui.MenuContent(menuOpen.Get(), ui.MenuPlacementBottomStart, "", - ui.MenuItem(ui.MenuItemProps{Icon: "check", OnClick: func() { menuOpen.Set(false) }}, Text("Profile")), - ui.MenuItem(ui.MenuItemProps{OnClick: func() { menuOpen.Set(false) }}, Text("Settings")), - ui.MenuDivider(""), - ui.MenuItem(ui.MenuItemProps{OnClick: func() { menuOpen.Set(false) }}, Text("Sign out")), - ), + ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Open modal", OnClick: modal.Open}), + + // The menu measures itself against the viewport: drag the window + // narrow, or scroll it to the bottom, and it flips/shifts to stay on + // screen. Items close the menu themselves — no callback plumbing. + menu.TriggerFunc(ui.MenuTriggerProps{Tag: "div"}, func(open bool) *VNode { + caret := " ▾" + if open { + caret = " ▴" + } + return ui.Button(ui.ButtonProps{Color: ui.ButtonWhite, Text: "Menu" + caret}) + }), + menu.Content("", + menu.Item(ui.MenuItemProps{Icon: "check"}, Text("Profile")), + menu.Item(ui.MenuItemProps{}, Text("Settings")), + ui.MenuDivider(""), + menu.Item(ui.MenuItemProps{}, Text("Sign out")), ), - // Tooltip (pure CSS hover). - ui.HoverTooltip(Span(Text("A CSS-only tooltip")), "top", "", - ui.Button(ui.ButtonProps{Color: ui.ButtonNeutral, Outline: true, Text: "Hover me"})), + + // The tooltip's arrow tracks the trigger even when the panel gets + // shifted away from it near a viewport edge. + tip.Render(Span(Text("A measured tooltip — try it near the window edge")), + ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Text: "Hover me"})), ), - ui.Modal(ui.ModalProps{ - IsOpen: modalOpen.Get(), - OnClose: func() { modalOpen.Set(false) }, - Size: ui.ModalMedium, - Header: H3(Attr("class", "text-lg font-semibold text-text-heading"), Text("Example modal")), + modal.Render(ui.ModalProps{ + Header: H3(Attr("class", "text-lg font-semibold text-text-heading"), Text("Example modal")), }, - P(Attr("class", "text-neutral-600"), Text("This modal is toggled by a signal; the backdrop and close button round-trip through OnClose.")), + P(Attr("class", "text-neutral-600"), + Text("Portaled to document.body, so it is not clipped by any ancestor. Escape closes "+ + "the topmost modal; the backdrop click closes too.")), ), ), - ui.Alert(ui.AlertGray, "About this page", - Text("Floating/positioned components (menus, tooltips, modals, dropdowns) are ported as "+ - "structure + Tailwind + signal/event wiring — the neutral runtime has no floating-ui, "+ - "portals, or element measurement, so positioning is approximated with static classes.")), + ui.Alert(ui.AlertGreen, "About this page", + Text("Menus, tooltips, modals and dropdowns are now really measured: they are portaled to "+ + "document.body, positioned from getBoundingClientRect against the viewport, and they "+ + "flip and shift to stay on screen. Resize the window or scroll while one is open.")), ) } } diff --git a/go/cmd/examples/go-wasm-web/app/overlays.go b/go/cmd/examples/go-wasm-web/app/overlays.go new file mode 100644 index 00000000..8f45d1bd --- /dev/null +++ b/go/cmd/examples/go-wasm-web/app/overlays.go @@ -0,0 +1,350 @@ +package app + +import ( + "strconv" + + . "kjol/vdom" + ui "kjol/webui" +) + +// Every floating component in the kit, on one page: tooltips, popovers, menus and +// submenus, the date picker, modals (plain, confirm, wizard, imperative), toasts, +// and the tutorial's spotlight coachmarks. +// +// All of them are CONTROLLERS. They own refs, timers and open state, so they are +// created once here — never inside the render closure, which runs on every signal +// write and would rebuild them (and their refs) from scratch every frame. That is +// the single rule to remember about the floating layer. +// +// The page is NOT `static`: nothing is open during SSR anyway, so pre-rendering it +// buys nothing, and it keeps the example honest about which routes need it. +// +//gowasm:page /overlays layout=app +func OverlaysPage(d Deps) func() *VNode { + // --- tooltips ----------------------------------------------------------- + tipTop := ui.NewHoverTooltip(ui.PlacementTop, "") + tipRight := ui.NewHoverTooltip(ui.PlacementRight, "") + tipFocus := ui.NewFocusTooltip(ui.PlacementBottom, "") + tipFast := ui.NewTooltip(ui.TooltipProps{Placement: ui.PlacementTop, Delay: -1}) + + // --- popovers ----------------------------------------------------------- + pop := ui.NewPopover(ui.PopoverProps{Placement: ui.PlacementBottomStart}) + popEnd := ui.NewPopover(ui.PopoverProps{Placement: ui.PlacementBottomEnd}) + hoverPop := ui.NewHoverPopover(ui.HoverPopoverProps{ + Placement: ui.PlacementTop, + // The bridge: the cursor gets 300ms of grace to cross the gap from the + // trigger onto the panel. Without it, the panel closes in the dead space + // between them — which is exactly what happens once a panel is portaled and + // CSS :hover no longer reaches it. + HoverCloseDelay: 300, + }) + + // --- menus -------------------------------------------------------------- + menu := ui.NewMenu(ui.MenuOptions{Placement: ui.PlacementBottomStart}) + sub := ui.NewSubmenu(menu) + hoverMenu := ui.NewMenu(ui.MenuOptions{Placement: ui.PlacementBottomStart, OpenOnHover: true}) + + // --- date pickers ------------------------------------------------------- + picked := NewSignal("") + dp := ui.NewDatePicker(ui.DatePickerProps{ + Placeholder: "Pick a date", + Clearable: true, + OnChange: func(v string) { picked.Set(v) }, + }) + dob := ui.NewDateOfBirthPicker(ui.DatePickerProps{Placeholder: "Date of birth"}) + + // --- modals ------------------------------------------------------------- + modal := ui.NewModal(ui.ModalOptions{Size: ui.ModalMedium}) + nested := ui.NewModal(ui.ModalOptions{Size: ui.ModalSmall}) + deleted := NewSignal(false) + confirm := ui.NewModal(ui.ModalOptions{}) + + // --- wizard ------------------------------------------------------------- + wizardName := NewSignal("") + wizardDone := NewSignal(false) + wizard := ui.NewWizard(ui.ModalOptions{}) + + // --- toasts ------------------------------------------------------------- + // The Toaster owns the queue AND the clocks: it generates IDs, runs the + // auto-dismiss timer, and animates the countdown bar down to zero. (ToastProvider, + // the dumb half, renders a list you hand it and removes nothing — a toast pushed + // through it stays until you take it away yourself.) + toaster := ui.NewToaster(ui.ToasterOptions{Position: ui.ToastBottomRight}) + pushToast := func(kind ui.ToastType, msg string) { + toaster.Push(ui.Toast{Message: msg, Type: kind}) + } + + // --- tutorial ----------------------------------------------------------- + // Steps target elements by CSS SELECTOR. The tour resolves each one with + // document.querySelector, measures it, scrolls it into view, and cuts a hole in + // the dimmed overlay around it — the spotlight animates from target to target. + tour := ui.NewTutorial(ui.TutorialOptions{ + Steps: []ui.TutorialStep{ + { + Title: "Tooltips", + Target: "#demo-tooltips", + Content: func() *VNode { return Text("Measured, portaled, and they flip near a viewport edge.") }, + }, + { + Title: "Popovers", + Target: "#demo-popovers", + Placement: ui.PlacementBottom, + Content: func() *VNode { return Text("Click or hover. The hover bridge lets you reach the panel.") }, + }, + { + Title: "Menus", + Target: "#demo-menus", + Content: func() *VNode { return Text("Items close the menu themselves; submenus are portaled.") }, + }, + { + // No Target: the page dims flat and the card centres in the viewport. + Title: "That's the tour", + Content: func() *VNode { return Text("Escape ends it. Arrow keys and Enter move between steps.") }, + }, + }, + }) + + return func() *VNode { + return Div(Attr("class", "space-y-8"), + Div( + H2(Attr("class", "text-2xl font-semibold tracking-tight text-text-heading"), Text("Overlays")), + P(Attr("class", "mt-1 text-neutral-500"), + Text("Every floating component, really measured: portaled to document.body, positioned from "+ + "getBoundingClientRect against the viewport, flipping and shifting to stay on screen. "+ + "Scroll or resize the window with one open.")), + row("mt-3 flex gap-2", tour.StartButton(0, "", Text("Take the tour"))), + ), + + // ---- tooltips ---- + El("div", Attr("id", "demo-tooltips"), + kitSection("Tooltips", + row("flex flex-wrap items-center gap-3", + tipTop.Render(Span(Text("Above — the default")), + ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Text: "Top"})), + tipRight.Render(Span(Text("To the right, unless it would run off the edge")), + ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Text: "Right"})), + tipFast.Render(Span(Text("No open delay")), + ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Text: "Instant"})), + tipFocus.Render(Span(Text("Shown on focus, not hover — tab to the field")), + ui.FormInput(ui.FormInputProps{Placeholder: "Focus me"})), + ), + P(Attr("class", "text-xs text-neutral-500"), + Text("Drag the window narrow and hover the Right one: it flips to the left, and its arrow "+ + "follows. Near an edge the panel shifts back on screen and the arrow slides to keep "+ + "pointing at the trigger — the original kit's arrow detached here.")), + ), + ), + + // ---- popovers ---- + El("div", Attr("id", "demo-popovers"), + kitSection("Popovers", + row("flex flex-wrap items-center gap-3", + pop.Trigger(ui.PopoverTriggerProps{}, + ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Click me"})), + pop.Content(ui.PopoverContentProps{Class: "w-64"}, + P(Attr("class", "text-sm text-neutral-600"), + Text("Click outside, or press Escape, to close. Only the topmost floating closes per press.")), + ), + + popEnd.Trigger(ui.PopoverTriggerProps{}, + ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Text: "Aligned to my right edge"})), + popEnd.Content(ui.PopoverContentProps{Class: "w-56"}, + P(Attr("class", "text-sm text-neutral-600"), Text("Placement bottom-end.")), + ), + + hoverPop.Trigger(ui.PopoverTriggerProps{}, + ui.Button(ui.ButtonProps{Color: ui.ButtonBlue, Outline: true, Text: "Hover me, then reach the panel"})), + hoverPop.Content(ui.PopoverContentProps{Class: "w-64"}, + P(Attr("class", "text-sm text-neutral-600"), + Text("Move the cursor across the gap and onto this panel — it stays open. "+ + "Select this text to prove it.")), + ), + ), + ), + ), + + // ---- menus ---- + El("div", Attr("id", "demo-menus"), + kitSection("Menus & submenus", + row("flex flex-wrap items-center gap-3", + menu.TriggerFunc(ui.MenuTriggerProps{Tag: "div"}, func(open bool) *VNode { + caret := " ▾" + if open { + caret = " ▴" + } + return ui.Button(ui.ButtonProps{Color: ui.ButtonWhite, Text: "Actions" + caret}) + }), + menu.Content("", + menu.Item(ui.MenuItemProps{Icon: "check", OnClick: func() { pushToast(ui.ToastSuccess, "Profile opened") }}, + Text("Profile")), + menu.Item(ui.MenuItemProps{OnClick: func() { pushToast(ui.ToastInfo, "Settings opened") }}, + Text("Settings")), + + // The submenu is portaled — it used to be clipped by the parent + // menu's own overflow-y-auto. + sub.Submenu(ui.SubmenuProps{Trigger: "More", Icon: "ellipsis"}, + sub.Item(ui.MenuItemProps{OnClick: func() { pushToast(ui.ToastInfo, "Archived") }}, Text("Archive")), + sub.Item(ui.MenuItemProps{OnClick: func() { pushToast(ui.ToastWarning, "Duplicated") }}, Text("Duplicate")), + ), + + ui.MenuDivider(""), + // KeepOpen is the TSX's closeOnClick inverted: by default an item + // closes the menu, which the first Go port dropped entirely. + menu.Item(ui.MenuItemProps{KeepOpen: true, OnClick: func() { pushToast(ui.ToastGeneric, "Menu stayed open") }}, + Text("Stay open (KeepOpen)")), + menu.Item(ui.MenuItemProps{Icon: "arrow-right-from-bracket", + OnClick: func() { pushToast(ui.ToastError, "Signed out") }}, Text("Sign out")), + ), + + hoverMenu.TriggerFunc(ui.MenuTriggerProps{Tag: "div"}, func(bool) *VNode { + return ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Text: "Opens on hover"}) + }), + hoverMenu.Content("", + hoverMenu.Item(ui.MenuItemProps{}, Text("One")), + hoverMenu.Item(ui.MenuItemProps{}, Text("Two")), + ), + ), + P(Attr("class", "text-xs text-neutral-500"), + Text("Opening one menu closes the other: a single-open manager, with submenus exempt "+ + "(Standalone), or a submenu would close its own parent.")), + ), + ), + + // ---- date pickers ---- + kitSection("Date picker", + row("grid gap-4 sm:grid-cols-2", + row("flex flex-col gap-1", + ui.FormLabel(ui.FormLabelProps{}, Text("Date (portaled, flips near the bottom)")), + dp.Render(), + ), + row("flex flex-col gap-1", + ui.FormLabel(ui.FormLabelProps{}, Text("Date of birth (inline, three selects)")), + dob.Render(), + ), + ), + P(Attr("class", "text-xs text-neutral-500"), + Text("Picked: \""+picked.Get()+"\". Type into the field too — it parses loosely "+ + "(7/4/26, Jul 4 2026, 2026-07-04) and commits on blur.")), + ), + + // ---- modals ---- + kitSection("Modals", + row("flex flex-wrap items-center gap-3", + ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Open modal", OnClick: modal.Open}), + ui.Button(ui.ButtonProps{Color: ui.ButtonRed, Outline: true, Text: "Delete something…", OnClick: confirm.Open}), + ui.Button(ui.ButtonProps{Color: ui.ButtonBlue, Outline: true, Text: "Open wizard", OnClick: wizard.Open}), + ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Text: "Open imperatively", + OnClick: func() { + // No component in the tree owns this one: OpenModal hands content + // to the shared host rendered in the layout. + ui.OpenModal(func() *VNode { + return ui.ModalContent(ui.ModalContentProps{ + Header: H3(Attr("class", "text-lg font-semibold text-text-heading"), Text("Opened from anywhere")), + }, + P(Attr("class", "text-neutral-600"), + Text("This content was not rendered by any component — it was handed to "+ + "ModalHost (see AppLayout) by webui.OpenModal.")), + ) + }, ui.ModalOptions{Size: ui.ModalSmall}) + }}), + ), + P(Attr("class", "text-xs text-neutral-500"), + Text("Deleted: "+strconv.FormatBool(deleted.Get())+ + ". Open the modal, then the nested one inside it, and press Escape twice — "+ + "modals unwind one layer per press.")), + + modal.Render(ui.ModalProps{ + Header: H3(Attr("class", "text-lg font-semibold text-text-heading"), Text("A modal")), + Footer: ui.Button(ui.ButtonProps{Color: ui.ButtonNeutral, Text: "Close", OnClick: modal.Close}), + }, + P(Attr("class", "text-neutral-600"), + Text("Portaled to document.body, so no ancestor's overflow:hidden can clip it. It fades "+ + "and scales in — a double requestAnimationFrame, because a single frame does not "+ + "give the browser time to commit the initial style.")), + row("mt-4", + ui.Button(ui.ButtonProps{Color: ui.ButtonBlue, Outline: true, Text: "Open a nested modal", OnClick: nested.Open}), + ), + ), + nested.Render(ui.ModalProps{ + Header: H3(Attr("class", "text-lg font-semibold text-text-heading"), Text("Nested")), + }, + P(Attr("class", "text-neutral-600"), Text("Escape closes THIS one first, not the one behind it.")), + ), + confirm.Confirm(ui.ConfirmModalProps{ + Title: "Delete row", + Message: "This cannot be undone.", + OnConfirm: func() { + deleted.Set(true) + pushToast(ui.ToastError, "Row deleted") + }, + }), + wizard.Render(ui.WizardProps{ + Title: "Set up your account", + FinishText: "Finish", + OnComplete: func() { + wizardDone.Set(true) + pushToast(ui.ToastSuccess, "Wizard complete: "+wizardName.Get()) + }, + Steps: []ui.WizardStep{ + { + Title: "Your name", + // Each step gets its own context: SetCanContinue gates THIS step's + // Next button, which a single shared bool could not express. + Content: func(ctx ui.WizardStepContext) *VNode { + ctx.SetCanContinue(wizardName.Get() != "") + return row("flex flex-col gap-1", + ui.FormLabel(ui.FormLabelProps{}, Text("Name (required to continue)")), + ui.FormInput(ui.FormInputProps{ + Value: wizardName.Get(), + Placeholder: "Ada Lovelace", + OnInput: func(v string) { wizardName.Set(v) }, + }), + ) + }, + }, + { + Title: "Confirm", + Content: func(ctx ui.WizardStepContext) *VNode { + ctx.SetCanContinue(true) + return P(Attr("class", "text-neutral-600"), + Text("All set for "+wizardName.Get()+". Finish to close.")) + }, + }, + }, + }), + ), + + // ---- toasts ---- + kitSection("Toasts", + row("flex flex-wrap items-center gap-2", + ui.Button(ui.ButtonProps{Color: ui.ButtonGreen, Small: true, Text: "Success", + OnClick: func() { toaster.Success("Saved.") }}), + ui.Button(ui.ButtonProps{Color: ui.ButtonRed, Small: true, Text: "Error", + OnClick: func() { toaster.Error("Something went wrong.") }}), + ui.Button(ui.ButtonProps{Color: ui.ButtonBlue, Small: true, Text: "Info", + OnClick: func() { toaster.Info("Just so you know.") }}), + ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Small: true, Text: "Sticky (no timer)", + OnClick: func() { + toaster.Push(ui.Toast{ + Message: "This one waits for you to dismiss it.", + Type: ui.ToastWarning, + Duration: ui.ToastSticky, + }) + }}), + ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Small: true, Text: "Clear all", + OnClick: toaster.Clear}), + ), + P(Attr("class", "text-xs text-neutral-500"), + Text("These auto-dismiss after 5 seconds — watch the bar count down; it is a CSS transition "+ + "driven straight at the DOM, not a re-render per frame. A sticky one (Duration: "+ + "ToastSticky) never leaves on its own. The menu items above raise toasts too, which is "+ + "how you can see that an item really does close its own menu.")), + ), + + // The toast container and the tutorial's overlay both render here; both are + // fixed-position, so where they sit in the tree does not matter. + toaster.Render(), + tour.Render(), + ) + } +} diff --git a/go/cmd/examples/go-wasm-web/app/pages.go b/go/cmd/examples/go-wasm-web/app/pages.go index eef647a4..1a4d989d 100644 --- a/go/cmd/examples/go-wasm-web/app/pages.go +++ b/go/cmd/examples/go-wasm-web/app/pages.go @@ -72,21 +72,40 @@ func PublicLayout(d Deps, content *VNode) *VNode { ) } +// wideRoutes get a roomier container. A table with a dozen columns, a drag handle +// and three calculated columns has no business being squeezed into a reading-width +// column; prose pages still are. +var wideRoutes = map[string]bool{"/table": true} + //gowasm:layout app func AppLayout(d Deps, content *VNode) *VNode { + width := "max-w-5xl" + if wideRoutes[d.Path()] { + width = "max-w-[100rem]" + } + return Div(Attr("class", "min-h-screen"), Nav(Attr("class", "app-nav border-b border-neutral-800 bg-neutral-900"), - Div(Attr("class", "mx-auto flex max-w-5xl items-center gap-2 px-4 py-3"), + // The nav tracks the content's width, so the logo stays flush with the page + // rather than floating in from the left on the wide routes. + Div(Attr("class", "mx-auto flex "+width+" items-center gap-2 px-4 py-3"), A(Attr("class", "text-lg font-semibold tracking-tight text-text-on-dark no-underline"), Attr("href", "/chart"), navigate(d, "/chart"), Text("gowasm · app")), Ul(Attr("class", "ml-4 flex items-center gap-1"), navItem(d, "/chart", "Chart", true), navItem(d, "/server", "Server", true), navItem(d, "/data", "Data", true), + navItem(d, "/table", "Table", true), + navItem(d, "/overlays", "Overlays", true), navItem(d, "/kit", "UI Kit", true)), Ul(Attr("class", "ml-auto flex items-center"), navItem(d, "/", "Home", true)), )), - Main(Attr("class", "mx-auto max-w-5xl px-4 py-8"), content), + Main(Attr("class", "mx-auto "+width+" px-4 py-8"), content), + + // The host for webui.OpenModal — content opened imperatively, by code that + // owns no component in the tree, is portaled out of here. Render it ONCE, + // near the root. It is an empty portal when nothing is open. + ui.ModalHost(), ) } diff --git a/go/cmd/examples/go-wasm-web/app/routes.gen.go b/go/cmd/examples/go-wasm-web/app/routes.gen.go index 60f9cf8c..ac5a961a 100644 --- a/go/cmd/examples/go-wasm-web/app/routes.gen.go +++ b/go/cmd/examples/go-wasm-web/app/routes.gen.go @@ -6,12 +6,14 @@ import "kjol/vdom" // Routes maps each //gowasm:page path to its instantiated render function. func Routes(d Deps) map[string]func() *vdom.VNode { return map[string]func() *vdom.VNode{ - "/": HomePage(d), - "/about": AboutPage(d), - "/chart": ChartPage(d), - "/data": DataPage(d), - "/kit": KitPage(d), - "/server": ServerPage(d), + "/": HomePage(d), + "/about": AboutPage(d), + "/chart": ChartPage(d), + "/data": DataPage(d), + "/kit": KitPage(d), + "/overlays": OverlaysPage(d), + "/server": ServerPage(d), + "/table": TablePage(d), } } @@ -21,16 +23,19 @@ var StaticPaths = map[string]bool{ "/about": true, "/chart": true, "/data": true, + "/table": true, } // RouteLayout maps each route to the name of the layout that wraps it. var RouteLayout = map[string]string{ - "/": "public", - "/about": "public", - "/chart": "app", - "/data": "app", - "/kit": "app", - "/server": "app", + "/": "public", + "/about": "public", + "/chart": "app", + "/data": "app", + "/kit": "app", + "/overlays": "app", + "/server": "app", + "/table": "app", } // LayoutFor wraps a page's content in the layout declared for its route. diff --git a/go/cmd/examples/go-wasm-web/app/ssr_test.go b/go/cmd/examples/go-wasm-web/app/ssr_test.go new file mode 100644 index 00000000..1f9e1a56 --- /dev/null +++ b/go/cmd/examples/go-wasm-web/app/ssr_test.go @@ -0,0 +1,171 @@ +package app + +import ( + "bytes" + "os" + "strings" + "testing" + + "kjol/vdom" + "kjol/webui" +) + +func TestSSRPages(t *testing.T) { + for _, path := range []string{"/", "/about", "/chart", "/data", "/table", "/overlays", "/kit"} { + deps := Deps{Path: func() string { return path }} + html := vdom.RenderHTML(Shell(deps, Routes(deps))) + t.Logf("%-8s %6d bytes portals=%d", path, len(html), strings.Count(html, "data-portal")) + if len(html) < 200 { + t.Errorf("%s rendered only %d bytes", path, len(html)) + } + } +} + +func TestSSRTablePage(t *testing.T) { + deps := Deps{Path: func() string { return "/table" }} + html := vdom.RenderHTML(Shell(deps, Routes(deps))) + + // The table persists a personal layout in localStorage, which the SERVER CANNOT + // READ. So the server renders a SKELETON, not the default table: if it rendered + // the default one, a user who had reordered their columns would watch them + // rearrange themselves once the wasm booted. + // + // This is a real cost — the page ships no table content — and it is the price of + // never showing the wrong table. See webui.RestoreLayout. + if !strings.Contains(html, `aria-busy="true"`) { + t.Error("SSR /table should render the loading skeleton, not a table") + } + if !strings.Contains(html, "animate-pulse") { + t.Error("the skeleton bars are missing") + } + if strings.Contains(html, "Ada Lovelace") { + t.Error("SSR rendered table CONTENT — a user with a saved layout would watch it rearrange") + } +} + +// renderedTable drives the very table the page renders, past its skeleton. Natively +// there is nothing to restore, so RestoreLayout just marks the layout settled. +func renderedTable(t *testing.T) string { + t.Helper() + highlight := vdom.NewSignal("") + table := newEmployeeTable(highlight) + table.SetRows(employees()) + table.RestoreLayout() + return vdom.RenderHTML(table.Render()) +} + +// Once the layout has settled, the table renders in full. +func TestTableRendersOnceSettled(t *testing.T) { + html := renderedTable(t) + + for _, want := range []string{"Ada Lovelace", "Salary"} { + if !strings.Contains(html, want) { + t.Errorf("settled table missing %q", want) + } + } + // PerPage is 5, so page one holds 5 of the 12 rows. + if got := strings.Count(html, "@example.com"); got != 5 { + t.Errorf("rendered %d rows, want 5 (one page)", got) + } + // The Rank column is HiddenByDefault. + if strings.Contains(html, ">Rank<") { + t.Error("a HiddenByDefault column was rendered") + } + if !strings.Contains(html, "Page 1 of 3") { + t.Error("pagination did not compute 3 pages for 12 rows at 5/page") + } +} + +// Calculated columns, end to end through the page, in all three shapes. +// +// Page 1 (declared order): +// +// salary 1200.50 1500.00 980.00 1340.00 1610.25 +// bonus 150.00 300.00 0.00 220.00 400.00 +func TestSSRCalculatedColumns(t *testing.T) { + html := renderedTable(t) + + // BASIC: sum over the operand columns [Salary, Bonus], combined ACROSS each row. + // If this ever aggregated DOWN the column instead, every row would read the same + // number — which is exactly the bug these values are here to catch. + for _, want := range []string{"$1,350.50", "$1,800.00", "$980.00", "$1,560.00", "$2,010.25"} { + if !strings.Contains(html, want) { + t.Errorf("Total comp missing %s (a per-row Salary + Bonus)", want) + } + } + + // ADVANCED: ([Salary] + [Bonus]) * 12. + for _, want := range []string{"$16,206.00", "$21,600.00", "$11,760.00"} { + if !strings.Contains(html, want) { + t.Errorf("Annual column missing %s", want) + } + } + + // ADVANCED, position-dependent: SUM({Salary:1:ROW()}) accumulates down the rows. + for _, want := range []string{"$2,700.50", "$3,680.50", "$5,020.50", "$6,630.75"} { + if !strings.Contains(html, want) { + t.Errorf("running total missing %s", want) + } + } + + // SUMMARY: aggregated DOWN the column, over ALL 12 filtered rows — not the 5 on + // this page. 1200.50+1500+980+1340+1610.25+1120+1275.75+1050+1400+860+1180+990. + if !strings.Contains(html, "$14,506.50") { + t.Error("footer did not total the whole filtered set ($14,506.50)") + } + if !strings.Contains(html, "Average salary") { + t.Error("summary row label missing") + } +} + +// The export path, driven through the very table the /table page renders. +// +// Export must write what the FILTER selected — every matching row across every page +// — not the five rows on screen; the columns the user can SEE, in their order; and +// the calculated columns, with each row's own value. +func TestTableExport(t *testing.T) { + highlight := vdom.NewSignal("") + table := newEmployeeTable(highlight) + table.RestoreLayout() // nothing to restore natively; reveals the table over its skeleton + table.SetRows(employees()) + + // Filter to one team, then render (which resolves FilteredRows). + table.SetSearchValue("Team", "Research", true) + table.Render() + + csv := string(webui.ExportCSV(table.ExportColumns(), table.FilteredRows(), nil)) + + // PerPage is 5 and Research has 4 members, but the point is that export ignores + // paging entirely: every filtered row, no one else's. + for _, want := range []string{"Alan Turing", "Katherine Johnson", "Barbara Liskov", "Evelyn Boyd Granville"} { + if !strings.Contains(csv, want) { + t.Errorf("CSV missing filtered row %q", want) + } + } + if strings.Contains(csv, "Ada Lovelace") { + t.Error("CSV contains a row the filter excluded") + } + // Rank is HiddenByDefault, so it must not be exported. + if strings.Contains(csv, "Item 10") { + t.Error("CSV exported a hidden column") + } + // The calculated columns come along, and the running total ACCUMULATES — + // $1,500.00 then $2,840.00 (Turing + Johnson), not the same number twice. + if !strings.Contains(csv, "Running total") || !strings.Contains(csv, "$2,840.00") { + t.Errorf("running total did not accumulate in the export:\n%s", csv) + } + + // And the PDF: a real file, with the same filtered content. + pdf := table.ExportPDFBytes(webui.AutoTablePDFHeader{ + Title: "Employees", ShowDate: true, Orientation: webui.PDF_ORIENTATION_LANDSCAPE, + }) + if !bytes.HasPrefix(pdf, []byte("%PDF-")) || !bytes.Contains(pdf, []byte("%%EOF")) { + t.Fatalf("PDF is not a PDF (%d bytes)", len(pdf)) + } + if out := os.Getenv("PDF_OUT"); out != "" { + if err := os.WriteFile(out, pdf, 0o644); err != nil { + t.Fatal(err) + } + t.Logf("wrote %s (%d bytes)", out, len(pdf)) + } +} diff --git a/go/cmd/examples/go-wasm-web/app/table.go b/go/cmd/examples/go-wasm-web/app/table.go new file mode 100644 index 00000000..22301e65 --- /dev/null +++ b/go/cmd/examples/go-wasm-web/app/table.go @@ -0,0 +1,290 @@ +package app + +import ( + . "kjol/vdom" + ui "kjol/webui" +) + +// Employee is a row in the table demo. Salary and Bonus are both money, so a +// calculated column has two numeric columns to combine ACROSS a row. +type Employee struct { + Name string + Email string + Team string + Status string + Salary string + Bonus string + Rank string + Note string +} + +func employees() []any { + rows := []Employee{ + {"Ada Lovelace", "ada@example.com", "Engineering", "active", "$1,200.50", "$150.00", "Item 2", "Wrote the first algorithm."}, + {"Alan Turing", "alan@example.com", "Research", "active", "$1,500.00", "$300.00", "Item 10", "Decidability, and the machine."}, + {"Grace Hopper", "grace@example.com", "Engineering", "inactive", "$980.00", "$0.00", "Item 1", "Found the first bug. Literally."}, + {"Katherine Johnson", "katherine@example.com", "Research", "active", "$1,340.00", "$220.00", "Item 3", "Orbital mechanics, by hand."}, + {"Margaret Hamilton", "margaret@example.com", "Engineering", "active", "$1,610.25", "$400.00", "Item 21", "Coined 'software engineering'."}, + {"Barbara Liskov", "barbara@example.com", "Research", "inactive", "$1,120.00", "$90.00", "Item 7", "The substitution principle."}, + {"Radia Perlman", "radia@example.com", "Networking", "active", "$1,275.75", "$180.00", "Item 12", "Spanning tree protocol."}, + {"Karen Sparck Jones", "karen@example.com", "Research", "active", "$1,050.00", "$60.00", "Item 5", "Inverse document frequency."}, + {"Frances Allen", "frances@example.com", "Engineering", "inactive", "$1,400.00", "$250.00", "Item 9", "Optimizing compilers."}, + {"Jean Bartik", "jean@example.com", "Engineering", "active", "$860.00", "$40.00", "Item 4", "Programmed the ENIAC."}, + {"Evelyn Boyd Granville", "evelyn@example.com", "Research", "active", "$1,180.00", "$130.00", "Item 15", "Trajectory analysis."}, + {"Annie Easley", "annie@example.com", "Networking", "inactive", "$990.00", "$75.00", "Item 6", "Rocket propulsion code."}, + } + out := make([]any, len(rows)) + for i, r := range rows { + out[i] = r + } + return out +} + +func emp(row any) Employee { return row.(Employee) } + +func tableColumns() []ui.AutoTableColumn { + return []ui.AutoTableColumn{ + { + Key: "name", DisplayName: "Name", Sortable: true, SortIdentifier: "Name", + CSV: true, CSVValue: func(r any) string { return emp(r).Name }, + // No Toggleable: the name is what identifies a row, so it cannot be hidden. + Cell: func(r any) *VNode { return ui.AutoTableTdLeft("text-neutral-800", Text(emp(r).Name)) }, + }, + { + Key: "email", DisplayName: "Email", Sortable: true, SortIdentifier: "Email", + Toggleable: true, CSV: true, CSVValue: func(r any) string { return emp(r).Email }, + Cell: func(r any) *VNode { return ui.AutoTableTdLeft("text-neutral-500", Text(emp(r).Email)) }, + }, + { + Key: "team", DisplayName: "Team", Sortable: true, SortIdentifier: "Team", + Toggleable: true, CSV: true, CSVValue: func(r any) string { return emp(r).Team }, + Cell: func(r any) *VNode { return ui.AutoTableTdLeft("", Text(emp(r).Team)) }, + }, + { + Key: "status", DisplayName: "Status", Sortable: true, SortIdentifier: "Status", + Toggleable: true, CSV: true, CSVValue: func(r any) string { return emp(r).Status }, + Cell: func(r any) *VNode { + color := ui.BadgeGreen + if emp(r).Status != "active" { + color = ui.BadgeNeutral + } + return ui.AutoTableTdLeft("", ui.Badge(ui.BadgeProps{Color: color}, Text(emp(r).Status))) + }, + }, + { + // SortTypeMoney parses "$1,200.50" as a number — a plain string sort would + // put $1,200.50 before $980.00. + Key: "salary", DisplayName: "Salary", DisplayPosition: ui.COL_POS_RIGHT, + Sortable: true, SortIdentifier: "Salary", SortType: ui.SortTypeMoney, + Toggleable: true, CSV: true, CSVValue: func(r any) string { return emp(r).Salary }, + Cell: func(r any) *VNode { return ui.AutoTableTdRight("tabular-nums", Text(emp(r).Salary)) }, + }, + { + Key: "bonus", DisplayName: "Bonus", DisplayPosition: ui.COL_POS_RIGHT, + Sortable: true, SortIdentifier: "Bonus", SortType: ui.SortTypeMoney, + Toggleable: true, CSV: true, CSVValue: func(r any) string { return emp(r).Bonus }, + Cell: func(r any) *VNode { return ui.AutoTableTdRight("tabular-nums", Text(emp(r).Bonus)) }, + }, + { + // SortTypeNumeric sorts "Item 2" before "Item 10". + Key: "rank", DisplayName: "Rank", Sortable: true, SortIdentifier: "Rank", + SortType: ui.SortTypeNumeric, Toggleable: true, HiddenByDefault: true, + CSV: true, CSVValue: func(r any) string { return emp(r).Rank }, + Cell: func(r any) *VNode { return ui.AutoTableTdLeft("", Text(emp(r).Rank)) }, + }, + } +} + +// newEmployeeTable builds the table controller. +// +// It is factored out of TablePage so a test can drive the very same table the page +// renders — the export test checks the bytes this exact configuration produces, +// rather than a second copy of it that could drift. +// +// The controller owns the search, sort, page, expansion and column state. Build it +// ONCE, never inside a render closure: rebuilding it per frame would reset every +// filter on each keystroke. + +func newEmployeeTable(highlight *Signal[string]) *ui.AutoTableState { + return ui.NewAutoTableState(tableColumns(), ui.AutoTableStateOptions{ + PerPage: 5, + + // The table PAGES ITSELF to wherever the highlighted row landed after + // filtering and sorting. + HighlightMatch: func(r any) bool { + return highlight.Get() != "" && emp(r).Email == highlight.Get() + }, + + // Calculated columns come in two shapes, and the difference is the thing to + // understand: + // + // BASIC — a function over OPERAND COLUMNS, combined ACROSS each row. + // Sum over [Salary, Bonus] is this row's salary + bonus. It does + // NOT total the column. Operands are column KEYS (SortIdentifier), + // and subtract/divide are binary and ORDERED. + // + // ADVANCED — an Excel-style formula, which names columns by DISPLAY name: + // [Salary] is this row's cell, {Salary} is the whole column, and + // {Salary:1:ROW()} is everything up to this row — a running total. + // + // Either way they are evaluated against the FILTERED, SORTED rows, so filtering + // re-runs them. (ToCalcNumber parses "$1,200.50" for you.) + Calculated: []ui.UserCalculatedColumn{ + { + // Basic: two columns, added together, per row. + ID: "comp", DisplayName: "Total comp", Fn: ui.CALC_FN_SUM, + Operands: []string{"Salary", "Bonus"}, + DataType: ui.CALC_TYPE_MONEY, DisplayPosition: ui.COL_POS_RIGHT, + }, + { + // Advanced: a formula. + ID: "annual", DisplayName: "Annual", Fn: ui.CALC_FN_CUSTOM, + Formula: "([Salary] + [Bonus]) * 12", DataType: ui.CALC_TYPE_MONEY, + DisplayPosition: ui.COL_POS_RIGHT, + }, + { + // Advanced, and position-dependent: a running total down the page. + ID: "running", DisplayName: "Running total", Fn: ui.CALC_FN_CUSTOM, + Formula: "SUM({Salary:1:ROW()})", DataType: ui.CALC_TYPE_MONEY, + DisplayPosition: ui.COL_POS_RIGHT, + }, + }, + // A summary row goes the OTHER way: one column, aggregated DOWN the whole + // filtered set — not just the page on screen. Basic mode does that with a + // function + one operand; this one uses a formula for the same thing. + SummaryRows: []ui.UserSummaryRow{ + {ID: "total", Label: "Total salary", Fn: ui.CALC_FN_SUM, + Operands: []string{"Salary"}, DataType: ui.CALC_TYPE_MONEY}, + {ID: "avg", Label: "Average salary", Fn: ui.CALC_FN_CUSTOM, + Formula: "AVERAGE({Salary})", DataType: ui.CALC_TYPE_MONEY}, + }, + + Accordion: true, + RowKey: func(r any) string { return emp(r).Email }, + AccordionContent: func(r any) *VNode { + return P(Attr("class", "px-4 py-2 text-sm text-neutral-600"), Text(emp(r).Note)) + }, + + Columns: ui.AutoTableColumnOptions{ + Toggleable: true, + Draggable: true, + Resizable: true, + StorageKey: "gowasm-example-employees", + }, + }) +} + +//gowasm:page /table layout=app static +func TablePage(d Deps) func() *VNode { + // Which row to spotlight, if any. + highlight := NewSignal("") + + table := newEmployeeTable(highlight) + table.SetRows(employees()) + + // The export menu, with a submenu for the PDF's page orientation. Both are + // controllers, both built once. A submenu is Standalone — opening it must not + // close the menu it lives in. + exportMenu := ui.NewMenu(ui.MenuOptions{Placement: ui.PlacementBottomEnd}) + pdfSub := ui.NewSubmenu(exportMenu) + + // What the PDF prints above the table. + // + // Note what is NOT here: the footer lines. The export takes the table's OWN + // summary rows — including any the user builds at runtime in the Calculated + // editor — and evaluates them against the same filtered rows it is printing. Only + // pass Summaries explicitly to print something that is not one of the table's own + // rows. + pdfHeader := func(landscape bool) ui.AutoTablePDFHeader { + orientation := ui.PDF_ORIENTATION_PORTRAIT + if landscape { + orientation = ui.PDF_ORIENTATION_LANDSCAPE + } + return ui.AutoTablePDFHeader{ + Title: "Employees", + Subtitle: "Exported from the gowasm example", + ShowDate: true, + Orientation: orientation, + } + } + + return func() *VNode { + return Div(Attr("class", "space-y-6"), + Div( + H2(Attr("class", "text-2xl font-semibold tracking-tight text-text-heading"), Text("AutoTable")), + P(Attr("class", "mt-1 text-neutral-500"), + Text("Filtering, sorting, pagination, expandable rows and column management — all in Go. "+ + "Drag a header to reorder, drag its right edge to resize; both persist across reloads.")), + ), + + table.Render( + ui.AutoTableWithHover(), + ui.AutoTableWithAlternate(), + ui.AutoTableWithSurroundingBorder(), + ui.AutoTableWithPaginationShowAll(), + ui.AutoTableWithSearchFields( + // One box, several fields: a global search. + table.GlobalSearch("Search name or email…", "Name", "Email"), + // Exact-match dropdown. + table.SelectSearch("Status", []string{"active", "inactive"}, "Any status"), + // IN-set: matches any of the selected teams. + table.MultiSelectSearch("Team", "Any team", []string{"Engineering", "Research", "Networking"}), + ), + ui.AutoTableWithToolbarActions( + table.ColumnPicker(), + + // Build calculated columns and footer rows at runtime. Basic picks a + // function and the columns it combines across each row; Advanced writes + // a formula, with insert menus for columns, functions and constants. + // The formula is compiled and previewed against the real first row as + // you type, so a typo shows up immediately rather than as a column of + // dashes. What you build is persisted with the rest of the layout. + table.CalculatedColumnEditor(), + + // Export writes what the FILTER selected — every matching row across + // every page — not the five rows on screen. And it writes the columns + // you can actually see, in the order you dragged them into. + exportMenu.TriggerFunc(ui.MenuTriggerProps{Tag: "div"}, func(open bool) *VNode { + return ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Small: true, + Icon: "download", Text: "Export"}) + }), + exportMenu.Content("", + exportMenu.Item(ui.MenuItemProps{Icon: "file-csv", + OnClick: func() { table.DownloadCSV("employees") }}, Text("Download CSV")), + + // A submenu — portaled, so it is not clipped by the menu's own + // overflow-y-auto, which is what broke it before. + pdfSub.Submenu(ui.SubmenuProps{Trigger: "Download PDF", Icon: "file-pdf"}, + pdfSub.Item(ui.MenuItemProps{ + OnClick: func() { table.DownloadPDF("employees", pdfHeader(false)) }}, Text("Portrait")), + pdfSub.Item(ui.MenuItemProps{ + OnClick: func() { table.DownloadPDF("employees", pdfHeader(true)) }}, Text("Landscape")), + ), + + ui.MenuDivider(""), + exportMenu.Item(ui.MenuItemProps{Icon: "print", + OnClick: func() { table.PrintPDF(pdfHeader(true)) }}, Text("Print")), + ), + ), + ), + + // Highlight + auto-page-jump: Radia is on page 3 by default, and the table + // pages itself to wherever she actually is once filters and sorting move her. + row("flex flex-wrap items-center gap-2", + ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Small: true, + Text: "Find Radia Perlman", + OnClick: func() { highlight.Set("radia@example.com") }}), + ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Small: true, + Text: "Clear highlight", + OnClick: func() { highlight.Set("") }}), + ), + + ui.Alert(ui.AlertBlue, "What to try", + Text("Search (it matches name OR email); pick a status; select several teams. Sort by Salary — "+ + "it parses the currency, so $980 sorts below $1,200.50. Unhide Rank and sort it: 'Item 2' "+ + "comes before 'Item 10'. Click a row to expand it. Drag a header to reorder, drag its right "+ + "edge to resize — both survive a reload. Filter the table, then export: you get every "+ + "matching row, not just this page. 'Find Radia' jumps to whichever page she is on.")), + ) + } +} diff --git a/go/cmd/examples/go-wasm-web/css/app.css b/go/cmd/examples/go-wasm-web/css/app.css index b4367c30..c0d7709d 100644 --- a/go/cmd/examples/go-wasm-web/css/app.css +++ b/go/cmd/examples/go-wasm-web/css/app.css @@ -1,5 +1,59 @@ @import "tailwindcss"; +/* --------------------------------------------------------------------------- + Lora, vendored from Google Fonts — self-hosted, not linked. + --------------------------------------------------------------------------- + The files live in wwwroot/fonts (served by the dev server straight out of + ./wwwroot), so the app pulls no third-party request at runtime: no CDN to be + blocked, no extra DNS round trip, and no dependency on fonts.gstatic.com being + up. It is a VARIABLE font, so ONE file per style covers weights 400-700 — + hence "font-weight: 400 700" rather than a file per weight. + + Only the latin and latin-ext subsets are vendored (~120 KB in total). The + unicode-range on each face is what lets the browser skip downloading latin-ext + entirely unless the page actually uses a character from it. + + To refresh: fetch + https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700 + and re-download the woff2 URLs it names. + --------------------------------------------------------------------------- */ + +@font-face { + font-family: "Lora"; + font-style: italic; + font-weight: 400 700; /* a variable font: one file covers the whole range */ + font-display: swap; + src: url("/fonts/lora-latin-ext-italic.woff2") format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: "Lora"; + font-style: italic; + font-weight: 400 700; /* a variable font: one file covers the whole range */ + font-display: swap; + src: url("/fonts/lora-latin-italic.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "Lora"; + font-style: normal; + font-weight: 400 700; /* a variable font: one file covers the whole range */ + font-display: swap; + src: url("/fonts/lora-latin-ext-normal.woff2") format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: "Lora"; + font-style: normal; + font-weight: 400 700; /* a variable font: one file covers the whole range */ + font-display: swap; + src: url("/fonts/lora-latin-normal.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + /* App-side design tokens the webui kit references (Tailwind v4 @theme). Brand values live with the app; the kit stays generic. */ @theme { @@ -11,4 +65,11 @@ --color-text-heading: #111827; --color-text-on-dark: #f9fafb; --color-text-on-dark-muted: #9ca3af; + + /* Lora is the body face. Declaring it as --font-sans makes it the default for + everything (Tailwind's preflight sets `font-family: var(--font-sans)` on + html), and also gives you `font-sans` as a utility. --font-serif points at it + too, so `font-serif` is not a different, unvendored face. */ + --font-sans: "Lora", ui-serif, Georgia, Cambria, "Times New Roman", serif; + --font-serif: "Lora", ui-serif, Georgia, Cambria, "Times New Roman", serif; } diff --git a/go/cmd/examples/go-wasm-web/server/main.go b/go/cmd/examples/go-wasm-web/server/main.go index 6cb2fb6e..ce8bf56a 100644 --- a/go/cmd/examples/go-wasm-web/server/main.go +++ b/go/cmd/examples/go-wasm-web/server/main.go @@ -60,7 +60,7 @@ func sampleQuotes() []app.Quote { } // render SSRs a static route's #app inner HTML; ok=false ships an empty #app -// (client-rendered). It's the same neutral render the client runs, so the client +// (client-rendered). It is the same neutral render the client runs, so the client // hydrates it. func render(path string) (string, bool) { if !app.StaticPaths[path] { diff --git a/go/cmd/examples/go-wasm-web/wwwroot/app.css b/go/cmd/examples/go-wasm-web/wwwroot/app.css index e176512b..7aac5cdc 100644 --- a/go/cmd/examples/go-wasm-web/wwwroot/app.css +++ b/go/cmd/examples/go-wasm-web/wwwroot/app.css @@ -1,6 +1,5 @@ -@layer theme,base,components,utilities;@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji';--font-serif:ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', - monospace;--color-red-50:oklch(97.1% 0.013 17.38);--color-red-100:oklch(93.6% 0.032 17.717);--color-red-200:oklch(88.5% 0.062 18.334);--color-red-300:oklch(80.8% 0.114 19.571);--color-red-400:oklch(70.4% 0.191 22.216);--color-red-500:oklch(63.7% 0.237 25.331);--color-red-600:oklch(57.7% 0.245 27.325);--color-red-700:oklch(50.5% 0.213 27.518);--color-red-800:oklch(44.4% 0.177 26.899);--color-red-900:oklch(39.6% 0.141 25.723);--color-red-950:oklch(25.8% 0.092 26.042);--color-orange-50:oklch(98% 0.016 73.684);--color-orange-100:oklch(95.4% 0.038 75.164);--color-orange-200:oklch(90.1% 0.076 70.697);--color-orange-300:oklch(83.7% 0.128 66.29);--color-orange-400:oklch(75% 0.183 55.934);--color-orange-500:oklch(70.5% 0.213 47.604);--color-orange-600:oklch(64.6% 0.222 41.116);--color-orange-700:oklch(55.3% 0.195 38.402);--color-orange-800:oklch(47% 0.157 37.304);--color-orange-900:oklch(40.8% 0.123 38.172);--color-orange-950:oklch(26.6% 0.079 36.259);--color-amber-50:oklch(98.7% 0.022 95.277);--color-amber-100:oklch(96.2% 0.059 95.617);--color-amber-200:oklch(92.4% 0.12 95.746);--color-amber-300:oklch(87.9% 0.169 91.605);--color-amber-400:oklch(82.8% 0.189 84.429);--color-amber-500:oklch(76.9% 0.188 70.08);--color-amber-600:oklch(66.6% 0.179 58.318);--color-amber-700:oklch(55.5% 0.163 48.998);--color-amber-800:oklch(47.3% 0.137 46.201);--color-amber-900:oklch(41.4% 0.112 45.904);--color-amber-950:oklch(27.9% 0.077 45.635);--color-yellow-50:oklch(98.7% 0.026 102.212);--color-yellow-100:oklch(97.3% 0.071 103.193);--color-yellow-200:oklch(94.5% 0.129 101.54);--color-yellow-300:oklch(90.5% 0.182 98.111);--color-yellow-400:oklch(85.2% 0.199 91.936);--color-yellow-500:oklch(79.5% 0.184 86.047);--color-yellow-600:oklch(68.1% 0.162 75.834);--color-yellow-700:oklch(55.4% 0.135 66.442);--color-yellow-800:oklch(47.6% 0.114 61.907);--color-yellow-900:oklch(42.1% 0.095 57.708);--color-yellow-950:oklch(28.6% 0.066 53.813);--color-lime-50:oklch(98.6% 0.031 120.757);--color-lime-100:oklch(96.7% 0.067 122.328);--color-lime-200:oklch(93.8% 0.127 124.321);--color-lime-300:oklch(89.7% 0.196 126.665);--color-lime-400:oklch(84.1% 0.238 128.85);--color-lime-500:oklch(76.8% 0.233 130.85);--color-lime-600:oklch(64.8% 0.2 131.684);--color-lime-700:oklch(53.2% 0.157 131.589);--color-lime-800:oklch(45.3% 0.124 130.933);--color-lime-900:oklch(40.5% 0.101 131.063);--color-lime-950:oklch(27.4% 0.072 132.109);--color-green-50:oklch(98.2% 0.018 155.826);--color-green-100:oklch(96.2% 0.044 156.743);--color-green-200:oklch(92.5% 0.084 155.995);--color-green-300:oklch(87.1% 0.15 154.449);--color-green-400:oklch(79.2% 0.209 151.711);--color-green-500:oklch(72.3% 0.219 149.579);--color-green-600:oklch(62.7% 0.194 149.214);--color-green-700:oklch(52.7% 0.154 150.069);--color-green-800:oklch(44.8% 0.119 151.328);--color-green-900:oklch(39.3% 0.095 152.535);--color-green-950:oklch(26.6% 0.065 152.934);--color-emerald-50:oklch(97.9% 0.021 166.113);--color-emerald-100:oklch(95% 0.052 163.051);--color-emerald-200:oklch(90.5% 0.093 164.15);--color-emerald-300:oklch(84.5% 0.143 164.978);--color-emerald-400:oklch(76.5% 0.177 163.223);--color-emerald-500:oklch(69.6% 0.17 162.48);--color-emerald-600:oklch(59.6% 0.145 163.225);--color-emerald-700:oklch(50.8% 0.118 165.612);--color-emerald-800:oklch(43.2% 0.095 166.913);--color-emerald-900:oklch(37.8% 0.077 168.94);--color-emerald-950:oklch(26.2% 0.051 172.552);--color-teal-50:oklch(98.4% 0.014 180.72);--color-teal-100:oklch(95.3% 0.051 180.801);--color-teal-200:oklch(91% 0.096 180.426);--color-teal-300:oklch(85.5% 0.138 181.071);--color-teal-400:oklch(77.7% 0.152 181.912);--color-teal-500:oklch(70.4% 0.14 182.503);--color-teal-600:oklch(60% 0.118 184.704);--color-teal-700:oklch(51.1% 0.096 186.391);--color-teal-800:oklch(43.7% 0.078 188.216);--color-teal-900:oklch(38.6% 0.063 188.416);--color-teal-950:oklch(27.7% 0.046 192.524);--color-cyan-50:oklch(98.4% 0.019 200.873);--color-cyan-100:oklch(95.6% 0.045 203.388);--color-cyan-200:oklch(91.7% 0.08 205.041);--color-cyan-300:oklch(86.5% 0.127 207.078);--color-cyan-400:oklch(78.9% 0.154 211.53);--color-cyan-500:oklch(71.5% 0.143 215.221);--color-cyan-600:oklch(60.9% 0.126 221.723);--color-cyan-700:oklch(52% 0.105 223.128);--color-cyan-800:oklch(45% 0.085 224.283);--color-cyan-900:oklch(39.8% 0.07 227.392);--color-cyan-950:oklch(30.2% 0.056 229.695);--color-sky-50:oklch(97.7% 0.013 236.62);--color-sky-100:oklch(95.1% 0.026 236.824);--color-sky-200:oklch(90.1% 0.058 230.902);--color-sky-300:oklch(82.8% 0.111 230.318);--color-sky-400:oklch(74.6% 0.16 232.661);--color-sky-500:oklch(68.5% 0.169 237.323);--color-sky-600:oklch(58.8% 0.158 241.966);--color-sky-700:oklch(50% 0.134 242.749);--color-sky-800:oklch(44.3% 0.11 240.79);--color-sky-900:oklch(39.1% 0.09 240.876);--color-sky-950:oklch(29.3% 0.066 243.157);--color-blue-50:oklch(97% 0.014 254.604);--color-blue-100:oklch(93.2% 0.032 255.585);--color-blue-200:oklch(88.2% 0.059 254.128);--color-blue-300:oklch(80.9% 0.105 251.813);--color-blue-400:oklch(70.7% 0.165 254.624);--color-blue-500:oklch(62.3% 0.214 259.815);--color-blue-600:oklch(54.6% 0.245 262.881);--color-blue-700:oklch(48.8% 0.243 264.376);--color-blue-800:oklch(42.4% 0.199 265.638);--color-blue-900:oklch(37.9% 0.146 265.522);--color-blue-950:oklch(28.2% 0.091 267.935);--color-indigo-50:oklch(96.2% 0.018 272.314);--color-indigo-100:oklch(93% 0.034 272.788);--color-indigo-200:oklch(87% 0.065 274.039);--color-indigo-300:oklch(78.5% 0.115 274.713);--color-indigo-400:oklch(67.3% 0.182 276.935);--color-indigo-500:oklch(58.5% 0.233 277.117);--color-indigo-600:oklch(51.1% 0.262 276.966);--color-indigo-700:oklch(45.7% 0.24 277.023);--color-indigo-800:oklch(39.8% 0.195 277.366);--color-indigo-900:oklch(35.9% 0.144 278.697);--color-indigo-950:oklch(25.7% 0.09 281.288);--color-violet-50:oklch(96.9% 0.016 293.756);--color-violet-100:oklch(94.3% 0.029 294.588);--color-violet-200:oklch(89.4% 0.057 293.283);--color-violet-300:oklch(81.1% 0.111 293.571);--color-violet-400:oklch(70.2% 0.183 293.541);--color-violet-500:oklch(60.6% 0.25 292.717);--color-violet-600:oklch(54.1% 0.281 293.009);--color-violet-700:oklch(49.1% 0.27 292.581);--color-violet-800:oklch(43.2% 0.232 292.759);--color-violet-900:oklch(38% 0.189 293.745);--color-violet-950:oklch(28.3% 0.141 291.089);--color-purple-50:oklch(97.7% 0.014 308.299);--color-purple-100:oklch(94.6% 0.033 307.174);--color-purple-200:oklch(90.2% 0.063 306.703);--color-purple-300:oklch(82.7% 0.119 306.383);--color-purple-400:oklch(71.4% 0.203 305.504);--color-purple-500:oklch(62.7% 0.265 303.9);--color-purple-600:oklch(55.8% 0.288 302.321);--color-purple-700:oklch(49.6% 0.265 301.924);--color-purple-800:oklch(43.8% 0.218 303.724);--color-purple-900:oklch(38.1% 0.176 304.987);--color-purple-950:oklch(29.1% 0.149 302.717);--color-fuchsia-50:oklch(97.7% 0.017 320.058);--color-fuchsia-100:oklch(95.2% 0.037 318.852);--color-fuchsia-200:oklch(90.3% 0.076 319.62);--color-fuchsia-300:oklch(83.3% 0.145 321.434);--color-fuchsia-400:oklch(74% 0.238 322.16);--color-fuchsia-500:oklch(66.7% 0.295 322.15);--color-fuchsia-600:oklch(59.1% 0.293 322.896);--color-fuchsia-700:oklch(51.8% 0.253 323.949);--color-fuchsia-800:oklch(45.2% 0.211 324.591);--color-fuchsia-900:oklch(40.1% 0.17 325.612);--color-fuchsia-950:oklch(29.3% 0.136 325.661);--color-pink-50:oklch(97.1% 0.014 343.198);--color-pink-100:oklch(94.8% 0.028 342.258);--color-pink-200:oklch(89.9% 0.061 343.231);--color-pink-300:oklch(82.3% 0.12 346.018);--color-pink-400:oklch(71.8% 0.202 349.761);--color-pink-500:oklch(65.6% 0.241 354.308);--color-pink-600:oklch(59.2% 0.249 0.584);--color-pink-700:oklch(52.5% 0.223 3.958);--color-pink-800:oklch(45.9% 0.187 3.815);--color-pink-900:oklch(40.8% 0.153 2.432);--color-pink-950:oklch(28.4% 0.109 3.907);--color-rose-50:oklch(96.9% 0.015 12.422);--color-rose-100:oklch(94.1% 0.03 12.58);--color-rose-200:oklch(89.2% 0.058 10.001);--color-rose-300:oklch(81% 0.117 11.638);--color-rose-400:oklch(71.2% 0.194 13.428);--color-rose-500:oklch(64.5% 0.246 16.439);--color-rose-600:oklch(58.6% 0.253 17.585);--color-rose-700:oklch(51.4% 0.222 16.935);--color-rose-800:oklch(45.5% 0.188 13.697);--color-rose-900:oklch(41% 0.159 10.272);--color-rose-950:oklch(27.1% 0.105 12.094);--color-slate-50:oklch(98.4% 0.003 247.858);--color-slate-100:oklch(96.8% 0.007 247.896);--color-slate-200:oklch(92.9% 0.013 255.508);--color-slate-300:oklch(86.9% 0.022 252.894);--color-slate-400:oklch(70.4% 0.04 256.788);--color-slate-500:oklch(55.4% 0.046 257.417);--color-slate-600:oklch(44.6% 0.043 257.281);--color-slate-700:oklch(37.2% 0.044 257.287);--color-slate-800:oklch(27.9% 0.041 260.031);--color-slate-900:oklch(20.8% 0.042 265.755);--color-slate-950:oklch(12.9% 0.042 264.695);--color-gray-50:oklch(98.5% 0.002 247.839);--color-gray-100:oklch(96.7% 0.003 264.542);--color-gray-200:oklch(92.8% 0.006 264.531);--color-gray-300:oklch(87.2% 0.01 258.338);--color-gray-400:oklch(70.7% 0.022 261.325);--color-gray-500:oklch(55.1% 0.027 264.364);--color-gray-600:oklch(44.6% 0.03 256.802);--color-gray-700:oklch(37.3% 0.034 259.733);--color-gray-800:oklch(27.8% 0.033 256.848);--color-gray-900:oklch(21% 0.034 264.665);--color-gray-950:oklch(13% 0.028 261.692);--color-zinc-50:oklch(98.5% 0 0);--color-zinc-100:oklch(96.7% 0.001 286.375);--color-zinc-200:oklch(92% 0.004 286.32);--color-zinc-300:oklch(87.1% 0.006 286.286);--color-zinc-400:oklch(70.5% 0.015 286.067);--color-zinc-500:oklch(55.2% 0.016 285.938);--color-zinc-600:oklch(44.2% 0.017 285.786);--color-zinc-700:oklch(37% 0.013 285.805);--color-zinc-800:oklch(27.4% 0.006 286.033);--color-zinc-900:oklch(21% 0.006 285.885);--color-zinc-950:oklch(14.1% 0.005 285.823);--color-neutral-50:oklch(98.5% 0 0);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-neutral-950:oklch(14.5% 0 0);--color-stone-50:oklch(98.5% 0.001 106.423);--color-stone-100:oklch(97% 0.001 106.424);--color-stone-200:oklch(92.3% 0.003 48.717);--color-stone-300:oklch(86.9% 0.005 56.366);--color-stone-400:oklch(70.9% 0.01 56.259);--color-stone-500:oklch(55.3% 0.013 58.071);--color-stone-600:oklch(44.4% 0.011 73.639);--color-stone-700:oklch(37.4% 0.01 67.558);--color-stone-800:oklch(26.8% 0.007 34.298);--color-stone-900:oklch(21.6% 0.006 56.043);--color-stone-950:oklch(14.7% 0.004 49.25);--color-mauve-50:oklch(98.5% 0 0);--color-mauve-100:oklch(96% 0.003 325.6);--color-mauve-200:oklch(92.2% 0.005 325.62);--color-mauve-300:oklch(86.5% 0.012 325.68);--color-mauve-400:oklch(71.1% 0.019 323.02);--color-mauve-500:oklch(54.2% 0.034 322.5);--color-mauve-600:oklch(43.5% 0.029 321.78);--color-mauve-700:oklch(36.4% 0.029 323.89);--color-mauve-800:oklch(26.3% 0.024 320.12);--color-mauve-900:oklch(21.2% 0.019 322.12);--color-mauve-950:oklch(14.5% 0.008 326);--color-olive-50:oklch(98.8% 0.003 106.5);--color-olive-100:oklch(96.6% 0.005 106.5);--color-olive-200:oklch(93% 0.007 106.5);--color-olive-300:oklch(88% 0.011 106.6);--color-olive-400:oklch(73.7% 0.021 106.9);--color-olive-500:oklch(58% 0.031 107.3);--color-olive-600:oklch(46.6% 0.025 107.3);--color-olive-700:oklch(39.4% 0.023 107.4);--color-olive-800:oklch(28.6% 0.016 107.4);--color-olive-900:oklch(22.8% 0.013 107.4);--color-olive-950:oklch(15.3% 0.006 107.1);--color-mist-50:oklch(98.7% 0.002 197.1);--color-mist-100:oklch(96.3% 0.002 197.1);--color-mist-200:oklch(92.5% 0.005 214.3);--color-mist-300:oklch(87.2% 0.007 219.6);--color-mist-400:oklch(72.3% 0.014 214.4);--color-mist-500:oklch(56% 0.021 213.5);--color-mist-600:oklch(45% 0.017 213.2);--color-mist-700:oklch(37.8% 0.015 216);--color-mist-800:oklch(27.5% 0.011 216.9);--color-mist-900:oklch(21.8% 0.008 223.9);--color-mist-950:oklch(14.8% 0.004 228.8);--color-taupe-50:oklch(98.6% 0.002 67.8);--color-taupe-100:oklch(96% 0.002 17.2);--color-taupe-200:oklch(92.2% 0.005 34.3);--color-taupe-300:oklch(86.8% 0.007 39.5);--color-taupe-400:oklch(71.4% 0.014 41.2);--color-taupe-500:oklch(54.7% 0.021 43.1);--color-taupe-600:oklch(43.8% 0.017 39.3);--color-taupe-700:oklch(36.7% 0.016 35.7);--color-taupe-800:oklch(26.8% 0.011 36.5);--color-taupe-900:oklch(21.4% 0.009 43.1);--color-taupe-950:oklch(14.7% 0.004 49.3);--color-black:#000;--color-white:#fff;--spacing:0.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--breakpoint-2xl:96rem;--container-3xs:16rem;--container-2xs:18rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:0.75rem;--text-xs--line-height:calc(1 / 0.75);--text-sm:0.875rem;--text-sm--line-height:calc(1.25 / 0.875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--text-9xl:8rem;--text-9xl--line-height:1;--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tighter:-0.05em;--tracking-tight:-0.025em;--tracking-normal:0em;--tracking-wide:0.025em;--tracking-wider:0.05em;--tracking-widest:0.1em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--leading-loose:2;--radius-xs:0.125rem;--radius-sm:0.25rem;--radius-md:0.375rem;--radius-lg:0.5rem;--radius-xl:0.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--radius-4xl:2rem;--shadow-2xs:0 1px rgb(0 0 0 / 0.05);--shadow-xs:0 1px 2px 0 rgb(0 0 0 / 0.05);--shadow-sm:0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);--shadow-md:0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);--shadow-lg:0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);--shadow-xl:0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);--shadow-2xl:0 25px 50px -12px rgb(0 0 0 / 0.25);--inset-shadow-2xs:inset 0 1px rgb(0 0 0 / 0.05);--inset-shadow-xs:inset 0 1px 1px rgb(0 0 0 / 0.05);--inset-shadow-sm:inset 0 2px 4px rgb(0 0 0 / 0.05);--drop-shadow-xs:0 1px 1px rgb(0 0 0 / 0.05);--drop-shadow-sm:0 1px 2px rgb(0 0 0 / 0.15);--drop-shadow-md:0 3px 3px rgb(0 0 0 / 0.12);--drop-shadow-lg:0 4px 4px rgb(0 0 0 / 0.15);--drop-shadow-xl:0 9px 7px rgb(0 0 0 / 0.1);--drop-shadow-2xl:0 25px 25px rgb(0 0 0 / 0.15);--text-shadow-2xs:0px 1px 0px rgb(0 0 0 / 0.15);--text-shadow-xs:0px 1px 1px rgb(0 0 0 / 0.2);--text-shadow-sm:0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075);--text-shadow-md:0px 1px 1px rgb(0 0 0 / 0.1), 0px 1px 2px rgb(0 0 0 / 0.1), 0px 2px 4px rgb(0 0 0 / 0.1);--text-shadow-lg:0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1);--ease-in:cubic-bezier(0.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, 0.2, 1);--ease-in-out:cubic-bezier(0.4, 0, 0.2, 1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;--animate-bounce:bounce 1s infinite;--blur-xs:4px;--blur-sm:8px;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--blur-2xl:40px;--blur-3xl:64px;--perspective-dramatic:100px;--perspective-near:300px;--perspective-normal:500px;--perspective-midrange:800px;--perspective-distant:1200px;--aspect-video:16 / 9;--default-transition-duration:150ms;--default-transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);--default-font-family:var(--font-sans);--default-font-feature-settings:initial;--default-font-variation-settings:initial;--default-mono-font-family:var(--font-mono);--default-mono-font-feature-settings:initial;--default-mono-font-variation-settings:initial;--radius-default:0.375rem;--color-primary:#4f46e5;--color-primary-hover:#4338ca;--color-text-heading:#111827;--color-text-on-dark:#f9fafb;--color-text-on-dark-muted:#9ca3af}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,100%{transform:scale(2);opacity:0}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,100%{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}@layer base{*,::after,::before,::backdrop,::file-selector-button{box-sizing:border-box;margin:0;padding:0;border:0 solid}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;tab-size:4;font-family:--theme( --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:--theme(--default-font-feature-settings,normal);font-variation-settings:--theme(--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;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:--theme( --default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace );font-feature-settings:--theme(--default-mono-font-feature-settings,normal);font-variation-settings:--theme(--default-mono-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}:-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{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea,::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;border-radius:0;background-color:initial;opacity:1}: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: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,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-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]),::file-selector-button{appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{inset-block:0}.-top-\[6px\]{top:calc(6px * -1)}.top-0{top:0}.top-1\/2{top:calc(1/2 * 100%)}.top-20{top:calc(var(--spacing) * 20)}.top-4{top:calc(var(--spacing) * 4)}.top-full{top:100%}.-right-2{right:calc(var(--spacing) * -2)}.-right-\[6px\]{right:calc(6px * -1)}.right-0{right:0}.right-0\.5{right:calc(var(--spacing) * .5)}.right-4{right:calc(var(--spacing) * 4)}.right-8{right:calc(var(--spacing) * 8)}.right-full{right:100%}.-bottom-\[6px\]{bottom:calc(6px * -1)}.bottom-0{bottom:0}.bottom-4{bottom:calc(var(--spacing) * 4)}.bottom-full{bottom:100%}.-left-\[6px\]{left:calc(6px * -1)}.left-0{left:0}.left-1\/2{left:calc(1/2 * 100%)}.left-4{left:calc(var(--spacing) * 4)}.left-full{left:100%}.isolate{isolation:isolate}.z-10{z-index:10}.z-150{z-index:150}.z-200{z-index:200}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[110\]{z-index:110}.z-\[1\]{z-index:1}.z-\[200\]{z-index:200}.z-\[51\]{z-index:51}.col-span-1{grid-column:span 1/span 1}.m-0{margin:0}.-mx-1\.5{margin-inline:calc(var(--spacing) * -1.5)}.mx-3{margin-inline:calc(var(--spacing) * 3)}.mx-4{margin-inline:calc(var(--spacing) * 4)}.mx-\[0\.15rem\]{margin-inline:.15rem}.mx-auto{margin-inline:auto}.my-1{margin-block:var(--spacing)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mr-1{margin-right:var(--spacing)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mr-5{margin-right:calc(var(--spacing) * 5)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-square{aspect-ratio:1/1}.h-0{height:0}.h-0\.5{height:calc(var(--spacing) * .5)}.h-1{height:var(--spacing)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-72{height:calc(var(--spacing) * 72)}.h-8{height:calc(var(--spacing) * 8)}.h-\[30px\]{height:30px}.h-\[38px\]{height:38px}.h-dvh{height:100dvh}.h-full{height:100%}.h-screen{height:100vh}.max-h-60{max-height:calc(var(--spacing) * 60)}.max-h-96{max-height:calc(var(--spacing) * 96)}.max-h-\[calc\(100dvh_-_5rem\)\]{max-height:calc(100dvh - 5rem)}.max-h-\[calc\(100vh_-_7rem\)\]{max-height:calc(100vh - 7rem)}.max-h-dvh{max-height:100dvh}.min-h-0{min-height:0}.min-h-9{min-height:calc(var(--spacing) * 9)}.min-h-\[6\.5rem\]{min-height:6.5rem}.min-h-screen{min-height:100vh}.w-0{width:0}.w-2{width:calc(var(--spacing) * 2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-auto{width:auto}.w-full{width:100%}.w-max{width:max-content}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-64{max-width:calc(var(--spacing) * 64)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-80{max-width:calc(var(--spacing) * 80)}.max-w-\[11rem\]{max-width:11rem}.max-w-\[14rem\]{max-width:14rem}.max-w-\[16rem\]{max-width:16rem}.max-w-\[20rem\]{max-width:20rem}.max-w-\[90rem\]{max-width:90rem}.max-w-full{max-width:100%}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.max-w-screen{max-width:100vw}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-48{min-width:calc(var(--spacing) * 48)}.min-w-5{min-width:calc(var(--spacing) * 5)}.min-w-72{min-width:calc(var(--spacing) * 72)}.min-w-8{min-width:calc(var(--spacing) * 8)}.min-w-\[10rem\]{min-width:10rem}.min-w-\[16rem\]{min-width:16rem}.min-w-\[240px\]{min-width:240px}.min-w-\[7rem\]{min-width:7rem}.min-w-\[8rem\]{min-width:8rem}.min-w-\[9rem\]{min-width:9rem}.min-w-full{min-width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.table-fixed{table-layout:fixed}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-0\.5{--tw-translate-x:calc(var(--spacing) * 0.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-\[18px\]{--tw-translate-x:18px;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x)var(--tw-translate-y)}.scale-110{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.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-8{gap:calc(var(--spacing) * 8)}.gap-\[2px\]{gap:2px}.space-y-6{ :where(& > :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))); }}.space-y-8{ :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; margin-block-start: calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse)); margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse))); }}.self-end{align-self:flex-end}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-default{border-radius:var(--radius-default)}.rounded-full{border-radius:calc(infinity * 1px)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-l-default{border-top-left-radius:var(--radius-default);border-bottom-left-radius:var(--radius-default)}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-b-default{border-bottom-right-radius:var(--radius-default);border-bottom-left-radius:var(--radius-default)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-t-\[6px\]{border-top-style:var(--tw-border-style);border-top-width:6px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-r-0{border-right-style:var(--tw-border-style);border-right-width:0}.border-r-\[6px\]{border-right-style:var(--tw-border-style);border-right-width:6px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-b-\[6px\]{border-bottom-style:var(--tw-border-style);border-bottom-width:6px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-l-\[6px\]{border-left-style:var(--tw-border-style);border-left-width:6px}.border-none{--tw-border-style:none;border-style:none}.border-emerald-300{border-color:var(--color-emerald-300)}.border-green-200{border-color:var(--color-green-200)}.border-green-500{border-color:var(--color-green-500)}.border-neutral-100{border-color:var(--color-neutral-100)}.border-neutral-200{border-color:var(--color-neutral-200)}.border-neutral-300{border-color:var(--color-neutral-300)}.border-neutral-700{border-color:var(--color-neutral-700)}.border-neutral-800{border-color:var(--color-neutral-800)}.border-primary{border-color:var(--color-primary)}.border-red-200{border-color:var(--color-red-200)}.border-red-500{border-color:var(--color-red-500)}.border-sky-200{border-color:var(--color-sky-200)}.border-sky-700{border-color:var(--color-sky-700)}.border-yellow-200{border-color:var(--color-yellow-200)}.border-t-neutral-800{border-top-color:var(--color-neutral-800)}.border-t-sky-700{border-top-color:var(--color-sky-700)}.border-t-transparent{border-top-color:transparent}.border-r-neutral-800{border-right-color:var(--color-neutral-800)}.border-r-transparent{border-right-color:transparent}.border-b-neutral-800{border-bottom-color:var(--color-neutral-800)}.border-b-transparent{border-bottom-color:transparent}.border-l-green-700{border-left-color:var(--color-green-700)}.border-l-neutral-300{border-left-color:var(--color-neutral-300)}.border-l-neutral-400{border-left-color:var(--color-neutral-400)}.border-l-neutral-800{border-left-color:var(--color-neutral-800)}.border-l-red-700{border-left-color:var(--color-red-700)}.border-l-sky-800{border-left-color:var(--color-sky-800)}.border-l-transparent{border-left-color:transparent}.border-l-yellow-500{border-left-color:var(--color-yellow-500)}.\!bg-primary{background-color:var(--color-primary)!important}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-700{background-color:var(--color-amber-700)}.bg-black\/10{background-color:color-mix(in oklab,var(--color-black) 10%,transparent)}.bg-black\/30{background-color:color-mix(in oklab,var(--color-black) 30%,transparent)}.bg-black\/5{background-color:color-mix(in oklab,var(--color-black) 5%,transparent)}.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-700{background-color:var(--color-green-700)}.bg-green-900{background-color:var(--color-green-900)}.bg-neutral-100{background-color:var(--color-neutral-100)}.bg-neutral-200{background-color:var(--color-neutral-200)}.bg-neutral-300{background-color:var(--color-neutral-300)}.bg-neutral-50{background-color:var(--color-neutral-50)}.bg-neutral-500{background-color:var(--color-neutral-500)}.bg-neutral-600{background-color:var(--color-neutral-600)}.bg-neutral-700{background-color:var(--color-neutral-700)}.bg-neutral-800{background-color:var(--color-neutral-800)}.bg-neutral-900{background-color:var(--color-neutral-900)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-orange-600{background-color:var(--color-orange-600)}.bg-primary{background-color:var(--color-primary)}.bg-primary\/10{background-color:color-mix(in oklab,var(--color-primary) 10%,transparent)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-600{background-color:var(--color-red-600)}.bg-red-700{background-color:var(--color-red-700)}.bg-red-900{background-color:var(--color-red-900)}.bg-sky-50{background-color:var(--color-sky-50)}.bg-sky-600{background-color:var(--color-sky-600)}.bg-sky-700{background-color:var(--color-sky-700)}.bg-sky-800{background-color:var(--color-sky-800)}.bg-sky-900{background-color:var(--color-sky-900)}.bg-transparent{background-color:initial}.bg-white{background-color:var(--color-white)}.bg-white\/10{background-color:color-mix(in oklab,var(--color-white) 10%,transparent)}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-50{background-color:var(--color-yellow-50)}.bg-yellow-700{background-color:var(--color-yellow-700)}.fill-current{fill:currentcolor}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:var(--spacing)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-8{padding:calc(var(--spacing) * 8)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-7{padding-inline:calc(var(--spacing) * 7)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-px{padding-block:1px}.pt-1\.5{padding-top:calc(var(--spacing) * 1.5)}.pt-10{padding-top:calc(var(--spacing) * 10)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pr-14{padding-right:calc(var(--spacing) * 14)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-9{padding-right:calc(var(--spacing) * 9)}.pb-0\.5{padding-bottom:calc(var(--spacing) * .5)}.pb-1{padding-bottom:var(--spacing)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-\[calc\(1rem_\+_env\(safe-area-inset-bottom\,0px\)\)\]{padding-bottom:calc(1rem + env(safe-area-inset-bottom,0px))}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-8{padding-left:calc(var(--spacing) * 8)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-\[inherit\]{font-family:inherit}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--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-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.5rem\]{font-size:.5rem}.text-\[10px\]{font-size:10px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.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-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.whitespace-nowrap{white-space:nowrap}.\!text-white{color:var(--color-white)!important}.text-amber-600{color:var(--color-amber-600)}.text-black{color:var(--color-black)}.text-current{color:currentcolor}.text-emerald-700{color:var(--color-emerald-700)}.text-gray-900{color:var(--color-gray-900)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-900{color:var(--color-green-900)}.text-neutral-100{color:var(--color-neutral-100)}.text-neutral-200{color:var(--color-neutral-200)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-600{color:var(--color-neutral-600)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-800{color:var(--color-neutral-800)}.text-neutral-900{color:var(--color-neutral-900)}.text-orange-600{color:var(--color-orange-600)}.text-primary{color:var(--color-primary)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-900{color:var(--color-red-900)}.text-sky-700{color:var(--color-sky-700)}.text-sky-900{color:var(--color-sky-900)}.text-text-heading{color:var(--color-text-heading)}.text-text-on-dark{color:var(--color-text-on-dark)}.text-text-on-dark-muted{color:var(--color-text-on-dark-muted)}.text-white{color:var(--color-white)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-700{color:var(--color-yellow-700)}.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,)}.no-underline{text-decoration-line:none}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0%}.opacity-50{opacity:50%}.shadow-\[0_4px_12px_rgba\(0\,0\,0\,0\.15\)\]{--tw-shadow:0 4px 12px var(--tw-shadow-color, rgba(0,0,0,0.15));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[inset_0_0_0_1px_currentColor\]{--tw-shadow:inset 0 0 0 1px var(--tw-shadow-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)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.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-\[color\,border-color\]{transition-property:color,border-color;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[opacity\,transform\]{transition-property:opacity,transform;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-all{transition-property:all;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-opacity{transition-property:opacity;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:150ms;transition-duration:150ms}.duration-200{--tw-duration:200ms;transition-duration:200ms}.duration-300{--tw-duration:300ms;transition-duration:300ms}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.group-focus-within\:visible{&:is(:where(.group):focus-within *){visibility:visible}}.group-focus-within\:opacity-100{&:is(:where(.group):focus-within *){opacity:100%}}.group-hover\:visible{&:is(:where(.group):hover *){@media(hover:hover){visibility: visible;}}}.group-hover\:opacity-100{&:is(:where(.group):hover *){@media(hover:hover){opacity: 100%;}}}.file\:mr-2{&::file-selector-button{margin-right:calc(var(--spacing) * 2)}}.file\:ml-1{&::file-selector-button{margin-left:var(--spacing)}}.file\:cursor-pointer{&::file-selector-button{cursor:pointer}}.file\:rounded-default{&::file-selector-button{border-radius:var(--radius-default)}}.file\:border{&::file-selector-button{border-style:var(--tw-border-style);border-width:1px}}.file\:border-neutral-300{&::file-selector-button{border-color:var(--color-neutral-300)}}.file\:bg-neutral-100{&::file-selector-button{background-color:var(--color-neutral-100)}}.file\:px-3{&::file-selector-button{padding-inline:calc(var(--spacing) * 3)}}.file\:px-4{&::file-selector-button{padding-inline:calc(var(--spacing) * 4)}}.file\:py-\[2px\]{&::file-selector-button{padding-block:2px}}.file\:py-\[3px\]{&::file-selector-button{padding-block:3px}}.file\:text-sm{&::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.file\:shadow-xs{&::file-selector-button{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.placeholder\:text-neutral-400{&::placeholder{color:var(--color-neutral-400)}}.before\:absolute{&::before{content:var(--tw-content);position:absolute}}.before\:inset-0{&::before{content:var(--tw-content);inset:0}}.before\:-z-20{&::before{content:var(--tw-content);z-index:calc(20 * -1)}}.before\:bg-neutral-300{&::before{content:var(--tw-content);background-color:var(--color-neutral-300)}}.before\:content-\[\'\'\]{&::before{--tw-content:'';content:var(--tw-content)}}.before\:\[clip-path\:polygon\(16px_0\,100\%_0\,100\%_calc\(100\%_-_16px\)\,calc\(100\%_-_16px\)_100\%\,0_100\%\,0_16px\)\]{&::before{content:var(--tw-content);clip-path:polygon(16px 0,100% 0,100% calc(100% - 16px),calc(100% - 16px) 100%,0 100%,0 16px)}}.after\:absolute{&::after{content:var(--tw-content);position:absolute}}.after\:inset-\[1px\]{&::after{content:var(--tw-content);inset:1px}}.after\:-z-10{&::after{content:var(--tw-content);z-index:calc(10 * -1)}}.after\:bg-white{&::after{content:var(--tw-content);background-color:var(--color-white)}}.after\:content-\[\'\'\]{&::after{--tw-content:'';content:var(--tw-content)}}.after\:\[clip-path\:polygon\(15px_0\,100\%_0\,100\%_calc\(100\%_-_15px\)\,calc\(100\%_-_15px\)_100\%\,0_100\%\,0_15px\)\]{&::after{content:var(--tw-content);clip-path:polygon(15px 0,100% 0,100% calc(100% - 15px),calc(100% - 15px) 100%,0 100%,0 15px)}}.last\:border-r-0{&:last-child{border-right-style:var(--tw-border-style);border-right-width:0}}.last\:border-b-0{&:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}}.odd\:bg-white{&:nth-child(odd){background-color:var(--color-white)}}.even\:bg-neutral-100{&:nth-child(even){background-color:var(--color-neutral-100)}}.hover\:bg-green-100{&:hover{@media(hover:hover){background-color: var(--color-green-100);}}}.hover\:bg-green-800{&:hover{@media(hover:hover){background-color: var(--color-green-800);}}}.hover\:bg-green-950{&:hover{@media(hover:hover){background-color: var(--color-green-950);}}}.hover\:bg-neutral-100{&:hover{@media(hover:hover){background-color: var(--color-neutral-100);}}}.hover\:bg-neutral-200{&:hover{@media(hover:hover){background-color: var(--color-neutral-200);}}}.hover\:bg-neutral-300{&:hover{@media(hover:hover){background-color: var(--color-neutral-300);}}}.hover\:bg-neutral-50{&:hover{@media(hover:hover){background-color: var(--color-neutral-50);}}}.hover\:bg-neutral-500{&:hover{@media(hover:hover){background-color: var(--color-neutral-500);}}}.hover\:bg-neutral-800{&:hover{@media(hover:hover){background-color: var(--color-neutral-800);}}}.hover\:bg-orange-700{&:hover{@media(hover:hover){background-color: var(--color-orange-700);}}}.hover\:bg-primary-hover{&:hover{@media(hover:hover){background-color: var(--color-primary-hover);}}}.hover\:bg-red-700{&:hover{@media(hover:hover){background-color: var(--color-red-700);}}}.hover\:bg-red-800{&:hover{@media(hover:hover){background-color: var(--color-red-800);}}}.hover\:bg-red-950{&:hover{@media(hover:hover){background-color: var(--color-red-950);}}}.hover\:bg-sky-100{&:hover{@media(hover:hover){background-color: var(--color-sky-100);}}}.hover\:bg-sky-500{&:hover{@media(hover:hover){background-color: var(--color-sky-500);}}}.hover\:bg-sky-800{&:hover{@media(hover:hover){background-color: var(--color-sky-800);}}}.hover\:bg-sky-950{&:hover{@media(hover:hover){background-color: var(--color-sky-950);}}}.hover\:bg-white\/5{&:hover{@media(hover:hover){background-color: color-mix(in oklab,var(--color-white) 5%,transparent);}}}.hover\:bg-yellow-800{&:hover{@media(hover:hover){background-color: var(--color-yellow-800);}}}.hover\:text-neutral-600{&:hover{@media(hover:hover){color: var(--color-neutral-600);}}}.hover\:text-neutral-700{&:hover{@media(hover:hover){color: var(--color-neutral-700);}}}.hover\:text-neutral-800{&:hover{@media(hover:hover){color: var(--color-neutral-800);}}}.hover\:text-neutral-900{&:hover{@media(hover:hover){color: var(--color-neutral-900);}}}.hover\:text-sky-800{&:hover{@media(hover:hover){color: var(--color-sky-800);}}}.hover\:text-text-on-dark{&:hover{@media(hover:hover){color: var(--color-text-on-dark);}}}.hover\:text-white{&:hover{@media(hover:hover){color: var(--color-white);}}}.hover\:underline{&:hover{@media(hover:hover){text-decoration-line: underline;}}}.hover\:decoration-1{&:hover{@media(hover:hover){text-decoration-thickness: 1px;}}}.hover\:shadow-\[inset_0_0_0_2px_currentColor\]{&:hover{@media(hover:hover){--tw-shadow: inset 0 0 0 2px var(--tw-shadow-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);}}}.file\:hover\:bg-neutral-200{&::file-selector-button{&:hover{@media(hover:hover){background-color: var(--color-neutral-200);}}}}.focus\:border-primary{&:focus{border-color:var(--color-primary)}}.focus\:bg-neutral-100{&:focus{background-color:var(--color-neutral-100)}}.focus\:bg-red-50{&:focus{background-color:var(--color-red-50)}}.focus\:shadow-\[inset_0_0_0_2px_var\(--color-red-500\)\]{&:focus{--tw-shadow:inset 0 0 0 2px var(--tw-shadow-color, var(--color-red-500));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:outline-hidden{&:focus{--tw-outline-style:none;outline-style:none;@media(forced-colors:active){outline: 2px solid transparent; outline-offset: 2px;}}}.focus\:outline-2{&:focus{outline-style:var(--tw-outline-style);outline-width:2px}}.focus\:outline-offset-1{&:focus{outline-offset:1px}}.focus\:outline-green-500{&:focus{outline-color:var(--color-green-500)}}.focus\:outline-red-500{&:focus{outline-color:var(--color-red-500)}}.focus\:outline-sky-500{&:focus{outline-color:var(--color-sky-500)}}.focus-visible\:outline-2{&:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}}.focus-visible\:outline-offset-2{&:focus-visible{outline-offset:2px}}.focus-visible\:outline-current{&:focus-visible{outline-color:currentcolor}}.focus-visible\:outline-primary{&:focus-visible{outline-color:var(--color-primary)}}.active\:bg-neutral-200{&:active{background-color:var(--color-neutral-200)}}.enabled\:hover\:bg-neutral-50{&:enabled{&:hover{@media(hover:hover){background-color: var(--color-neutral-50);}}}}.enabled\:hover\:bg-neutral-900{&:enabled{&:hover{@media(hover:hover){background-color: var(--color-neutral-900);}}}}.enabled\:hover\:bg-red-700{&:enabled{&:hover{@media(hover:hover){background-color: var(--color-red-700);}}}}.disabled\:cursor-not-allowed{&:disabled{cursor:not-allowed}}.disabled\:bg-neutral-100{&:disabled{background-color:var(--color-neutral-100)}}.disabled\:bg-neutral-50{&:disabled{background-color:var(--color-neutral-50)}}.disabled\:text-neutral-300{&:disabled{color:var(--color-neutral-300)}}.disabled\:text-neutral-400{&:disabled{color:var(--color-neutral-400)}}.disabled\:opacity-40{&:disabled{opacity:40%}}.disabled\:opacity-50{&:disabled{opacity:50%}}.disabled\:hover\:bg-transparent{&:disabled{&:hover{@media(hover:hover){background-color: transparent;}}}}.sm\:block{@media(width >= 40rem){display: block;}}.sm\:flex{@media(width >= 40rem){display: flex;}}.sm\:grid-cols-2{@media(width >= 40rem){grid-template-columns: repeat(2,minmax(0,1fr));}}.sm\:grid-cols-3{@media(width >= 40rem){grid-template-columns: repeat(3,minmax(0,1fr));}}.md\:flex-initial{@media(width >= 48rem){flex: 0 auto;}}.md\:justify-start{@media(width >= 48rem){justify-content: flex-start;}}.lg\:col-span-10{@media(width >= 64rem){grid-column: span 10 / span 10;}}.lg\:col-span-2{@media(width >= 64rem){grid-column: span 2 / span 2;}}.lg\:col-span-5{@media(width >= 64rem){grid-column: span 5 / span 5;}}.lg\:col-span-7{@media(width >= 64rem){grid-column: span 7 / span 7;}}.lg\:block{@media(width >= 64rem){display: block;}}.lg\:h-full{@media(width >= 64rem){height: 100%;}}.lg\:grid-cols-12{@media(width >= 64rem){grid-template-columns: repeat(12,minmax(0,1fr));}}.lg\:self-start{@media(width >= 64rem){align-self: flex-start;}}.lg\:pr-8{@media(width >= 64rem){padding-right: calc(var(--spacing) * 8);}}.\[\&_\.ui-form\]\:m-0{& .ui-form{margin:0}}.\[\&_input\]\:cursor-text{& input{cursor:text}}.\[\&_td\]\:p-4{& td{padding:calc(var(--spacing) * 4)}}.\[\&_td\]\:px-2{& td{padding-inline:calc(var(--spacing) * 2)}}.\[\&_td\]\:py-0\.5{& td{padding-block:calc(var(--spacing) * .5)}}.\[\&_td\]\:py-1{& td{padding-block:var(--spacing)}}.\[\&_td\+td\]\:border-l{& td+td{border-left-style:var(--tw-border-style);border-left-width:1px}}.\[\&_td\+td\]\:border-neutral-300{& td+td{border-color:var(--color-neutral-300)}}.\[\&_th\]\:border-b{& th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}}.\[\&_th\]\:border-neutral-300{& th{border-color:var(--color-neutral-300)}}.\[\&_tr\:hover\]\:\!bg-green-100{& tr:hover{background-color:var(--color-green-100)!important}}.\[\&_tr\:hover\]\:\!bg-neutral-200{& tr:hover{background-color:var(--color-neutral-200)!important}}.\[\&_tr\:hover\]\:\!bg-sky-100{& tr:hover{background-color:var(--color-sky-100)!important}}.\[\&_tr\:not\(\:last-child\)\]\:border-b{& tr:not(:last-child){border-bottom-style:var(--tw-border-style);border-bottom-width:1px}}.\[\&_tr\:not\(\:last-child\)\]\:border-neutral-300{& tr:not(:last-child){border-color:var(--color-neutral-300)}}.\[\&_tr\:nth-child\(even\)\]\:bg-neutral-100{& tr:nth-child(even){background-color:var(--color-neutral-100)}}}@property --tw-translate-x{syntax: "*"; +@layer theme,base,components,utilities;@layer theme{:root,:host{--font-sans:"Lora", ui-serif, Georgia, Cambria, "Times New Roman", serif;--font-serif:"Lora", ui-serif, Georgia, Cambria, "Times New Roman", serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', + monospace;--color-red-50:oklch(97.1% 0.013 17.38);--color-red-100:oklch(93.6% 0.032 17.717);--color-red-200:oklch(88.5% 0.062 18.334);--color-red-300:oklch(80.8% 0.114 19.571);--color-red-400:oklch(70.4% 0.191 22.216);--color-red-500:oklch(63.7% 0.237 25.331);--color-red-600:oklch(57.7% 0.245 27.325);--color-red-700:oklch(50.5% 0.213 27.518);--color-red-800:oklch(44.4% 0.177 26.899);--color-red-900:oklch(39.6% 0.141 25.723);--color-red-950:oklch(25.8% 0.092 26.042);--color-orange-50:oklch(98% 0.016 73.684);--color-orange-100:oklch(95.4% 0.038 75.164);--color-orange-200:oklch(90.1% 0.076 70.697);--color-orange-300:oklch(83.7% 0.128 66.29);--color-orange-400:oklch(75% 0.183 55.934);--color-orange-500:oklch(70.5% 0.213 47.604);--color-orange-600:oklch(64.6% 0.222 41.116);--color-orange-700:oklch(55.3% 0.195 38.402);--color-orange-800:oklch(47% 0.157 37.304);--color-orange-900:oklch(40.8% 0.123 38.172);--color-orange-950:oklch(26.6% 0.079 36.259);--color-amber-50:oklch(98.7% 0.022 95.277);--color-amber-100:oklch(96.2% 0.059 95.617);--color-amber-200:oklch(92.4% 0.12 95.746);--color-amber-300:oklch(87.9% 0.169 91.605);--color-amber-400:oklch(82.8% 0.189 84.429);--color-amber-500:oklch(76.9% 0.188 70.08);--color-amber-600:oklch(66.6% 0.179 58.318);--color-amber-700:oklch(55.5% 0.163 48.998);--color-amber-800:oklch(47.3% 0.137 46.201);--color-amber-900:oklch(41.4% 0.112 45.904);--color-amber-950:oklch(27.9% 0.077 45.635);--color-yellow-50:oklch(98.7% 0.026 102.212);--color-yellow-100:oklch(97.3% 0.071 103.193);--color-yellow-200:oklch(94.5% 0.129 101.54);--color-yellow-300:oklch(90.5% 0.182 98.111);--color-yellow-400:oklch(85.2% 0.199 91.936);--color-yellow-500:oklch(79.5% 0.184 86.047);--color-yellow-600:oklch(68.1% 0.162 75.834);--color-yellow-700:oklch(55.4% 0.135 66.442);--color-yellow-800:oklch(47.6% 0.114 61.907);--color-yellow-900:oklch(42.1% 0.095 57.708);--color-yellow-950:oklch(28.6% 0.066 53.813);--color-lime-50:oklch(98.6% 0.031 120.757);--color-lime-100:oklch(96.7% 0.067 122.328);--color-lime-200:oklch(93.8% 0.127 124.321);--color-lime-300:oklch(89.7% 0.196 126.665);--color-lime-400:oklch(84.1% 0.238 128.85);--color-lime-500:oklch(76.8% 0.233 130.85);--color-lime-600:oklch(64.8% 0.2 131.684);--color-lime-700:oklch(53.2% 0.157 131.589);--color-lime-800:oklch(45.3% 0.124 130.933);--color-lime-900:oklch(40.5% 0.101 131.063);--color-lime-950:oklch(27.4% 0.072 132.109);--color-green-50:oklch(98.2% 0.018 155.826);--color-green-100:oklch(96.2% 0.044 156.743);--color-green-200:oklch(92.5% 0.084 155.995);--color-green-300:oklch(87.1% 0.15 154.449);--color-green-400:oklch(79.2% 0.209 151.711);--color-green-500:oklch(72.3% 0.219 149.579);--color-green-600:oklch(62.7% 0.194 149.214);--color-green-700:oklch(52.7% 0.154 150.069);--color-green-800:oklch(44.8% 0.119 151.328);--color-green-900:oklch(39.3% 0.095 152.535);--color-green-950:oklch(26.6% 0.065 152.934);--color-emerald-50:oklch(97.9% 0.021 166.113);--color-emerald-100:oklch(95% 0.052 163.051);--color-emerald-200:oklch(90.5% 0.093 164.15);--color-emerald-300:oklch(84.5% 0.143 164.978);--color-emerald-400:oklch(76.5% 0.177 163.223);--color-emerald-500:oklch(69.6% 0.17 162.48);--color-emerald-600:oklch(59.6% 0.145 163.225);--color-emerald-700:oklch(50.8% 0.118 165.612);--color-emerald-800:oklch(43.2% 0.095 166.913);--color-emerald-900:oklch(37.8% 0.077 168.94);--color-emerald-950:oklch(26.2% 0.051 172.552);--color-teal-50:oklch(98.4% 0.014 180.72);--color-teal-100:oklch(95.3% 0.051 180.801);--color-teal-200:oklch(91% 0.096 180.426);--color-teal-300:oklch(85.5% 0.138 181.071);--color-teal-400:oklch(77.7% 0.152 181.912);--color-teal-500:oklch(70.4% 0.14 182.503);--color-teal-600:oklch(60% 0.118 184.704);--color-teal-700:oklch(51.1% 0.096 186.391);--color-teal-800:oklch(43.7% 0.078 188.216);--color-teal-900:oklch(38.6% 0.063 188.416);--color-teal-950:oklch(27.7% 0.046 192.524);--color-cyan-50:oklch(98.4% 0.019 200.873);--color-cyan-100:oklch(95.6% 0.045 203.388);--color-cyan-200:oklch(91.7% 0.08 205.041);--color-cyan-300:oklch(86.5% 0.127 207.078);--color-cyan-400:oklch(78.9% 0.154 211.53);--color-cyan-500:oklch(71.5% 0.143 215.221);--color-cyan-600:oklch(60.9% 0.126 221.723);--color-cyan-700:oklch(52% 0.105 223.128);--color-cyan-800:oklch(45% 0.085 224.283);--color-cyan-900:oklch(39.8% 0.07 227.392);--color-cyan-950:oklch(30.2% 0.056 229.695);--color-sky-50:oklch(97.7% 0.013 236.62);--color-sky-100:oklch(95.1% 0.026 236.824);--color-sky-200:oklch(90.1% 0.058 230.902);--color-sky-300:oklch(82.8% 0.111 230.318);--color-sky-400:oklch(74.6% 0.16 232.661);--color-sky-500:oklch(68.5% 0.169 237.323);--color-sky-600:oklch(58.8% 0.158 241.966);--color-sky-700:oklch(50% 0.134 242.749);--color-sky-800:oklch(44.3% 0.11 240.79);--color-sky-900:oklch(39.1% 0.09 240.876);--color-sky-950:oklch(29.3% 0.066 243.157);--color-blue-50:oklch(97% 0.014 254.604);--color-blue-100:oklch(93.2% 0.032 255.585);--color-blue-200:oklch(88.2% 0.059 254.128);--color-blue-300:oklch(80.9% 0.105 251.813);--color-blue-400:oklch(70.7% 0.165 254.624);--color-blue-500:oklch(62.3% 0.214 259.815);--color-blue-600:oklch(54.6% 0.245 262.881);--color-blue-700:oklch(48.8% 0.243 264.376);--color-blue-800:oklch(42.4% 0.199 265.638);--color-blue-900:oklch(37.9% 0.146 265.522);--color-blue-950:oklch(28.2% 0.091 267.935);--color-indigo-50:oklch(96.2% 0.018 272.314);--color-indigo-100:oklch(93% 0.034 272.788);--color-indigo-200:oklch(87% 0.065 274.039);--color-indigo-300:oklch(78.5% 0.115 274.713);--color-indigo-400:oklch(67.3% 0.182 276.935);--color-indigo-500:oklch(58.5% 0.233 277.117);--color-indigo-600:oklch(51.1% 0.262 276.966);--color-indigo-700:oklch(45.7% 0.24 277.023);--color-indigo-800:oklch(39.8% 0.195 277.366);--color-indigo-900:oklch(35.9% 0.144 278.697);--color-indigo-950:oklch(25.7% 0.09 281.288);--color-violet-50:oklch(96.9% 0.016 293.756);--color-violet-100:oklch(94.3% 0.029 294.588);--color-violet-200:oklch(89.4% 0.057 293.283);--color-violet-300:oklch(81.1% 0.111 293.571);--color-violet-400:oklch(70.2% 0.183 293.541);--color-violet-500:oklch(60.6% 0.25 292.717);--color-violet-600:oklch(54.1% 0.281 293.009);--color-violet-700:oklch(49.1% 0.27 292.581);--color-violet-800:oklch(43.2% 0.232 292.759);--color-violet-900:oklch(38% 0.189 293.745);--color-violet-950:oklch(28.3% 0.141 291.089);--color-purple-50:oklch(97.7% 0.014 308.299);--color-purple-100:oklch(94.6% 0.033 307.174);--color-purple-200:oklch(90.2% 0.063 306.703);--color-purple-300:oklch(82.7% 0.119 306.383);--color-purple-400:oklch(71.4% 0.203 305.504);--color-purple-500:oklch(62.7% 0.265 303.9);--color-purple-600:oklch(55.8% 0.288 302.321);--color-purple-700:oklch(49.6% 0.265 301.924);--color-purple-800:oklch(43.8% 0.218 303.724);--color-purple-900:oklch(38.1% 0.176 304.987);--color-purple-950:oklch(29.1% 0.149 302.717);--color-fuchsia-50:oklch(97.7% 0.017 320.058);--color-fuchsia-100:oklch(95.2% 0.037 318.852);--color-fuchsia-200:oklch(90.3% 0.076 319.62);--color-fuchsia-300:oklch(83.3% 0.145 321.434);--color-fuchsia-400:oklch(74% 0.238 322.16);--color-fuchsia-500:oklch(66.7% 0.295 322.15);--color-fuchsia-600:oklch(59.1% 0.293 322.896);--color-fuchsia-700:oklch(51.8% 0.253 323.949);--color-fuchsia-800:oklch(45.2% 0.211 324.591);--color-fuchsia-900:oklch(40.1% 0.17 325.612);--color-fuchsia-950:oklch(29.3% 0.136 325.661);--color-pink-50:oklch(97.1% 0.014 343.198);--color-pink-100:oklch(94.8% 0.028 342.258);--color-pink-200:oklch(89.9% 0.061 343.231);--color-pink-300:oklch(82.3% 0.12 346.018);--color-pink-400:oklch(71.8% 0.202 349.761);--color-pink-500:oklch(65.6% 0.241 354.308);--color-pink-600:oklch(59.2% 0.249 0.584);--color-pink-700:oklch(52.5% 0.223 3.958);--color-pink-800:oklch(45.9% 0.187 3.815);--color-pink-900:oklch(40.8% 0.153 2.432);--color-pink-950:oklch(28.4% 0.109 3.907);--color-rose-50:oklch(96.9% 0.015 12.422);--color-rose-100:oklch(94.1% 0.03 12.58);--color-rose-200:oklch(89.2% 0.058 10.001);--color-rose-300:oklch(81% 0.117 11.638);--color-rose-400:oklch(71.2% 0.194 13.428);--color-rose-500:oklch(64.5% 0.246 16.439);--color-rose-600:oklch(58.6% 0.253 17.585);--color-rose-700:oklch(51.4% 0.222 16.935);--color-rose-800:oklch(45.5% 0.188 13.697);--color-rose-900:oklch(41% 0.159 10.272);--color-rose-950:oklch(27.1% 0.105 12.094);--color-slate-50:oklch(98.4% 0.003 247.858);--color-slate-100:oklch(96.8% 0.007 247.896);--color-slate-200:oklch(92.9% 0.013 255.508);--color-slate-300:oklch(86.9% 0.022 252.894);--color-slate-400:oklch(70.4% 0.04 256.788);--color-slate-500:oklch(55.4% 0.046 257.417);--color-slate-600:oklch(44.6% 0.043 257.281);--color-slate-700:oklch(37.2% 0.044 257.287);--color-slate-800:oklch(27.9% 0.041 260.031);--color-slate-900:oklch(20.8% 0.042 265.755);--color-slate-950:oklch(12.9% 0.042 264.695);--color-gray-50:oklch(98.5% 0.002 247.839);--color-gray-100:oklch(96.7% 0.003 264.542);--color-gray-200:oklch(92.8% 0.006 264.531);--color-gray-300:oklch(87.2% 0.01 258.338);--color-gray-400:oklch(70.7% 0.022 261.325);--color-gray-500:oklch(55.1% 0.027 264.364);--color-gray-600:oklch(44.6% 0.03 256.802);--color-gray-700:oklch(37.3% 0.034 259.733);--color-gray-800:oklch(27.8% 0.033 256.848);--color-gray-900:oklch(21% 0.034 264.665);--color-gray-950:oklch(13% 0.028 261.692);--color-zinc-50:oklch(98.5% 0 0);--color-zinc-100:oklch(96.7% 0.001 286.375);--color-zinc-200:oklch(92% 0.004 286.32);--color-zinc-300:oklch(87.1% 0.006 286.286);--color-zinc-400:oklch(70.5% 0.015 286.067);--color-zinc-500:oklch(55.2% 0.016 285.938);--color-zinc-600:oklch(44.2% 0.017 285.786);--color-zinc-700:oklch(37% 0.013 285.805);--color-zinc-800:oklch(27.4% 0.006 286.033);--color-zinc-900:oklch(21% 0.006 285.885);--color-zinc-950:oklch(14.1% 0.005 285.823);--color-neutral-50:oklch(98.5% 0 0);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-neutral-950:oklch(14.5% 0 0);--color-stone-50:oklch(98.5% 0.001 106.423);--color-stone-100:oklch(97% 0.001 106.424);--color-stone-200:oklch(92.3% 0.003 48.717);--color-stone-300:oklch(86.9% 0.005 56.366);--color-stone-400:oklch(70.9% 0.01 56.259);--color-stone-500:oklch(55.3% 0.013 58.071);--color-stone-600:oklch(44.4% 0.011 73.639);--color-stone-700:oklch(37.4% 0.01 67.558);--color-stone-800:oklch(26.8% 0.007 34.298);--color-stone-900:oklch(21.6% 0.006 56.043);--color-stone-950:oklch(14.7% 0.004 49.25);--color-mauve-50:oklch(98.5% 0 0);--color-mauve-100:oklch(96% 0.003 325.6);--color-mauve-200:oklch(92.2% 0.005 325.62);--color-mauve-300:oklch(86.5% 0.012 325.68);--color-mauve-400:oklch(71.1% 0.019 323.02);--color-mauve-500:oklch(54.2% 0.034 322.5);--color-mauve-600:oklch(43.5% 0.029 321.78);--color-mauve-700:oklch(36.4% 0.029 323.89);--color-mauve-800:oklch(26.3% 0.024 320.12);--color-mauve-900:oklch(21.2% 0.019 322.12);--color-mauve-950:oklch(14.5% 0.008 326);--color-olive-50:oklch(98.8% 0.003 106.5);--color-olive-100:oklch(96.6% 0.005 106.5);--color-olive-200:oklch(93% 0.007 106.5);--color-olive-300:oklch(88% 0.011 106.6);--color-olive-400:oklch(73.7% 0.021 106.9);--color-olive-500:oklch(58% 0.031 107.3);--color-olive-600:oklch(46.6% 0.025 107.3);--color-olive-700:oklch(39.4% 0.023 107.4);--color-olive-800:oklch(28.6% 0.016 107.4);--color-olive-900:oklch(22.8% 0.013 107.4);--color-olive-950:oklch(15.3% 0.006 107.1);--color-mist-50:oklch(98.7% 0.002 197.1);--color-mist-100:oklch(96.3% 0.002 197.1);--color-mist-200:oklch(92.5% 0.005 214.3);--color-mist-300:oklch(87.2% 0.007 219.6);--color-mist-400:oklch(72.3% 0.014 214.4);--color-mist-500:oklch(56% 0.021 213.5);--color-mist-600:oklch(45% 0.017 213.2);--color-mist-700:oklch(37.8% 0.015 216);--color-mist-800:oklch(27.5% 0.011 216.9);--color-mist-900:oklch(21.8% 0.008 223.9);--color-mist-950:oklch(14.8% 0.004 228.8);--color-taupe-50:oklch(98.6% 0.002 67.8);--color-taupe-100:oklch(96% 0.002 17.2);--color-taupe-200:oklch(92.2% 0.005 34.3);--color-taupe-300:oklch(86.8% 0.007 39.5);--color-taupe-400:oklch(71.4% 0.014 41.2);--color-taupe-500:oklch(54.7% 0.021 43.1);--color-taupe-600:oklch(43.8% 0.017 39.3);--color-taupe-700:oklch(36.7% 0.016 35.7);--color-taupe-800:oklch(26.8% 0.011 36.5);--color-taupe-900:oklch(21.4% 0.009 43.1);--color-taupe-950:oklch(14.7% 0.004 49.3);--color-black:#000;--color-white:#fff;--spacing:0.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--breakpoint-2xl:96rem;--container-3xs:16rem;--container-2xs:18rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:0.75rem;--text-xs--line-height:calc(1 / 0.75);--text-sm:0.875rem;--text-sm--line-height:calc(1.25 / 0.875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--text-9xl:8rem;--text-9xl--line-height:1;--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tighter:-0.05em;--tracking-tight:-0.025em;--tracking-normal:0em;--tracking-wide:0.025em;--tracking-wider:0.05em;--tracking-widest:0.1em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--leading-loose:2;--radius-xs:0.125rem;--radius-sm:0.25rem;--radius-md:0.375rem;--radius-lg:0.5rem;--radius-xl:0.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--radius-4xl:2rem;--shadow-2xs:0 1px rgb(0 0 0 / 0.05);--shadow-xs:0 1px 2px 0 rgb(0 0 0 / 0.05);--shadow-sm:0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);--shadow-md:0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);--shadow-lg:0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);--shadow-xl:0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);--shadow-2xl:0 25px 50px -12px rgb(0 0 0 / 0.25);--inset-shadow-2xs:inset 0 1px rgb(0 0 0 / 0.05);--inset-shadow-xs:inset 0 1px 1px rgb(0 0 0 / 0.05);--inset-shadow-sm:inset 0 2px 4px rgb(0 0 0 / 0.05);--drop-shadow-xs:0 1px 1px rgb(0 0 0 / 0.05);--drop-shadow-sm:0 1px 2px rgb(0 0 0 / 0.15);--drop-shadow-md:0 3px 3px rgb(0 0 0 / 0.12);--drop-shadow-lg:0 4px 4px rgb(0 0 0 / 0.15);--drop-shadow-xl:0 9px 7px rgb(0 0 0 / 0.1);--drop-shadow-2xl:0 25px 25px rgb(0 0 0 / 0.15);--text-shadow-2xs:0px 1px 0px rgb(0 0 0 / 0.15);--text-shadow-xs:0px 1px 1px rgb(0 0 0 / 0.2);--text-shadow-sm:0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075);--text-shadow-md:0px 1px 1px rgb(0 0 0 / 0.1), 0px 1px 2px rgb(0 0 0 / 0.1), 0px 2px 4px rgb(0 0 0 / 0.1);--text-shadow-lg:0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1);--ease-in:cubic-bezier(0.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, 0.2, 1);--ease-in-out:cubic-bezier(0.4, 0, 0.2, 1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;--animate-bounce:bounce 1s infinite;--blur-xs:4px;--blur-sm:8px;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--blur-2xl:40px;--blur-3xl:64px;--perspective-dramatic:100px;--perspective-near:300px;--perspective-normal:500px;--perspective-midrange:800px;--perspective-distant:1200px;--aspect-video:16 / 9;--default-transition-duration:150ms;--default-transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);--default-font-family:var(--font-sans);--default-font-feature-settings:initial;--default-font-variation-settings:initial;--default-mono-font-family:var(--font-mono);--default-mono-font-feature-settings:initial;--default-mono-font-variation-settings:initial;--radius-default:0.375rem;--color-primary:#4f46e5;--color-primary-hover:#4338ca;--color-text-heading:#111827;--color-text-on-dark:#f9fafb;--color-text-on-dark-muted:#9ca3af}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,100%{transform:scale(2);opacity:0}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,100%{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}@layer base{*,::after,::before,::backdrop,::file-selector-button{box-sizing:border-box;margin:0;padding:0;border:0 solid}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;tab-size:4;font-family:var(--default-font-family);font-feature-settings:var(--default-font-feature-settings);font-variation-settings:var(--default-font-variation-settings);-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;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family);font-feature-settings:var(--default-mono-font-feature-settings);font-variation-settings:var(--default-mono-font-variation-settings);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}:-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{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea,::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;border-radius:0;background-color:initial;opacity:1}: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: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,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-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]),::file-selector-button{appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@font-face{font-family:lora;font-style:italic;font-weight:400 700;font-display:swap;src:url(/fonts/lora-latin-ext-italic.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:lora;font-style:italic;font-weight:400 700;font-display:swap;src:url(/fonts/lora-latin-italic.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:lora;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/lora-latin-ext-normal.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:lora;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/lora-latin-normal.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{inset-block:0}.top-0{top:0}.top-1\/2{top:calc(1/2 * 100%)}.top-20{top:calc(var(--spacing) * 20)}.top-4{top:calc(var(--spacing) * 4)}.top-full{top:100%}.-right-2{right:calc(var(--spacing) * -2)}.right-0{right:0}.right-0\.5{right:calc(var(--spacing) * .5)}.right-4{right:calc(var(--spacing) * 4)}.right-8{right:calc(var(--spacing) * 8)}.bottom-4{bottom:calc(var(--spacing) * 4)}.left-0{left:0}.left-1\/2{left:calc(1/2 * 100%)}.left-4{left:calc(var(--spacing) * 4)}.isolate{isolation:isolate}.-z-10{z-index:calc(10 * -1)}.z-10{z-index:10}.z-150{z-index:150}.z-200{z-index:200}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[110\]{z-index:110}.z-\[1\]{z-index:1}.z-\[200\]{z-index:200}.col-span-1{grid-column:span 1/span 1}.m-0{margin:0}.-mx-1\.5{margin-inline:calc(var(--spacing) * -1.5)}.mx-3{margin-inline:calc(var(--spacing) * 3)}.mx-4{margin-inline:calc(var(--spacing) * 4)}.mx-\[0\.15rem\]{margin-inline:.15rem}.mx-auto{margin-inline:auto}.my-1{margin-block:var(--spacing)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mr-5{margin-right:calc(var(--spacing) * 5)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-auto{margin-left:auto}.box-border{box-sizing:border-box}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-square{aspect-ratio:1/1}.h-0{height:0}.h-0\.5{height:calc(var(--spacing) * .5)}.h-1{height:var(--spacing)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-72{height:calc(var(--spacing) * 72)}.h-8{height:calc(var(--spacing) * 8)}.h-\[30px\]{height:30px}.h-\[38px\]{height:38px}.h-dvh{height:100dvh}.h-full{height:100%}.h-screen{height:100vh}.max-h-60{max-height:calc(var(--spacing) * 60)}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-96{max-height:calc(var(--spacing) * 96)}.max-h-\[34rem\]{max-height:34rem}.max-h-\[calc\(100dvh_-_5rem\)\]{max-height:calc(100dvh - 5rem)}.max-h-\[calc\(100vh_-_7rem\)\]{max-height:calc(100vh - 7rem)}.max-h-dvh{max-height:100dvh}.min-h-0{min-height:0}.min-h-9{min-height:calc(var(--spacing) * 9)}.min-h-\[6\.5rem\]{min-height:6.5rem}.min-h-screen{min-height:100vh}.w-0{width:0}.w-1{width:var(--spacing)}.w-10{width:calc(var(--spacing) * 10)}.w-2{width:calc(var(--spacing) * 2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-4{width:calc(var(--spacing) * 4)}.w-40{width:calc(var(--spacing) * 40)}.w-52{width:calc(var(--spacing) * 52)}.w-56{width:calc(var(--spacing) * 56)}.w-6{width:calc(var(--spacing) * 6)}.w-64{width:calc(var(--spacing) * 64)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-\[26rem\]{width:26rem}.w-auto{width:auto}.w-full{width:100%}.w-max{width:max-content}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-64{max-width:calc(var(--spacing) * 64)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-80{max-width:calc(var(--spacing) * 80)}.max-w-\[100rem\]{max-width:100rem}.max-w-\[11rem\]{max-width:11rem}.max-w-\[14rem\]{max-width:14rem}.max-w-\[16rem\]{max-width:16rem}.max-w-\[20rem\]{max-width:20rem}.max-w-\[90rem\]{max-width:90rem}.max-w-full{max-width:100%}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.max-w-screen{max-width:100vw}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-48{min-width:calc(var(--spacing) * 48)}.min-w-5{min-width:calc(var(--spacing) * 5)}.min-w-72{min-width:calc(var(--spacing) * 72)}.min-w-8{min-width:calc(var(--spacing) * 8)}.min-w-\[10rem\]{min-width:10rem}.min-w-\[16rem\]{min-width:16rem}.min-w-\[240px\]{min-width:240px}.min-w-\[7rem\]{min-width:7rem}.min-w-\[8rem\]{min-width:8rem}.min-w-\[9rem\]{min-width:9rem}.min-w-full{min-width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.table-fixed{table-layout:fixed}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-0\.5{--tw-translate-x:calc(var(--spacing) * 0.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-\[18px\]{--tw-translate-x:18px;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x)var(--tw-translate-y)}.scale-110{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-95{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.rotate-180{rotate:180deg}.rotate-90{rotate:90deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-col-resize{cursor:col-resize}.cursor-default{cursor:default}.cursor-grab{cursor:grab}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-\[2px\]{gap:2px}.space-y-6{ :where(& > :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))); }}.space-y-8{ :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; margin-block-start: calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse)); margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse))); }}.self-end{align-self:flex-end}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.rounded{border-radius:.25rem}.rounded-default{border-radius:var(--radius-default)}.rounded-full{border-radius:calc(infinity * 1px)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-l-default{border-top-left-radius:var(--radius-default);border-bottom-left-radius:var(--radius-default)}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-b-default{border-bottom-right-radius:var(--radius-default);border-bottom-left-radius:var(--radius-default)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-r-0{border-right-style:var(--tw-border-style);border-right-width:0}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-none{--tw-border-style:none;border-style:none}.border-emerald-300{border-color:var(--color-emerald-300)}.border-green-200{border-color:var(--color-green-200)}.border-green-500{border-color:var(--color-green-500)}.border-neutral-100{border-color:var(--color-neutral-100)}.border-neutral-200{border-color:var(--color-neutral-200)}.border-neutral-300{border-color:var(--color-neutral-300)}.border-neutral-700{border-color:var(--color-neutral-700)}.border-neutral-800{border-color:var(--color-neutral-800)}.border-primary{border-color:var(--color-primary)}.border-red-200{border-color:var(--color-red-200)}.border-red-500{border-color:var(--color-red-500)}.border-sky-200{border-color:var(--color-sky-200)}.border-sky-700{border-color:var(--color-sky-700)}.border-transparent{border-color:transparent}.border-yellow-200{border-color:var(--color-yellow-200)}.border-t-sky-700{border-top-color:var(--color-sky-700)}.border-t-transparent{border-top-color:transparent}.border-l-green-700{border-left-color:var(--color-green-700)}.border-l-neutral-300{border-left-color:var(--color-neutral-300)}.border-l-neutral-400{border-left-color:var(--color-neutral-400)}.border-l-red-700{border-left-color:var(--color-red-700)}.border-l-sky-800{border-left-color:var(--color-sky-800)}.border-l-yellow-500{border-left-color:var(--color-yellow-500)}.\!bg-primary{background-color:var(--color-primary)!important}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-100\!{background-color:var(--color-amber-100)!important}.bg-amber-700{background-color:var(--color-amber-700)}.bg-black\/10{background-color:color-mix(in oklab,var(--color-black) 10%,transparent)}.bg-black\/30{background-color:color-mix(in oklab,var(--color-black) 30%,transparent)}.bg-black\/5{background-color:color-mix(in oklab,var(--color-black) 5%,transparent)}.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-700{background-color:var(--color-green-700)}.bg-green-900{background-color:var(--color-green-900)}.bg-neutral-100{background-color:var(--color-neutral-100)}.bg-neutral-200{background-color:var(--color-neutral-200)}.bg-neutral-300{background-color:var(--color-neutral-300)}.bg-neutral-50{background-color:var(--color-neutral-50)}.bg-neutral-500{background-color:var(--color-neutral-500)}.bg-neutral-600{background-color:var(--color-neutral-600)}.bg-neutral-700{background-color:var(--color-neutral-700)}.bg-neutral-800{background-color:var(--color-neutral-800)}.bg-neutral-900{background-color:var(--color-neutral-900)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-orange-600{background-color:var(--color-orange-600)}.bg-primary{background-color:var(--color-primary)}.bg-primary\/10{background-color:color-mix(in oklab,var(--color-primary) 10%,transparent)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-600{background-color:var(--color-red-600)}.bg-red-700{background-color:var(--color-red-700)}.bg-red-900{background-color:var(--color-red-900)}.bg-sky-50{background-color:var(--color-sky-50)}.bg-sky-600{background-color:var(--color-sky-600)}.bg-sky-700{background-color:var(--color-sky-700)}.bg-sky-800{background-color:var(--color-sky-800)}.bg-sky-900{background-color:var(--color-sky-900)}.bg-transparent{background-color:initial}.bg-white{background-color:var(--color-white)}.bg-white\/10{background-color:color-mix(in oklab,var(--color-white) 10%,transparent)}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-50{background-color:var(--color-yellow-50)}.bg-yellow-700{background-color:var(--color-yellow-700)}.fill-current{fill:currentcolor}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:var(--spacing)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-8{padding:calc(var(--spacing) * 8)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-7{padding-inline:calc(var(--spacing) * 7)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-px{padding-block:1px}.pt-1{padding-top:var(--spacing)}.pt-1\.5{padding-top:calc(var(--spacing) * 1.5)}.pt-10{padding-top:calc(var(--spacing) * 10)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pr-14{padding-right:calc(var(--spacing) * 14)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-9{padding-right:calc(var(--spacing) * 9)}.pb-0\.5{padding-bottom:calc(var(--spacing) * .5)}.pb-1{padding-bottom:var(--spacing)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-\[calc\(1rem_\+_env\(safe-area-inset-bottom\,0px\)\)\]{padding-bottom:calc(1rem + env(safe-area-inset-bottom,0px))}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-8{padding-left:calc(var(--spacing) * 8)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-\[inherit\]{font-family:inherit}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--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-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.5rem\]{font-size:.5rem}.text-\[10px\]{font-size:10px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.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-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.\!text-white{color:var(--color-white)!important}.text-amber-600{color:var(--color-amber-600)}.text-black{color:var(--color-black)}.text-current{color:currentcolor}.text-emerald-700{color:var(--color-emerald-700)}.text-gray-900{color:var(--color-gray-900)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-900{color:var(--color-green-900)}.text-neutral-100{color:var(--color-neutral-100)}.text-neutral-200{color:var(--color-neutral-200)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-600{color:var(--color-neutral-600)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-800{color:var(--color-neutral-800)}.text-neutral-900{color:var(--color-neutral-900)}.text-orange-600{color:var(--color-orange-600)}.text-primary{color:var(--color-primary)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-900{color:var(--color-red-900)}.text-sky-600{color:var(--color-sky-600)}.text-sky-700{color:var(--color-sky-700)}.text-sky-900{color:var(--color-sky-900)}.text-text-heading{color:var(--color-text-heading)}.text-text-on-dark{color:var(--color-text-on-dark)}.text-text-on-dark-muted{color:var(--color-text-on-dark-muted)}.text-transparent{color:transparent}.text-violet-600{color:var(--color-violet-600)}.text-white{color:var(--color-white)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-700{color:var(--color-yellow-700)}.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,)}.no-underline{text-decoration-line:none}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.caret-neutral-800{caret-color:var(--color-neutral-800)}.opacity-0{opacity:0%}.opacity-50{opacity:50%}.opacity-60{opacity:60%}.shadow-\[0_4px_12px_rgba\(0\,0\,0\,0\.15\)\]{--tw-shadow:0 4px 12px var(--tw-shadow-color, rgba(0,0,0,0.15));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[inset_0_0_0_1px_currentColor\]{--tw-shadow:inset 0 0 0 1px var(--tw-shadow-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)}.shadow-\[inset_3px_0_0_0_theme\(colors\.amber\.500\)\]{--tw-shadow:inset 3px 0 0 0 var(--tw-shadow-color, theme(colors.amber.500));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.outline-hidden{--tw-outline-style:none;outline-style:none;@media(forced-colors:active){outline: 2px solid transparent; outline-offset: 2px;}}.outline-2{outline-style:var(--tw-outline-style);outline-width:2px}.outline-sky-500{outline-color:var(--color-sky-500)}.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{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-\[color\,border-color\]{transition-property:color,border-color;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[opacity\,transform\]{transition-property:opacity,transform;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-all{transition-property:all;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:150ms;transition-duration:150ms}.duration-200{--tw-duration:200ms;transition-duration:200ms}.duration-300{--tw-duration:300ms;transition-duration:300ms}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.group-hover\/th\:opacity-50{&:is(:where(.group\/th):hover *){@media(hover:hover){opacity: 50%;}}}.file\:mr-2{&::file-selector-button{margin-right:calc(var(--spacing) * 2)}}.file\:ml-1{&::file-selector-button{margin-left:var(--spacing)}}.file\:cursor-pointer{&::file-selector-button{cursor:pointer}}.file\:rounded-default{&::file-selector-button{border-radius:var(--radius-default)}}.file\:border{&::file-selector-button{border-style:var(--tw-border-style);border-width:1px}}.file\:border-neutral-300{&::file-selector-button{border-color:var(--color-neutral-300)}}.file\:bg-neutral-100{&::file-selector-button{background-color:var(--color-neutral-100)}}.file\:px-3{&::file-selector-button{padding-inline:calc(var(--spacing) * 3)}}.file\:px-4{&::file-selector-button{padding-inline:calc(var(--spacing) * 4)}}.file\:py-\[2px\]{&::file-selector-button{padding-block:2px}}.file\:py-\[3px\]{&::file-selector-button{padding-block:3px}}.file\:text-sm{&::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.file\:shadow-xs{&::file-selector-button{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.placeholder\:text-neutral-400{&::placeholder{color:var(--color-neutral-400)}}.before\:absolute{&::before{content:var(--tw-content);position:absolute}}.before\:inset-0{&::before{content:var(--tw-content);inset:0}}.before\:-z-20{&::before{content:var(--tw-content);z-index:calc(20 * -1)}}.before\:bg-neutral-300{&::before{content:var(--tw-content);background-color:var(--color-neutral-300)}}.before\:content-\[\'\'\]{&::before{--tw-content:'';content:var(--tw-content)}}.before\:\[clip-path\:polygon\(16px_0\,100\%_0\,100\%_calc\(100\%_-_16px\)\,calc\(100\%_-_16px\)_100\%\,0_100\%\,0_16px\)\]{&::before{content:var(--tw-content);clip-path:polygon(16px 0,100% 0,100% calc(100% - 16px),calc(100% - 16px) 100%,0 100%,0 16px)}}.after\:absolute{&::after{content:var(--tw-content);position:absolute}}.after\:inset-\[1px\]{&::after{content:var(--tw-content);inset:1px}}.after\:-z-10{&::after{content:var(--tw-content);z-index:calc(10 * -1)}}.after\:bg-white{&::after{content:var(--tw-content);background-color:var(--color-white)}}.after\:content-\[\'\'\]{&::after{--tw-content:'';content:var(--tw-content)}}.after\:\[clip-path\:polygon\(15px_0\,100\%_0\,100\%_calc\(100\%_-_15px\)\,calc\(100\%_-_15px\)_100\%\,0_100\%\,0_15px\)\]{&::after{content:var(--tw-content);clip-path:polygon(15px 0,100% 0,100% calc(100% - 15px),calc(100% - 15px) 100%,0 100%,0 15px)}}.last\:border-r-0{&:last-child{border-right-style:var(--tw-border-style);border-right-width:0}}.last\:border-b-0{&:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}}.odd\:bg-white{&:nth-child(odd){background-color:var(--color-white)}}.even\:bg-neutral-100{&:nth-child(even){background-color:var(--color-neutral-100)}}.hover\:bg-green-100{&:hover{@media(hover:hover){background-color: var(--color-green-100);}}}.hover\:bg-green-800{&:hover{@media(hover:hover){background-color: var(--color-green-800);}}}.hover\:bg-green-950{&:hover{@media(hover:hover){background-color: var(--color-green-950);}}}.hover\:bg-neutral-100{&:hover{@media(hover:hover){background-color: var(--color-neutral-100);}}}.hover\:bg-neutral-200{&:hover{@media(hover:hover){background-color: var(--color-neutral-200);}}}.hover\:bg-neutral-300{&:hover{@media(hover:hover){background-color: var(--color-neutral-300);}}}.hover\:bg-neutral-50{&:hover{@media(hover:hover){background-color: var(--color-neutral-50);}}}.hover\:bg-neutral-500{&:hover{@media(hover:hover){background-color: var(--color-neutral-500);}}}.hover\:bg-neutral-800{&:hover{@media(hover:hover){background-color: var(--color-neutral-800);}}}.hover\:bg-orange-700{&:hover{@media(hover:hover){background-color: var(--color-orange-700);}}}.hover\:bg-primary-hover{&:hover{@media(hover:hover){background-color: var(--color-primary-hover);}}}.hover\:bg-red-700{&:hover{@media(hover:hover){background-color: var(--color-red-700);}}}.hover\:bg-red-800{&:hover{@media(hover:hover){background-color: var(--color-red-800);}}}.hover\:bg-red-950{&:hover{@media(hover:hover){background-color: var(--color-red-950);}}}.hover\:bg-sky-100{&:hover{@media(hover:hover){background-color: var(--color-sky-100);}}}.hover\:bg-sky-500{&:hover{@media(hover:hover){background-color: var(--color-sky-500);}}}.hover\:bg-sky-500\/50{&:hover{@media(hover:hover){background-color: color-mix(in oklab,var(--color-sky-500) 50%,transparent);}}}.hover\:bg-sky-800{&:hover{@media(hover:hover){background-color: var(--color-sky-800);}}}.hover\:bg-sky-950{&:hover{@media(hover:hover){background-color: var(--color-sky-950);}}}.hover\:bg-white\/5{&:hover{@media(hover:hover){background-color: color-mix(in oklab,var(--color-white) 5%,transparent);}}}.hover\:bg-yellow-800{&:hover{@media(hover:hover){background-color: var(--color-yellow-800);}}}.hover\:text-neutral-600{&:hover{@media(hover:hover){color: var(--color-neutral-600);}}}.hover\:text-neutral-700{&:hover{@media(hover:hover){color: var(--color-neutral-700);}}}.hover\:text-neutral-800{&:hover{@media(hover:hover){color: var(--color-neutral-800);}}}.hover\:text-neutral-900{&:hover{@media(hover:hover){color: var(--color-neutral-900);}}}.hover\:text-sky-800{&:hover{@media(hover:hover){color: var(--color-sky-800);}}}.hover\:text-text-on-dark{&:hover{@media(hover:hover){color: var(--color-text-on-dark);}}}.hover\:text-white{&:hover{@media(hover:hover){color: var(--color-white);}}}.hover\:underline{&:hover{@media(hover:hover){text-decoration-line: underline;}}}.hover\:decoration-1{&:hover{@media(hover:hover){text-decoration-thickness: 1px;}}}.hover\:shadow-\[inset_0_0_0_2px_currentColor\]{&:hover{@media(hover:hover){--tw-shadow: inset 0 0 0 2px var(--tw-shadow-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);}}}.file\:hover\:bg-neutral-200{&::file-selector-button{&:hover{@media(hover:hover){background-color: var(--color-neutral-200);}}}}.focus\:border-primary{&:focus{border-color:var(--color-primary)}}.focus\:border-sky-500{&:focus{border-color:var(--color-sky-500)}}.focus\:bg-neutral-100{&:focus{background-color:var(--color-neutral-100)}}.focus\:bg-red-50{&:focus{background-color:var(--color-red-50)}}.focus\:shadow-\[inset_0_0_0_2px_var\(--color-red-500\)\]{&:focus{--tw-shadow:inset 0 0 0 2px var(--tw-shadow-color, var(--color-red-500));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:outline-hidden{&:focus{--tw-outline-style:none;outline-style:none;@media(forced-colors:active){outline: 2px solid transparent; outline-offset: 2px;}}}.focus\:outline-2{&:focus{outline-style:var(--tw-outline-style);outline-width:2px}}.focus\:outline-offset-1{&:focus{outline-offset:1px}}.focus\:outline-green-500{&:focus{outline-color:var(--color-green-500)}}.focus\:outline-red-500{&:focus{outline-color:var(--color-red-500)}}.focus\:outline-sky-500{&:focus{outline-color:var(--color-sky-500)}}.focus-visible\:outline-2{&:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}}.focus-visible\:outline-offset-2{&:focus-visible{outline-offset:2px}}.focus-visible\:outline-current{&:focus-visible{outline-color:currentcolor}}.focus-visible\:outline-primary{&:focus-visible{outline-color:var(--color-primary)}}.active\:bg-neutral-200{&:active{background-color:var(--color-neutral-200)}}.enabled\:hover\:bg-neutral-50{&:enabled{&:hover{@media(hover:hover){background-color: var(--color-neutral-50);}}}}.enabled\:hover\:bg-neutral-900{&:enabled{&:hover{@media(hover:hover){background-color: var(--color-neutral-900);}}}}.enabled\:hover\:bg-red-700{&:enabled{&:hover{@media(hover:hover){background-color: var(--color-red-700);}}}}.disabled\:cursor-not-allowed{&:disabled{cursor:not-allowed}}.disabled\:bg-neutral-100{&:disabled{background-color:var(--color-neutral-100)}}.disabled\:bg-neutral-50{&:disabled{background-color:var(--color-neutral-50)}}.disabled\:text-neutral-300{&:disabled{color:var(--color-neutral-300)}}.disabled\:text-neutral-400{&:disabled{color:var(--color-neutral-400)}}.disabled\:opacity-40{&:disabled{opacity:40%}}.disabled\:opacity-50{&:disabled{opacity:50%}}.disabled\:hover\:bg-transparent{&:disabled{&:hover{@media(hover:hover){background-color: transparent;}}}}.sm\:block{@media(width >= 40rem){display: block;}}.sm\:flex{@media(width >= 40rem){display: flex;}}.sm\:hidden{@media(width >= 40rem){display: none;}}.sm\:w-48{@media(width >= 40rem){width: calc(var(--spacing) * 48);}}.sm\:w-64{@media(width >= 40rem){width: calc(var(--spacing) * 64);}}.sm\:grow-0{@media(width >= 40rem){flex-grow: 0;}}.sm\:grid-cols-2{@media(width >= 40rem){grid-template-columns: repeat(2,minmax(0,1fr));}}.sm\:grid-cols-3{@media(width >= 40rem){grid-template-columns: repeat(3,minmax(0,1fr));}}.sm\:flex-row{@media(width >= 40rem){flex-direction: row;}}.md\:flex-initial{@media(width >= 48rem){flex: 0 auto;}}.md\:justify-start{@media(width >= 48rem){justify-content: flex-start;}}.lg\:col-span-10{@media(width >= 64rem){grid-column: span 10 / span 10;}}.lg\:col-span-2{@media(width >= 64rem){grid-column: span 2 / span 2;}}.lg\:col-span-5{@media(width >= 64rem){grid-column: span 5 / span 5;}}.lg\:col-span-7{@media(width >= 64rem){grid-column: span 7 / span 7;}}.lg\:block{@media(width >= 64rem){display: block;}}.lg\:h-full{@media(width >= 64rem){height: 100%;}}.lg\:grid-cols-12{@media(width >= 64rem){grid-template-columns: repeat(12,minmax(0,1fr));}}.lg\:self-start{@media(width >= 64rem){align-self: flex-start;}}.lg\:pr-8{@media(width >= 64rem){padding-right: calc(var(--spacing) * 8);}}.\[\&_\.ui-form\]\:m-0{& .ui-form{margin:0}}.\[\&_input\]\:cursor-text{& input{cursor:text}}.\[\&_td\]\:p-3{& td{padding:calc(var(--spacing) * 3)}}.\[\&_td\]\:p-4{& td{padding:calc(var(--spacing) * 4)}}.\[\&_td\]\:px-2{& td{padding-inline:calc(var(--spacing) * 2)}}.\[\&_td\]\:py-0\.5{& td{padding-block:calc(var(--spacing) * .5)}}.\[\&_td\]\:py-1{& td{padding-block:var(--spacing)}}.\[\&_td\+td\]\:border-l{& td+td{border-left-style:var(--tw-border-style);border-left-width:1px}}.\[\&_td\+td\]\:border-neutral-300{& td+td{border-color:var(--color-neutral-300)}}.\[\&_th\]\:border-b{& th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}}.\[\&_th\]\:border-neutral-300{& th{border-color:var(--color-neutral-300)}}.\[\&_tr\:hover\]\:\!bg-green-100{& tr:hover{background-color:var(--color-green-100)!important}}.\[\&_tr\:hover\]\:\!bg-neutral-200{& tr:hover{background-color:var(--color-neutral-200)!important}}.\[\&_tr\:hover\]\:\!bg-sky-100{& tr:hover{background-color:var(--color-sky-100)!important}}.\[\&_tr\:not\(\:last-child\)\]\:border-b{& tr:not(:last-child){border-bottom-style:var(--tw-border-style);border-bottom-width:1px}}.\[\&_tr\:not\(\:last-child\)\]\:border-neutral-300{& tr:not(:last-child){border-color:var(--color-neutral-300)}}.\[\&_tr\:nth-child\(even\)\]\:bg-neutral-100{& tr:nth-child(even){background-color:var(--color-neutral-100)}}}@property --tw-translate-x{syntax: "*"; inherits: false; initial-value: 0; }@property --tw-translate-y{syntax: "*"; @@ -87,6 +86,36 @@ }@property --tw-ring-offset-shadow{syntax: "*"; inherits: false; initial-value: 0 0 #0000; +}@property --tw-outline-style{syntax: "*"; + inherits: false; + initial-value: solid; +}@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: ""; + inherits: false; + initial-value: 100%; +}@property --tw-drop-shadow-size{syntax: "*"; + inherits: false; }@property --tw-duration{syntax: "*"; inherits: false; }@property --tw-ease{syntax: "*"; @@ -94,7 +123,4 @@ }@property --tw-content{syntax: "*"; initial-value: ""; inherits: false; -}@property --tw-outline-style{syntax: "*"; - inherits: false; - initial-value: solid; } \ No newline at end of file diff --git a/go/cmd/examples/go-wasm-web/wwwroot/fonts/lora-latin-ext-italic.woff2 b/go/cmd/examples/go-wasm-web/wwwroot/fonts/lora-latin-ext-italic.woff2 new file mode 100644 index 00000000..61ff30a0 Binary files /dev/null and b/go/cmd/examples/go-wasm-web/wwwroot/fonts/lora-latin-ext-italic.woff2 differ diff --git a/go/cmd/examples/go-wasm-web/wwwroot/fonts/lora-latin-ext-normal.woff2 b/go/cmd/examples/go-wasm-web/wwwroot/fonts/lora-latin-ext-normal.woff2 new file mode 100644 index 00000000..b8d5a4fb Binary files /dev/null and b/go/cmd/examples/go-wasm-web/wwwroot/fonts/lora-latin-ext-normal.woff2 differ diff --git a/go/cmd/examples/go-wasm-web/wwwroot/fonts/lora-latin-italic.woff2 b/go/cmd/examples/go-wasm-web/wwwroot/fonts/lora-latin-italic.woff2 new file mode 100644 index 00000000..856707df Binary files /dev/null and b/go/cmd/examples/go-wasm-web/wwwroot/fonts/lora-latin-italic.woff2 differ diff --git a/go/cmd/examples/go-wasm-web/wwwroot/fonts/lora-latin-normal.woff2 b/go/cmd/examples/go-wasm-web/wwwroot/fonts/lora-latin-normal.woff2 new file mode 100644 index 00000000..918a4772 Binary files /dev/null and b/go/cmd/examples/go-wasm-web/wwwroot/fonts/lora-latin-normal.woff2 differ diff --git a/go/rsc/server.go b/go/rsc/server.go index ea92f325..b91b5037 100644 --- a/go/rsc/server.go +++ b/go/rsc/server.go @@ -89,7 +89,19 @@ func renderIDs(root *vdom.VNode) (*SNode, map[int]map[string]func(vdom.Event)) { return walk(root), handlers } +// serverEvent is the vdom.Event a server component sees when the client replays a +// handler invocation. Only the target's value survives the round trip (it is all +// the client sends); everything else — key, pointer coordinates, the DOM target, +// the DataTransfer — is meaningless on the server and reads as its zero value. type serverEvent struct{ value string } -func (e serverEvent) PreventDefault() {} -func (e serverEvent) Value() string { return e.value } +func (e serverEvent) PreventDefault() {} +func (e serverEvent) StopPropagation() {} +func (e serverEvent) Value() string { return e.value } +func (e serverEvent) Checked() bool { return false } +func (e serverEvent) Key() string { return "" } +func (e serverEvent) ClientX() int { return 0 } +func (e serverEvent) ClientY() int { return 0 } +func (e serverEvent) Target() any { return nil } +func (e serverEvent) SetData(_, _ string) {} +func (e serverEvent) GetData(string) string { return "" } diff --git a/go/vdom/events.go b/go/vdom/events.go index 8cf79818..4e5aeaa9 100644 --- a/go/vdom/events.go +++ b/go/vdom/events.go @@ -2,16 +2,55 @@ package vdom // DOM event-name constants for On / OnEvent. const ( - EVENT_CLICK = "click" - EVENT_DBLCLICK = "dblclick" - EVENT_INPUT = "input" - EVENT_CHANGE = "change" - EVENT_SUBMIT = "submit" - EVENT_KEYDOWN = "keydown" - EVENT_KEYUP = "keyup" - EVENT_FOCUS = "focus" - EVENT_BLUR = "blur" - EVENT_MOUSEDOWN = "mousedown" - EVENT_MOUSEUP = "mouseup" - EVENT_MOUSEMOVE = "mousemove" + EVENT_CLICK = "click" + EVENT_DBLCLICK = "dblclick" + EVENT_INPUT = "input" + EVENT_CHANGE = "change" + EVENT_SUBMIT = "submit" + EVENT_KEYDOWN = "keydown" + EVENT_KEYUP = "keyup" + EVENT_FOCUS = "focus" + EVENT_BLUR = "blur" + EVENT_MOUSEDOWN = "mousedown" + EVENT_MOUSEUP = "mouseup" + EVENT_MOUSEMOVE = "mousemove" + EVENT_MOUSEENTER = "mouseenter" + EVENT_MOUSELEAVE = "mouseleave" + EVENT_CONTEXTMENU = "contextmenu" + EVENT_SCROLL = "scroll" + EVENT_RESIZE = "resize" + EVENT_WHEEL = "wheel" + + // focusin/focusout BUBBLE; focus/blur do not. Anything that needs to know a + // subtree gained focus (a tooltip staying open while a child is focused) must + // use these. + EVENT_FOCUSIN = "focusin" + EVENT_FOCUSOUT = "focusout" + + // HTML5 drag-and-drop. The handler's Event carries the DataTransfer via + // SetData / GetData. + EVENT_DRAGSTART = "dragstart" + EVENT_DRAGOVER = "dragover" + EVENT_DRAGENTER = "dragenter" + EVENT_DRAGLEAVE = "dragleave" + EVENT_DROP = "drop" + EVENT_DRAGEND = "dragend" +) + +// KeyboardEvent.key values, for Event.Key(). +const ( + KEY_ESCAPE = "Escape" + KEY_ENTER = "Enter" + KEY_SPACE = " " + KEY_TAB = "Tab" + KEY_BACKSPACE = "Backspace" + KEY_DELETE = "Delete" + KEY_ARROW_UP = "ArrowUp" + KEY_ARROW_DOWN = "ArrowDown" + KEY_ARROW_LEFT = "ArrowLeft" + KEY_ARROW_RIGHT = "ArrowRight" + KEY_HOME = "Home" + KEY_END = "End" + KEY_PAGE_UP = "PageUp" + KEY_PAGE_DOWN = "PageDown" ) diff --git a/go/vdom/ref.go b/go/vdom/ref.go new file mode 100644 index 00000000..51c81bf5 --- /dev/null +++ b/go/vdom/ref.go @@ -0,0 +1,55 @@ +package vdom + +// Ref is a handle on the real DOM node a VNode rendered to. It is the bridge the +// neutral component code uses to ask the browser for things it cannot know on its +// own — how big an element is, where it sits in the viewport — without importing +// anything platform-specific: the value inside is opaque (`any`), filled in by the +// wasm reconciler and always nil on the server. Read it through the wasmruntime +// host API (wasmruntime.Rect, SetStyle, Focus, …), which no-ops when it is nil. +// +// A component creates one ref per element it needs to measure and keeps it across +// renders (in a closure, next to its signals — NOT rebuilt inside the render +// function, or it would be a fresh empty ref every frame): +// +// panel := vdom.NewRef() +// return func() *VNode { +// return Div(WithRef(panel), Attr("class", "…"), …) +// } +// +// and later, after the DOM exists (see wasmruntime.AfterRender): +// +// r := wasmruntime.Rect(panel) // zero Rect if unmounted or on the server +type Ref struct{ node any } + +// NewRef returns an unattached Ref. +func NewRef() *Ref { return &Ref{} } + +// Node is the platform DOM handle (a js.Value in the browser), or nil if the ref +// is not currently attached to a mounted element. Component code should not need +// this — it is for the host API and the reconciler. +func (r *Ref) Node() any { + if r == nil { + return nil + } + return r.node +} + +// Mounted reports whether the ref currently points at a live DOM node. +func (r *Ref) Mounted() bool { return r != nil && r.node != nil } + +type refMod struct{ ref *Ref } + +func (m refMod) apply(n *VNode) { n.Ref = m.ref } + +// WithRef attaches a Ref to this element, so the DOM node it renders to can be +// measured and manipulated. Ignored on the server. +func WithRef(r *Ref) Mod { return refMod{r} } + +// SetRefNode points a Ref at a platform DOM handle (or nil to detach). It exists +// for the reconciler and the host API, which live in another package; component +// code has no reason to call it. +func SetRefNode(r *Ref, node any) { + if r != nil { + r.node = node + } +} diff --git a/go/vdom/tags.go b/go/vdom/tags.go index 7be47b2d..0f40727a 100644 --- a/go/vdom/tags.go +++ b/go/vdom/tags.go @@ -1,30 +1,81 @@ package vdom -// HTML tag helpers over El, for dot-import. +// HTML tag helpers over El. +// +// The set covers every tag kjol/webui actually renders, which is the bar that +// matters: a partial set is worse than none, because it forces a half-and-half style +// where `Div(...)` sits next to `El("thead", ...)` and the reader has to know which +// tags happen to be covered. If you reach for a tag that is not here, add it rather +// than falling back to El. +// +// El itself stays exported for the genuinely dynamic case — a tag chosen at runtime, +// as a component's `Tag` prop does. + +// --- structure --- func Div(m ...Mod) *VNode { return El("div", m...) } func Span(m ...Mod) *VNode { return El("span", m...) } func P(m ...Mod) *VNode { return El("p", m...) } func Section(m ...Mod) *VNode { return El("section", m...) } +func Article(m ...Mod) *VNode { return El("article", m...) } +func Aside(m ...Mod) *VNode { return El("aside", m...) } func Nav(m ...Mod) *VNode { return El("nav", m...) } func Header(m ...Mod) *VNode { return El("header", m...) } func Main(m ...Mod) *VNode { return El("main", m...) } func Footer(m ...Mod) *VNode { return El("footer", m...) } -func H1(m ...Mod) *VNode { return El("h1", m...) } -func H2(m ...Mod) *VNode { return El("h2", m...) } -func H3(m ...Mod) *VNode { return El("h3", m...) } func Hr(m ...Mod) *VNode { return El("hr", m...) } -func A(m ...Mod) *VNode { return El("a", m...) } -func Strong(m ...Mod) *VNode { return El("strong", m...) } -func Em(m ...Mod) *VNode { return El("em", m...) } -func Small(m ...Mod) *VNode { return El("small", m...) } -func Code(m ...Mod) *VNode { return El("code", m...) } -func Label(m ...Mod) *VNode { return El("label", m...) } -func Ul(m ...Mod) *VNode { return El("ul", m...) } -func Li(m ...Mod) *VNode { return El("li", m...) } -func Form(m ...Mod) *VNode { return El("form", m...) } -func Input(m ...Mod) *VNode { return El("input", m...) } -func Button(m ...Mod) *VNode { return El("button", m...) } +func Br(m ...Mod) *VNode { return El("br", m...) } + +// --- headings --- +func H1(m ...Mod) *VNode { return El("h1", m...) } +func H2(m ...Mod) *VNode { return El("h2", m...) } +func H3(m ...Mod) *VNode { return El("h3", m...) } +func H4(m ...Mod) *VNode { return El("h4", m...) } +func H5(m ...Mod) *VNode { return El("h5", m...) } +func H6(m ...Mod) *VNode { return El("h6", m...) } + +// --- inline --- +func A(m ...Mod) *VNode { return El("a", m...) } +func Strong(m ...Mod) *VNode { return El("strong", m...) } +func B(m ...Mod) *VNode { return El("b", m...) } +func Em(m ...Mod) *VNode { return El("em", m...) } +func I(m ...Mod) *VNode { return El("i", m...) } +func Small(m ...Mod) *VNode { return El("small", m...) } +func Code(m ...Mod) *VNode { return El("code", m...) } +func Pre(m ...Mod) *VNode { return El("pre", m...) } + +// --- lists --- +func Ul(m ...Mod) *VNode { return El("ul", m...) } +func Ol(m ...Mod) *VNode { return El("ol", m...) } +func Li(m ...Mod) *VNode { return El("li", m...) } + +// --- forms --- +func Form(m ...Mod) *VNode { return El("form", m...) } +func Fieldset(m ...Mod) *VNode { return El("fieldset", m...) } +func Legend(m ...Mod) *VNode { return El("legend", m...) } +func Label(m ...Mod) *VNode { return El("label", m...) } +func Input(m ...Mod) *VNode { return El("input", m...) } +func Textarea(m ...Mod) *VNode { return El("textarea", m...) } +func Select(m ...Mod) *VNode { return El("select", m...) } +func Option(m ...Mod) *VNode { return El("option", m...) } +func Button(m ...Mod) *VNode { return El("button", m...) } + +// --- tables --- func Table(m ...Mod) *VNode { return El("table", m...) } +func Thead(m ...Mod) *VNode { return El("thead", m...) } +func Tbody(m ...Mod) *VNode { return El("tbody", m...) } +func Tfoot(m ...Mod) *VNode { return El("tfoot", m...) } func Tr(m ...Mod) *VNode { return El("tr", m...) } +func Th(m ...Mod) *VNode { return El("th", m...) } func Td(m ...Mod) *VNode { return El("td", m...) } -func Img(m ...Mod) *VNode { return El("img", m...) } +func Caption(m ...Mod) *VNode { return El("caption", m...) } + +// --- media and misc --- +func Img(m ...Mod) *VNode { return El("img", m...) } +func Canvas(m ...Mod) *VNode { return El("canvas", m...) } +func Dialog(m ...Mod) *VNode { return El("dialog", m...) } + +// Svg and Path are the two SVG tags the icon layer needs. The reconciler creates +// them in the SVG namespace (see wasmruntime): an built as ordinary HTML is an +// inert unknown element that renders nothing. +func Svg(m ...Mod) *VNode { return El("svg", m...) } +func Path(m ...Mod) *VNode { return El("path", m...) } diff --git a/go/vdom/vnode.go b/go/vdom/vnode.go index f9226d0b..c891a49b 100644 --- a/go/vdom/vnode.go +++ b/go/vdom/vnode.go @@ -11,14 +11,24 @@ import ( ) // Event is a DOM event passed to handlers. The client provides a concrete -// implementation; on the server events are never invoked. +// implementation; on the server events are never invoked, and every accessor +// returns its zero value. type Event interface { PreventDefault() - Value() string // target.value (for inputs) + StopPropagation() + Value() string // target.value (for inputs) + Checked() bool // target.checked (for checkboxes / radios) + Key() string // KeyboardEvent.key — compare against the KEY_* constants + ClientX() int // pointer position, viewport coordinates + ClientY() int // + Target() any // the platform DOM handle of event.target; nil on the server + SetData(format, data string) // DataTransfer — no-ops off a drag event + GetData(format string) string } // VNode is a virtual DOM node. Tag == "" is a text node (content in Text). HTML, -// if set on an element, is raw innerHTML (children ignored). +// if set on an element, is raw innerHTML (children ignored). Tag == TagPortal +// renders its children into document.body instead of in place. type VNode struct { Tag string Text string @@ -28,6 +38,9 @@ type VNode struct { Events map[string]func(Event) Children []*VNode + // Ref, if set, receives the DOM node this VNode renders to (see WithRef). + Ref *Ref + // Runtime holds the wasm reconciler's per-node bookkeeping (DOM handle, // listener wrappers). It's `any` so this package stays platform-neutral; it // is nil on the server. @@ -37,6 +50,13 @@ type VNode struct { // Mod configures a VNode while it is built. type Mod interface{ apply(*VNode) } +// TagPortal marks a node whose children are mounted into document.body rather +// than into its own parent. It is how a floating panel escapes an ancestor's +// `overflow: hidden` (menus scroll, modals clip) or `transform` (which would +// re-root `position: fixed` onto the modal instead of the viewport). In the tree +// it occupies a hidden, zero-size placeholder; the children live at body level. +const TagPortal = "#portal" + // El builds an element VNode. func El(tag string, mods ...Mod) *VNode { n := &VNode{Tag: tag, Attrs: map[string]string{}, Props: map[string]string{}, Events: map[string]func(Event){}} @@ -46,6 +66,17 @@ func El(tag string, mods ...Mod) *VNode { return n } +// Portal renders its children into document.body. On the server it renders the +// placeholder only — floating content is closed during SSR, so there is nothing +// to emit, and the placeholder keeps the client's hydration walk aligned. +func Portal(children ...*VNode) *VNode { + n := El(TagPortal) + for _, c := range children { + c.apply(n) + } + return n +} + // Text builds a text VNode. func Text(s string) *VNode { return &VNode{Text: s} } @@ -106,6 +137,13 @@ func writeNode(b *strings.Builder, n *VNode) { b.WriteString(html.EscapeString(n.Text)) return } + if n.Tag == TagPortal { + // Placeholder only — the children belong to document.body, which SSR does + // not own. Emitting the same element the client creates keeps hydration's + // childNodes lined up. + b.WriteString(`
`) + return + } b.WriteByte('<') b.WriteString(n.Tag) writeAttrs(b, n.Attrs) diff --git a/go/wasmruntime/host.go b/go/wasmruntime/host.go new file mode 100644 index 00000000..eb09b125 --- /dev/null +++ b/go/wasmruntime/host.go @@ -0,0 +1,92 @@ +package wasmruntime + +import ( + "strconv" + "strings" +) + +// The host API: the channel through which the browser tells WebAssembly things Go +// cannot work out on its own — how big an element is, where the pointer is, how +// tall the viewport is — plus the imperative escape hatches (write a style, focus +// an input, download a file) that a pure re-render cannot express. +// +// Every function here exists in two builds: the real one (host_wasm.go, js+wasm) +// and a no-op that returns zero values (host_native.go). That is what lets the +// neutral kjol/webui components — which must also compile on the server for SSR — +// call them unconditionally. On the server a measurement is simply the zero Rect, +// a listener is never installed, and a timer never fires; components render their +// pre-measurement state (a panel with `visibility: hidden`), which is exactly what +// SSR should ship. +// +// Two rules for callers: +// +// 1. Measure only after the DOM exists. A ref is empty until the reconciler has +// committed; use AfterRender to run measurement code once the current render +// is on screen. +// 2. Do NOT position through signals. Signal.Set re-renders the whole tree; doing +// that on every scroll frame is pathological. Write positions with SetStyle, +// which mutates the DOM node directly and leaves the vdom alone. + +// Rect is an element's box in viewport coordinates — the result of +// getBoundingClientRect. Viewport coordinates compose directly with +// `position: fixed`, so no scroll compensation is needed (or wanted) anywhere. +type Rect struct{ X, Y, Width, Height float64 } + +func (r Rect) Top() float64 { return r.Y } +func (r Rect) Left() float64 { return r.X } +func (r Rect) Right() float64 { return r.X + r.Width } +func (r Rect) Bottom() float64 { return r.Y + r.Height } +func (r Rect) CenterX() float64 { return r.X + r.Width/2 } +func (r Rect) CenterY() float64 { return r.Y + r.Height/2 } + +// Empty reports whether the rect carries no useful geometry — an unmounted ref, or +// an element that has not been laid out yet. Positioning code must treat this as +// "cannot measure yet" rather than as a box at the origin, or the panel paints at +// 0,0 for a frame. +func (r Rect) Empty() bool { return r.Width == 0 && r.Height == 0 } + +// Size is a width/height pair — the viewport, or an element's size. +type Size struct{ Width, Height float64 } + +// Unsub removes a listener installed by OnWindow / OnDocument / ObserveResize. +// Always call it when the thing that installed it goes away; a floating panel that +// leaks a window scroll listener per open will crawl. +type Unsub func() + +// ScrollBlock values for ScrollIntoView. +const ( + ScrollBlockStart = "start" + ScrollBlockCenter = "center" + ScrollBlockEnd = "end" + ScrollBlockNearest = "nearest" +) + +// parseCSSLength resolves a CSS length to pixels. rootFontSize is the computed +// font-size of :root (itself a px string), which is what a rem is measured +// against. Unitless and unparseable values yield 0. +func parseCSSLength(value, rootFontSize string) float64 { + value = strings.TrimSpace(value) + if value == "" { + return 0 + } + switch { + case strings.HasSuffix(value, "px"): + return parseFloat(strings.TrimSuffix(value, "px")) + case strings.HasSuffix(value, "rem"): + root := parseFloat(strings.TrimSuffix(strings.TrimSpace(rootFontSize), "px")) + if root == 0 { + root = 16 // the browser default, if :root's font-size is unreadable + } + return parseFloat(strings.TrimSuffix(value, "rem")) * root + default: + return parseFloat(value) + } +} + +func parseFloat(s string) float64 { + f, err := strconv.ParseFloat(strings.TrimSpace(s), 64) + if err != nil { + return 0 + } + return f +} diff --git a/go/wasmruntime/host_native.go b/go/wasmruntime/host_native.go new file mode 100644 index 00000000..d11a58c9 --- /dev/null +++ b/go/wasmruntime/host_native.go @@ -0,0 +1,49 @@ +//go:build !(js && wasm) + +package wasmruntime + +import "kjol/vdom" + +// The server half of the host API (see host.go). Nothing here touches a browser, +// because there isn't one: measurements are zero, listeners are never installed, +// timers never fire, storage is always empty. Components call these +// unconditionally and render their unmeasured state, which is what SSR ships. + +func Measure(*vdom.Ref) Rect { return Rect{} } +func Viewport() Size { return Size{} } +func CSSVarPx(string) float64 { return 0 } + +func RAF(func()) int { return 0 } +func CancelRAF(int) {} +func AfterRender(func()) {} + +func SetStyle(*vdom.Ref, string, string) {} +func RemoveStyle(*vdom.Ref, string) {} +func SetText(*vdom.Ref, string) {} +func SetHTML(*vdom.Ref, string) {} + +func Focus(*vdom.Ref) {} +func Blur(*vdom.Ref) {} +func SelectionRange(*vdom.Ref) (int, int) { return 0, 0 } +func SetSelectionRange(*vdom.Ref, int, int) {} +func ScrollIntoView(*vdom.Ref, bool, string) {} +func ScrollLeft(*vdom.Ref) float64 { return 0 } +func SetScrollLeft(*vdom.Ref, float64) {} + +func Contains(*vdom.Ref, any) bool { return false } +func ClosestAttr(any, string, string) (string, bool) { return "", false } +func QuerySelector(string) *vdom.Ref { return vdom.NewRef() } + +func OnWindow(string, bool, func(vdom.Event)) Unsub { return func() {} } +func OnDocument(string, bool, func(vdom.Event)) Unsub { return func() {} } +func ObserveResize(*vdom.Ref, func()) Unsub { return func() {} } + +func SetTimeout(int, func()) int { return 0 } +func ClearTimeout(int) {} + +func StorageGet(string) (string, bool) { return "", false } +func StorageSet(string, string) {} +func StorageRemove(string) {} + +func Download(string, string, []byte) {} +func Print(string, []byte) {} diff --git a/go/wasmruntime/host_test.go b/go/wasmruntime/host_test.go new file mode 100644 index 00000000..111ea611 --- /dev/null +++ b/go/wasmruntime/host_test.go @@ -0,0 +1,214 @@ +//go:build js && wasm + +package wasmruntime + +import ( + "strings" + "syscall/js" + "testing" + + "kjol/vdom" +) + +// Run from kjol/go: +// +// GOOS=js GOARCH=wasm go test -exec="node testdata/domexec.js" ./wasmruntime + +// A Ref must be attached when the element is created, survive a diff that reuses +// the element, and be nil again once the element is gone — otherwise measurement +// code silently reads a stale node. +func TestRefLifecycle(t *testing.T) { + root := document.Call("createElement", "div") + panel := vdom.NewRef() + + if panel.Mounted() { + t.Fatal("a fresh ref should not be mounted") + } + + a := vdom.El("div", vdom.WithRef(panel), vdom.Attr("class", "one")) + patchChildren(root, nil, one(a)) + if !panel.Mounted() { + t.Fatal("ref was not attached on create") + } + created, _ := panel.Node().(js.Value) + + // A re-render builds a fresh VNode carrying the same *Ref; the diff adopts the + // existing element, so the ref must still point at it. + b := vdom.El("div", vdom.WithRef(panel), vdom.Attr("class", "two")) + patchChildren(root, one(a), one(b)) + if !panel.Mounted() { + t.Fatal("ref was detached by a re-render that reused the element") + } + if adopted, _ := panel.Node().(js.Value); !adopted.Equal(created) { + t.Error("ref points at a different node after a reusing diff") + } + + // Removing the element must clear the ref. + patchChildren(root, one(b), nil) + if panel.Mounted() { + t.Error("ref still points at a removed element") + } +} + +// A tag change replaces the element. createDOM runs before release (replaceChild +// needs both nodes), so a naive detach-on-release would nil the ref that was just +// pointed at the NEW element. +func TestRefSurvivesTagChange(t *testing.T) { + root := document.Call("createElement", "div") + r := vdom.NewRef() + + a := vdom.El("div", vdom.WithRef(r)) + patchChildren(root, nil, one(a)) + + b := vdom.El("span", vdom.WithRef(r)) + patchChildren(root, one(a), one(b)) + + if !r.Mounted() { + t.Fatal("ref was cleared when the element changed tag") + } + if got := Measure(r); got != (Rect{}) { // shim reports a zero rect by default + _ = got + } + if !strings.Contains(root.Get("innerHTML").String(), "/