diff --git a/docs/sending-and-step-up.md b/docs/sending-and-step-up.md index e2f8096ff..88d0849ab 100644 --- a/docs/sending-and-step-up.md +++ b/docs/sending-and-step-up.md @@ -131,8 +131,16 @@ dispatched agents and the chief panel have no Queue. status without its time. - 🔴 **A usage limit holds the queue.** See [A usage limit holds the queue](#a-usage-limit-holds-the-queue) below. -- **At most 20** messages can wait per window. Finished ones are shown for a - while and deleted after 7 days, or sooner with their **Delete** (below). +- **At most 20** messages can wait per window. The list shows only what still + needs something: messages queued or being sent, and your own failed messages + that you have not retried or deleted. A sent or cancelled message leaves the + list at once. A failed message of your own stays until you **Retry** or + **Delete** it, or until it is deleted 7 days after it failed (every finished + message is); only the 20 newest are listed. A failed message your chief + routed, or a chief nudge that failed, is not listed: its outcome goes back to + the chief, which handles it. + A tap on a message in the list shows its whole text (the list shows the first + two lines). - **Never twice.** A message is claimed (`queued` → `sending`) before it is sent and never put back once anything may have reached the window. (The one way back to `queued` is the Send-wins case above, where nothing was pasted.) The @@ -284,14 +292,15 @@ must be fresh like a queue post's. A message that may already have been sent asks you to confirm first. A message your chief routed has no Retry: its failed route goes back to the chief instead. -A finished message (sent, failed, expired or cancelled) has a **Delete**, which -removes it from the list on every device at once (`POST /send-queue/{id}/delete`). -A message still queued has no Delete (Cancel it first), and neither has one -being sent: the send in progress still holds it. Deleting a sent message does -not let the next one in early: the next message still waits for the turn the -deleted one started. Deleting a failed message your chief routed is fine: its -route is not affected, and if the route times out, the message still goes back -to the chief. +A failed message (expired included) has a **Delete**, which removes it from the +list on every device at once (`POST /send-queue/{id}/delete`). The route deletes +any finished message (sent, failed or cancelled), though the list draws only +failed ones. A message still queued has no Delete (Cancel it first), and +neither has one being sent: the send in progress still holds it. Deleting a +sent message does not let the next one in early: the next message still waits +for the turn the deleted one started. A failed message your chief routed is not +listed, but deleting one through the route is fine: its route is not affected, +and if the route times out, the message still goes back to the chief. 🔴 **A queued message does not outlive the account that queued it.** When a message is sent, the server checks the account again, from the database: if it diff --git a/e2e/tests/attention.spec.ts b/e2e/tests/attention.spec.ts index 019447d77..9615bd877 100644 --- a/e2e/tests/attention.spec.ts +++ b/e2e/tests/attention.spec.ts @@ -378,7 +378,7 @@ test('a queued message answers the ask, and its Cancel says the answer stays', a await row.getByRole('button', { name: 'More actions for this queued message' }).click(); await row.getByRole('menuitem', { name: 'Cancel this queued message' }).click(); await expect(page.locator('#mv-note')).toHaveText(/^Cancelled — but this message had already answered .+'s question, and that answer was delivered: it can't be un-sent\.$/); - await expect(row).toHaveAttribute('data-queue-state', 'cancelled'); + await expect(row, 'cancelled: it leaves the list').toHaveCount(0); }); // ANSWERED ON THE ASKING SESSION'S OWN VIEW (attention_asker.go): the diff --git a/e2e/tests/send-queue.spec.ts b/e2e/tests/send-queue.spec.ts index ec034b17c..18039de57 100644 --- a/e2e/tests/send-queue.spec.ts +++ b/e2e/tests/send-queue.spec.ts @@ -214,11 +214,12 @@ test.describe('phone 390×844 touch', () => { busy(false); await expect.poll(pasted, { message: `the first message is sent once idle (log:\n${mirrorLog})`, timeout: 30_000 }).toEqual([`${pane} first queued`]); - // Another device sees the queue: the first sent, the second waiting. + // Another device sees the queue: the first sent (so no longer listed: only + // what is still pending is), the second waiting. const other = await otherDevice(browser); await openView(other.page, newHost); - await expect(row(other.page, 'first queued')).toHaveAttribute('data-queue-state', 'sent'); await expect(row(other.page, 'second queued')).toHaveAttribute('data-queue-state', 'queued'); + await expect(row(other.page, 'first queued'), 'a sent message is not listed').toHaveCount(0); // Still idle — since BEFORE the first was sent: its turn has not ended, // so the second waits. The server says so, then holds it for as long as a // wrong send would take. @@ -235,7 +236,10 @@ test.describe('phone 390×844 touch', () => { `${pane} first queued`, `${pane} second queued`, ]); - await expect(row(other.page, 'second queued'), 'the list updates live').toHaveAttribute('data-queue-state', 'sent', { timeout: 20_000 }); + // The list updates live: the sent message leaves it, and with nothing + // pending the list is empty — the old rows do not linger. + await expect(row(other.page, 'second queued'), 'the list updates live').toHaveCount(0, { timeout: 20_000 }); + await expect(rows(other.page), 'nothing pending: an empty list').toHaveCount(0); await other.ctx.close(); }); @@ -250,9 +254,9 @@ test.describe('phone 390×844 touch', () => { const r = row(other.page, 'cancel me please'); await expect(r).toHaveAttribute('data-queue-state', 'queued'); await cancelRow(r, true); - await expect(r).toHaveAttribute('data-queue-state', 'cancelled'); + await expect(r, 'a cancelled message leaves the list').toHaveCount(0); // The first device's list follows (it polls while anything is open). - await expect(row(page, 'cancel me please'), 'the first device sees the cancel').toHaveAttribute('data-queue-state', 'cancelled', { timeout: 20_000 }); + await expect(row(page, 'cancel me please'), 'the first device sees the cancel').toHaveCount(0, { timeout: 20_000 }); // Idle: had the cancel not held, the cancelled message would be the head and // go first. A message queued after it is the observable — it is sent, and @@ -309,7 +313,7 @@ test.describe('desktop 1280×800', () => { await expect.poll(pasted, { message: 'Send pastes at once, busy or not' }).toEqual([`${pane} desktop sent now`]); // Tidy: the queued one is cancelled, so no later run sees it sent. await cancelRow(row(page, 'desktop queued')); - await expect(row(page, 'desktop queued')).toHaveAttribute('data-queue-state', 'cancelled'); + await expect(row(page, 'desktop queued'), 'cancelled: no longer listed').toHaveCount(0); busy(false); }); @@ -415,10 +419,12 @@ test.describe('desktop 1280×800', () => { await busyAndSeen(); await failed.getByRole('button', { name: 'Queue this failed message again' }).click(); await expect(rowIn(page, text, 'queued'), 'Retry queues it again').toHaveCount(1, { timeout: 20_000 }); - await expect(failed.getByRole('button', { name: /Queue this/ }), 'a retried message offers no second Retry').toHaveCount(0); + // A retried failure leaves the list (its retry row carries it), so it + // offers no second Retry. + await expect(failed, 'a retried message is no longer listed as failed').toHaveCount(0); busy(false); await expect.poll(pasted, { message: `the retried message is sent (log:\n${mirrorLog})`, timeout: 30_000 }).toEqual([`${pane} ${text}`]); - await expect(rowIn(page, text, 'sent')).toHaveCount(1, { timeout: 20_000 }); + await expect(row(page, text), 'sent: it leaves the list').toHaveCount(0, { timeout: 20_000 }); }); // 🔴 A MESSAGE PAST ITS 7 DAYS EXPIRES EVEN WHILE ITS WINDOW NEVER GOES IDLE: @@ -441,11 +447,13 @@ test.describe('desktop 1280×800', () => { expect(pasted(), 'an expired message is not pasted').toEqual([]); }); - // 🔴 A ROUTED MESSAGE THAT FAILED OFFERS NO RETRY: Retry would queue it again - // as the person's own, with no route and no session binding, past the gate - // its route is. A failed route goes back to the chief instead. The test - // above is the control: a person's failed row draws the same button. - test('a routed message that failed offers no Retry', async ({ page }) => { + // 🔴 A ROUTED MESSAGE THAT FAILED IS HANDED BACK, NOT LISTED: it has no Retry + // (Retry would queue it again as the person's own, with no route and no + // session binding, past the gate its route is) — its failed route goes back + // to the chief, which is its handling, so it leaves the list rather than + // waiting there for a Delete. The test above is the control: a person's own + // failure stays, with its Retry. + test('a routed message that failed leaves the list and is not pasted', async ({ page }) => { await busyAndSeen(); fs.rmSync(tr + '.pasted', { force: true }); await openView(page, newHost); @@ -454,18 +462,18 @@ test.describe('desktop 1280×800', () => { await expect(rowIn(page, text, 'queued')).toHaveCount(1); sqfix('route', newHost, text); busy(false); - const failed = rowIn(page, text, 'failed'); - await expect(failed, `the route gate fails it (log:\n${mirrorLog})`).toContainText('its route could not be read', { timeout: 30_000 }); + await expect(row(page, text), `the route gate fails it, and it leaves the list (log:\n${mirrorLog})`).toHaveCount(0, { timeout: 30_000 }); + await holdWindow(); expect(pasted(), 'a routed message whose route is gone is not pasted').toEqual([]); - await expect(failed.getByRole('button', { name: /Queue this/ }), 'a failed routed row offers no Retry').toHaveCount(0); + await expect(row(page, text), 'and it does not come back').toHaveCount(0); await busyAndSeen(); }); - // 🔴 A FINISHED MESSAGE CAN BE DELETED — the operator's case: a failed - // message the chief routed has no Retry, and used to sit in the list until it - // was pruned. Its Delete removes the row without a page reload, and another - // device's list no longer has it. A queued message has no Delete (that is - // Cancel). On origin/main there is no Delete, so this fails at the button. + // 🔴 A FAILED MESSAGE CAN BE DELETED: a person's own failure stays in the + // list until it is retried or deleted. Its Delete removes the row without a + // page reload, and another device's list no longer has it. A queued message + // has no Delete (that is Cancel). On origin/main there is no Delete, so this + // fails at the button. test('a failed message has Delete, and Delete removes it without a reload, on every device', async ({ page, browser }) => { await busyAndSeen(); fs.rmSync(tr + '.pasted', { force: true }); @@ -475,11 +483,10 @@ test.describe('desktop 1280×800', () => { const queuedRow = rowIn(page, text, 'queued'); await expect(queuedRow).toHaveCount(1); await expect(queuedRow.getByRole('button', { name: 'Delete this message' }), 'a queued message has no Delete').toHaveCount(0); - sqfix('route', newHost, text); - busy(false); + // Expired while busy: the dispatcher's own pass fails it, nothing pasted. + sqfix('age', newHost, text, String(7 * 24 * 3600 + 60)); const failed = rowIn(page, text, 'failed'); - await expect(failed, `the route gate fails it (log:\n${mirrorLog})`).toContainText('its route could not be read', { timeout: 30_000 }); - await expect(failed.getByRole('button', { name: /Queue this/ }), 'no Retry: the operator case').toHaveCount(0); + await expect(failed, `the dispatcher expires it (log:\n${mirrorLog})`).toContainText('queued more than 7 days ago', { timeout: 30_000 }); const other = await otherDevice(browser); await openView(other.page, newHost); @@ -515,10 +522,10 @@ test.describe('desktop 1280×800', () => { const text = 'deleted mid-read ' + stamp; await queue(page, text, false); await expect(rowIn(page, text, 'queued')).toHaveCount(1); - sqfix('route', newHost, text); - busy(false); + // Expired while busy: the dispatcher's own pass fails it. + sqfix('age', newHost, text, String(7 * 24 * 3600 + 60)); const failed = rowIn(page, text, 'failed'); - await expect(failed, `the route gate fails it (log:\n${mirrorLog})`).toContainText('its route could not be read', { timeout: 30_000 }); + await expect(failed, `the dispatcher expires it (log:\n${mirrorLog})`).toContainText('queued more than 7 days ago', { timeout: 30_000 }); // Hold the NEXT list read: read the real list now (the row in it), answer // only once released. @@ -595,8 +602,9 @@ test.describe('desktop 1280×800', () => { await expect(rowIn(page, text, 'queued'), 'an accepted Retry queues it again').toHaveCount(1, { timeout: 20_000 }); expect(posts, 'CONTROL: the accepted Retry posted once, saying it was confirmed').toEqual(['confirm=1']); // Tidy: cancel the copy, so no later run sees it sent. + await expect(rowIn(page, text, 'failed'), 'the retried failure leaves the list').toHaveCount(0); await cancelRow(rowIn(page, text, 'queued')); - await expect(rowIn(page, text, 'cancelled')).toHaveCount(1); + await expect(row(page, text), 'cancelled: it leaves the list').toHaveCount(0); busy(false); }); @@ -615,7 +623,7 @@ test.describe('desktop 1280×800', () => { await expect(row(page, 'queued without a submitter')).toHaveAttribute('data-queue-state', 'queued'); expect(pasted(), 'nothing was sent now').toEqual([]); await cancelRow(row(page, 'queued without a submitter')); - await expect(row(page, 'queued without a submitter')).toHaveAttribute('data-queue-state', 'cancelled'); + await expect(row(page, 'queued without a submitter'), 'cancelled: it leaves the list').toHaveCount(0); busy(false); }); }); @@ -639,7 +647,7 @@ test.describe('statuses', () => { // The turn ends with a background shell still running: "shell", a newer time. setStatus('shell'); await expect.poll(pasted, { message: `sent once Claude Code reports "shell" (log:\n${mirrorLog})`, timeout: 30_000 }).toEqual([`${pane} ${text}`]); - await expect(rowIn(page, text, 'sent')).toHaveCount(1, { timeout: 20_000 }); + await expect(row(page, text), 'sent: it leaves the list').toHaveCount(0, { timeout: 20_000 }); await busyAndSeen(); }); }); @@ -666,7 +674,7 @@ test.describe('queued row menu, phone 390×844 touch', () => { `${pane} ${second}`, ]); await expect(page.locator('#mv-note')).toHaveText('Sent.'); - await expect(rowIn(page, second, 'sent'), 'the row shows it sent').toHaveCount(1, { timeout: 20_000 }); + await expect(row(page, second), 'sent: it leaves the list').toHaveCount(0, { timeout: 20_000 }); await expect(rowIn(page, first, 'queued'), 'the other stays queued').toHaveCount(1); await holdWindow(); expect(pasted(), 'the other is not sent while busy').toEqual([`${pane} ${second}`]); @@ -691,10 +699,73 @@ test.describe('queued row menu, phone 390×844 touch', () => { expect(await page.evaluate(() => navigator.clipboard.readText()), 'the clipboard holds the whole message').toBe(text); await expect(moreOf(r), 'the menu closed').toHaveAttribute('aria-expanded', 'false'); await cancelRow(r, true); - await expect(rowIn(page, 'copy me ' + stamp, 'cancelled')).toHaveCount(1); + await expect(row(page, 'copy me ' + stamp), 'cancelled: it leaves the list').toHaveCount(0); expect(pasted().some((l) => l.includes('copy me ' + stamp)), 'a copy sends nothing').toBe(false); }); + // 🔴 THE WHOLE MESSAGE: a queued row's preview is cut (200 bytes) and clamped + // (two lines); a tap on it shows the whole text in a popover — the tail the + // preview cannot show included — moving nothing on the page, and the list's + // poll does not redraw it away while it is open. On origin/main the preview + // is plain text with nothing to tap, so this fails at the button. + test('a tap on a long queued message shows the whole of it, moves nothing, and stays open across the poll', async ({ page }) => { + await busyAndSeen(); + await openView(page, newHost); + const tail = 'and the tail end ' + stamp; + const text = 'whole message ' + stamp + ' ' + 'consectetur adipiscing elit '.repeat(10) + tail; + await queue(page, text, true); + const r = rowIn(page, 'whole message ' + stamp, 'queued'); + await expect(r).toBeVisible(); + await expect(r.locator('[data-queue-full-open]'), 'the preview does not show the tail').not.toContainText(tail); + // As the menu test: the row drawn and painted before shifts are counted. + await page.evaluate(() => new Promise((res) => requestAnimationFrame(() => requestAnimationFrame(() => res())))); + await page.evaluate(() => { + const w = window as unknown as { __shift: number; __shifts: string[] }; + w.__shift = 0; + w.__shifts = []; + new PerformanceObserver((l) => { + for (const e of l.getEntries() as unknown as { value: number; sources: { node?: Node | null }[] }[]) { + const nodes = (e.sources || []).map((x) => x.node).filter((n): n is Node => !!n); + const ours = nodes.some((n) => { + const el = n instanceof Element ? n : n.parentElement; + return !!el && !!el.closest('#mv-compose, [data-queue-full]'); + }); + if (ours) w.__shift += e.value; + w.__shifts.push(`${e.value.toFixed(4)} ours=${ours} ` + nodes.map((n) => (n instanceof Element ? n.id || n.tagName : n.nodeName)).join(',')); + } + }).observe({ type: 'layout-shift', buffered: false }); + }); + const before = await r.boundingBox(); + const open = r.getByRole('button', { name: /^Show the whole message/ }); + const ob = await open.boundingBox(); + expect(ob && ob.height >= 44, `the preview is a 44px target: ${JSON.stringify(ob)}`).toBe(true); + await open.tap(); + const full = r.getByRole('dialog', { name: 'The whole message' }); + await expect(full).toBeVisible(); + await expect(full, 'the whole text, its tail included').toContainText(tail); + const fb = await full.boundingBox(); + const vw = page.viewportSize()!.width; + expect(fb && fb.x >= 0 && fb.x + fb.width <= vw, `the popover is on screen: ${JSON.stringify(fb)}`).toBe(true); + // Open across more than one poll (3 s while anything is queued): not + // redrawn away. + await page.waitForTimeout(4_000); + await expect(full, 'still open after a poll').toBeVisible(); + await full.getByRole('button', { name: 'Close' }).tap(); + await expect(full).toBeHidden(); + expect(await r.boundingBox(), 'the row did not move').toEqual(before); + // Enter on the focused preview opens it too; Escape closes it. + await open.focus(); + await page.keyboard.press('Enter'); + await expect(full).toBeVisible(); + await page.keyboard.press('Escape'); + await expect(full).toBeHidden(); + await page.waitForTimeout(300); + expect(await page.evaluate(() => (window as unknown as { __shift: number }).__shift), 'no layout shift opening or closing it: ' + + JSON.stringify(await page.evaluate(() => (window as unknown as { __shifts: string[] }).__shifts))).toBeLessThan(0.001); + await cancelRow(r, true); + await expect(row(page, 'whole message ' + stamp), 'cancelled: it leaves the list').toHaveCount(0); + }); + test('the menu fits the phone, moves nothing, and closes on a tap outside', async ({ page }) => { await busyAndSeen(); await openView(page, newHost); @@ -755,7 +826,7 @@ test.describe('queued row menu, phone 390×844 touch', () => { expect(await page.evaluate(() => (window as unknown as { __shift: number }).__shift), 'no layout shift opening or closing it: ' + JSON.stringify(await page.evaluate(() => (window as unknown as { __shifts: string[] }).__shifts))).toBeLessThan(0.001); await cancelRow(r, true); - await expect(rowIn(page, text, 'cancelled')).toHaveCount(1); + await expect(row(page, text), 'cancelled: it leaves the list').toHaveCount(0); }); }); @@ -793,7 +864,7 @@ test.describe('queued row menu, desktop 1280×800 keyboard', () => { await page.keyboard.press('End'); await expect(itemOf(r, 'Cancel this queued message')).toBeFocused(); await page.keyboard.press('Enter'); - await expect(rowIn(page, text, 'cancelled')).toHaveCount(1, { timeout: 20_000 }); + await expect(row(page, text), 'cancelled: it leaves the list').toHaveCount(0, { timeout: 20_000 }); expect(pasted().some((l) => l.includes(text)), 'a cancelled message is not sent').toBe(false); }); @@ -827,7 +898,7 @@ test.describe('queued row menu, desktop 1280×800 keyboard', () => { await moreOf(c).click(); await itemOf(c, 'Cancel this queued message').click(); await expect(page.locator('#mv-note')).toContainText('Cancelled'); - await expect(rowIn(page, cancelText, 'cancelled')).toHaveCount(1); + await expect(row(page, cancelText), 'cancelled: it leaves the list').toHaveCount(0); await ctx.close(); }); }); @@ -893,7 +964,7 @@ for (const vp of [ // The second waits for the edited one's turn, then follows it. setStatus('idle'); await expect.poll(pasted, { message: 'then the one behind it', timeout: 30_000 }).toEqual([`${pane} ${edited}`, `${pane} ${second}`]); - await expect(rowIn(page, edited, 'sent'), 'the list shows the edited text sent').toHaveCount(1, { timeout: 20_000 }); + await expect(row(page, edited), 'sent: the edited message leaves the list').toHaveCount(0, { timeout: 20_000 }); await busyAndSeen(); }); @@ -1049,7 +1120,7 @@ for (const vp of [ await page.goto(origin + viewPath(newHost)); await expect(page.locator('#mv-input'), 'a queued draft is not restored').toHaveValue(''); await cancelRow(rowIn(page, queued, 'queued'), vp.touch); - await expect(rowIn(page, queued, 'cancelled')).toHaveCount(1); + await expect(row(page, queued), 'cancelled: it leaves the list').toHaveCount(0); } finally { writeFileAtomic(windowsFile, before); } @@ -1258,7 +1329,7 @@ test.describe('a usage limit, phone 390×844 touch', () => { await expect(clock).toContainText(month); expect(pasted(), 'held: nothing is sent').toEqual([]); await cancelRow(rowIn(page, text, 'queued'), true); - await expect(rowIn(page, text, 'cancelled')).toHaveCount(1); + await expect(row(page, text), 'cancelled: it leaves the list').toHaveCount(0); answered('the limit has lifted'); await busyAndSeen(); }); @@ -1376,7 +1447,7 @@ test.describe('a usage limit, phone 390×844 touch', () => { await page.setViewportSize({ width: 390, height: 844 }); // Tidy: cancel it, and leave the session answered for the specs after. await cancelRow(rowIn(page, text, 'queued'), true); - await expect(rowIn(page, text, 'cancelled')).toHaveCount(1); + await expect(row(page, text), 'cancelled: it leaves the list').toHaveCount(0); answered('the limit has lifted'); await busyAndSeen(); }); diff --git a/internal/api/send_queue.go b/internal/api/send_queue.go index feec5e5cb..b091845b3 100644 --- a/internal/api/send_queue.go +++ b/internal/api/send_queue.go @@ -390,11 +390,11 @@ func (s *Server) sendQueueListView(ctx context.Context, t sendqueue.Target, edit // A cut through a character leaves invalid bytes; they are dropped. preview = strings.ToValidUTF8(preview[:sendQueuePreview], "") + "…" } - body := "" - if it.State == sendqueue.Queued { - body = it.Body // Copy message copies all of it, not the preview. - } - row := ui.SendQueueItem{ID: it.ID, State: string(it.State), Preview: preview, Body: body, + // The whole text rides in every row: a tap on the preview shows it, and + // a queued row's Copy message copies it (the preview is cut). Every row + // listed is the caller's own (row-level security), so this is text the + // reader already wrote or was routed. + row := ui.SendQueueItem{ID: it.ID, State: string(it.State), Preview: preview, Body: it.Body, Detail: it.Detail, Cancellable: it.State == sendqueue.Queued, Answered: answered[it.ID], Retryable: sendQueueRetryable(it), Deletable: it.State.Finished(), MayHaveDelivered: it.MayHaveDelivered, Editable: it.State == sendqueue.Queued && it.RouteID == "" && it.ChiefNudgeID == 0, Held: it.State == sendqueue.Queued && it.Held, diff --git a/internal/api/send_queue_delete_pg_test.go b/internal/api/send_queue_delete_pg_test.go index fb13cf367..f97e05ae4 100644 --- a/internal/api/send_queue_delete_pg_test.go +++ b/internal/api/send_queue_delete_pg_test.go @@ -18,8 +18,8 @@ import ( // THE SEND QUEUE'S DELETE (POST /send-queue/{id}/delete), through the real // mux and a real Postgres (row-level security included): a FINISHED message — -// sent, failed (expired included), cancelled — is deleted and leaves the -// list; a queued one is refused (Cancel it), and so is one being sent (its +// sent, failed (expired included), cancelled — is deleted (the list draws a +// Delete only on a failed one: it does not draw the others at all); a queued one is refused (Cancel it), and so is one being sent (its // claim still holds the row); another user's is 404 and untouched. // // RED AT origin/main (0405925): the route is not registered, so every Delete @@ -151,13 +151,20 @@ func TestAnotherOwnersDeleteIsNotFound(t *testing.T) { } } -// TestTheListDrawsDeleteOnFinishedRowsOnly: through GET /send-queue, the -// finished rows (failed, cancelled, sent) each draw a Delete posting to their -// own route, and the queued row draws none. -func TestTheListDrawsDeleteOnFinishedRowsOnly(t *testing.T) { +// TestTheListDrawsDeleteOnAnUnhandledFailureOnly: through GET /send-queue, a +// failed row draws a Delete posting to its own route; a sent and a cancelled +// row are not drawn at all (they are done — the list used to keep them, and +// each had to be deleted by hand), nor a failed row already retried (its retry +// row is drawn in its place); and the queued rows draw no Delete. +func TestTheListDrawsDeleteOnAnUnhandledFailureOnly(t *testing.T) { f := newSQFixture(t) f.mirror.agentState = &mirror.AgentState{Status: mirror.StatusBusy, Since: time.Now().UnixMilli()} failed := f.expired(t, "a failed one") + retried := f.expired(t, "a failed one retried") + again, err := f.queue.Retry(asQueueUser(1, time.Now()), retried.ID, time.Now(), 0) + if err != nil { + t.Fatal(err) + } cancelled := f.enqueue(t, "a cancelled one", time.Hour, 0) if _, err := f.queue.Cancel(asQueueUser(1, time.Now()), cancelled.ID, time.Now()); err != nil { t.Fatal(err) @@ -165,16 +172,25 @@ func TestTheListDrawsDeleteOnFinishedRowsOnly(t *testing.T) { sent := f.sent(t, "a sent one") queued := f.enqueue(t, "a queued one", time.Hour, 0) _, page := mirrorGet(t, f.srv, f.ts, ui.SendQueuePath+"?host=ws&session=main&window=%401") - for _, id := range []int64{failed.ID, cancelled.ID, sent.ID} { - if !strings.Contains(page, `action="`+ui.SendQueueDeletePath(id)+`"`) { - t.Fatalf("finished row %d draws no Delete: %s", id, page) + if !strings.Contains(page, `action="`+ui.SendQueueDeletePath(failed.ID)+`"`) { + t.Fatalf("the failed row draws no Delete: %s", page) + } + for _, id := range []int64{retried.ID, cancelled.ID, sent.ID} { + if strings.Contains(page, `data-queue-id="`+strconv.FormatInt(id, 10)+`"`) { + t.Fatalf("handled row %d is drawn: %s", id, page) } } - if strings.Contains(page, ui.SendQueueDeletePath(queued.ID)) { - t.Fatalf("the queued row draws a Delete: %s", page) + // CONTROL: the open rows are drawn — the retry's and the queued one. + for _, id := range []int64{again.ID, queued.ID} { + if !strings.Contains(page, `data-queue-id="`+strconv.FormatInt(id, 10)+`"`) { + t.Fatalf("open row %d is not drawn: %s", id, page) + } + if strings.Contains(page, ui.SendQueueDeletePath(id)) { + t.Fatalf("open row %d draws a Delete: %s", id, page) + } } - if got := strings.Count(page, `aria-label="`+ui.SendQueueDeleteLabel+`"`); got != 3 { - t.Fatalf("%d Delete buttons; want 3 (one per finished row): %s", got, page) + if got := strings.Count(page, `aria-label="`+ui.SendQueueDeleteLabel+`"`); got != 1 { + t.Fatalf("%d Delete buttons; want 1 (the unhandled failure's): %s", got, page) } } diff --git a/internal/api/send_queue_list_test.go b/internal/api/send_queue_list_test.go index a5910d009..54610bbbe 100644 --- a/internal/api/send_queue_list_test.go +++ b/internal/api/send_queue_list_test.go @@ -79,11 +79,45 @@ func TestAFailedRowDrawsRetryOnce(t *testing.T) { } } -// TestDeleteIsDrawnOnExactlyTheFinishedStates: through the real list route, a -// row in each state the schema allows (0019's send_queue_state_check) draws a -// Delete exactly when it is finished — sent, failed, cancelled — and never -// when it is queued (that is Cancel) or being sent. -func TestDeleteIsDrawnOnExactlyTheFinishedStates(t *testing.T) { +// TestEveryListedRowCarriesItsWholeText: through the real list route, the +// server cuts each preview at sendQueuePreview bytes, and a queued, a sending +// and a failed row each carry the WHOLE text in the popover a tap on the +// preview opens (ui.SendQueueFullID) — the tail past the cut included. +func TestEveryListedRowCarriesItsWholeText(t *testing.T) { + q := &memQueue{} + s, ts := queueServer(t, queueMirror(), q, &turnGateway{}) + states := []sendqueue.State{sendqueue.Queued, sendqueue.Sending, sendqueue.Failed} + for _, st := range states { + body := strings.Repeat(string(st)+" filler ", 40) + "tail of the " + string(st) + " one" + it, err := q.Enqueue(asQueueUser(1, time.Now()), sendqueue.Item{Target: queueTarget, Body: body, StepUpAt: time.Now()}) + if err != nil { + t.Fatal(err) + } + q.mu.Lock() + q.items[it.ID-1].State = st + q.mu.Unlock() + } + _, page := mirrorGet(t, s, ts, ui.SendQueuePath+"?"+url.Values{"host": {"ws"}, "session": {"main"}, "window": {"@1"}}.Encode()) + for i, st := range states { + id := ui.SendQueueFullID(int64(i + 1)) + at := strings.Index(page, `id="`+id+`"`) + if at < 0 { + t.Fatalf("the %s row has no #%s: %s", st, id, page) + } + end := strings.Index(page[at:], "") + if end < 0 || !strings.Contains(page[at:at+end], "tail of the "+string(st)+" one") { + t.Fatalf("the %s row's popover lacks the tail: %s", st, page[at:]) + } + } +} + +// TestDeleteIsDrawnOnAFailedRowOnly:through the real list route, with a row +// in each state the schema allows (0019's send_queue_state_check), the failed +// row draws a Delete, and the queued (that is Cancel) and sending rows do not. +// The sent and cancelled rows are not drawn at all: the store does not list +// them (memQueue as PGStore.List: internal/sendqueue's +// TestListShowsOnlyWhatStillNeedsSomething pins the real one). +func TestDeleteIsDrawnOnAFailedRowOnly(t *testing.T) { q := &memQueue{} s, ts := queueServer(t, queueMirror(), q, &turnGateway{}) states := []sendqueue.State{sendqueue.Queued, sendqueue.Sending, sendqueue.Sent, sendqueue.Failed, sendqueue.Cancelled} @@ -97,12 +131,16 @@ func TestDeleteIsDrawnOnExactlyTheFinishedStates(t *testing.T) { q.mu.Unlock() } _, page := mirrorGet(t, s, ts, ui.SendQueuePath+"?"+url.Values{"host": {"ws"}, "session": {"main"}, "window": {"@1"}}.Encode()) - want := map[sendqueue.State]bool{sendqueue.Sent: true, sendqueue.Failed: true, sendqueue.Cancelled: true} + want := map[sendqueue.State]bool{sendqueue.Failed: true} for i, st := range states { drawn := strings.Contains(page, `action="`+ui.SendQueueDeletePath(int64(i+1))+`"`) if drawn != want[st] { t.Errorf("a %s row draws Delete: %v; want %v", st, drawn, want[st]) } + listed := strings.Contains(page, "row "+string(st)) + if wantListed := st.Open() || st == sendqueue.Failed; listed != wantListed { + t.Errorf("a %s row is drawn: %v; want %v", st, listed, wantListed) + } } if !strings.Contains(page, "row sending") { t.Fatalf("CONTROL: the list drew no rows: %s", page) diff --git a/internal/api/send_queue_now_pg_test.go b/internal/api/send_queue_now_pg_test.go index 76f7d396f..4ce89a009 100644 --- a/internal/api/send_queue_now_pg_test.go +++ b/internal/api/send_queue_now_pg_test.go @@ -234,14 +234,16 @@ func TestSendNowIsRefusedWhereASendIsAndWhereTheDispatchersDeliveryIs(t *testing // TestTheListDrawsSendNowAndCopyOnQueuedRowsOnly: a queued row's ⋯ menu holds // Send now (a real form posting to its route), Copy message (carrying the -// whole message, not the preview) and Cancel; a finished row has no menu and +// whole message, not the preview) and Cancel; a failed row has no menu and // keeps its own buttons. func TestTheListDrawsSendNowAndCopyOnQueuedRowsOnly(t *testing.T) { f := newSQFixture(t) f.mirror.agentState = &mirror.AgentState{Status: mirror.StatusBusy, Since: time.Now().UnixMilli()} + // First: expired() runs an idle dispatcher pass, which would send a row + // queued before it. + failed := f.expired(t, "already failed") long := strings.Repeat("a long queued message ", 20) + "THE END" q := f.enqueue(t, long, time.Minute, 0) - sent := f.sent(t, "already sent") _, page := mirrorGet(t, f.srv, f.ts, ui.SendQueuePath+"?"+url.Values{"host": {"ws"}, "session": {"main"}, "window": {"@1"}}.Encode()) rowOf := func(id int64) string { i := strings.Index(page, `data-queue-id="`+strconv.FormatInt(id, 10)+`"`) @@ -251,7 +253,7 @@ func TestTheListDrawsSendNowAndCopyOnQueuedRowsOnly(t *testing.T) { j := strings.Index(page[i:], "") return page[i : i+j] } - qr, sr := rowOf(q.ID), rowOf(sent.ID) + qr, sr := rowOf(q.ID), rowOf(failed.ID) for _, want := range []string{`aria-haspopup="menu"`, `action="` + sendNowPath(q.ID) + `"`, `action="` + ui.SendQueueCancelPath(q.ID) + `"`, `data-queue-copy`, "THE END"} { if !strings.Contains(qr, want) { @@ -260,10 +262,10 @@ func TestTheListDrawsSendNowAndCopyOnQueuedRowsOnly(t *testing.T) { } for _, not := range []string{`aria-haspopup="menu"`, "send-now", "data-queue-copy"} { if strings.Contains(sr, not) { - t.Errorf("the sent row has %q: %s", not, sr) + t.Errorf("the failed row has %q: %s", not, sr) } } - if !strings.Contains(sr, `action="`+ui.SendQueueDeletePath(sent.ID)+`"`) { - t.Errorf("CONTROL: the sent row lost its Delete: %s", sr) + if !strings.Contains(sr, `action="`+ui.SendQueueDeletePath(failed.ID)+`"`) { + t.Errorf("CONTROL: the failed row lost its Delete: %s", sr) } } diff --git a/internal/api/send_queue_test.go b/internal/api/send_queue_test.go index 020497122..00e615fc7 100644 --- a/internal/api/send_queue_test.go +++ b/internal/api/send_queue_test.go @@ -63,6 +63,12 @@ func (m *memQueue) List(ctx context.Context, t sendqueue.Target) ([]sendqueue.It defer m.mu.Unlock() var out []sendqueue.Item for _, it := range m.items { + // What PGStore.List lists: the open rows and the person's own + // unretried failures — no routed message, no chief nudge (order aside: + // these are in queue order). + if !it.State.Open() && (it.State != sendqueue.Failed || it.RetriedAs != 0 || it.RouteID != "" || it.ChiefNudgeID != 0) { + continue + } if it.Owner == queueUser(ctx) && it.Target == t { out = append(out, it) } diff --git a/internal/api/session_routes_pg_test.go b/internal/api/session_routes_pg_test.go index 0731f20c2..460b7a294 100644 --- a/internal/api/session_routes_pg_test.go +++ b/internal/api/session_routes_pg_test.go @@ -158,10 +158,34 @@ func routeArgs(m plugins.Message, sub string) string { // queued is the owner's send-queue rows for the routed window. func (f *routeFixture) queued(t *testing.T) []sendqueue.Item { t.Helper() - items, err := f.queue.List(f.asOwner, sendqueue.Target{Host: "tower", Session: "work", Window: "@4"}) + // Every row the owner has for the window, oldest first, read as the owner + // (row-level security applies) — NOT List, which leaves out what is done + // (sent, cancelled, retried), which is what these tests assert on. + rows, err := f.pool.Query(f.asOwner, `SELECT id FROM send_queue + WHERE mirror_host = 'tower' AND mirror_session = 'work' AND mirror_window = '@4' ORDER BY id`) if err != nil { t.Fatal(err) } + var ids []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + t.Fatal(err) + } + ids = append(ids, id) + } + rows.Close() + if err := rows.Err(); err != nil { + t.Fatal(err) + } + var items []sendqueue.Item + for _, id := range ids { + it, err := f.queue.Get(f.asOwner, id) + if err != nil { + t.Fatal(err) + } + items = append(items, it) + } return items } diff --git a/internal/sendqueue/store.go b/internal/sendqueue/store.go index bb60c62a9..9de320af3 100644 --- a/internal/sendqueue/store.go +++ b/internal/sendqueue/store.go @@ -199,9 +199,16 @@ const MaxOpenPerTarget = 20 // MaxDetail bounds an outcome's detail (0019's send_queue_detail_cap). const MaxDetail = 1024 -// HistoryShown is how many finished messages List returns beside the open -// ones: the recent outcomes a composer shows under its queue. -const HistoryShown = 5 +// FailedShown bounds the failed messages List returns beside the open ones: +// the FailedShown most recent of a person's own failures not yet retried, +// newest first. A sent or cancelled message is never listed — it has been +// handled — nor a routed message or a chief nudge that failed (its outcome +// goes back to the chief, which is its handling). A person's own failure is +// listed until they Retry or Delete it, or Prune deletes it Retention after it +// failed, and only the newest FailedShown of them; an older one beyond that is +// not shown. The bound is the open bound's size (TestFailedShownCoversAWholeQueue), +// so a whole queue that failed is shown whole. +const FailedShown = MaxOpenPerTarget var ( // ErrFull refuses a message past MaxOpenPerTarget. @@ -403,8 +410,12 @@ func (s *PGStore) Retry(ctx context.Context, id int64, stepUpAt time.Time, after return out, nil } -// List is the caller's messages for t: every open one, oldest first, then -// the HistoryShown most recently finished, newest first. +// List is the caller's messages for t that still need something: every open +// one (queued or being sent), oldest first, then the FailedShown most recently +// failed of the person's own (no route, no chief nudge) that were not +// retried, newest first. A sent or cancelled message is not listed, nor a +// failed one already retried (its retry row, a new message of its own, carries +// it), nor a failed routed message or chief nudge (handed back to the chief). func (s *PGStore) List(ctx context.Context, t Target) ([]Item, error) { if err := t.Valid(); err != nil { return nil, err @@ -412,8 +423,9 @@ func (s *PGStore) List(ctx context.Context, t Target) ([]Item, error) { where, args := targetWhere(t, 1) rows, err := s.pool.Query(ctx, `(SELECT `+cols+`, 0 AS part FROM send_queue WHERE state IN ('queued', 'sending') AND `+where+`) UNION ALL - (SELECT `+cols+`, 1 AS part FROM send_queue WHERE state NOT IN ('queued', 'sending') AND `+where+` - ORDER BY finished_at DESC, id DESC LIMIT `+fmt.Sprint(HistoryShown)+`) + (SELECT `+cols+`, 1 AS part FROM send_queue WHERE state = 'failed' AND retried_as IS NULL + AND route_id IS NULL AND chief_nudge_id IS NULL AND `+where+` + ORDER BY finished_at DESC, id DESC LIMIT `+fmt.Sprint(FailedShown)+`) ORDER BY part, id`, args...) if err != nil { return nil, fmt.Errorf("sendqueue: list: %w", err) @@ -872,8 +884,8 @@ func (s *PGStore) Reject(ctx context.Context, id int64, detail string) (bool, er return tag.RowsAffected() == 1, nil } -// Retention is how long a finished message is kept (its outcome is shown -// under the composer) before Prune deletes it. +// Retention is how long a finished message is kept (a person's own failure is +// listed under the composer, List) before Prune deletes it. const Retention = 7 * 24 * time.Hour // Prune deletes every message that finished before before. diff --git a/internal/sendqueue/store_list_pg_test.go b/internal/sendqueue/store_list_pg_test.go new file mode 100644 index 000000000..e6b3d08ba --- /dev/null +++ b/internal/sendqueue/store_list_pg_test.go @@ -0,0 +1,155 @@ +package sendqueue + +import ( + "context" + "fmt" + "strconv" + "strings" + "testing" + "time" +) + +// listed is items as "state:body#id", in List's order. +func listed(items []Item) string { + out := make([]string, 0, len(items)) + for _, it := range items { + out = append(out, string(it.State)+":"+it.Body+"#"+strconv.FormatInt(it.ID, 10)) + } + return strings.Join(out, " ") +} + +// TestListShowsOnlyWhatStillNeedsSomething: List draws what is queued or being +// sent, and a failure nobody has handled yet — never a sent or a cancelled +// message (they are done; the operator had to Delete each by hand), nor a +// failure already retried (its retry row carries it). Every row's body is its +// own, so a row in the wrong part, or a missing one, names itself. +func TestListShowsOnlyWhatStillNeedsSomething(t *testing.T) { + s, _ := pgStore(t) + ctx := context.Background() + + sent := mustEnqueue(t, s, 2, mirrorItem("alpha sent")) + if _, ok, err := s.Claim(ctx, sent.ID, time.Now(), claimFor("t", time.Now())); err != nil || !ok { + t.Fatal(ok, err) + } + if err := s.Finish(ctx, sent.ID, "t", Outcome{Sent: true, Detail: "Sent."}, time.Now()); err != nil { + t.Fatal(err) + } + cancelled := mustEnqueue(t, s, 2, mirrorItem("bravo cancelled")) + if _, err := s.Cancel(as(2), cancelled.ID, time.Now()); err != nil { + t.Fatal(err) + } + retried := mustEnqueue(t, s, 2, mirrorItem("charlie failed then retried")) + failRow(t, s, retried.ID, false) + again, err := s.Retry(as(2), retried.ID, time.Now(), 0) + if err != nil { + t.Fatal(err) + } + failed := mustEnqueue(t, s, 2, mirrorItem("delta failed")) + failed2 := mustEnqueue(t, s, 2, mirrorItem("echo failed later")) + failRow(t, s, failed.ID, true) + failRow(t, s, failed2.ID, false) + sending := mustEnqueue(t, s, 2, mirrorItem("foxtrot sending")) + if _, ok, err := s.Claim(ctx, sending.ID, time.Now(), claimFor("live", time.Now())); err != nil || !ok { + t.Fatal(ok, err) + } + queued := mustEnqueue(t, s, 2, mirrorItem("golf queued")) + // Another owner's failure at the same address, and the caller's own at + // another window: neither is the caller's list for win. + theirs := mustEnqueue(t, s, 3, mirrorItem("hotel user 3 failed")) + failRow(t, s, theirs.ID, false) + elsewhere := mirrorItem("india another window failed") + elsewhere.Target.Window = "@8" + failRow(t, s, mustEnqueue(t, s, 2, elsewhere).ID, false) + + items, err := s.List(as(2), win) + if err != nil { + t.Fatal(err) + } + // Open oldest first (the retry row was queued before the sending one), + // then the unretried failures newest first. + want := strings.Join([]string{ + "queued:charlie failed then retried#" + strconv.FormatInt(again.ID, 10), + "sending:foxtrot sending#" + strconv.FormatInt(sending.ID, 10), + "queued:golf queued#" + strconv.FormatInt(queued.ID, 10), + "failed:echo failed later#" + strconv.FormatInt(failed2.ID, 10), + "failed:delta failed#" + strconv.FormatInt(failed.ID, 10), + }, " ") + if got := listed(items); got != want { + t.Fatalf("user 2's list:\n got %s\nwant %s", got, want) + } + // CONTROL: the hidden rows are still there (List hides; it deletes nothing). + for _, id := range []int64{sent.ID, cancelled.ID, retried.ID} { + if _, err := s.Get(as(2), id); err != nil { + t.Fatalf("hidden row %d is gone: %v", id, err) + } + } +} + +// TestListLeavesOutAFailureHandedBackToTheChief: a failed ROUTED row and a +// failed CHIEF NUDGE have no Retry here — their outcome goes back to the +// chief, which is their handling — so neither is listed (left in, each sat in +// the list until deleted by hand, the backlog this list exists to avoid). A +// person's own failure, beside them, is. +func TestListLeavesOutAFailureHandedBackToTheChief(t *testing.T) { + s, _ := pgStore(t) + routed := mustEnqueue(t, s, 2, Item{Target: win, Body: "lima routed", StepUpAt: time.Now(), RouteID: "rt_0123456789abcdef01234567"}) + failRow(t, s, routed.ID, false) + nudge := mustEnqueue(t, s, 2, Item{Target: win, Body: "mike chief nudge", StepUpAt: time.Now(), ChiefNudgeID: 4242}) + failRow(t, s, nudge.ID, false) + own := mustEnqueue(t, s, 2, mirrorItem("november own failure")) + failRow(t, s, own.ID, false) + items, err := s.List(as(2), win) + if err != nil { + t.Fatal(err) + } + if got, want := listed(items), "failed:november own failure#"+strconv.FormatInt(own.ID, 10); got != want { + t.Fatalf("user 2's list:\n got %s\nwant %s", got, want) + } + // CONTROL: the fixture really made them a routed and a nudge failure. + for _, c := range []struct { + id int64 + check func(Item) bool + }{{routed.ID, func(it Item) bool { return it.RouteID != "" }}, {nudge.ID, func(it Item) bool { return it.ChiefNudgeID == 4242 }}} { + if it, err := s.Get(as(2), c.id); err != nil || it.State != Failed || !c.check(it) { + t.Fatalf("fixture row %d: %+v %v", c.id, it, err) + } + } +} + +// TestFailedShownCoversAWholeQueue pins the bound itself: a whole queue that +// failed (MaxOpenPerTarget messages, literally 20) is listed whole. The bound +// test below derives its fixture from FailedShown, so a smaller FailedShown +// would pass it. +func TestFailedShownCoversAWholeQueue(t *testing.T) { + if MaxOpenPerTarget != 20 || FailedShown < MaxOpenPerTarget { + t.Fatalf("FailedShown = %d, MaxOpenPerTarget = %d; want at least 20, so a whole failed queue is shown", FailedShown, MaxOpenPerTarget) + } +} + +// TestListBoundsTheFailures: a target that failed more than FailedShown times +// lists the FailedShown newest failures, newest first, and its open rows +// whatever the bound. +func TestListBoundsTheFailures(t *testing.T) { + s, _ := pgStore(t) + n := FailedShown + 3 + for i := range n { + it := mustEnqueue(t, s, 2, mirrorItem(fmt.Sprintf("failure %02d", i))) + failRow(t, s, it.ID, false) + } + open := mustEnqueue(t, s, 2, mirrorItem("still queued")) + items, err := s.List(as(2), win) + if err != nil { + t.Fatal(err) + } + // 21: the literal, not FailedShown+1 — the expectation is not derived from + // the constant it checks. + if len(items) != 21 || items[0].ID != open.ID { + t.Fatalf("list of %d failures and 1 queued: %d rows, first %s; want 21, the queued one first", + n, len(items), listed(items[:1])) + } + for i, it := range items[1:] { + if want := fmt.Sprintf("failure %02d", n-1-i); it.State != Failed || it.Body != want { + t.Fatalf("failure row %d: %s:%s; want failed:%s", i, it.State, it.Body, want) + } + } +} diff --git a/internal/sendqueue/store_retry_pg_test.go b/internal/sendqueue/store_retry_pg_test.go index c77d1369e..7e4e589c1 100644 --- a/internal/sendqueue/store_retry_pg_test.go +++ b/internal/sendqueue/store_retry_pg_test.go @@ -64,8 +64,9 @@ func TestRetryQueuesAFailedMessageAgainOnce(t *testing.T) { if err != nil { t.Fatal(err) } - if b := bodies(items); len(b) != 2 || b[0] != "queued:try me again" || b[1] != "failed:try me again" { - t.Fatalf("user 2's list: %v; want the retry queued and the failed row", b) + // The retried failure is not listed: its retry row carries it. + if b := bodies(items); len(b) != 1 || b[0] != "queued:try me again" || items[0].ID != n.ID { + t.Fatalf("user 2's list: %v; want the retry queued, and the retried failure not listed", b) } // A Retry needs the retrying sign-in's time. other := mustEnqueue(t, s, 2, Item{Target: win, Body: "no time", StepUpAt: time.Now()}) diff --git a/internal/ui/send_queue.go b/internal/ui/send_queue.go index 469f6113f..a38321711 100644 --- a/internal/ui/send_queue.go +++ b/internal/ui/send_queue.go @@ -14,8 +14,11 @@ import ( // "Queue for when idle" item behind Send's caret (composer_menu.go), and the // window's queue under the status line — what is waiting, what // it is waiting for, a ⋯ menu for each message not yet being sent (Send now, -// Copy message, Cancel: sendQueueRowMenu), and the last few outcomes, each -// with a Delete. +// Copy message, Cancel: sendQueueRowMenu), and each of the person's own +// failures not yet handled, with a Retry and a Delete. A sent or cancelled +// message leaves the list (sendqueue.PGStore.List), and so does a failed +// routed message or chief nudge (it goes back to the chief); a person's own +// failure stays until it is retried, deleted, or pruned. A tap on any row's preview shows its whole text (sendQueuePreview). // // 🔴 THE QUEUE IS THE SERVER'S, NOT THE PAGE'S. Queue posts the message to // SendQueuePath and the server sends it when the target has been idle long @@ -106,8 +109,9 @@ type SendQueueItem struct { State string // Preview is the start of the message (the owner's own text). Preview string - // Body is the whole message, for a queued one's Copy message; "" for the - // others, which have no Copy. + // Body is the whole message: what a tap on the preview shows + // (sendQueuePreview), and a queued one's Copy message. "" draws the + // preview alone, with nothing to open. Body string // Detail is the outcome in words, for a finished one. Detail string @@ -118,9 +122,10 @@ type SendQueueItem struct { Answered string // Retryable: it failed and has not been retried; drawn with a Retry. Retryable bool - // Deletable: it is finished (sent, failed, cancelled), so its owner may - // remove it from the list; drawn with a Delete. Never a queued message - // (that is Cancel) or one being sent. + // Deletable: it is finished, so its owner may remove it from the list; + // drawn with a Delete. The list holds only failed ones among the finished + // (sendqueue.PGStore.List). Never a queued message (that is Cancel) or one + // being sent. Deletable bool // MayHaveDelivered: muster stopped while sending it, so it may have // reached the window. Its Retry warns, and asks once before it queues it. @@ -259,7 +264,7 @@ func SendQueueList(v SendQueueListView) g.Node { Class("flex items-start gap-2 py-1"), Span(Class("mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[11px] font-semibold "+tone), g.Text(label)), Div(Class("min-w-0 flex-1"), - P(Class("line-clamp-2 text-xs text-fg [overflow-wrap:anywhere]"), g.Text(it.Preview)), + sendQueuePreview(it), g.If(it.Detail != "" && it.State != "queued" && it.State != "sending", P(Class("text-[11px] text-muted [overflow-wrap:anywhere]"), g.Text(it.Detail))), g.If(it.Answered != "", P(g.Attr("data-queue-answered", ""), Class("text-[11px] text-muted [overflow-wrap:anywhere]"), @@ -300,6 +305,61 @@ func SendQueueList(v SendQueueListView) g.Node { ) } +// SendQueueFullLabel is the start of what a screen reader names a row's +// preview, which a tap opens whole (sendQueuePreview). +const SendQueueFullLabel = "Show the whole message" + +// SendQueueFullID is the popover holding message id's whole text. +func SendQueueFullID(id int64) string { return "mv-qf-" + strconv.FormatInt(id, 10) } + +// sendQueuePreviewClass is a row's preview: at most two lines. +const sendQueuePreviewClass = "line-clamp-2 text-xs text-fg [overflow-wrap:anywhere]" + +// sendQueuePreview is a row's preview — the start of the message, clamped to +// two lines — and, when the row carries the whole text (Body), a tap on it +// shows all of it: the preview is a button opening a popover with the whole +// message (and a Close). +// +// IT WORKS WITHOUT SCRIPT: the button opens the popover itself +// (popovertarget), which closes on Escape, on a tap outside and on its Close. +// Enter and Space are the button's own. +// +// 🔴 OUT OF FLOW, like the row's ⋯ menu: the popover is in the top layer, so +// opening it moves nothing on the page (the preview stays clamped), and the +// list's poll does not redraw the rows while it is open (sendQueueScript's +// menuOpen counts every popover in the list). +func sendQueuePreview(it SendQueueItem) g.Node { + if it.Body == "" { + return P(Class(sendQueuePreviewClass), g.Text(it.Preview)) + } + full := SendQueueFullID(it.ID) + return g.Group([]g.Node{ + Button( + Type("button"), + g.Attr("popovertarget", full), + g.Attr("aria-haspopup", "dialog"), + g.Attr("aria-controls", full), + g.Attr("aria-label", SendQueueFullLabel+": "+it.Preview), + g.Attr("data-queue-full-open", ""), + Class("block min-h-[44px] w-full cursor-pointer rounded-lg text-left hover:bg-s2 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus"), + Span(Class(sendQueuePreviewClass), g.Text(it.Preview)), + ), + Div( + ID(full), + g.Attr("popover", ""), + g.Attr("role", "dialog"), + g.Attr("aria-label", "The whole message"), + g.Attr("data-queue-full", ""), + Class("m-auto max-h-[70dvh] w-[min(90vw,32rem)] overflow-y-auto rounded-xl border-0 bg-s1 p-3 text-fg shadow-lg ring-1 ring-line"), + P(g.Attr("data-queue-full-text", ""), Class("whitespace-pre-wrap text-sm [overflow-wrap:anywhere]"), g.Text(it.Body)), + Div(Class("mt-2 flex justify-end"), + Button(Type("button"), g.Attr("popovertarget", full), g.Attr("popovertargetaction", "hide"), + Class("press min-h-[44px] rounded-lg px-3 text-sm font-semibold text-fg2 ring-1 ring-inset ring-line transition hover:bg-s2"), + g.Text("Close"))), + ), + }) +} + // sendQueueWaiting is the line saying what the next message waits for. It // RESERVES two lines whatever it says, so the queue going from "working" (one // line) to held on a usage limit (two, at a phone's width) does not grow the @@ -446,7 +506,7 @@ const sendQueueMenuItemClass = "press flex min-h-[44px] w-full items-center roun // opens itself (popovertarget), which closes on Escape and on a tap outside // with no script at all, and Send now and Cancel are each a real form. Copy // message needs the clipboard, so it is drawn hidden and the list's script -// shows it; the whole message rides in the row (a template), because the +// shows it; the whole message rides in the row (sendQueuePreview's popover), because the // preview is cut. sendQueueScript adds the menu-button keyboard pattern, // places the menu beside its button and keeps aria-expanded true. // @@ -492,8 +552,6 @@ func sendQueueRowMenu(it SendQueueItem) g.Node { Button(Type("submit"), g.Attr("role", "menuitem"), g.Attr("aria-label", SendQueueCancelLabel), Class(sendQueueMenuItemClass+" text-st-error-fg"), g.Text("Cancel"))), ), - // The whole message, for Copy message: inert, never rendered. - Template(g.Attr("data-queue-body", ""), g.Text(it.Body)), ) } @@ -610,10 +668,10 @@ func sendQueueBox(q *ComposerQueue, off bool) g.Node { // sendQueueScript keeps #mv-queue current: re-read every 3 s while anything is // open there, every 15 s otherwise, at once on a Queue (the composer's script -// dispatches mv-queued) and when the page is shown again — never while a row's -// menu is open; it sends each Send now, Cancel, Retry and Delete without -// leaving the page; it copies a queued message; and it drives the rows' ⋯ -// menus (sendQueueRowMenu). +// dispatches mv-queued) and when the page is shown again — never while one of +// its popovers (a row's menu, a row's whole message) is open; it sends each +// Send now, Cancel, Retry and Delete without leaving the page; it copies a +// queued message; and it drives the rows' ⋯ menus (sendQueueRowMenu). func sendQueueScript() g.Node { return Script(g.Raw(` (function () { @@ -629,11 +687,12 @@ func sendQueueScript() g.Node { clearTimeout(timer); timer = setTimeout(load, box.querySelector('[data-active]') ? 3000 : 15000); } - // menuOpen: a row's ⋯ menu is open. The list is not redrawn under it (a - // redraw would close it, and drop the focus inside it); the next load after - // it closes catches up. + // menuOpen: a popover of the list is open — a row's ⋯ menu, a row's whole + // message (sendQueuePreview), the whole waiting line. The list is not + // redrawn under it (a redraw would close it, and drop the focus inside it); + // the next load after it closes catches up. function menuOpen() { - var ms = box.querySelectorAll('[data-queue-menu] [popover], #mv-queue-waiting-full'); + var ms = box.querySelectorAll('[popover]'); for (var i = 0; i < ms.length; i++) { try { if (ms[i].matches(':popover-open')) return true; } catch (e) { return false; } } @@ -835,7 +894,7 @@ func sendQueueScript() g.Node { if (m) { try { m.hidePopover(); } catch (err) {} } } - // Copy message: the WHOLE message (the row's template; the preview is cut), + // Copy message: the WHOLE message (the row's popover text; the preview is cut), // to the clipboard, then a brief "Copied." Where the async clipboard is // refused (no secure context), a selected textarea and execCommand. function copyText(text) { @@ -859,8 +918,8 @@ func sendQueueScript() g.Node { var c = e.target && e.target.closest ? e.target.closest('[data-queue-copy]') : null; if (!c) return; var row = c.closest('[data-queue-id]'); - var tpl = row ? row.querySelector('template[data-queue-body]') : null; - var text = tpl ? (tpl.content ? tpl.content.textContent : tpl.textContent) : ''; + var whole = row ? row.querySelector('[data-queue-full-text]') : null; + var text = whole ? whole.textContent : ''; closeMenu(c); copyText(text).then(function () { noteOK('Copied.'); }, function () { note('Not copied: this browser did not allow it.'); }); }); diff --git a/internal/ui/send_queue_full_test.go b/internal/ui/send_queue_full_test.go new file mode 100644 index 000000000..4025668f2 --- /dev/null +++ b/internal/ui/send_queue_full_test.go @@ -0,0 +1,137 @@ +package ui + +import ( + "bytes" + "strings" + "testing" + + "golang.org/x/net/html" +) + +// THE WHOLE MESSAGE (sendQueuePreview): a row's preview is cut (the server's +// 200 bytes) and clamped (two lines), so a tap on it opens the whole text in a +// popover — a native popovertarget button, so it works without script. + +// queueDoc renders v's list and parses it. +func queueDoc(t *testing.T, v SendQueueListView) *html.Node { + t.Helper() + var b bytes.Buffer + if err := SendQueueList(v).Render(&b); err != nil { + t.Fatal(err) + } + doc, err := html.Parse(&b) + if err != nil { + t.Fatal(err) + } + return doc +} + +func sqAttr(n *html.Node, key string) (string, bool) { + for _, a := range n.Attr { + if a.Key == key { + return a.Val, true + } + } + return "", false +} + +// findNode is the first node under n for which ok holds, or nil. +func sqFind(n *html.Node, ok func(*html.Node) bool) *html.Node { + if ok(n) { + return n + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + if f := sqFind(c, ok); f != nil { + return f + } + } + return nil +} + +func sqText(n *html.Node) string { + var b strings.Builder + var walk func(*html.Node) + walk = func(n *html.Node) { + if n.Type == html.TextNode { + b.WriteString(n.Data) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + walk(c) + } + } + walk(n) + return b.String() +} + +// TestATapOnAPreviewOpensTheWholeMessage: a queued, a sending and a failed +// row whose text is longer than its preview each draw the preview as a button +// (popovertarget, aria-label, 44px) opening a popover that holds the WHOLE +// text — its tail, which the preview does not — with a Close. A row with no +// whole text draws the preview alone, with nothing to open. +func TestATapOnAPreviewOpensTheWholeMessage(t *testing.T) { + const tail = "kilo-tango-tail-0731" + for i, state := range []string{"queued", "sending", "failed"} { + id := int64(41 + i) + body := strings.Repeat(state+" words ", 40) + tail + preview := body[:200] + "…" + it := SendQueueItem{ID: id, State: state, Preview: preview, Body: body, Cancellable: state == "queued", + Retryable: state == "failed", Deletable: state == "failed"} + doc := queueDoc(t, SendQueueListView{Items: []SendQueueItem{it}}) + full := SendQueueFullID(id) + pop := sqFind(doc, func(n *html.Node) bool { v, _ := sqAttr(n, "id"); return n.Type == html.ElementNode && v == full }) + if pop == nil { + t.Fatalf("%s row: no #%s", state, full) + } + if _, ok := sqAttr(pop, "popover"); !ok { + t.Errorf("%s row: #%s is not a popover", state, full) + } + if got := sqText(sqFind(pop, func(n *html.Node) bool { _, ok := sqAttr(n, "data-queue-full-text"); return ok })); got != body { + t.Errorf("%s row: the popover's text is %q; want the whole message", state, got) + } + if closer := sqFind(pop, func(n *html.Node) bool { + a, _ := sqAttr(n, "popovertargetaction") + v, _ := sqAttr(n, "popovertarget") + return n.Data == "button" && a == "hide" && v == full + }); closer == nil { + t.Errorf("%s row: the popover has no Close", state) + } + open := sqFind(doc, func(n *html.Node) bool { + v, _ := sqAttr(n, "popovertarget") + _, hide := sqAttr(n, "popovertargetaction") + return n.Data == "button" && v == full && !hide + }) + if open == nil { + t.Fatalf("%s row: no button opens #%s", state, full) + } + label, _ := sqAttr(open, "aria-label") + cls, _ := sqAttr(open, "class") + typ, _ := sqAttr(open, "type") + if !strings.HasPrefix(label, SendQueueFullLabel) || !strings.Contains(cls, "min-h-[44px]") || typ != "button" { + t.Errorf("%s row: the opener is %q class %q type %q", state, label, cls, typ) + } + if strings.Contains(sqText(open), tail) || !strings.Contains(sqText(open), preview) { + t.Errorf("%s row: the opener shows %q; want the preview, not the tail", state, sqText(open)) + } + } + // No whole text: the preview alone. + var b bytes.Buffer + if err := SendQueueList(SendQueueListView{Items: []SendQueueItem{{ID: 7, State: "failed", Preview: "short"}}}).Render(&b); err != nil { + t.Fatal(err) + } + if p := b.String(); strings.Contains(p, "popovertarget") || strings.Contains(p, SendQueueFullID(7)) || !strings.Contains(p, "short") { + t.Fatalf("a row with no whole text: %s", p) + } +} + +// TestThePollDoesNotRedrawUnderAnOpenWholeMessage: the list's script skips a +// redraw while ANY popover in the list is open — a row's whole message +// included, not only the ⋯ menus and the waiting line. +func TestThePollDoesNotRedrawUnderAnOpenWholeMessage(t *testing.T) { + var b bytes.Buffer + if err := sendQueueScript().Render(&b); err != nil { + t.Fatal(err) + } + if !strings.Contains(b.String(), "var ms = box.querySelectorAll('[popover]');") { + t.Fatalf("menuOpen does not count every popover of the list:\n%s", b.String()) + } +} diff --git a/web/static/app.css b/web/static/app.css index 7475b9dff..32efb2f52 100644 --- a/web/static/app.css +++ b/web/static/app.css @@ -1,2 +1,2 @@ *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: } -/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.dark,:root{color-scheme:dark;--mu-bg:17 15 28;--mu-s1:25 22 42;--mu-s2:34 30 55;--mu-s3:45 40 70;--mu-line:47 42 72;--mu-edge:110 103 144;--mu-text:238 235 247;--mu-text2:195 189 217;--mu-muted:154 147 181;--mu-accent:228 181 86;--mu-on-accent:30 22 6;--mu-focus:242 203 120;--mu-danger:209 59 69;--mu-on-danger:255 255 255;--mu-st-open-fg:185 179 211;--mu-st-open-bg:36 32 58;--mu-st-progress-fg:143 184 255;--mu-st-progress-bg:27 37 71;--mu-st-review-fg:215 166 255;--mu-st-review-bg:46 31 69;--mu-st-complete-fg:127 214 164;--mu-st-complete-bg:22 52 40;--mu-st-error-fg:255 143 143;--mu-st-error-bg:61 26 34;--mu-st-warning-fg:255 174 94;--mu-st-warning-bg:58 38 21;--mu-st-running-fg:95 224 192;--mu-st-running-bg:18 51 47;--mu-idle-0:184 187 38;--mu-idle-1:152 151 26;--mu-idle-2:104 157 106;--mu-idle-3:215 153 33;--mu-idle-4:214 93 14;--mu-idle-5:204 36 29;--mu-idle-6:177 98 134;--mu-idle-7:102 92 84}:root:not(.dark){color-scheme:light;--mu-bg:246 244 251;--mu-s1:255 255 255;--mu-s2:238 235 246;--mu-s3:227 223 239;--mu-line:220 215 234;--mu-edge:125 118 153;--mu-text:26 22 48;--mu-text2:68 62 94;--mu-muted:98 92 125;--mu-accent:131 93 15;--mu-on-accent:255 255 255;--mu-focus:107 76 14;--mu-danger:180 38 48;--mu-on-danger:255 255 255;--mu-st-open-fg:79 72 112;--mu-st-open-bg:236 233 245;--mu-st-progress-fg:31 79 176;--mu-st-progress-bg:227 236 255;--mu-st-review-fg:116 40 181;--mu-st-review-bg:243 232 255;--mu-st-complete-fg:28 107 63;--mu-st-complete-bg:226 245 233;--mu-st-error-fg:180 35 47;--mu-st-error-bg:253 231 232;--mu-st-warning-fg:154 75 0;--mu-st-warning-bg:255 238 219;--mu-st-running-fg:13 107 90;--mu-st-running-bg:221 245 239;--mu-idle-0:184 187 38;--mu-idle-1:152 151 26;--mu-idle-2:104 157 106;--mu-idle-3:215 153 33;--mu-idle-4:214 93 14;--mu-idle-5:204 36 29;--mu-idle-6:177 98 134;--mu-idle-7:102 92 84}:root{--glyph-knock:rgb(var(--mu-s1))}[data-identity]{--id-bd:var(--id-bd-d);--id-tt:var(--id-tt-d)}:root:not(.dark) [data-identity]{--id-bd:var(--id-bd-l);--id-tt:var(--id-tt-l)}[hidden]{display:none!important}@media (max-width:639px){input:not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]),select,textarea{font-size:16px!important;min-height:44px!important}input[type=file]{font-size:16px!important;min-height:44px}summary{min-height:44px}}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.card-enter{animation:card-in .28s cubic-bezier(.16,1,.3,1) both,card-highlight 1.4s ease-out .12s both}.press{transition:transform 80ms ease;touch-action:manipulation}.press:active{transform:scale(.97)}.selecting .card-link:after{pointer-events:none}#session-drawer:focus-visible{position:fixed;top:.75rem;right:.75rem;z-index:60;width:2.75rem;height:2.75rem;margin:0;padding:0;overflow:visible;clip:auto;clip-path:none;white-space:normal;border-radius:.5rem;outline:2px solid rgb(var(--mu-focus));outline-offset:2px}#sidebar-root .sidebar-panel{transform:translateX(-100%);visibility:hidden;transition-property:transform,visibility}#sidebar-root .sidebar-backdrop{opacity:0;pointer-events:none}#sidebar-root[data-open=true] .sidebar-panel{transform:translateX(0);visibility:visible}#sidebar-root[data-open=true] .sidebar-backdrop{opacity:1;pointer-events:auto}@media (min-width:1024px){#sidebar-root:not([data-overlay]) .sidebar-panel{transform:translateX(0);visibility:visible}#sidebar-root:not([data-overlay]) .sidebar-backdrop{display:none}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-x-0{left:0;right:0}.inset-x-2{left:.5rem;right:.5rem}.inset-y-0{top:0;bottom:0}.-right-1{right:-.25rem}.-top-1{top:-.25rem}.bottom-0{bottom:0}.bottom-2\.5{bottom:.625rem}.bottom-4{bottom:1rem}.bottom-6{bottom:1.5rem}.bottom-\[calc\(1\.5rem\+env\(safe-area-inset-bottom\)\)\]{bottom:calc(1.5rem + env(safe-area-inset-bottom))}.bottom-\[calc\(6rem\+env\(safe-area-inset-bottom\)\)\]{bottom:calc(6rem + env(safe-area-inset-bottom))}.bottom-auto{bottom:auto}.bottom-full{bottom:100%}.left-0{left:0}.left-1\.5{left:.375rem}.left-1\/2{left:50%}.left-\[calc\(1\.5rem\+env\(safe-area-inset-left\)\)\]{left:calc(1.5rem + env(safe-area-inset-left))}.right-0{right:0}.right-3{right:.75rem}.right-\[calc\(1\.5rem\+env\(safe-area-inset-right\)\)\]{right:calc(1.5rem + env(safe-area-inset-right))}.right-\[calc\(env\(safe-area-inset-right\)\+0\.75rem\)\]{right:calc(env(safe-area-inset-right) + .75rem)}.top-0{top:0}.top-1\/2{top:50%}.top-2{top:.5rem}.top-\[calc\(env\(safe-area-inset-top\)\+0\.5rem\)\]{top:calc(env(safe-area-inset-top) + .5rem)}.top-\[max\(0\.5rem\2c 10vh\)\]{top:max(.5rem,10vh)}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[35\]{z-index:35}.m-0{margin:0}.m-auto{margin:auto}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0{margin-top:0;margin-bottom:0}.my-0\.5{margin-top:.125rem;margin-bottom:.125rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.-ml-1{margin-left:-.25rem}.-mr-0\.5{margin-right:-.125rem}.-mt-2{margin-top:-.5rem}.mb-0{margin-bottom:0}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1\.5{margin-left:.375rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-auto{margin-right:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.mt-auto{margin-top:auto}.line-clamp-1{-webkit-line-clamp:1}.line-clamp-1,.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical}.line-clamp-2{-webkit-line-clamp:2}.\!block{display:block!important}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-\[2\.8125rem\]{height:2.8125rem}.h-\[70dvh\]{height:70dvh}.h-\[calc\(2\.8125rem\+env\(safe-area-inset-bottom\)\)\]{height:calc(2.8125rem + env(safe-area-inset-bottom))}.h-\[var\(--mv-vh\2c 100dvh\)\]{height:var(--mv-vh,100dvh)}.h-dvh{height:100dvh}.h-full{height:100%}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-56{max-height:14rem}.max-h-8{max-height:2rem}.max-h-\[45vh\]{max-height:45vh}.max-h-\[60dvh\]{max-height:60dvh}.max-h-\[70vh\]{max-height:70vh}.max-h-\[85dvh\]{max-height:85dvh}.max-h-\[min\(24rem\2c 60vh\)\]{max-height:min(24rem,60vh)}.min-h-0{min-height:0}.min-h-11{min-height:2.75rem}.min-h-\[2\.25rem\]{min-height:2.25rem}.min-h-\[2lh\]{min-height:2lh}.min-h-\[32px\]{min-height:32px}.min-h-\[36px\]{min-height:36px}.min-h-\[4\.25rem\]{min-height:4.25rem}.min-h-\[40px\]{min-height:40px}.min-h-\[44px\]{min-height:44px}.min-h-\[48px\]{min-height:48px}.min-h-\[calc\(100dvh-6rem\)\]{min-height:calc(100dvh - 6rem)}.min-h-\[calc\(2\.75rem\+1px\+env\(safe-area-inset-bottom\)\)\]{min-height:calc(2.75rem + 1px + env(safe-area-inset-bottom))}.min-h-dvh{min-height:100dvh}.w-1\.5{width:.375rem}.w-1\/2{width:50%}.w-1\/3{width:33.333333%}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-2\/3{width:66.666667%}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-3\/4{width:75%}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[22rem\]{width:22rem}.w-\[4\.5rem\]{width:4.5rem}.w-\[6\.25rem\]{width:6.25rem}.w-\[9\.75rem\]{width:9.75rem}.w-\[calc\(100\%-2rem\)\]{width:calc(100% - 2rem)}.w-\[min\(28rem\2c calc\(100vw-1rem\)\)\]{width:min(28rem,calc(100vw - 1rem))}.w-\[min\(36rem\2c calc\(100vw-1rem\)\)\]{width:min(36rem,calc(100vw - 1rem))}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[1\.25rem\]{min-width:1.25rem}.min-w-\[1\.5rem\]{min-width:1.5rem}.min-w-\[11rem\]{min-width:11rem}.min-w-\[12rem\]{min-width:12rem}.min-w-\[14rem\]{min-width:14rem}.min-w-\[36px\]{min-width:36px}.min-w-\[44px\]{min-width:44px}.min-w-\[6rem\]{min-width:6rem}.max-w-2xl{max-width:42rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-\[110rem\]{max-width:110rem}.max-w-\[12rem\]{max-width:12rem}.max-w-\[150rem\]{max-width:150rem}.max-w-\[16rem\]{max-width:16rem}.max-w-\[45\%\]{max-width:45%}.max-w-\[50\%\]{max-width:50%}.max-w-\[70ch\]{max-width:70ch}.max-w-\[85\%\]{max-width:85%}.max-w-\[85vw\]{max-width:85vw}.max-w-\[92\%\]{max-width:92%}.max-w-\[calc\(100vw-16px\)\]{max-width:calc(100vw - 16px)}.max-w-\[calc\(100vw-3rem\)\]{max-width:calc(100vw - 3rem)}.max-w-\[min\(90vw\2c 28rem\)\]{max-width:min(90vw,28rem)}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-none{flex:none}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.origin-bottom-left{transform-origin:bottom left}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.translate-x-full,.translate-y-2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-2{--tw-translate-y:0.5rem}.scale-95{--tw-scale-x:.95;--tw-scale-y:.95}.scale-95,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-bounce{animation:bounce 1s infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-col-resize{cursor:col-resize}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.select-all{-webkit-user-select:all;-moz-user-select:all;user-select:all}.resize-none{resize:none}.resize-y{resize:vertical}.resize{resize:both}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.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-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-1{-moz-column-gap:.25rem;column-gap:.25rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-1{row-gap:.25rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-line>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(var(--mu-line)/var(--tw-divide-opacity,1))}.self-start{align-self:flex-start}.self-end{align-self:flex-end}.self-center{align-self:center}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-hidden{overflow-y:hidden}.overscroll-contain{overscroll-behavior:contain}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-clip{text-overflow:clip}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-balance{text-wrap:balance}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-l-xl{border-top-left-radius:.75rem;border-bottom-left-radius:.75rem}.rounded-r-full{border-top-right-radius:9999px;border-bottom-right-radius:9999px}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.rounded-r-xl{border-top-right-radius:.75rem;border-bottom-right-radius:.75rem}.rounded-t-2xl{border-top-left-radius:1rem;border-top-right-radius:1rem}.border{border-width:1px}.border-0{border-width:0}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-l-4{border-left-width:4px}.border-t{border-top-width:1px}.border-accent\/30{border-color:rgb(var(--mu-accent)/.3)}.border-accent\/40{border-color:rgb(var(--mu-accent)/.4)}.border-accent\/60{border-color:rgb(var(--mu-accent)/.6)}.border-edge{--tw-border-opacity:1;border-color:rgb(var(--mu-edge)/var(--tw-border-opacity,1))}.border-line{--tw-border-opacity:1;border-color:rgb(var(--mu-line)/var(--tw-border-opacity,1))}.border-on-accent\/30{border-color:rgb(var(--mu-on-accent)/.3)}.border-st-warning-fg\/40{border-color:rgb(var(--mu-st-warning-fg)/.4)}.border-transparent{border-color:transparent}.border-l-\[rgb\(var\(--id-bd\)\)\]{border-left-color:rgb(var(--id-bd))}.border-l-transparent{border-left-color:transparent}.bg-accent{--tw-bg-opacity:1;background-color:rgb(var(--mu-accent)/var(--tw-bg-opacity,1))}.bg-accent\/10{background-color:rgb(var(--mu-accent)/.1)}.bg-accent\/15{background-color:rgb(var(--mu-accent)/.15)}.bg-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-bg)/var(--tw-bg-opacity,1))}.bg-bg\/40{background-color:rgb(var(--mu-bg)/.4)}.bg-bg\/50{background-color:rgb(var(--mu-bg)/.5)}.bg-bg\/60{background-color:rgb(var(--mu-bg)/.6)}.bg-bg\/80{background-color:rgb(var(--mu-bg)/.8)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-black\/60{background-color:rgba(0,0,0,.6)}.bg-current{background-color:currentColor}.bg-danger{--tw-bg-opacity:1;background-color:rgb(var(--mu-danger)/var(--tw-bg-opacity,1))}.bg-danger\/15{background-color:rgb(var(--mu-danger)/.15)}.bg-idle-0{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-0)/var(--tw-bg-opacity,1))}.bg-idle-1{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-1)/var(--tw-bg-opacity,1))}.bg-idle-2{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-2)/var(--tw-bg-opacity,1))}.bg-idle-3{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-3)/var(--tw-bg-opacity,1))}.bg-idle-4{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-4)/var(--tw-bg-opacity,1))}.bg-idle-5{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-5)/var(--tw-bg-opacity,1))}.bg-idle-6{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-6)/var(--tw-bg-opacity,1))}.bg-idle-7{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-7)/var(--tw-bg-opacity,1))}.bg-muted{--tw-bg-opacity:1;background-color:rgb(var(--mu-muted)/var(--tw-bg-opacity,1))}.bg-s1{--tw-bg-opacity:1;background-color:rgb(var(--mu-s1)/var(--tw-bg-opacity,1))}.bg-s1\/40{background-color:rgb(var(--mu-s1)/.4)}.bg-s1\/50{background-color:rgb(var(--mu-s1)/.5)}.bg-s1\/60{background-color:rgb(var(--mu-s1)/.6)}.bg-s1\/70{background-color:rgb(var(--mu-s1)/.7)}.bg-s2{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.bg-s2\/80{background-color:rgb(var(--mu-s2)/.8)}.bg-s3{--tw-bg-opacity:1;background-color:rgb(var(--mu-s3)/var(--tw-bg-opacity,1))}.bg-s3\/50{background-color:rgb(var(--mu-s3)/.5)}.bg-s3\/60{background-color:rgb(var(--mu-s3)/.6)}.bg-st-complete-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-complete-bg)/var(--tw-bg-opacity,1))}.bg-st-error-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-error-bg)/var(--tw-bg-opacity,1))}.bg-st-open-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-open-bg)/var(--tw-bg-opacity,1))}.bg-st-progress-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-progress-bg)/var(--tw-bg-opacity,1))}.bg-st-review-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-review-bg)/var(--tw-bg-opacity,1))}.bg-st-review-bg\/40{background-color:rgb(var(--mu-st-review-bg)/.4)}.bg-st-running-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-running-bg)/var(--tw-bg-opacity,1))}.bg-st-warning-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-warning-bg)/var(--tw-bg-opacity,1))}.bg-transparent{background-color:transparent}.bg-gradient-to-l{background-image:linear-gradient(to left,var(--tw-gradient-stops))}.from-bg{--tw-gradient-from:rgb(var(--mu-bg)/1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(var(--mu-bg)/0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-bg\/80{--tw-gradient-to:rgb(var(--mu-bg)/0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgb(var(--mu-bg)/0.8) var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-28{padding-bottom:7rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-\[2\.8125rem\]{padding-bottom:2.8125rem}.pb-\[calc\(1\.25rem\+env\(safe-area-inset-bottom\)\)\]{padding-bottom:calc(1.25rem + env(safe-area-inset-bottom))}.pb-\[calc\(2\.8125rem\+env\(safe-area-inset-bottom\)\)\]{padding-bottom:calc(2.8125rem + env(safe-area-inset-bottom))}.pb-\[calc\(4rem\+env\(safe-area-inset-bottom\)\)\]{padding-bottom:calc(4rem + env(safe-area-inset-bottom))}.pb-\[calc\(7\.5rem\+env\(safe-area-inset-bottom\)\)\]{padding-bottom:calc(7.5rem + env(safe-area-inset-bottom))}.pb-\[env\(safe-area-inset-bottom\)\]{padding-bottom:env(safe-area-inset-bottom)}.pb-\[max\(0\.5rem\2c env\(safe-area-inset-bottom\)\)\]{padding-bottom:max(.5rem,env(safe-area-inset-bottom))}.pb-\[max\(0\.75rem\2c env\(safe-area-inset-bottom\)\)\]{padding-bottom:max(.75rem,env(safe-area-inset-bottom))}.pb-\[var\(--mv-compose-h\2c 4\.5rem\)\]{padding-bottom:var(--mv-compose-h,4.5rem)}.pb-\[var\(--mv-compose-h\2c 7rem\)\]{padding-bottom:var(--mv-compose-h,7rem)}.pl-11{padding-left:2.75rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pl-5{padding-left:1.25rem}.pl-72{padding-left:18rem}.pl-9{padding-left:2.25rem}.pr-1{padding-right:.25rem}.pr-2{padding-right:.5rem}.pr-28{padding-right:7rem}.pt-0{padding-top:0}.pt-1{padding-top:.25rem}.pt-10{padding-top:2.5rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.pt-\[env\(safe-area-inset-top\)\]{padding-top:env(safe-area-inset-top)}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[0\.7rem\]{font-size:.7rem}.text-\[0\.85em\]{font-size:.85em}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.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)}.leading-5{line-height:1.25rem}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.text-accent{--tw-text-opacity:1;color:rgb(var(--mu-accent)/var(--tw-text-opacity,1))}.text-current{color:currentColor}.text-fg{--tw-text-opacity:1;color:rgb(var(--mu-text)/var(--tw-text-opacity,1))}.text-fg2{--tw-text-opacity:1;color:rgb(var(--mu-text2)/var(--tw-text-opacity,1))}.text-muted{--tw-text-opacity:1;color:rgb(var(--mu-muted)/var(--tw-text-opacity,1))}.text-on-accent{--tw-text-opacity:1;color:rgb(var(--mu-on-accent)/var(--tw-text-opacity,1))}.text-on-danger{--tw-text-opacity:1;color:rgb(var(--mu-on-danger)/var(--tw-text-opacity,1))}.text-st-complete-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-complete-fg)/var(--tw-text-opacity,1))}.text-st-error-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-error-fg)/var(--tw-text-opacity,1))}.text-st-open-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-open-fg)/var(--tw-text-opacity,1))}.text-st-progress-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-progress-fg)/var(--tw-text-opacity,1))}.text-st-review-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-review-fg)/var(--tw-text-opacity,1))}.text-st-running-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-running-fg)/var(--tw-text-opacity,1))}.text-st-warning-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-warning-fg)/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.decoration-accent\/40{text-decoration-color:rgb(var(--mu-accent)/.4)}.decoration-dotted{text-decoration-style:dotted}.underline-offset-2{text-underline-offset:2px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-\[0_-16px_16px_-4px_rgb\(var\(--mu-s1\)\)\]{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[0_-16px_16px_-4px_rgb\(var\(--mu-s1\)\)\]{--tw-shadow:0 -16px 16px -4px rgb(var(--mu-s1));--tw-shadow-colored:0 -16px 16px -4px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-accent\/20{--tw-shadow-color:rgb(var(--mu-accent)/0.2);--tw-shadow:var(--tw-shadow-colored)}.shadow-accent\/30{--tw-shadow-color:rgb(var(--mu-accent)/0.3);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/20{--tw-shadow-color:rgba(0,0,0,.2);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/30{--tw-shadow-color:rgba(0,0,0,.3);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/40{--tw-shadow-color:rgba(0,0,0,.4);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/50{--tw-shadow-color:rgba(0,0,0,.5);--tw-shadow:var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-accent\/20{--tw-ring-color:rgb(var(--mu-accent)/0.2)}.ring-accent\/30{--tw-ring-color:rgb(var(--mu-accent)/0.3)}.ring-accent\/40{--tw-ring-color:rgb(var(--mu-accent)/0.4)}.ring-accent\/50{--tw-ring-color:rgb(var(--mu-accent)/0.5)}.ring-bg{--tw-ring-opacity:1;--tw-ring-color:rgb(var(--mu-bg)/var(--tw-ring-opacity,1))}.ring-black\/20{--tw-ring-color:rgba(0,0,0,.2)}.ring-danger\/40{--tw-ring-color:rgb(var(--mu-danger)/0.4)}.ring-edge{--tw-ring-opacity:1;--tw-ring-color:rgb(var(--mu-edge)/var(--tw-ring-opacity,1))}.ring-line{--tw-ring-opacity:1;--tw-ring-color:rgb(var(--mu-line)/var(--tw-ring-opacity,1))}.ring-st-complete-fg\/40{--tw-ring-color:rgb(var(--mu-st-complete-fg)/0.4)}.ring-st-error-fg\/40{--tw-ring-color:rgb(var(--mu-st-error-fg)/0.4)}.ring-st-open-fg\/40{--tw-ring-color:rgb(var(--mu-st-open-fg)/0.4)}.ring-st-progress-fg\/40{--tw-ring-color:rgb(var(--mu-st-progress-fg)/0.4)}.ring-st-review-fg\/40{--tw-ring-color:rgb(var(--mu-st-review-fg)/0.4)}.ring-st-running-fg\/40{--tw-ring-color:rgb(var(--mu-st-running-fg)/0.4)}.ring-st-warning-fg\/40{--tw-ring-color:rgb(var(--mu-st-warning-fg)/0.4)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur:blur(8px)}.backdrop-blur,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[animation-delay\:-0\.15s\]{animation-delay:-.15s}.\[animation-delay\:-0\.3s\]{animation-delay:-.3s}.\[i\:j\]{i:j}.\[overflow-anchor\:none\]{overflow-anchor:none}.\[overflow-wrap\:anywhere\]{overflow-wrap:anywhere}.\[scrollbar-width\:none\]{scrollbar-width:none}@keyframes card-in{0%{opacity:0;transform:translateY(12px) scale(.985)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes card-out{0%{opacity:1;transform:translateY(0) scale(1);max-height:60rem}to{opacity:0;transform:translateY(-6px) scale(.985);max-height:0}}@keyframes card-highlight{0%{box-shadow:0 0 0 0 rgb(var(--mu-accent)/0)}25%{box-shadow:0 0 0 2px rgb(var(--mu-accent)/.5)}to{box-shadow:0 0 0 0 rgb(var(--mu-accent)/0)}}@media (prefers-reduced-motion:reduce){.card-enter,.htmx-swapping{animation:none}.press{transition:none}.sidebar-backdrop,.sidebar-panel{transition:none!important}}[data-clamp]{overflow:hidden;flex-shrink:0;transition:max-height .22s cubic-bezier(.2,.8,.2,1)}[data-clamp=closed]{cursor:pointer;-webkit-mask-image:linear-gradient(180deg,#000 calc(100% - 2.5rem),transparent);mask-image:linear-gradient(180deg,#000 calc(100% - 2.5rem),transparent)}@media (prefers-reduced-motion:reduce){[data-clamp]{transition:none}}[data-flash]{animation:card-highlight 1.4s ease-out both}@media (prefers-reduced-motion:reduce){[data-flash]{animation:none;box-shadow:0 0 0 2px rgb(var(--mu-accent)/.5)}}.composer-fit{max-height:var(--composer-max-h,40dvh);overflow-y:auto}@supports (field-sizing:content){.composer-fit{field-sizing:content}}[data-tool-group][open]>[data-tool-group-body]{animation:tool-group-open .18s cubic-bezier(.2,.8,.2,1) both}[data-tool-group-body]>[data-tool-row]{max-width:100%}@keyframes tool-group-open{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}[data-tool-group]>summary [data-tool-group-chevron]{transition:transform .18s ease}[data-tool-group][open]>summary [data-tool-group-chevron]{transform:rotate(90deg)}[data-tool-ctx]:not([hidden])+*{margin-left:0}@media (prefers-reduced-motion:reduce){[data-tool-group][open]>[data-tool-group-body]{animation:none}[data-tool-group]>summary [data-tool-group-chevron]{transition:none}}#switcher-search,#switcher-views{opacity:0;transition:opacity .16s cubic-bezier(.2,.8,.2,1),transform .16s cubic-bezier(.2,.8,.2,1),overlay allow-discrete .16s,display allow-discrete .16s}#switcher-views{transform:translateY(8px) scale(.96);transform-origin:bottom left}#switcher-search{transform:translateY(-8px) scale(.98);transform-origin:top center}#switcher-search:popover-open,#switcher-views:popover-open{opacity:1;transform:none}@starting-style{#switcher-views:popover-open{opacity:0;transform:translateY(8px) scale(.96)}#switcher-search:popover-open{opacity:0;transform:translateY(-8px) scale(.98)}}@media (prefers-reduced-motion:reduce){#switcher-search,#switcher-views{transition:none}}body:has(#agent-status:is([data-agent-status=running],[data-agent-status=provisioning])) #agent-actions [data-lifecycle=idle],body:not(:has(#agent-status:is([data-agent-status=running],[data-agent-status=provisioning]))) #agent-actions [data-lifecycle=live]{display:none}.marker\:content-\[\'\'\] ::marker{--tw-content:"";content:var(--tw-content)}.marker\:content-\[\'\'\]::marker{--tw-content:"";content:var(--tw-content)}.selection\:bg-accent\/30 ::-moz-selection{background-color:rgb(var(--mu-accent)/.3)}.selection\:bg-accent\/30 ::selection{background-color:rgb(var(--mu-accent)/.3)}.selection\:bg-accent\/30::-moz-selection{background-color:rgb(var(--mu-accent)/.3)}.selection\:bg-accent\/30::selection{background-color:rgb(var(--mu-accent)/.3)}.file\:mr-3::file-selector-button{margin-right:.75rem}.file\:rounded-lg::file-selector-button{border-radius:.5rem}.file\:border-0::file-selector-button{border-width:0}.file\:bg-s2::file-selector-button{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.file\:px-3::file-selector-button{padding-left:.75rem;padding-right:.75rem}.file\:py-1\.5::file-selector-button{padding-top:.375rem;padding-bottom:.375rem}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.file\:text-fg::file-selector-button{--tw-text-opacity:1;color:rgb(var(--mu-text)/var(--tw-text-opacity,1))}.placeholder\:text-fg2::-moz-placeholder{--tw-text-opacity:1;color:rgb(var(--mu-text2)/var(--tw-text-opacity,1))}.placeholder\:text-fg2::placeholder{--tw-text-opacity:1;color:rgb(var(--mu-text2)/var(--tw-text-opacity,1))}.placeholder\:text-muted::-moz-placeholder{--tw-text-opacity:1;color:rgb(var(--mu-muted)/var(--tw-text-opacity,1))}.placeholder\:text-muted::placeholder{--tw-text-opacity:1;color:rgb(var(--mu-muted)/var(--tw-text-opacity,1))}.backdrop\:bg-black\/40::backdrop{background-color:rgba(0,0,0,.4)}.backdrop\:bg-black\/60::backdrop{background-color:rgba(0,0,0,.6)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-0:after{content:var(--tw-content);inset:0}.after\:inset-x-0:after{content:var(--tw-content);left:0;right:0}.after\:top-1\/2:after{content:var(--tw-content);top:50%}.after\:z-10:after{content:var(--tw-content);z-index:10}.after\:h-11:after{content:var(--tw-content);height:2.75rem}.after\:-translate-y-1\/2:after{content:var(--tw-content);--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.empty\:hidden:empty{display:none}.hover\:bg-accent\/10:hover{background-color:rgb(var(--mu-accent)/.1)}.hover\:bg-accent\/15:hover{background-color:rgb(var(--mu-accent)/.15)}.hover\:bg-accent\/25:hover{background-color:rgb(var(--mu-accent)/.25)}.hover\:bg-accent\/40:hover{background-color:rgb(var(--mu-accent)/.4)}.hover\:bg-accent\/90:hover{background-color:rgb(var(--mu-accent)/.9)}.hover\:bg-danger\/90:hover{background-color:rgb(var(--mu-danger)/.9)}.hover\:bg-s1\/80:hover{background-color:rgb(var(--mu-s1)/.8)}.hover\:bg-s2:hover{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.hover\:bg-s2\/60:hover{background-color:rgb(var(--mu-s2)/.6)}.hover\:bg-s2\/80:hover{background-color:rgb(var(--mu-s2)/.8)}.hover\:bg-s3:hover{--tw-bg-opacity:1;background-color:rgb(var(--mu-s3)/var(--tw-bg-opacity,1))}.hover\:bg-s3\/80:hover{background-color:rgb(var(--mu-s3)/.8)}.hover\:bg-st-error-bg:hover{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-error-bg)/var(--tw-bg-opacity,1))}.hover\:bg-st-review-bg:hover{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-review-bg)/var(--tw-bg-opacity,1))}.hover\:bg-st-running-bg:hover{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-running-bg)/var(--tw-bg-opacity,1))}.hover\:text-accent:hover{--tw-text-opacity:1;color:rgb(var(--mu-accent)/var(--tw-text-opacity,1))}.hover\:text-fg:hover{--tw-text-opacity:1;color:rgb(var(--mu-text)/var(--tw-text-opacity,1))}.hover\:text-st-error-fg:hover{--tw-text-opacity:1;color:rgb(var(--mu-st-error-fg)/var(--tw-text-opacity,1))}.hover\:text-st-running-fg:hover{--tw-text-opacity:1;color:rgb(var(--mu-st-running-fg)/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:opacity-80:hover{opacity:.8}.hover\:file\:bg-s3::file-selector-button:hover{--tw-bg-opacity:1;background-color:rgb(var(--mu-s3)/var(--tw-bg-opacity,1))}.focus\:bg-accent\/60:focus{background-color:rgb(var(--mu-accent)/.6)}.focus\:bg-s2:focus{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-focus:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(var(--mu-focus)/var(--tw-ring-opacity,1))}.focus-visible\:bg-s2:focus-visible{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus-visible\:ring-inset:focus-visible{--tw-ring-inset:inset}.focus-visible\:ring-focus:focus-visible{--tw-ring-opacity:1;--tw-ring-color:rgb(var(--mu-focus)/var(--tw-ring-opacity,1))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px}.focus-visible\:ring-offset-bg:focus-visible{--tw-ring-offset-color:rgb(var(--mu-bg)/1)}.active\:scale-95:active{--tw-scale-x:.95;--tw-scale-y:.95}.active\:scale-95:active,.active\:scale-\[0\.98\]:active{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-\[0\.98\]:active{--tw-scale-x:0.98;--tw-scale-y:0.98}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}.disabled\:after\:hidden:disabled:after{content:var(--tw-content);display:none}.group\/pw[open] .group-open\/pw\:mb-2{margin-bottom:.5rem}.group\/adv[open] .group-open\/adv\:rotate-180{--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/code[open] .group-open\/code\:rotate-90,.group\/idle[open] .group-open\/idle\:rotate-90{--tw-rotate:90deg}.group\/code[open] .group-open\/code\:rotate-90,.group\/idle[open] .group-open\/idle\:rotate-90,.group\/pw[open] .group-open\/pw\:rotate-180{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/pw[open] .group-open\/pw\:rotate-180{--tw-rotate:180deg}.group\/s[open] .group-open\/s\:rotate-90{--tw-rotate:90deg}.group[open] .group-open\:rotate-180,.group\/s[open] .group-open\/s\:rotate-90{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group[open] .group-open\:rotate-180{--tw-rotate:180deg}.group:hover .group-hover\:bg-accent\/25{background-color:rgb(var(--mu-accent)/.25)}.group:hover .group-hover\:bg-accent\/90{background-color:rgb(var(--mu-accent)/.9)}.group:hover .group-hover\:bg-s2{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.group:hover .group-hover\:bg-s3{--tw-bg-opacity:1;background-color:rgb(var(--mu-s3)/var(--tw-bg-opacity,1))}.group:hover .group-hover\:text-fg2{--tw-text-opacity:1;color:rgb(var(--mu-text2)/var(--tw-text-opacity,1))}.peer:checked~.peer-checked\:block{display:block}.peer:checked~.peer-checked\:translate-x-0{--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.has-\[\:disabled\]\:cursor-not-allowed:has(:disabled){cursor:not-allowed}.has-\[\:checked\]\:bg-accent:has(:checked){--tw-bg-opacity:1;background-color:rgb(var(--mu-accent)/var(--tw-bg-opacity,1))}.has-\[\:checked\]\:text-on-accent:has(:checked){--tw-text-opacity:1;color:rgb(var(--mu-on-accent)/var(--tw-text-opacity,1))}.has-\[\:disabled\]\:opacity-50:has(:disabled){opacity:.5}.has-\[\:focus-visible\]\:ring-2:has(:focus-visible){--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.has-\[\:focus-visible\]\:ring-focus:has(:focus-visible){--tw-ring-opacity:1;--tw-ring-color:rgb(var(--mu-focus)/var(--tw-ring-opacity,1))}.aria-checked\:text-accent[aria-checked=true]{--tw-text-opacity:1;color:rgb(var(--mu-accent)/var(--tw-text-opacity,1))}.aria-disabled\:cursor-wait[aria-disabled=true]{cursor:wait}.aria-disabled\:opacity-60[aria-disabled=true]{opacity:.6}.aria-expanded\:bg-s2[aria-expanded=true]{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.aria-expanded\:bg-s3[aria-expanded=true]{--tw-bg-opacity:1;background-color:rgb(var(--mu-s3)/var(--tw-bg-opacity,1))}.aria-pressed\:bg-accent[aria-pressed=true]{--tw-bg-opacity:1;background-color:rgb(var(--mu-accent)/var(--tw-bg-opacity,1))}.aria-pressed\:text-on-accent[aria-pressed=true]{--tw-text-opacity:1;color:rgb(var(--mu-on-accent)/var(--tw-text-opacity,1))}.aria-selected\:border-accent[aria-selected=true]{--tw-border-opacity:1;border-color:rgb(var(--mu-accent)/var(--tw-border-opacity,1))}.aria-selected\:text-fg[aria-selected=true]{--tw-text-opacity:1;color:rgb(var(--mu-text)/var(--tw-text-opacity,1))}.group[aria-checked=true] .group-aria-checked\:visible{visibility:visible}.group[aria-expanded=true] .group-aria-expanded\:bg-s3{--tw-bg-opacity:1;background-color:rgb(var(--mu-s3)/var(--tw-bg-opacity,1))}.group[aria-expanded=true] .group-aria-expanded\:text-fg{--tw-text-opacity:1;color:rgb(var(--mu-text)/var(--tw-text-opacity,1))}.group[aria-pressed=true] .group-aria-pressed\:bg-accent\/15{background-color:rgb(var(--mu-accent)/.15)}.group[aria-pressed=true] .group-aria-pressed\:text-accent{--tw-text-opacity:1;color:rgb(var(--mu-accent)/var(--tw-text-opacity,1))}.group[aria-pressed=true] .group-aria-pressed\:ring-accent\/40{--tw-ring-color:rgb(var(--mu-accent)/0.4)}.data-\[active\=true\]\:bg-accent\/15[data-active=true]{background-color:rgb(var(--mu-accent)/.15)}.data-\[state\=recording\]\:bg-danger[data-state=recording]{--tw-bg-opacity:1;background-color:rgb(var(--mu-danger)/var(--tw-bg-opacity,1))}.data-\[active\=true\]\:text-fg[data-active=true]{--tw-text-opacity:1;color:rgb(var(--mu-text)/var(--tw-text-opacity,1))}.data-\[state\=recording\]\:text-on-accent[data-state=recording]{--tw-text-opacity:1;color:rgb(var(--mu-on-accent)/var(--tw-text-opacity,1))}.data-\[state\=recording\]\:ring-0[data-state=recording]{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.group[data-state=busy] .group-data-\[state\=busy\]\:block,.group[data-state=recording] .group-data-\[state\=recording\]\:block{display:block}.group[data-state=busy] .group-data-\[state\=busy\]\:hidden,.group[data-state=recording] .group-data-\[state\=recording\]\:hidden{display:none}@supports ((-webkit-backdrop-filter:var(--tw )) or (backdrop-filter:var(--tw ))){.supports-\[backdrop-filter\]\:bg-bg\/60{background-color:rgb(var(--mu-bg)/.6)}}@media (prefers-reduced-motion:no-preference){@keyframes ping{75%,to{transform:scale(2);opacity:0}}.motion-safe\:animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite}}@media not all and (min-width:640px){.max-sm\:hidden{display:none}.max-sm\:h-11{height:2.75rem}.max-sm\:w-11{width:2.75rem}.max-sm\:justify-center{justify-content:center}.max-sm\:px-0{padding-left:0;padding-right:0}}@media (min-width:640px){.sm\:not-sr-only{position:static;width:auto;height:auto;padding:0;margin:0;overflow:visible;clip:auto;white-space:normal}.sm\:m-auto{margin:auto}.sm\:inline{display:inline}.sm\:hidden{display:none}.sm\:w-\[28rem\]{width:28rem}.sm\:items-center{align-items:center}.sm\:rounded-2xl{border-radius:1rem}.sm\:pb-5{padding-bottom:1.25rem}}@media (min-width:1024px){.lg\:hidden{display:none}.lg\:max-w-5xl{max-width:64rem}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:pl-72{padding-left:18rem}}@media (min-width:1280px){.xl\:max-w-6xl{max-width:72rem}}@media (min-width:1536px){.\32xl\:max-w-\[110rem\]{max-width:110rem}}@media (min-width:2560px){.min-\[2560px\]\:max-w-\[150rem\]{max-width:150rem}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker,.\[\&\:\:-webkit-scrollbar\]\:hidden::-webkit-scrollbar{display:none}.\[\&\:not\(\[hidden\]\)\]\:flex:not([hidden]){display:flex}.\[\&\:popover-open\]\:flex:popover-open{display:flex}.\[\&\:popover-open\]\:grid:popover-open{display:grid}.\[\&\>\*\]\:pointer-events-auto>*{pointer-events:auto}.\[\&\[data-cg-unlaid\]\>\*\]\:invisible[data-cg-unlaid]>*,.\[\&\[data-cg-unlaid\]\]\:invisible[data-cg-unlaid],.\[\&\[data-compose-pending\]\]\:invisible[data-compose-pending],.\[\&\[data-switcher-pending\]\]\:invisible[data-switcher-pending]{visibility:hidden}.\[\&\[hidden\]\]\:hidden[hidden]{display:none}.\[\&\[open\]\]\:flex[open]{display:flex}.\[\&_\[data-task-title\]\]\:text-fg2 [data-task-title]{--tw-text-opacity:1;color:rgb(var(--mu-text2)/var(--tw-text-opacity,1))} \ No newline at end of file +/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.dark,:root{color-scheme:dark;--mu-bg:17 15 28;--mu-s1:25 22 42;--mu-s2:34 30 55;--mu-s3:45 40 70;--mu-line:47 42 72;--mu-edge:110 103 144;--mu-text:238 235 247;--mu-text2:195 189 217;--mu-muted:154 147 181;--mu-accent:228 181 86;--mu-on-accent:30 22 6;--mu-focus:242 203 120;--mu-danger:209 59 69;--mu-on-danger:255 255 255;--mu-st-open-fg:185 179 211;--mu-st-open-bg:36 32 58;--mu-st-progress-fg:143 184 255;--mu-st-progress-bg:27 37 71;--mu-st-review-fg:215 166 255;--mu-st-review-bg:46 31 69;--mu-st-complete-fg:127 214 164;--mu-st-complete-bg:22 52 40;--mu-st-error-fg:255 143 143;--mu-st-error-bg:61 26 34;--mu-st-warning-fg:255 174 94;--mu-st-warning-bg:58 38 21;--mu-st-running-fg:95 224 192;--mu-st-running-bg:18 51 47;--mu-idle-0:184 187 38;--mu-idle-1:152 151 26;--mu-idle-2:104 157 106;--mu-idle-3:215 153 33;--mu-idle-4:214 93 14;--mu-idle-5:204 36 29;--mu-idle-6:177 98 134;--mu-idle-7:102 92 84}:root:not(.dark){color-scheme:light;--mu-bg:246 244 251;--mu-s1:255 255 255;--mu-s2:238 235 246;--mu-s3:227 223 239;--mu-line:220 215 234;--mu-edge:125 118 153;--mu-text:26 22 48;--mu-text2:68 62 94;--mu-muted:98 92 125;--mu-accent:131 93 15;--mu-on-accent:255 255 255;--mu-focus:107 76 14;--mu-danger:180 38 48;--mu-on-danger:255 255 255;--mu-st-open-fg:79 72 112;--mu-st-open-bg:236 233 245;--mu-st-progress-fg:31 79 176;--mu-st-progress-bg:227 236 255;--mu-st-review-fg:116 40 181;--mu-st-review-bg:243 232 255;--mu-st-complete-fg:28 107 63;--mu-st-complete-bg:226 245 233;--mu-st-error-fg:180 35 47;--mu-st-error-bg:253 231 232;--mu-st-warning-fg:154 75 0;--mu-st-warning-bg:255 238 219;--mu-st-running-fg:13 107 90;--mu-st-running-bg:221 245 239;--mu-idle-0:184 187 38;--mu-idle-1:152 151 26;--mu-idle-2:104 157 106;--mu-idle-3:215 153 33;--mu-idle-4:214 93 14;--mu-idle-5:204 36 29;--mu-idle-6:177 98 134;--mu-idle-7:102 92 84}:root{--glyph-knock:rgb(var(--mu-s1))}[data-identity]{--id-bd:var(--id-bd-d);--id-tt:var(--id-tt-d)}:root:not(.dark) [data-identity]{--id-bd:var(--id-bd-l);--id-tt:var(--id-tt-l)}[hidden]{display:none!important}@media (max-width:639px){input:not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]),select,textarea{font-size:16px!important;min-height:44px!important}input[type=file]{font-size:16px!important;min-height:44px}summary{min-height:44px}}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.card-enter{animation:card-in .28s cubic-bezier(.16,1,.3,1) both,card-highlight 1.4s ease-out .12s both}.press{transition:transform 80ms ease;touch-action:manipulation}.press:active{transform:scale(.97)}.selecting .card-link:after{pointer-events:none}#session-drawer:focus-visible{position:fixed;top:.75rem;right:.75rem;z-index:60;width:2.75rem;height:2.75rem;margin:0;padding:0;overflow:visible;clip:auto;clip-path:none;white-space:normal;border-radius:.5rem;outline:2px solid rgb(var(--mu-focus));outline-offset:2px}#sidebar-root .sidebar-panel{transform:translateX(-100%);visibility:hidden;transition-property:transform,visibility}#sidebar-root .sidebar-backdrop{opacity:0;pointer-events:none}#sidebar-root[data-open=true] .sidebar-panel{transform:translateX(0);visibility:visible}#sidebar-root[data-open=true] .sidebar-backdrop{opacity:1;pointer-events:auto}@media (min-width:1024px){#sidebar-root:not([data-overlay]) .sidebar-panel{transform:translateX(0);visibility:visible}#sidebar-root:not([data-overlay]) .sidebar-backdrop{display:none}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-x-0{left:0;right:0}.inset-x-2{left:.5rem;right:.5rem}.inset-y-0{top:0;bottom:0}.-right-1{right:-.25rem}.-top-1{top:-.25rem}.bottom-0{bottom:0}.bottom-2\.5{bottom:.625rem}.bottom-4{bottom:1rem}.bottom-6{bottom:1.5rem}.bottom-\[calc\(1\.5rem\+env\(safe-area-inset-bottom\)\)\]{bottom:calc(1.5rem + env(safe-area-inset-bottom))}.bottom-\[calc\(6rem\+env\(safe-area-inset-bottom\)\)\]{bottom:calc(6rem + env(safe-area-inset-bottom))}.bottom-auto{bottom:auto}.bottom-full{bottom:100%}.left-0{left:0}.left-1\.5{left:.375rem}.left-1\/2{left:50%}.left-\[calc\(1\.5rem\+env\(safe-area-inset-left\)\)\]{left:calc(1.5rem + env(safe-area-inset-left))}.right-0{right:0}.right-3{right:.75rem}.right-\[calc\(1\.5rem\+env\(safe-area-inset-right\)\)\]{right:calc(1.5rem + env(safe-area-inset-right))}.right-\[calc\(env\(safe-area-inset-right\)\+0\.75rem\)\]{right:calc(env(safe-area-inset-right) + .75rem)}.top-0{top:0}.top-1\/2{top:50%}.top-2{top:.5rem}.top-\[calc\(env\(safe-area-inset-top\)\+0\.5rem\)\]{top:calc(env(safe-area-inset-top) + .5rem)}.top-\[max\(0\.5rem\2c 10vh\)\]{top:max(.5rem,10vh)}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[35\]{z-index:35}.m-0{margin:0}.m-auto{margin:auto}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0{margin-top:0;margin-bottom:0}.my-0\.5{margin-top:.125rem;margin-bottom:.125rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.-ml-1{margin-left:-.25rem}.-mr-0\.5{margin-right:-.125rem}.-mt-2{margin-top:-.5rem}.mb-0{margin-bottom:0}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1\.5{margin-left:.375rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-auto{margin-right:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.mt-auto{margin-top:auto}.line-clamp-1{-webkit-line-clamp:1}.line-clamp-1,.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical}.line-clamp-2{-webkit-line-clamp:2}.\!block{display:block!important}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-\[2\.8125rem\]{height:2.8125rem}.h-\[70dvh\]{height:70dvh}.h-\[calc\(2\.8125rem\+env\(safe-area-inset-bottom\)\)\]{height:calc(2.8125rem + env(safe-area-inset-bottom))}.h-\[var\(--mv-vh\2c 100dvh\)\]{height:var(--mv-vh,100dvh)}.h-dvh{height:100dvh}.h-full{height:100%}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-56{max-height:14rem}.max-h-8{max-height:2rem}.max-h-\[45vh\]{max-height:45vh}.max-h-\[60dvh\]{max-height:60dvh}.max-h-\[70dvh\]{max-height:70dvh}.max-h-\[70vh\]{max-height:70vh}.max-h-\[85dvh\]{max-height:85dvh}.max-h-\[min\(24rem\2c 60vh\)\]{max-height:min(24rem,60vh)}.min-h-0{min-height:0}.min-h-11{min-height:2.75rem}.min-h-\[2\.25rem\]{min-height:2.25rem}.min-h-\[2lh\]{min-height:2lh}.min-h-\[32px\]{min-height:32px}.min-h-\[36px\]{min-height:36px}.min-h-\[4\.25rem\]{min-height:4.25rem}.min-h-\[40px\]{min-height:40px}.min-h-\[44px\]{min-height:44px}.min-h-\[48px\]{min-height:48px}.min-h-\[calc\(100dvh-6rem\)\]{min-height:calc(100dvh - 6rem)}.min-h-\[calc\(2\.75rem\+1px\+env\(safe-area-inset-bottom\)\)\]{min-height:calc(2.75rem + 1px + env(safe-area-inset-bottom))}.min-h-dvh{min-height:100dvh}.w-1\.5{width:.375rem}.w-1\/2{width:50%}.w-1\/3{width:33.333333%}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-2\/3{width:66.666667%}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-3\/4{width:75%}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[22rem\]{width:22rem}.w-\[4\.5rem\]{width:4.5rem}.w-\[6\.25rem\]{width:6.25rem}.w-\[9\.75rem\]{width:9.75rem}.w-\[calc\(100\%-2rem\)\]{width:calc(100% - 2rem)}.w-\[min\(28rem\2c calc\(100vw-1rem\)\)\]{width:min(28rem,calc(100vw - 1rem))}.w-\[min\(36rem\2c calc\(100vw-1rem\)\)\]{width:min(36rem,calc(100vw - 1rem))}.w-\[min\(90vw\2c 32rem\)\]{width:min(90vw,32rem)}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[1\.25rem\]{min-width:1.25rem}.min-w-\[1\.5rem\]{min-width:1.5rem}.min-w-\[11rem\]{min-width:11rem}.min-w-\[12rem\]{min-width:12rem}.min-w-\[14rem\]{min-width:14rem}.min-w-\[36px\]{min-width:36px}.min-w-\[44px\]{min-width:44px}.min-w-\[6rem\]{min-width:6rem}.max-w-2xl{max-width:42rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-\[110rem\]{max-width:110rem}.max-w-\[12rem\]{max-width:12rem}.max-w-\[150rem\]{max-width:150rem}.max-w-\[16rem\]{max-width:16rem}.max-w-\[45\%\]{max-width:45%}.max-w-\[50\%\]{max-width:50%}.max-w-\[70ch\]{max-width:70ch}.max-w-\[85\%\]{max-width:85%}.max-w-\[85vw\]{max-width:85vw}.max-w-\[92\%\]{max-width:92%}.max-w-\[calc\(100vw-16px\)\]{max-width:calc(100vw - 16px)}.max-w-\[calc\(100vw-3rem\)\]{max-width:calc(100vw - 3rem)}.max-w-\[min\(90vw\2c 28rem\)\]{max-width:min(90vw,28rem)}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-none{flex:none}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.origin-bottom-left{transform-origin:bottom left}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.translate-x-full,.translate-y-2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-2{--tw-translate-y:0.5rem}.scale-95{--tw-scale-x:.95;--tw-scale-y:.95}.scale-95,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-bounce{animation:bounce 1s infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-col-resize{cursor:col-resize}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.select-all{-webkit-user-select:all;-moz-user-select:all;user-select:all}.resize-none{resize:none}.resize-y{resize:vertical}.resize{resize:both}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.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-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-1{-moz-column-gap:.25rem;column-gap:.25rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-1{row-gap:.25rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-line>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(var(--mu-line)/var(--tw-divide-opacity,1))}.self-start{align-self:flex-start}.self-end{align-self:flex-end}.self-center{align-self:center}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-hidden{overflow-y:hidden}.overscroll-contain{overscroll-behavior:contain}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-clip{text-overflow:clip}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-balance{text-wrap:balance}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-l-xl{border-top-left-radius:.75rem;border-bottom-left-radius:.75rem}.rounded-r-full{border-top-right-radius:9999px;border-bottom-right-radius:9999px}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.rounded-r-xl{border-top-right-radius:.75rem;border-bottom-right-radius:.75rem}.rounded-t-2xl{border-top-left-radius:1rem;border-top-right-radius:1rem}.border{border-width:1px}.border-0{border-width:0}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-l-4{border-left-width:4px}.border-t{border-top-width:1px}.border-accent\/30{border-color:rgb(var(--mu-accent)/.3)}.border-accent\/40{border-color:rgb(var(--mu-accent)/.4)}.border-accent\/60{border-color:rgb(var(--mu-accent)/.6)}.border-edge{--tw-border-opacity:1;border-color:rgb(var(--mu-edge)/var(--tw-border-opacity,1))}.border-line{--tw-border-opacity:1;border-color:rgb(var(--mu-line)/var(--tw-border-opacity,1))}.border-on-accent\/30{border-color:rgb(var(--mu-on-accent)/.3)}.border-st-warning-fg\/40{border-color:rgb(var(--mu-st-warning-fg)/.4)}.border-transparent{border-color:transparent}.border-l-\[rgb\(var\(--id-bd\)\)\]{border-left-color:rgb(var(--id-bd))}.border-l-transparent{border-left-color:transparent}.bg-accent{--tw-bg-opacity:1;background-color:rgb(var(--mu-accent)/var(--tw-bg-opacity,1))}.bg-accent\/10{background-color:rgb(var(--mu-accent)/.1)}.bg-accent\/15{background-color:rgb(var(--mu-accent)/.15)}.bg-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-bg)/var(--tw-bg-opacity,1))}.bg-bg\/40{background-color:rgb(var(--mu-bg)/.4)}.bg-bg\/50{background-color:rgb(var(--mu-bg)/.5)}.bg-bg\/60{background-color:rgb(var(--mu-bg)/.6)}.bg-bg\/80{background-color:rgb(var(--mu-bg)/.8)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-black\/60{background-color:rgba(0,0,0,.6)}.bg-current{background-color:currentColor}.bg-danger{--tw-bg-opacity:1;background-color:rgb(var(--mu-danger)/var(--tw-bg-opacity,1))}.bg-danger\/15{background-color:rgb(var(--mu-danger)/.15)}.bg-idle-0{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-0)/var(--tw-bg-opacity,1))}.bg-idle-1{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-1)/var(--tw-bg-opacity,1))}.bg-idle-2{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-2)/var(--tw-bg-opacity,1))}.bg-idle-3{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-3)/var(--tw-bg-opacity,1))}.bg-idle-4{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-4)/var(--tw-bg-opacity,1))}.bg-idle-5{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-5)/var(--tw-bg-opacity,1))}.bg-idle-6{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-6)/var(--tw-bg-opacity,1))}.bg-idle-7{--tw-bg-opacity:1;background-color:rgb(var(--mu-idle-7)/var(--tw-bg-opacity,1))}.bg-muted{--tw-bg-opacity:1;background-color:rgb(var(--mu-muted)/var(--tw-bg-opacity,1))}.bg-s1{--tw-bg-opacity:1;background-color:rgb(var(--mu-s1)/var(--tw-bg-opacity,1))}.bg-s1\/40{background-color:rgb(var(--mu-s1)/.4)}.bg-s1\/50{background-color:rgb(var(--mu-s1)/.5)}.bg-s1\/60{background-color:rgb(var(--mu-s1)/.6)}.bg-s1\/70{background-color:rgb(var(--mu-s1)/.7)}.bg-s2{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.bg-s2\/80{background-color:rgb(var(--mu-s2)/.8)}.bg-s3{--tw-bg-opacity:1;background-color:rgb(var(--mu-s3)/var(--tw-bg-opacity,1))}.bg-s3\/50{background-color:rgb(var(--mu-s3)/.5)}.bg-s3\/60{background-color:rgb(var(--mu-s3)/.6)}.bg-st-complete-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-complete-bg)/var(--tw-bg-opacity,1))}.bg-st-error-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-error-bg)/var(--tw-bg-opacity,1))}.bg-st-open-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-open-bg)/var(--tw-bg-opacity,1))}.bg-st-progress-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-progress-bg)/var(--tw-bg-opacity,1))}.bg-st-review-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-review-bg)/var(--tw-bg-opacity,1))}.bg-st-review-bg\/40{background-color:rgb(var(--mu-st-review-bg)/.4)}.bg-st-running-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-running-bg)/var(--tw-bg-opacity,1))}.bg-st-warning-bg{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-warning-bg)/var(--tw-bg-opacity,1))}.bg-transparent{background-color:transparent}.bg-gradient-to-l{background-image:linear-gradient(to left,var(--tw-gradient-stops))}.from-bg{--tw-gradient-from:rgb(var(--mu-bg)/1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(var(--mu-bg)/0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-bg\/80{--tw-gradient-to:rgb(var(--mu-bg)/0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgb(var(--mu-bg)/0.8) var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-28{padding-bottom:7rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-\[2\.8125rem\]{padding-bottom:2.8125rem}.pb-\[calc\(1\.25rem\+env\(safe-area-inset-bottom\)\)\]{padding-bottom:calc(1.25rem + env(safe-area-inset-bottom))}.pb-\[calc\(2\.8125rem\+env\(safe-area-inset-bottom\)\)\]{padding-bottom:calc(2.8125rem + env(safe-area-inset-bottom))}.pb-\[calc\(4rem\+env\(safe-area-inset-bottom\)\)\]{padding-bottom:calc(4rem + env(safe-area-inset-bottom))}.pb-\[calc\(7\.5rem\+env\(safe-area-inset-bottom\)\)\]{padding-bottom:calc(7.5rem + env(safe-area-inset-bottom))}.pb-\[env\(safe-area-inset-bottom\)\]{padding-bottom:env(safe-area-inset-bottom)}.pb-\[max\(0\.5rem\2c env\(safe-area-inset-bottom\)\)\]{padding-bottom:max(.5rem,env(safe-area-inset-bottom))}.pb-\[max\(0\.75rem\2c env\(safe-area-inset-bottom\)\)\]{padding-bottom:max(.75rem,env(safe-area-inset-bottom))}.pb-\[var\(--mv-compose-h\2c 4\.5rem\)\]{padding-bottom:var(--mv-compose-h,4.5rem)}.pb-\[var\(--mv-compose-h\2c 7rem\)\]{padding-bottom:var(--mv-compose-h,7rem)}.pl-11{padding-left:2.75rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pl-5{padding-left:1.25rem}.pl-72{padding-left:18rem}.pl-9{padding-left:2.25rem}.pr-1{padding-right:.25rem}.pr-2{padding-right:.5rem}.pr-28{padding-right:7rem}.pt-0{padding-top:0}.pt-1{padding-top:.25rem}.pt-10{padding-top:2.5rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.pt-\[env\(safe-area-inset-top\)\]{padding-top:env(safe-area-inset-top)}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[0\.7rem\]{font-size:.7rem}.text-\[0\.85em\]{font-size:.85em}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.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)}.leading-5{line-height:1.25rem}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.text-accent{--tw-text-opacity:1;color:rgb(var(--mu-accent)/var(--tw-text-opacity,1))}.text-current{color:currentColor}.text-fg{--tw-text-opacity:1;color:rgb(var(--mu-text)/var(--tw-text-opacity,1))}.text-fg2{--tw-text-opacity:1;color:rgb(var(--mu-text2)/var(--tw-text-opacity,1))}.text-muted{--tw-text-opacity:1;color:rgb(var(--mu-muted)/var(--tw-text-opacity,1))}.text-on-accent{--tw-text-opacity:1;color:rgb(var(--mu-on-accent)/var(--tw-text-opacity,1))}.text-on-danger{--tw-text-opacity:1;color:rgb(var(--mu-on-danger)/var(--tw-text-opacity,1))}.text-st-complete-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-complete-fg)/var(--tw-text-opacity,1))}.text-st-error-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-error-fg)/var(--tw-text-opacity,1))}.text-st-open-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-open-fg)/var(--tw-text-opacity,1))}.text-st-progress-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-progress-fg)/var(--tw-text-opacity,1))}.text-st-review-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-review-fg)/var(--tw-text-opacity,1))}.text-st-running-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-running-fg)/var(--tw-text-opacity,1))}.text-st-warning-fg{--tw-text-opacity:1;color:rgb(var(--mu-st-warning-fg)/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.decoration-accent\/40{text-decoration-color:rgb(var(--mu-accent)/.4)}.decoration-dotted{text-decoration-style:dotted}.underline-offset-2{text-underline-offset:2px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-\[0_-16px_16px_-4px_rgb\(var\(--mu-s1\)\)\]{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[0_-16px_16px_-4px_rgb\(var\(--mu-s1\)\)\]{--tw-shadow:0 -16px 16px -4px rgb(var(--mu-s1));--tw-shadow-colored:0 -16px 16px -4px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-accent\/20{--tw-shadow-color:rgb(var(--mu-accent)/0.2);--tw-shadow:var(--tw-shadow-colored)}.shadow-accent\/30{--tw-shadow-color:rgb(var(--mu-accent)/0.3);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/20{--tw-shadow-color:rgba(0,0,0,.2);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/30{--tw-shadow-color:rgba(0,0,0,.3);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/40{--tw-shadow-color:rgba(0,0,0,.4);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/50{--tw-shadow-color:rgba(0,0,0,.5);--tw-shadow:var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-accent\/20{--tw-ring-color:rgb(var(--mu-accent)/0.2)}.ring-accent\/30{--tw-ring-color:rgb(var(--mu-accent)/0.3)}.ring-accent\/40{--tw-ring-color:rgb(var(--mu-accent)/0.4)}.ring-accent\/50{--tw-ring-color:rgb(var(--mu-accent)/0.5)}.ring-bg{--tw-ring-opacity:1;--tw-ring-color:rgb(var(--mu-bg)/var(--tw-ring-opacity,1))}.ring-black\/20{--tw-ring-color:rgba(0,0,0,.2)}.ring-danger\/40{--tw-ring-color:rgb(var(--mu-danger)/0.4)}.ring-edge{--tw-ring-opacity:1;--tw-ring-color:rgb(var(--mu-edge)/var(--tw-ring-opacity,1))}.ring-line{--tw-ring-opacity:1;--tw-ring-color:rgb(var(--mu-line)/var(--tw-ring-opacity,1))}.ring-st-complete-fg\/40{--tw-ring-color:rgb(var(--mu-st-complete-fg)/0.4)}.ring-st-error-fg\/40{--tw-ring-color:rgb(var(--mu-st-error-fg)/0.4)}.ring-st-open-fg\/40{--tw-ring-color:rgb(var(--mu-st-open-fg)/0.4)}.ring-st-progress-fg\/40{--tw-ring-color:rgb(var(--mu-st-progress-fg)/0.4)}.ring-st-review-fg\/40{--tw-ring-color:rgb(var(--mu-st-review-fg)/0.4)}.ring-st-running-fg\/40{--tw-ring-color:rgb(var(--mu-st-running-fg)/0.4)}.ring-st-warning-fg\/40{--tw-ring-color:rgb(var(--mu-st-warning-fg)/0.4)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur:blur(8px)}.backdrop-blur,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[animation-delay\:-0\.15s\]{animation-delay:-.15s}.\[animation-delay\:-0\.3s\]{animation-delay:-.3s}.\[i\:j\]{i:j}.\[overflow-anchor\:none\]{overflow-anchor:none}.\[overflow-wrap\:anywhere\]{overflow-wrap:anywhere}.\[scrollbar-width\:none\]{scrollbar-width:none}@keyframes card-in{0%{opacity:0;transform:translateY(12px) scale(.985)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes card-out{0%{opacity:1;transform:translateY(0) scale(1);max-height:60rem}to{opacity:0;transform:translateY(-6px) scale(.985);max-height:0}}@keyframes card-highlight{0%{box-shadow:0 0 0 0 rgb(var(--mu-accent)/0)}25%{box-shadow:0 0 0 2px rgb(var(--mu-accent)/.5)}to{box-shadow:0 0 0 0 rgb(var(--mu-accent)/0)}}@media (prefers-reduced-motion:reduce){.card-enter,.htmx-swapping{animation:none}.press{transition:none}.sidebar-backdrop,.sidebar-panel{transition:none!important}}[data-clamp]{overflow:hidden;flex-shrink:0;transition:max-height .22s cubic-bezier(.2,.8,.2,1)}[data-clamp=closed]{cursor:pointer;-webkit-mask-image:linear-gradient(180deg,#000 calc(100% - 2.5rem),transparent);mask-image:linear-gradient(180deg,#000 calc(100% - 2.5rem),transparent)}@media (prefers-reduced-motion:reduce){[data-clamp]{transition:none}}[data-flash]{animation:card-highlight 1.4s ease-out both}@media (prefers-reduced-motion:reduce){[data-flash]{animation:none;box-shadow:0 0 0 2px rgb(var(--mu-accent)/.5)}}.composer-fit{max-height:var(--composer-max-h,40dvh);overflow-y:auto}@supports (field-sizing:content){.composer-fit{field-sizing:content}}[data-tool-group][open]>[data-tool-group-body]{animation:tool-group-open .18s cubic-bezier(.2,.8,.2,1) both}[data-tool-group-body]>[data-tool-row]{max-width:100%}@keyframes tool-group-open{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}[data-tool-group]>summary [data-tool-group-chevron]{transition:transform .18s ease}[data-tool-group][open]>summary [data-tool-group-chevron]{transform:rotate(90deg)}[data-tool-ctx]:not([hidden])+*{margin-left:0}@media (prefers-reduced-motion:reduce){[data-tool-group][open]>[data-tool-group-body]{animation:none}[data-tool-group]>summary [data-tool-group-chevron]{transition:none}}#switcher-search,#switcher-views{opacity:0;transition:opacity .16s cubic-bezier(.2,.8,.2,1),transform .16s cubic-bezier(.2,.8,.2,1),overlay allow-discrete .16s,display allow-discrete .16s}#switcher-views{transform:translateY(8px) scale(.96);transform-origin:bottom left}#switcher-search{transform:translateY(-8px) scale(.98);transform-origin:top center}#switcher-search:popover-open,#switcher-views:popover-open{opacity:1;transform:none}@starting-style{#switcher-views:popover-open{opacity:0;transform:translateY(8px) scale(.96)}#switcher-search:popover-open{opacity:0;transform:translateY(-8px) scale(.98)}}@media (prefers-reduced-motion:reduce){#switcher-search,#switcher-views{transition:none}}body:has(#agent-status:is([data-agent-status=running],[data-agent-status=provisioning])) #agent-actions [data-lifecycle=idle],body:not(:has(#agent-status:is([data-agent-status=running],[data-agent-status=provisioning]))) #agent-actions [data-lifecycle=live]{display:none}.marker\:content-\[\'\'\] ::marker{--tw-content:"";content:var(--tw-content)}.marker\:content-\[\'\'\]::marker{--tw-content:"";content:var(--tw-content)}.selection\:bg-accent\/30 ::-moz-selection{background-color:rgb(var(--mu-accent)/.3)}.selection\:bg-accent\/30 ::selection{background-color:rgb(var(--mu-accent)/.3)}.selection\:bg-accent\/30::-moz-selection{background-color:rgb(var(--mu-accent)/.3)}.selection\:bg-accent\/30::selection{background-color:rgb(var(--mu-accent)/.3)}.file\:mr-3::file-selector-button{margin-right:.75rem}.file\:rounded-lg::file-selector-button{border-radius:.5rem}.file\:border-0::file-selector-button{border-width:0}.file\:bg-s2::file-selector-button{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.file\:px-3::file-selector-button{padding-left:.75rem;padding-right:.75rem}.file\:py-1\.5::file-selector-button{padding-top:.375rem;padding-bottom:.375rem}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.file\:text-fg::file-selector-button{--tw-text-opacity:1;color:rgb(var(--mu-text)/var(--tw-text-opacity,1))}.placeholder\:text-fg2::-moz-placeholder{--tw-text-opacity:1;color:rgb(var(--mu-text2)/var(--tw-text-opacity,1))}.placeholder\:text-fg2::placeholder{--tw-text-opacity:1;color:rgb(var(--mu-text2)/var(--tw-text-opacity,1))}.placeholder\:text-muted::-moz-placeholder{--tw-text-opacity:1;color:rgb(var(--mu-muted)/var(--tw-text-opacity,1))}.placeholder\:text-muted::placeholder{--tw-text-opacity:1;color:rgb(var(--mu-muted)/var(--tw-text-opacity,1))}.backdrop\:bg-black\/40::backdrop{background-color:rgba(0,0,0,.4)}.backdrop\:bg-black\/60::backdrop{background-color:rgba(0,0,0,.6)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-0:after{content:var(--tw-content);inset:0}.after\:inset-x-0:after{content:var(--tw-content);left:0;right:0}.after\:top-1\/2:after{content:var(--tw-content);top:50%}.after\:z-10:after{content:var(--tw-content);z-index:10}.after\:h-11:after{content:var(--tw-content);height:2.75rem}.after\:-translate-y-1\/2:after{content:var(--tw-content);--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.empty\:hidden:empty{display:none}.hover\:bg-accent\/10:hover{background-color:rgb(var(--mu-accent)/.1)}.hover\:bg-accent\/15:hover{background-color:rgb(var(--mu-accent)/.15)}.hover\:bg-accent\/25:hover{background-color:rgb(var(--mu-accent)/.25)}.hover\:bg-accent\/40:hover{background-color:rgb(var(--mu-accent)/.4)}.hover\:bg-accent\/90:hover{background-color:rgb(var(--mu-accent)/.9)}.hover\:bg-danger\/90:hover{background-color:rgb(var(--mu-danger)/.9)}.hover\:bg-s1\/80:hover{background-color:rgb(var(--mu-s1)/.8)}.hover\:bg-s2:hover{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.hover\:bg-s2\/60:hover{background-color:rgb(var(--mu-s2)/.6)}.hover\:bg-s2\/80:hover{background-color:rgb(var(--mu-s2)/.8)}.hover\:bg-s3:hover{--tw-bg-opacity:1;background-color:rgb(var(--mu-s3)/var(--tw-bg-opacity,1))}.hover\:bg-s3\/80:hover{background-color:rgb(var(--mu-s3)/.8)}.hover\:bg-st-error-bg:hover{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-error-bg)/var(--tw-bg-opacity,1))}.hover\:bg-st-review-bg:hover{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-review-bg)/var(--tw-bg-opacity,1))}.hover\:bg-st-running-bg:hover{--tw-bg-opacity:1;background-color:rgb(var(--mu-st-running-bg)/var(--tw-bg-opacity,1))}.hover\:text-accent:hover{--tw-text-opacity:1;color:rgb(var(--mu-accent)/var(--tw-text-opacity,1))}.hover\:text-fg:hover{--tw-text-opacity:1;color:rgb(var(--mu-text)/var(--tw-text-opacity,1))}.hover\:text-st-error-fg:hover{--tw-text-opacity:1;color:rgb(var(--mu-st-error-fg)/var(--tw-text-opacity,1))}.hover\:text-st-running-fg:hover{--tw-text-opacity:1;color:rgb(var(--mu-st-running-fg)/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:opacity-80:hover{opacity:.8}.hover\:file\:bg-s3::file-selector-button:hover{--tw-bg-opacity:1;background-color:rgb(var(--mu-s3)/var(--tw-bg-opacity,1))}.focus\:bg-accent\/60:focus{background-color:rgb(var(--mu-accent)/.6)}.focus\:bg-s2:focus{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-focus:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(var(--mu-focus)/var(--tw-ring-opacity,1))}.focus-visible\:bg-s2:focus-visible{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus-visible\:ring-inset:focus-visible{--tw-ring-inset:inset}.focus-visible\:ring-focus:focus-visible{--tw-ring-opacity:1;--tw-ring-color:rgb(var(--mu-focus)/var(--tw-ring-opacity,1))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px}.focus-visible\:ring-offset-bg:focus-visible{--tw-ring-offset-color:rgb(var(--mu-bg)/1)}.active\:scale-95:active{--tw-scale-x:.95;--tw-scale-y:.95}.active\:scale-95:active,.active\:scale-\[0\.98\]:active{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-\[0\.98\]:active{--tw-scale-x:0.98;--tw-scale-y:0.98}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}.disabled\:after\:hidden:disabled:after{content:var(--tw-content);display:none}.group\/pw[open] .group-open\/pw\:mb-2{margin-bottom:.5rem}.group\/adv[open] .group-open\/adv\:rotate-180{--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/code[open] .group-open\/code\:rotate-90,.group\/idle[open] .group-open\/idle\:rotate-90{--tw-rotate:90deg}.group\/code[open] .group-open\/code\:rotate-90,.group\/idle[open] .group-open\/idle\:rotate-90,.group\/pw[open] .group-open\/pw\:rotate-180{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/pw[open] .group-open\/pw\:rotate-180{--tw-rotate:180deg}.group\/s[open] .group-open\/s\:rotate-90{--tw-rotate:90deg}.group[open] .group-open\:rotate-180,.group\/s[open] .group-open\/s\:rotate-90{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group[open] .group-open\:rotate-180{--tw-rotate:180deg}.group:hover .group-hover\:bg-accent\/25{background-color:rgb(var(--mu-accent)/.25)}.group:hover .group-hover\:bg-accent\/90{background-color:rgb(var(--mu-accent)/.9)}.group:hover .group-hover\:bg-s2{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.group:hover .group-hover\:bg-s3{--tw-bg-opacity:1;background-color:rgb(var(--mu-s3)/var(--tw-bg-opacity,1))}.group:hover .group-hover\:text-fg2{--tw-text-opacity:1;color:rgb(var(--mu-text2)/var(--tw-text-opacity,1))}.peer:checked~.peer-checked\:block{display:block}.peer:checked~.peer-checked\:translate-x-0{--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.has-\[\:disabled\]\:cursor-not-allowed:has(:disabled){cursor:not-allowed}.has-\[\:checked\]\:bg-accent:has(:checked){--tw-bg-opacity:1;background-color:rgb(var(--mu-accent)/var(--tw-bg-opacity,1))}.has-\[\:checked\]\:text-on-accent:has(:checked){--tw-text-opacity:1;color:rgb(var(--mu-on-accent)/var(--tw-text-opacity,1))}.has-\[\:disabled\]\:opacity-50:has(:disabled){opacity:.5}.has-\[\:focus-visible\]\:ring-2:has(:focus-visible){--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.has-\[\:focus-visible\]\:ring-focus:has(:focus-visible){--tw-ring-opacity:1;--tw-ring-color:rgb(var(--mu-focus)/var(--tw-ring-opacity,1))}.aria-checked\:text-accent[aria-checked=true]{--tw-text-opacity:1;color:rgb(var(--mu-accent)/var(--tw-text-opacity,1))}.aria-disabled\:cursor-wait[aria-disabled=true]{cursor:wait}.aria-disabled\:opacity-60[aria-disabled=true]{opacity:.6}.aria-expanded\:bg-s2[aria-expanded=true]{--tw-bg-opacity:1;background-color:rgb(var(--mu-s2)/var(--tw-bg-opacity,1))}.aria-expanded\:bg-s3[aria-expanded=true]{--tw-bg-opacity:1;background-color:rgb(var(--mu-s3)/var(--tw-bg-opacity,1))}.aria-pressed\:bg-accent[aria-pressed=true]{--tw-bg-opacity:1;background-color:rgb(var(--mu-accent)/var(--tw-bg-opacity,1))}.aria-pressed\:text-on-accent[aria-pressed=true]{--tw-text-opacity:1;color:rgb(var(--mu-on-accent)/var(--tw-text-opacity,1))}.aria-selected\:border-accent[aria-selected=true]{--tw-border-opacity:1;border-color:rgb(var(--mu-accent)/var(--tw-border-opacity,1))}.aria-selected\:text-fg[aria-selected=true]{--tw-text-opacity:1;color:rgb(var(--mu-text)/var(--tw-text-opacity,1))}.group[aria-checked=true] .group-aria-checked\:visible{visibility:visible}.group[aria-expanded=true] .group-aria-expanded\:bg-s3{--tw-bg-opacity:1;background-color:rgb(var(--mu-s3)/var(--tw-bg-opacity,1))}.group[aria-expanded=true] .group-aria-expanded\:text-fg{--tw-text-opacity:1;color:rgb(var(--mu-text)/var(--tw-text-opacity,1))}.group[aria-pressed=true] .group-aria-pressed\:bg-accent\/15{background-color:rgb(var(--mu-accent)/.15)}.group[aria-pressed=true] .group-aria-pressed\:text-accent{--tw-text-opacity:1;color:rgb(var(--mu-accent)/var(--tw-text-opacity,1))}.group[aria-pressed=true] .group-aria-pressed\:ring-accent\/40{--tw-ring-color:rgb(var(--mu-accent)/0.4)}.data-\[active\=true\]\:bg-accent\/15[data-active=true]{background-color:rgb(var(--mu-accent)/.15)}.data-\[state\=recording\]\:bg-danger[data-state=recording]{--tw-bg-opacity:1;background-color:rgb(var(--mu-danger)/var(--tw-bg-opacity,1))}.data-\[active\=true\]\:text-fg[data-active=true]{--tw-text-opacity:1;color:rgb(var(--mu-text)/var(--tw-text-opacity,1))}.data-\[state\=recording\]\:text-on-accent[data-state=recording]{--tw-text-opacity:1;color:rgb(var(--mu-on-accent)/var(--tw-text-opacity,1))}.data-\[state\=recording\]\:ring-0[data-state=recording]{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.group[data-state=busy] .group-data-\[state\=busy\]\:block,.group[data-state=recording] .group-data-\[state\=recording\]\:block{display:block}.group[data-state=busy] .group-data-\[state\=busy\]\:hidden,.group[data-state=recording] .group-data-\[state\=recording\]\:hidden{display:none}@supports ((-webkit-backdrop-filter:var(--tw )) or (backdrop-filter:var(--tw ))){.supports-\[backdrop-filter\]\:bg-bg\/60{background-color:rgb(var(--mu-bg)/.6)}}@media (prefers-reduced-motion:no-preference){@keyframes ping{75%,to{transform:scale(2);opacity:0}}.motion-safe\:animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite}}@media not all and (min-width:640px){.max-sm\:hidden{display:none}.max-sm\:h-11{height:2.75rem}.max-sm\:w-11{width:2.75rem}.max-sm\:justify-center{justify-content:center}.max-sm\:px-0{padding-left:0;padding-right:0}}@media (min-width:640px){.sm\:not-sr-only{position:static;width:auto;height:auto;padding:0;margin:0;overflow:visible;clip:auto;white-space:normal}.sm\:m-auto{margin:auto}.sm\:inline{display:inline}.sm\:hidden{display:none}.sm\:w-\[28rem\]{width:28rem}.sm\:items-center{align-items:center}.sm\:rounded-2xl{border-radius:1rem}.sm\:pb-5{padding-bottom:1.25rem}}@media (min-width:1024px){.lg\:hidden{display:none}.lg\:max-w-5xl{max-width:64rem}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:pl-72{padding-left:18rem}}@media (min-width:1280px){.xl\:max-w-6xl{max-width:72rem}}@media (min-width:1536px){.\32xl\:max-w-\[110rem\]{max-width:110rem}}@media (min-width:2560px){.min-\[2560px\]\:max-w-\[150rem\]{max-width:150rem}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker,.\[\&\:\:-webkit-scrollbar\]\:hidden::-webkit-scrollbar{display:none}.\[\&\:not\(\[hidden\]\)\]\:flex:not([hidden]){display:flex}.\[\&\:popover-open\]\:flex:popover-open{display:flex}.\[\&\:popover-open\]\:grid:popover-open{display:grid}.\[\&\>\*\]\:pointer-events-auto>*{pointer-events:auto}.\[\&\[data-cg-unlaid\]\>\*\]\:invisible[data-cg-unlaid]>*,.\[\&\[data-cg-unlaid\]\]\:invisible[data-cg-unlaid],.\[\&\[data-compose-pending\]\]\:invisible[data-compose-pending],.\[\&\[data-switcher-pending\]\]\:invisible[data-switcher-pending]{visibility:hidden}.\[\&\[hidden\]\]\:hidden[hidden]{display:none}.\[\&\[open\]\]\:flex[open]{display:flex}.\[\&_\[data-task-title\]\]\:text-fg2 [data-task-title]{--tw-text-opacity:1;color:rgb(var(--mu-text2)/var(--tw-text-opacity,1))} \ No newline at end of file