diff --git a/packages/tailwind-theme/theme.css b/packages/tailwind-theme/theme.css index 9f18ae0..f82c70e 100644 --- a/packages/tailwind-theme/theme.css +++ b/packages/tailwind-theme/theme.css @@ -103,9 +103,26 @@ background: radial-gradient(circle, rgba(251, 191, 36, 0.14), transparent 70%); } + :focus-visible { + outline: 2px solid var(--color-accent-400); + outline-offset: 3px; + } + } @layer utilities { + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border-width: 0; + } + .rounded, .rounded-sm, .rounded-md, diff --git a/src/AudioSharp.App/Components/Pages/Home.razor b/src/AudioSharp.App/Components/Pages/Home.razor index 62328bd..8a78a6c 100644 --- a/src/AudioSharp.App/Components/Pages/Home.razor +++ b/src/AudioSharp.App/Components/Pages/Home.razor @@ -28,10 +28,10 @@
Agentic audio to FHIR

Capture Patient Concerns

-

+

Record or upload audio, extract concerns, and generate FHIR R4 Observation bundles with a review step.

-
+
OpenRouter audio Text: @ProviderOptions.Value.TextProvider FHIR R4 Observations @@ -39,45 +39,54 @@
-
Table of contents
+
Table of contents
+
+
+ Progress + Step @CurrentStep of @TotalSteps +
+
+
+
+
-
    +
    1. - - 01 - - Capture + + 01 + + Capture
    2. - - 02 - - Extracted concerns + + 02 + + Extracted concerns
    3. - - 03 - - Follow-up questions + + 03 + + Follow-up questions
    4. - - 04 - - FHIR bundle + + 04 + + FHIR bundle
    5. @if (RecentRecords.Count > 0) {
    6. - - 05 - - Recent captures + + 05 + + Recent captures
    7. } @@ -111,53 +120,98 @@
-
+
+
-
+
Recording - @FormatTime(RecordingSeconds) / @FormatTime(MaxRecordingSeconds) - Max @MaxRecordingSeconds s + + @FormatTime(RecordingSeconds) / @FormatTime(MaxRecordingSeconds) + + Max @MaxRecordingSeconds s
+ @RecordingAnnouncement
- Upload audio + -

Recording produces 16 kHz mono WAV. For best results, upload WAV or MP3.

+ accept="audio/*" + aria-describedby="@AudioUploadDescribedBy" + aria-invalid="@(!string.IsNullOrWhiteSpace(UploadValidationMessage))" /> +

Recording produces 16 kHz mono WAV. For best results, upload WAV or MP3.

+ @if (!string.IsNullOrWhiteSpace(UploadValidationMessage)) + { + + }
-
- - @StatusMessage +
+
+ +
+
@StatusLabel
+
@StatusMessage
+
+
+ @bind-value:event="oninput" + aria-describedby="@TranscriptDescribedBy" + aria-invalid="@(!string.IsNullOrWhiteSpace(TranscriptValidationMessage))"> + @if (!string.IsNullOrWhiteSpace(TranscriptValidationMessage)) + { + + }
@@ -255,7 +310,7 @@ @if (!string.IsNullOrWhiteSpace(FollowUpStatusMessage)) { -
@FollowUpStatusMessage
+
@FollowUpStatusMessage
}
@@ -273,19 +328,19 @@ Send to FHIR
- @if (!string.IsNullOrWhiteSpace(FhirStatusMessage)) + @if (!string.IsNullOrWhiteSpace(FhirStatusMessage) || !IsFhirConfigured) { -
@FhirStatusMessage
- } - else if (!IsFhirConfigured) - { -
Configure FhirServer:BaseUrl to enable sending.
+
+ @(string.IsNullOrWhiteSpace(FhirStatusMessage) + ? "Configure FhirServer:BaseUrl to enable sending." + : FhirStatusMessage) +
}

FHIR Observation bundle

@if (string.IsNullOrWhiteSpace(BundleJson)) { -

Bundle JSON will appear here after extraction.

+

Bundle JSON will appear here after extraction.

} else { @@ -302,7 +357,7 @@ {
@record.CreatedAtUtc.ToLocalTime().ToString("g")
-
@(record.SubjectDisplay ?? record.SubjectReference ?? "Unknown subject")
+
@(record.SubjectDisplay ?? record.SubjectReference ?? "Unknown subject")
}
@@ -313,15 +368,30 @@ @code { private string SubjectReference { get; set; } = string.Empty; private string SubjectDisplay { get; set; } = string.Empty; - private string Transcript { get; set; } = string.Empty; + private string _transcript = string.Empty; + private string Transcript + { + get => _transcript; + set + { + _transcript = value; + TranscriptValidationMessage = string.Empty; + } + } private string StatusMessage { get; set; } = "Ready."; - private string StatusState { get; set; } = "idle"; + private string StatusState { get; set; } = "ready"; private string BundleJson { get; set; } = string.Empty; private string FhirStatusMessage { get; set; } = string.Empty; + private string FhirStatusState { get; set; } = "idle"; private string FollowUpStatusMessage { get; set; } = string.Empty; + private string UploadValidationMessage { get; set; } = string.Empty; + private string TranscriptValidationMessage { get; set; } = string.Empty; + private string RecordingAnnouncement { get; set; } = string.Empty; private int RecordingSeconds { get; set; } + private int WorkflowStep { get; set; } = 1; private bool IsRecording { get; set; } + private bool IsRecordingPaused { get; set; } private bool IsProcessing { get; set; } private bool IsSendingToFhir { get; set; } @@ -339,6 +409,34 @@ private bool CanGenerateFollowUps => !IsProcessing && !IsRecording && Concerns.Count > 0; private bool CanApplyFollowUps => !IsProcessing && !IsRecording && FollowUpEntries.Any(entry => !string.IsNullOrWhiteSpace(entry.Answer)); + private int TotalSteps => 4; + private int CurrentStep => + TotalSteps > 0 ? Math.Clamp(WorkflowStep, 1, TotalSteps) : 0; + private int ProgressPercent => + TotalSteps > 0 + ? (int)Math.Round(CurrentStep / (double)TotalSteps * 100, MidpointRounding.AwayFromZero) + : 0; + private string StatusLabel => StatusState switch + { + "recording" => "Recording active", + "paused" => "Recording paused", + "processing" => "Processing", + "ready" => "Ready", + "error" => "Action needed", + _ => "Idle" + }; + private string StatusRole => StatusState == "error" ? "alert" : "status"; + private string StatusLive => StatusState == "error" ? "assertive" : "polite"; + private string FhirStatusRole => FhirStatusState == "error" ? "alert" : "status"; + private string FhirStatusLive => FhirStatusState == "error" ? "assertive" : "polite"; + private string RecordingTimerLabel => + $"Recording time {FormatTime(RecordingSeconds)} of {FormatTime(MaxRecordingSeconds)}"; + private string AudioUploadDescribedBy => + string.IsNullOrWhiteSpace(UploadValidationMessage) + ? "audioUploadHelp" + : "audioUploadHelp audioUploadError"; + private string? TranscriptDescribedBy => + string.IsNullOrWhiteSpace(TranscriptValidationMessage) ? null : "transcriptValidation"; protected override async Task OnInitializedAsync() { @@ -347,16 +445,23 @@ private async Task StartRecordingAsync() { + ClearValidationMessages(); + FhirStatusMessage = string.Empty; + FhirStatusState = "idle"; + WorkflowStep = 1; + try { await JsRuntime.InvokeVoidAsync("audioRecorder.start"); IsRecording = true; + IsRecordingPaused = false; RecordingSeconds = 0; - FhirStatusMessage = string.Empty; ResetFollowUps(); StartRecordingTimer(); StatusMessage = "Recording..."; StatusState = "recording"; + RecordingAnnouncement = "Recording started."; + await PlayStatusCueAsync("start"); } catch (Exception ex) { @@ -373,12 +478,16 @@ } IsRecording = false; + IsRecordingPaused = false; + ClearValidationMessages(); ResetProcessing(); ResetFollowUps(); IsProcessing = true; StopRecordingTimer(); StatusMessage = "Processing audio..."; StatusState = "processing"; + RecordingAnnouncement = $"Recording stopped at {FormatTime(RecordingSeconds)}."; + await PlayStatusCueAsync("stop"); try { @@ -391,7 +500,7 @@ if (string.IsNullOrWhiteSpace(response.Transcript)) { StatusMessage = "No audio captured."; - StatusState = "idle"; + StatusState = "error"; return; } @@ -401,6 +510,7 @@ BundleJson = response.BundleJson; StatusMessage = "Analysis complete."; StatusState = "ready"; + WorkflowStep = 2; } catch (OperationCanceledException) { @@ -418,6 +528,58 @@ } } + private async Task ToggleRecordingPauseAsync() + { + if (!IsRecording || IsProcessing) + { + return; + } + + if (IsRecordingPaused) + { + await ResumeRecordingAsync(); + return; + } + + await PauseRecordingAsync(); + } + + private async Task PauseRecordingAsync() + { + try + { + await JsRuntime.InvokeVoidAsync("audioRecorder.pause"); + IsRecordingPaused = true; + StatusMessage = "Recording paused."; + StatusState = "paused"; + RecordingAnnouncement = $"Recording paused at {FormatTime(RecordingSeconds)}."; + await PlayStatusCueAsync("pause"); + } + catch (Exception ex) + { + StatusMessage = $"Pause failed: {ex.Message}"; + StatusState = "error"; + } + } + + private async Task ResumeRecordingAsync() + { + try + { + await JsRuntime.InvokeVoidAsync("audioRecorder.resume"); + IsRecordingPaused = false; + StatusMessage = "Recording resumed."; + StatusState = "recording"; + RecordingAnnouncement = $"Recording resumed at {FormatTime(RecordingSeconds)}."; + await PlayStatusCueAsync("resume"); + } + catch (Exception ex) + { + StatusMessage = $"Resume failed: {ex.Message}"; + StatusState = "error"; + } + } + private async Task OnFileSelectedAsync(InputFileChangeEventArgs args) { var file = args.File; @@ -426,9 +588,20 @@ return; } + ClearValidationMessages(); + if (file.Size > MaxAudioBytes) + { + UploadValidationMessage = $"File exceeds the {FormatBytes(MaxAudioBytes)} limit."; + StatusMessage = "Upload validation failed."; + StatusState = "error"; + return; + } + + WorkflowStep = 1; ResetProcessing(); ResetFollowUps(); FhirStatusMessage = string.Empty; + FhirStatusState = "idle"; StatusMessage = "Processing audio file..."; StatusState = "processing"; @@ -452,13 +625,18 @@ { if (string.IsNullOrWhiteSpace(Transcript)) { + TranscriptValidationMessage = "Transcript is required before extraction."; + StatusMessage = TranscriptValidationMessage; + StatusState = "error"; return; } + ClearValidationMessages(); ResetProcessing(); IsProcessing = true; ResetFollowUps(); FhirStatusMessage = string.Empty; + FhirStatusState = "idle"; StatusMessage = "Extracting concerns..."; StatusState = "processing"; @@ -470,6 +648,7 @@ UpdateResults(extraction); StatusMessage = "Extraction complete."; StatusState = "ready"; + WorkflowStep = 2; } catch (OperationCanceledException) { @@ -489,6 +668,7 @@ private async Task ProcessAudioAsync(AudioInput audioInput) { + ClearValidationMessages(); ResetProcessing(); IsProcessing = true; ResetFollowUps(); @@ -508,6 +688,7 @@ BundleJson = JsonSerializer.Serialize(result.Bundle, JsonOptions); StatusMessage = "Analysis complete."; StatusState = "ready"; + WorkflowStep = 2; } catch (OperationCanceledException) { @@ -574,6 +755,7 @@ private void UpdateBundle(IReadOnlyList concerns) { FhirStatusMessage = string.Empty; + FhirStatusState = "idle"; var context = new ProcessingContext( string.IsNullOrWhiteSpace(SubjectReference) ? null : SubjectReference, @@ -591,6 +773,7 @@ return; } + ClearValidationMessages(); ResetProcessing(); IsProcessing = true; FollowUpStatusMessage = "Generating follow-up questions..."; @@ -613,6 +796,7 @@ : "Answer the questions and apply the updates."; StatusMessage = "Follow-up questions ready."; StatusState = "ready"; + WorkflowStep = FollowUpEntries.Count == 0 ? 4 : 3; } catch (OperationCanceledException) { @@ -639,6 +823,7 @@ return; } + ClearValidationMessages(); ResetProcessing(); IsProcessing = true; FollowUpStatusMessage = "Applying follow-up answers..."; @@ -665,6 +850,7 @@ FollowUpStatusMessage = "Concerns updated with follow-up answers."; StatusMessage = "Concerns updated."; StatusState = "ready"; + WorkflowStep = 4; } catch (OperationCanceledException) { @@ -693,6 +879,7 @@ IsSendingToFhir = true; FhirStatusMessage = "Sending bundle to FHIR server..."; + FhirStatusState = "processing"; try { @@ -704,10 +891,13 @@ ? "FHIR server accepted the bundle." : $"FHIR server accepted the bundle: {result.Location}") : $"FHIR server returned {result.StatusCode}."; + FhirStatusState = result.Success ? "ready" : "error"; + WorkflowStep = 4; } catch (Exception ex) { FhirStatusMessage = $"FHIR send failed: {ex.Message}"; + FhirStatusState = "error"; } finally { @@ -715,6 +905,81 @@ } } + private string GetTocLinkClass(int step) + { + var tone = CurrentStep == step + ? "text-ink-900" + : "text-ink-700 hover:text-ink-900"; + return $"group flex items-center gap-3 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-200 {tone}"; + } + + private string GetTocNumberClass(int step) => + CurrentStep == step ? "text-[12px] text-ink-900" : "text-[12px] text-ink-700"; + + private string GetTocDotClass(int step) => + CurrentStep == step + ? "mt-0.5 h-2.5 w-2.5 rounded-full bg-brand-500 shadow-raise transition" + : "mt-0.5 h-1.5 w-1.5 rounded-full bg-brand-500/60 transition group-hover:bg-brand-500"; + + private string GetTocLabelClass(int step) => + CurrentStep == step + ? "flex-1 border-b border-brand-300/80 pb-2 text-ink-900" + : "flex-1 border-b border-black/20 pb-2 text-ink-700"; + + private string GetPauseButtonClass() + { + var baseClass = "inline-flex items-center justify-center rounded-full border px-4 py-2 text-[12px] font-semibold uppercase tracking-[0.2em] transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-200 disabled:cursor-not-allowed disabled:opacity-40"; + return IsRecordingPaused + ? $"{baseClass} border-amber-400 bg-amber-400/10 text-ink-900" + : $"{baseClass} border-black/20 bg-surface text-ink-700 hover:border-black/40 hover:text-ink-900"; + } + + private string GetFhirStatusClass() + { + const string baseClass = "mt-3 rounded-2xl border px-4 py-3 text-sm font-semibold"; + return FhirStatusState switch + { + "processing" => $"{baseClass} border-brand-300/80 bg-brand-500/10 text-ink-900", + "ready" => $"{baseClass} border-emerald-400 bg-emerald-400/10 text-ink-900", + "error" => $"{baseClass} border-red-600 bg-red-600/10 text-red-600", + _ => $"{baseClass} border-black/20 bg-surface-soft text-ink-700" + }; + } + + private void ClearValidationMessages() + { + UploadValidationMessage = string.Empty; + TranscriptValidationMessage = string.Empty; + } + + private async Task PlayStatusCueAsync(string cue) + { + try + { + await JsRuntime.InvokeVoidAsync("audioRecorder.playCue", cue); + } + catch (Exception ex) + { + // Log but don't block capture flow on audio cue failures. + System.Console.Error.WriteLine($"Audio cue '{cue}' failed: {ex.Message}"); + } + } + + private static string FormatBytes(long bytes) + { + const double scale = 1024; + var sizes = new[] { "B", "KB", "MB", "GB" }; + var order = 0; + var len = (double)bytes; + while (len >= scale && order < sizes.Length - 1) + { + order++; + len /= scale; + } + + return $"{len:0.#} {sizes[order]}"; + } + private void CancelProcessing() { ProcessingCts?.Cancel(); @@ -760,7 +1025,17 @@ return; } + if (IsRecordingPaused) + { + await InvokeAsync(StateHasChanged); + continue; + } + RecordingSeconds++; + if (RecordingSeconds % 15 == 0) + { + RecordingAnnouncement = $"Recording time {FormatTime(RecordingSeconds)}."; + } await InvokeAsync(StateHasChanged); if (RecordingSeconds >= MaxRecordingSeconds) diff --git a/src/AudioSharp.App/wwwroot/app.css b/src/AudioSharp.App/wwwroot/app.css index 21446d4..147fa83 100644 --- a/src/AudioSharp.App/wwwroot/app.css +++ b/src/AudioSharp.App/wwwroot/app.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */ -@import "https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&display=swap";@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}@layer theme{:root,:host{--font-sans:"Noto Sans","Segoe UI",system-ui,sans-serif;--font-mono:"Noto Sans","Segoe UI",system-ui,sans-serif;--color-red-600:oklch(57.7% .245 27.325);--color-amber-400:oklch(82.8% .189 84.429);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-semibold:600;--tracking-tight:-.025em;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink-950:#111827;--color-ink-900:#1f2937;--color-ink-700:#4b5563;--color-ink-500:#6b7280;--color-brand-200:#fde68a;--color-brand-300:#fcd34d;--color-brand-400:#fbbf24;--color-brand-500:#f59e0b;--color-brand-600:#d97706;--color-accent-200:#fde68a;--color-accent-400:#f59e0b;--color-surface:#fff;--color-surface-soft:#f8fafc;--radius-card:0px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root,.dark{color-scheme:light;--background:#f8fafc;--foreground:var(--color-ink-950);--card:var(--color-surface);--card-foreground:var(--color-ink-950);--popover:var(--color-surface);--popover-foreground:var(--color-ink-950);--primary:var(--color-brand-500);--primary-foreground:#fff;--secondary:var(--color-surface-soft);--secondary-foreground:var(--color-ink-950);--muted:var(--color-surface-soft);--muted-foreground:var(--color-ink-700);--accent:var(--color-accent-400);--accent-foreground:#fff;--destructive:#ef4444;--destructive-foreground:#f8fafc;--border:#e5e7eb;--input:#e5e7eb;--ring:var(--color-accent-400);--radius:0px;--radius-sm:0px;--radius-md:0px;--radius-lg:0px;--radius-xl:0px;--radius-2xl:0px;--radius-3xl:0px;--font-sans:"Noto Sans","Segoe UI",system-ui,sans-serif;--font-display:"Noto Sans","Segoe UI",system-ui,sans-serif;--font-mono:"Noto Sans","Segoe UI",system-ui,sans-serif}html,body{height:100%}body{font-family:var(--font-sans);color:var(--color-ink-950);background:radial-gradient(circle at 12% 16%,#f59e0b14,#0000 60%),radial-gradient(circle at 86% 12%,#fbbf241a,#0000 58%),linear-gradient(#f9fafb 0%,#fff 55%,#f8fafc 100%);margin:0}body:before,body:after{content:"";z-index:0;pointer-events:none;border-radius:999px;position:fixed}body:before{background:radial-gradient(circle,#f59e0b29,#0000 70%);width:320px;height:320px;top:-120px;right:-140px}body:after{background:radial-gradient(circle,#fbbf2424,#0000 70%);width:300px;height:300px;bottom:-140px;left:-140px}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.top-2{top:calc(var(--spacing)*2)}.right-0{right:calc(var(--spacing)*0)}.right-4{right:calc(var(--spacing)*4)}.bottom-4{bottom:calc(var(--spacing)*4)}.left-1{left:calc(var(--spacing)*1)}.left-4{left:calc(var(--spacing)*4)}.z-10{z-index:10}.z-30{z-index:30}.z-50{z-index:50}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-5{margin-top:calc(var(--spacing)*5)}.mt-8{margin-top:calc(var(--spacing)*8)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.h-1\.5{height:calc(var(--spacing)*1.5)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-\[calc\(100\%-0\.5rem\)\]{height:calc(100% - .5rem)}.max-h-80{max-height:calc(var(--spacing)*80)}.min-h-screen{min-height:100vh}.w-1\.5{width:calc(var(--spacing)*1.5)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-44{width:calc(var(--spacing)*44)}.w-full{width:100%}.w-px{width:1px}.max-w-6xl{max-width:var(--container-6xl)}.flex-1{flex:1}.cursor-pointer{cursor:pointer}.scroll-mt-24{scroll-margin-top:calc(var(--spacing)*24)}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.gap-10{gap:calc(var(--spacing)*10)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.overflow-auto{overflow:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-card{border-radius:var(--radius-card)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-black\/20{border-color:#0003}@supports (color:color-mix(in lab, red, red)){.border-black\/20{border-color:color-mix(in oklab,var(--color-black)20%,transparent)}}.border-brand-300\/40{border-color:#fcd34d66}@supports (color:color-mix(in lab, red, red)){.border-brand-300\/40{border-color:color-mix(in oklab,var(--color-brand-300)40%,transparent)}}.border-brand-300\/80{border-color:#fcd34dcc}@supports (color:color-mix(in lab, red, red)){.border-brand-300\/80{border-color:color-mix(in oklab,var(--color-brand-300)80%,transparent)}}.border-emerald-400{border-color:var(--color-emerald-400)}.border-rose-400{border-color:var(--color-rose-400)}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.bg-black\/20{background-color:#0003}@supports (color:color-mix(in lab, red, red)){.bg-black\/20{background-color:color-mix(in oklab,var(--color-black)20%,transparent)}}.bg-brand-500{background-color:var(--color-brand-500)}.bg-brand-500\/10{background-color:#f59e0b1a}@supports (color:color-mix(in lab, red, red)){.bg-brand-500\/10{background-color:color-mix(in oklab,var(--color-brand-500)10%,transparent)}}.bg-brand-500\/60{background-color:#f59e0b99}@supports (color:color-mix(in lab, red, red)){.bg-brand-500\/60{background-color:color-mix(in oklab,var(--color-brand-500)60%,transparent)}}.bg-brand-500\/80{background-color:#f59e0bcc}@supports (color:color-mix(in lab, red, red)){.bg-brand-500\/80{background-color:color-mix(in oklab,var(--color-brand-500)80%,transparent)}}.bg-ink-950{background-color:var(--color-ink-950)}.bg-ink-950\/95{background-color:#111827f2}@supports (color:color-mix(in lab, red, red)){.bg-ink-950\/95{background-color:color-mix(in oklab,var(--color-ink-950)95%,transparent)}}.bg-surface{background-color:var(--color-surface)}.bg-surface-soft{background-color:var(--color-surface-soft)}.bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.bg-white\/10{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-5{padding:calc(var(--spacing)*5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.pt-4{padding-top:calc(var(--spacing)*4)}.pt-8{padding-top:calc(var(--spacing)*8)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-16{padding-bottom:calc(var(--spacing)*16)}.pl-3{padding-left:calc(var(--spacing)*3)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.leading-\[1\.05\]{--tw-leading:1.05;line-height:1.05}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[-0\.02em\]{--tw-tracking:-.02em;letter-spacing:-.02em}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-\[0\.3em\]{--tw-tracking:.3em;letter-spacing:.3em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.text-brand-300{color:var(--color-brand-300)}.text-brand-500{color:var(--color-brand-500)}.text-ink-500{color:var(--color-ink-500)}.text-ink-700{color:var(--color-ink-700)}.text-ink-900{color:var(--color-ink-900)}.text-ink-950{color:var(--color-ink-950)}.text-red-600{color:var(--color-red-600)}.text-white\/60{color:#fff9}@supports (color:color-mix(in lab, red, red)){.text-white\/60{color:color-mix(in oklab,var(--color-white)60%,transparent)}}.text-white\/70{color:#ffffffb3}@supports (color:color-mix(in lab, red, red)){.text-white\/70{color:color-mix(in oklab,var(--color-white)70%,transparent)}}.text-white\/80{color:#fffc}@supports (color:color-mix(in lab, red, red)){.text-white\/80{color:color-mix(in oklab,var(--color-white)80%,transparent)}}.text-white\/90{color:#ffffffe6}@supports (color:color-mix(in lab, red, red)){.text-white\/90{color:color-mix(in oklab,var(--color-white)90%,transparent)}}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.decoration-brand-300\/70{text-decoration-color:#fcd34db3}@supports (color:color-mix(in lab, red, red)){.decoration-brand-300\/70{-webkit-text-decoration-color:color-mix(in oklab,var(--color-brand-300)70%,transparent);-webkit-text-decoration-color:color-mix(in oklab,var(--color-brand-300)70%,transparent);text-decoration-color:color-mix(in oklab,var(--color-brand-300)70%,transparent)}}.underline-offset-4{text-underline-offset:4px}.shadow-card{--tw-shadow:0 18px 40px -25px var(--tw-shadow-color,#1118271f);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-raise{--tw-shadow:0 12px 22px -14px var(--tw-shadow-color,#f59e0b59);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.group-hover\:bg-brand-500:is(:where(.group):hover *){background-color:var(--color-brand-500)}}.file\:mr-4::file-selector-button{margin-right:calc(var(--spacing)*4)}.file\:rounded-none::file-selector-button{border-radius:0}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-brand-500::file-selector-button{background-color:var(--color-brand-500)}.file\:px-4::file-selector-button{padding-inline:calc(var(--spacing)*4)}.file\:py-2::file-selector-button{padding-block:calc(var(--spacing)*2)}.file\:text-xs::file-selector-button{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.file\:font-semibold::file-selector-button{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.file\:text-ink-950::file-selector-button{color:var(--color-ink-950)}.placeholder\:text-ink-500::placeholder{color:var(--color-ink-500)}@media (hover:hover){.hover\:border-black\/40:hover{border-color:#0006}@supports (color:color-mix(in lab, red, red)){.hover\:border-black\/40:hover{border-color:color-mix(in oklab,var(--color-black)40%,transparent)}}.hover\:bg-brand-400:hover{background-color:var(--color-brand-400)}.hover\:text-ink-900:hover{color:var(--color-ink-900)}.hover\:text-white:hover{color:var(--color-white)}.hover\:file\:bg-brand-400:hover::file-selector-button{background-color:var(--color-brand-400)}}.focus\:border-accent-400:focus{border-color:var(--color-accent-400)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-accent-200:focus{--tw-ring-color:var(--color-accent-200)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-accent-200:focus-visible{--tw-ring-color:var(--color-accent-200)}.focus-visible\:ring-brand-200:focus-visible{--tw-ring-color:var(--color-brand-200)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.data-\[state\=error\]\:bg-red-600[data-state=error]{background-color:var(--color-red-600)}.data-\[state\=error\]\:shadow-\[0_0_0_6px_rgba\(220\,38\,38\,0\.2\)\][data-state=error]{--tw-shadow:0 0 0 6px var(--tw-shadow-color,#dc262633);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=processing\]\:bg-amber-400[data-state=processing]{background-color:var(--color-amber-400)}.data-\[state\=processing\]\:shadow-\[0_0_0_6px_rgba\(251\,191\,36\,0\.25\)\][data-state=processing]{--tw-shadow:0 0 0 6px var(--tw-shadow-color,#fbbf2440);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=ready\]\:bg-emerald-500[data-state=ready]{background-color:var(--color-emerald-500)}.data-\[state\=ready\]\:shadow-\[0_0_0_6px_rgba\(16\,185\,129\,0\.2\)\][data-state=ready]{--tw-shadow:0 0 0 6px var(--tw-shadow-color,#10b98133);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=recording\]\:bg-rose-500[data-state=recording]{background-color:var(--color-rose-500)}.data-\[state\=recording\]\:shadow-\[0_0_0_6px_rgba\(244\,63\,94\,0\.2\)\][data-state=recording]{--tw-shadow:0 0 0 6px var(--tw-shadow-color,#f43f5e33);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media (min-width:40rem){.sm\:right-6{right:calc(var(--spacing)*6)}.sm\:left-auto{left:auto}.sm\:ml-auto{margin-left:auto}.sm\:w-auto{width:auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:p-5{padding:calc(var(--spacing)*5)}.sm\:px-8{padding-inline:calc(var(--spacing)*8)}.sm\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}}@media (min-width:48rem){.md\:flex{display:flex}.md\:hidden{display:none}.md\:flex-row{flex-direction:row}.md\:items-start{align-items:flex-start}.md\:justify-between{justify-content:space-between}}@media (min-width:64rem){.lg\:grid-cols-\[minmax\(0\,320px\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,320px) minmax(0,1fr)}.lg\:px-12{padding-inline:calc(var(--spacing)*12)}.lg\:text-\[3\.75rem\]{font-size:3.75rem}}@media (min-width:80rem){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}.rounded,.rounded-sm,.rounded-md,.rounded-lg,.rounded-xl,.rounded-2xl,.rounded-3xl,.rounded-full,.rounded-card{border-radius:0}.file\:rounded-full::file-selector-button{border-radius:0}.animate-rise{animation:.7s cubic-bezier(.16,1,.3,1) both rise-in}.animate-float{animation:10s ease-in-out infinite float}}@keyframes rise-in{0%{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}@keyframes float{0%{transform:translateY(0)}50%{transform:translateY(-12px)}to{transform:translateY(0)}}@media (prefers-reduced-motion:reduce){.animate-rise,.animate-float{animation:none}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false} \ No newline at end of file +@import "https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&display=swap";@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}@layer theme{:root,:host{--font-sans:"Noto Sans","Segoe UI",system-ui,sans-serif;--font-mono:"Noto Sans","Segoe UI",system-ui,sans-serif;--color-red-600:oklch(57.7% .245 27.325);--color-amber-400:oklch(82.8% .189 84.429);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-semibold:600;--tracking-tight:-.025em;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink-950:#111827;--color-ink-900:#1f2937;--color-ink-700:#4b5563;--color-ink-500:#6b7280;--color-brand-200:#fde68a;--color-brand-300:#fcd34d;--color-brand-400:#fbbf24;--color-brand-500:#f59e0b;--color-brand-600:#d97706;--color-accent-200:#fde68a;--color-accent-400:#f59e0b;--color-surface:#fff;--color-surface-soft:#f8fafc;--radius-card:0px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root,.dark{color-scheme:light;--background:#f8fafc;--foreground:var(--color-ink-950);--card:var(--color-surface);--card-foreground:var(--color-ink-950);--popover:var(--color-surface);--popover-foreground:var(--color-ink-950);--primary:var(--color-brand-500);--primary-foreground:#fff;--secondary:var(--color-surface-soft);--secondary-foreground:var(--color-ink-950);--muted:var(--color-surface-soft);--muted-foreground:var(--color-ink-700);--accent:var(--color-accent-400);--accent-foreground:#fff;--destructive:#ef4444;--destructive-foreground:#f8fafc;--border:#e5e7eb;--input:#e5e7eb;--ring:var(--color-accent-400);--radius:0px;--radius-sm:0px;--radius-md:0px;--radius-lg:0px;--radius-xl:0px;--radius-2xl:0px;--radius-3xl:0px;--font-sans:"Noto Sans","Segoe UI",system-ui,sans-serif;--font-display:"Noto Sans","Segoe UI",system-ui,sans-serif;--font-mono:"Noto Sans","Segoe UI",system-ui,sans-serif}html,body{height:100%}body{font-family:var(--font-sans);color:var(--color-ink-950);background:radial-gradient(circle at 12% 16%,#f59e0b14,#0000 60%),radial-gradient(circle at 86% 12%,#fbbf241a,#0000 58%),linear-gradient(#f9fafb 0%,#fff 55%,#f8fafc 100%);margin:0}body:before,body:after{content:"";z-index:0;pointer-events:none;border-radius:999px;position:fixed}body:before{background:radial-gradient(circle,#f59e0b29,#0000 70%);width:320px;height:320px;top:-120px;right:-140px}body:after{background:radial-gradient(circle,#fbbf2424,#0000 70%);width:300px;height:300px;bottom:-140px;left:-140px}:focus-visible{outline:2px solid var(--color-accent-400);outline-offset:3px}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.top-2{top:calc(var(--spacing)*2)}.right-0{right:calc(var(--spacing)*0)}.right-4{right:calc(var(--spacing)*4)}.bottom-4{bottom:calc(var(--spacing)*4)}.left-1{left:calc(var(--spacing)*1)}.left-4{left:calc(var(--spacing)*4)}.z-10{z-index:10}.z-30{z-index:30}.z-50{z-index:50}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-5{margin-top:calc(var(--spacing)*5)}.mt-8{margin-top:calc(var(--spacing)*8)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.h-1\.5{height:calc(var(--spacing)*1.5)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-\[calc\(100\%-0\.5rem\)\]{height:calc(100% - .5rem)}.max-h-80{max-height:calc(var(--spacing)*80)}.min-h-screen{min-height:100vh}.w-1\.5{width:calc(var(--spacing)*1.5)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-44{width:calc(var(--spacing)*44)}.w-full{width:100%}.w-px{width:1px}.max-w-6xl{max-width:var(--container-6xl)}.flex-1{flex:1}.cursor-pointer{cursor:pointer}.scroll-mt-24{scroll-margin-top:calc(var(--spacing)*24)}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.gap-10{gap:calc(var(--spacing)*10)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.overflow-auto{overflow:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-card{border-radius:var(--radius-card)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-400{border-color:var(--color-amber-400)}.border-black\/20{border-color:#0003}@supports (color:color-mix(in lab, red, red)){.border-black\/20{border-color:color-mix(in oklab,var(--color-black)20%,transparent)}}.border-brand-300\/40{border-color:#fcd34d66}@supports (color:color-mix(in lab, red, red)){.border-brand-300\/40{border-color:color-mix(in oklab,var(--color-brand-300)40%,transparent)}}.border-brand-300\/80{border-color:#fcd34dcc}@supports (color:color-mix(in lab, red, red)){.border-brand-300\/80{border-color:color-mix(in oklab,var(--color-brand-300)80%,transparent)}}.border-emerald-400{border-color:var(--color-emerald-400)}.border-red-600{border-color:var(--color-red-600)}.border-rose-400{border-color:var(--color-rose-400)}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.bg-amber-400\/10{background-color:#fcbb001a}@supports (color:color-mix(in lab, red, red)){.bg-amber-400\/10{background-color:color-mix(in oklab,var(--color-amber-400)10%,transparent)}}.bg-black\/20{background-color:#0003}@supports (color:color-mix(in lab, red, red)){.bg-black\/20{background-color:color-mix(in oklab,var(--color-black)20%,transparent)}}.bg-brand-500{background-color:var(--color-brand-500)}.bg-brand-500\/10{background-color:#f59e0b1a}@supports (color:color-mix(in lab, red, red)){.bg-brand-500\/10{background-color:color-mix(in oklab,var(--color-brand-500)10%,transparent)}}.bg-brand-500\/60{background-color:#f59e0b99}@supports (color:color-mix(in lab, red, red)){.bg-brand-500\/60{background-color:color-mix(in oklab,var(--color-brand-500)60%,transparent)}}.bg-emerald-400\/10{background-color:#00d2941a}@supports (color:color-mix(in lab, red, red)){.bg-emerald-400\/10{background-color:color-mix(in oklab,var(--color-emerald-400)10%,transparent)}}.bg-ink-950{background-color:var(--color-ink-950)}.bg-ink-950\/95{background-color:#111827f2}@supports (color:color-mix(in lab, red, red)){.bg-ink-950\/95{background-color:color-mix(in oklab,var(--color-ink-950)95%,transparent)}}.bg-red-600\/10{background-color:#e400141a}@supports (color:color-mix(in lab, red, red)){.bg-red-600\/10{background-color:color-mix(in oklab,var(--color-red-600)10%,transparent)}}.bg-surface{background-color:var(--color-surface)}.bg-surface-soft{background-color:var(--color-surface-soft)}.bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.bg-white\/10{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-5{padding:calc(var(--spacing)*5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.pt-4{padding-top:calc(var(--spacing)*4)}.pt-8{padding-top:calc(var(--spacing)*8)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-16{padding-bottom:calc(var(--spacing)*16)}.pl-3{padding-left:calc(var(--spacing)*3)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.leading-\[1\.05\]{--tw-leading:1.05;line-height:1.05}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[-0\.02em\]{--tw-tracking:-.02em;letter-spacing:-.02em}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-\[0\.3em\]{--tw-tracking:.3em;letter-spacing:.3em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.text-brand-300{color:var(--color-brand-300)}.text-brand-500{color:var(--color-brand-500)}.text-ink-500{color:var(--color-ink-500)}.text-ink-700{color:var(--color-ink-700)}.text-ink-900{color:var(--color-ink-900)}.text-ink-950{color:var(--color-ink-950)}.text-red-600{color:var(--color-red-600)}.text-white\/60{color:#fff9}@supports (color:color-mix(in lab, red, red)){.text-white\/60{color:color-mix(in oklab,var(--color-white)60%,transparent)}}.text-white\/70{color:#ffffffb3}@supports (color:color-mix(in lab, red, red)){.text-white\/70{color:color-mix(in oklab,var(--color-white)70%,transparent)}}.text-white\/80{color:#fffc}@supports (color:color-mix(in lab, red, red)){.text-white\/80{color:color-mix(in oklab,var(--color-white)80%,transparent)}}.text-white\/90{color:#ffffffe6}@supports (color:color-mix(in lab, red, red)){.text-white\/90{color:color-mix(in oklab,var(--color-white)90%,transparent)}}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.decoration-brand-300\/70{text-decoration-color:#fcd34db3}@supports (color:color-mix(in lab, red, red)){.decoration-brand-300\/70{-webkit-text-decoration-color:color-mix(in oklab,var(--color-brand-300)70%,transparent);-webkit-text-decoration-color:color-mix(in oklab,var(--color-brand-300)70%,transparent);text-decoration-color:color-mix(in oklab,var(--color-brand-300)70%,transparent)}}.underline-offset-4{text-underline-offset:4px}.shadow-card{--tw-shadow:0 18px 40px -25px var(--tw-shadow-color,#1118271f);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-raise{--tw-shadow:0 12px 22px -14px var(--tw-shadow-color,#f59e0b59);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.group-hover\:bg-brand-500:is(:where(.group):hover *){background-color:var(--color-brand-500)}}.file\:mr-4::file-selector-button{margin-right:calc(var(--spacing)*4)}.file\:rounded-none::file-selector-button{border-radius:0}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-brand-500::file-selector-button{background-color:var(--color-brand-500)}.file\:px-4::file-selector-button{padding-inline:calc(var(--spacing)*4)}.file\:py-2::file-selector-button{padding-block:calc(var(--spacing)*2)}.file\:text-xs::file-selector-button{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.file\:font-semibold::file-selector-button{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.file\:text-ink-950::file-selector-button{color:var(--color-ink-950)}.placeholder\:text-ink-500::placeholder{color:var(--color-ink-500)}@media (hover:hover){.hover\:border-black\/40:hover{border-color:#0006}@supports (color:color-mix(in lab, red, red)){.hover\:border-black\/40:hover{border-color:color-mix(in oklab,var(--color-black)40%,transparent)}}.hover\:bg-brand-400:hover{background-color:var(--color-brand-400)}.hover\:text-ink-900:hover{color:var(--color-ink-900)}.hover\:text-white:hover{color:var(--color-white)}.hover\:file\:bg-brand-400:hover::file-selector-button{background-color:var(--color-brand-400)}}.focus\:border-accent-400:focus{border-color:var(--color-accent-400)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-accent-200:focus{--tw-ring-color:var(--color-accent-200)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-accent-200:focus-visible{--tw-ring-color:var(--color-accent-200)}.focus-visible\:ring-brand-200:focus-visible{--tw-ring-color:var(--color-brand-200)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.data-\[state\=error\]\:border-red-600[data-state=error]{border-color:var(--color-red-600)}.data-\[state\=error\]\:bg-red-600[data-state=error]{background-color:var(--color-red-600)}.data-\[state\=error\]\:bg-red-600\/10[data-state=error]{background-color:#e400141a}@supports (color:color-mix(in lab, red, red)){.data-\[state\=error\]\:bg-red-600\/10[data-state=error]{background-color:color-mix(in oklab,var(--color-red-600)10%,transparent)}}.data-\[state\=error\]\:shadow-\[0_0_0_6px_rgba\(220\,38\,38\,0\.2\)\][data-state=error]{--tw-shadow:0 0 0 6px var(--tw-shadow-color,#dc262633);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=paused\]\:border-amber-400[data-state=paused]{border-color:var(--color-amber-400)}.data-\[state\=paused\]\:bg-amber-400[data-state=paused]{background-color:var(--color-amber-400)}.data-\[state\=paused\]\:bg-amber-400\/10[data-state=paused]{background-color:#fcbb001a}@supports (color:color-mix(in lab, red, red)){.data-\[state\=paused\]\:bg-amber-400\/10[data-state=paused]{background-color:color-mix(in oklab,var(--color-amber-400)10%,transparent)}}.data-\[state\=paused\]\:shadow-\[0_0_0_6px_rgba\(251\,191\,36\,0\.25\)\][data-state=paused]{--tw-shadow:0 0 0 6px var(--tw-shadow-color,#fbbf2440);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=processing\]\:border-brand-300\/80[data-state=processing]{border-color:#fcd34dcc}@supports (color:color-mix(in lab, red, red)){.data-\[state\=processing\]\:border-brand-300\/80[data-state=processing]{border-color:color-mix(in oklab,var(--color-brand-300)80%,transparent)}}.data-\[state\=processing\]\:bg-amber-400[data-state=processing]{background-color:var(--color-amber-400)}.data-\[state\=processing\]\:bg-brand-500\/10[data-state=processing]{background-color:#f59e0b1a}@supports (color:color-mix(in lab, red, red)){.data-\[state\=processing\]\:bg-brand-500\/10[data-state=processing]{background-color:color-mix(in oklab,var(--color-brand-500)10%,transparent)}}.data-\[state\=processing\]\:shadow-\[0_0_0_6px_rgba\(251\,191\,36\,0\.25\)\][data-state=processing]{--tw-shadow:0 0 0 6px var(--tw-shadow-color,#fbbf2440);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=ready\]\:border-emerald-400[data-state=ready]{border-color:var(--color-emerald-400)}.data-\[state\=ready\]\:bg-emerald-400\/10[data-state=ready]{background-color:#00d2941a}@supports (color:color-mix(in lab, red, red)){.data-\[state\=ready\]\:bg-emerald-400\/10[data-state=ready]{background-color:color-mix(in oklab,var(--color-emerald-400)10%,transparent)}}.data-\[state\=ready\]\:bg-emerald-500[data-state=ready]{background-color:var(--color-emerald-500)}.data-\[state\=ready\]\:shadow-\[0_0_0_6px_rgba\(16\,185\,129\,0\.2\)\][data-state=ready]{--tw-shadow:0 0 0 6px var(--tw-shadow-color,#10b98133);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=recording\]\:border-rose-400[data-state=recording]{border-color:var(--color-rose-400)}.data-\[state\=recording\]\:bg-rose-500[data-state=recording]{background-color:var(--color-rose-500)}.data-\[state\=recording\]\:bg-rose-500\/10[data-state=recording]{background-color:#ff23571a}@supports (color:color-mix(in lab, red, red)){.data-\[state\=recording\]\:bg-rose-500\/10[data-state=recording]{background-color:color-mix(in oklab,var(--color-rose-500)10%,transparent)}}.data-\[state\=recording\]\:shadow-\[0_0_0_6px_rgba\(244\,63\,94\,0\.2\)\][data-state=recording]{--tw-shadow:0 0 0 6px var(--tw-shadow-color,#f43f5e33);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media (min-width:40rem){.sm\:right-6{right:calc(var(--spacing)*6)}.sm\:left-auto{left:auto}.sm\:ml-auto{margin-left:auto}.sm\:w-auto{width:auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:p-5{padding:calc(var(--spacing)*5)}.sm\:px-8{padding-inline:calc(var(--spacing)*8)}.sm\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}}@media (min-width:48rem){.md\:flex{display:flex}.md\:hidden{display:none}.md\:flex-row{flex-direction:row}.md\:items-start{align-items:flex-start}.md\:justify-between{justify-content:space-between}}@media (min-width:64rem){.lg\:grid-cols-\[minmax\(0\,320px\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,320px) minmax(0,1fr)}.lg\:px-12{padding-inline:calc(var(--spacing)*12)}.lg\:text-\[3\.75rem\]{font-size:3.75rem}}@media (min-width:80rem){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.rounded,.rounded-sm,.rounded-md,.rounded-lg,.rounded-xl,.rounded-2xl,.rounded-3xl,.rounded-full,.rounded-card{border-radius:0}.file\:rounded-full::file-selector-button{border-radius:0}.animate-rise{animation:.7s cubic-bezier(.16,1,.3,1) both rise-in}.animate-float{animation:10s ease-in-out infinite float}}@keyframes rise-in{0%{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}@keyframes float{0%{transform:translateY(0)}50%{transform:translateY(-12px)}to{transform:translateY(0)}}@media (prefers-reduced-motion:reduce){.animate-rise,.animate-float{animation:none}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false} \ No newline at end of file diff --git a/src/AudioSharp.App/wwwroot/js/audio-recorder.js b/src/AudioSharp.App/wwwroot/js/audio-recorder.js index 4ea87d1..0dbe041 100644 --- a/src/AudioSharp.App/wwwroot/js/audio-recorder.js +++ b/src/AudioSharp.App/wwwroot/js/audio-recorder.js @@ -7,8 +7,10 @@ window.audioRecorder = (() => { let recorder = null; let stream = null; let isRecording = false; + let isPaused = false; let options = { ...defaultOptions }; let antiforgeryToken = null; + let audioContext = null; const getAntiforgeryToken = async () => { if (antiforgeryToken) { @@ -44,9 +46,15 @@ window.audioRecorder = (() => { recorder.destroy(); } + if (audioContext) { + audioContext.close().catch(() => {}); + audioContext = null; + } + recorder = null; stream = null; isRecording = false; + isPaused = false; }; const start = async (customOptions = {}) => { @@ -55,6 +63,7 @@ window.audioRecorder = (() => { } options = { ...defaultOptions, ...customOptions }; + isPaused = false; if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new Error("Audio capture is not supported in this browser."); @@ -85,6 +94,7 @@ window.audioRecorder = (() => { recorder.startRecording(); isRecording = true; + isPaused = false; return true; } catch (error) { cleanup(); @@ -92,6 +102,26 @@ window.audioRecorder = (() => { } }; + const pause = () => { + if (!recorder || !isRecording || isPaused) { + return false; + } + + recorder.pauseRecording(); + isPaused = true; + return true; + }; + + const resume = () => { + if (!recorder || !isRecording || !isPaused) { + return false; + } + + recorder.resumeRecording(); + isPaused = false; + return true; + }; + const stopInternal = async () => { if (!isRecording) { return null; @@ -193,6 +223,66 @@ window.audioRecorder = (() => { return parsedResponse; }; + const getAudioContext = () => { + if (!audioContext) { + const AudioContext = window.AudioContext || window.webkitAudioContext; + if (!AudioContext) { + return null; + } + audioContext = new AudioContext(); + } + return audioContext; + }; + + const playTone = (context, frequency, startAt, duration, gainNode) => { + const oscillator = context.createOscillator(); + oscillator.type = "sine"; + oscillator.frequency.value = frequency; + oscillator.connect(gainNode); + oscillator.start(startAt); + oscillator.stop(startAt + duration); + }; + + const playCue = async (cue) => { + const context = getAudioContext(); + if (!context) { + return false; + } + + const validCues = ["start", "stop", "pause", "resume"]; + if (!validCues.includes(cue)) { + return false; + } + + if (context.state === "suspended") { + await context.resume(); + } + + const gainNode = context.createGain(); + gainNode.gain.value = 0.05; + gainNode.connect(context.destination); + + const now = context.currentTime; + switch (cue) { + case "start": + playTone(context, 880, now, 0.08, gainNode); + playTone(context, 1320, now + 0.1, 0.1, gainNode); + break; + case "stop": + playTone(context, 520, now, 0.12, gainNode); + break; + case "pause": + playTone(context, 660, now, 0.12, gainNode); + break; + case "resume": + playTone(context, 780, now, 0.08, gainNode); + playTone(context, 1040, now + 0.09, 0.08, gainNode); + break; + } + + return true; + }; + const blobToBase64 = (blob) => new Promise((resolve, reject) => { const reader = new FileReader(); @@ -209,5 +299,5 @@ window.audioRecorder = (() => { reader.readAsDataURL(blob); }); - return { start, stop, stopAndProcess }; + return { start, stop, stopAndProcess, pause, resume, playCue }; })();