diff --git a/Wave.Tests/Data/ApplicationDbContextTest.cs b/Wave.Tests/Data/ApplicationDbContextTest.cs index 14755be..6833941 100644 --- a/Wave.Tests/Data/ApplicationDbContextTest.cs +++ b/Wave.Tests/Data/ApplicationDbContextTest.cs @@ -1,34 +1,12 @@ using Microsoft.EntityFrameworkCore; -using Testcontainers.PostgreSql; using Wave.Data; +using Wave.Tests.TestUtilities; namespace Wave.Tests.Data; [TestFixture, FixtureLifeCycle(LifeCycle.InstancePerTestCase)] [TestOf(typeof(ApplicationDbContext))] -public class ApplicationDbContextTest { - private PostgreSqlContainer PostgresContainer { get; } = new PostgreSqlBuilder().WithImage("postgres:16.1-alpine").Build(); - - [SetUp] - public async Task SetUp() { - await PostgresContainer.StartAsync(); - } - - [TearDown] - public async Task TearDown() { - await PostgresContainer.DisposeAsync(); - } - - private ApplicationDbContext GetContext() { - return new ApplicationDbContext( - new DbContextOptionsBuilder() - .UseNpgsql(PostgresContainer.GetConnectionString()) - .EnableSensitiveDataLogging() - .EnableDetailedErrors() - .EnableThreadSafetyChecks() - .Options); - } - +public class ApplicationDbContextTest : DbContextTest { [Test] public async Task Migration() { await using var context = GetContext(); diff --git a/Wave.Tests/Data/ApplicationRepositoryTest.cs b/Wave.Tests/Data/ApplicationRepositoryTest.cs new file mode 100644 index 0000000..c83ff60 --- /dev/null +++ b/Wave.Tests/Data/ApplicationRepositoryTest.cs @@ -0,0 +1,353 @@ +using System.Security.Claims; +using System.Text; +using Microsoft.EntityFrameworkCore; +using Wave.Data; +using Wave.Data.Transactional; +using Wave.Tests.TestUtilities; + +// ReSharper disable InconsistentNaming + +namespace Wave.Tests.Data; + +public abstract class ApplicationRepositoryTests : DbContextTest { + protected ApplicationRepository Repository { get; set; } = null!; + + protected const string TestUserName = "testuser@example.com"; + protected const string AuthorUserName = "author@example.com"; + protected const string ReviewerUserName = "reviewer@example.com"; + + protected ClaimsPrincipal AnonymousPrincipal { get; set; } = null!; + protected ClaimsPrincipal UserPrincipal { get; set; } = null!; + protected ClaimsPrincipal AuthorPrincipal { get; set; } = null!; + protected ClaimsPrincipal ReviewerPrincipal { get; set; } = null!; + + protected Guid PrimaryCategoryId { get; set; } + protected Guid SecondaryCategoryId { get; set; } + + protected virtual ValueTask InitializeTestEntities(ApplicationDbContext context) { + return ValueTask.CompletedTask; + } + + protected override async ValueTask AndThenSetUp() { + Repository = new ApplicationRepository(new MockDbContextFactory(GetContext)); + + List categories = [ + new Category { + Name = "Primary Category", + Color = CategoryColors.Primary + }, + new Category { + Name = "Secondary Category", + Color = CategoryColors.Secondary + } + ]; + + await using var context = GetContext(); + var user = new ApplicationUser { + UserName = TestUserName + }; + var author = new ApplicationUser { + UserName = AuthorUserName + }; + var reviewer = new ApplicationUser { + UserName = ReviewerUserName + }; + + context.AddRange(categories); + context.Users.AddRange([user, author, reviewer]); + + await context.Database.EnsureCreatedAsync(); + await context.SaveChangesAsync(); + + AnonymousPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); + UserPrincipal = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim(ClaimTypes.Name, user.UserName), + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim("Id", user.Id), + ], "Mock Authentication")); + AuthorPrincipal = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim(ClaimTypes.Name, author.UserName), + new Claim(ClaimTypes.NameIdentifier, author.Id), + new Claim("Id", author.Id), + new Claim(ClaimTypes.Role, "Author"), + ], "Mock Authentication")); + ReviewerPrincipal = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim(ClaimTypes.Name, reviewer.UserName), + new Claim(ClaimTypes.NameIdentifier, reviewer.Id), + new Claim("Id", reviewer.Id), + new Claim(ClaimTypes.Role, "Author"), + new Claim(ClaimTypes.Role, "Reviewer"), + ], "Mock Authentication")); + + PrimaryCategoryId = categories[0].Id; + SecondaryCategoryId = categories[1].Id; + + await InitializeTestEntities(context); + } + + private sealed class MockDbContextFactory(Func supplier) + : IDbContextFactory { + public ApplicationDbContext CreateDbContext() => supplier(); + } +} + +[TestFixture, FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +[TestOf(typeof(ApplicationRepository))] +public class ApplicationRepositoryTest_GetCategories : ApplicationRepositoryTests { + + #region Success Tests + + [Test] + public async Task AnonymousDefaultOneArticleOneCategory_Success() { + await Repository.CreateArticleAsync( + new ArticleCreateDto("test", "*test*", null, null, [PrimaryCategoryId], null), AuthorPrincipal); + + var result = await Repository.GetCategories(AnonymousPrincipal); + Assert.Multiple(() => { + Assert.That(result, Is.Not.Empty); + Assert.That(result.First().Id, Is.EqualTo(PrimaryCategoryId)); + Assert.That(result, Has.Count.EqualTo(1)); + }); + } + + #endregion + + #region Permission Tests + + [Test] + public async Task AnonymousDefaultNoArticles_Success() { + var result = await Repository.GetCategories(AnonymousPrincipal); + + Assert.Multiple(() => { Assert.That(result, Is.Empty); }); + } + + [Test] + public void AnonymousNoArticlesAllCategories_ThrowsMissingPermissions() { + Assert.ThrowsAsync(async () => await Repository.GetCategories(AnonymousPrincipal, true)); + } + + [Test] + public async Task AuthorDefaultNoArticles_Success() { + var result = await Repository.GetCategories(AuthorPrincipal); + + Assert.Multiple(() => { Assert.That(result, Is.Empty); }); + } + + [Test] + public async Task AuthorDefaultNoArticlesAllCategories_Success() { + var result = await Repository.GetCategories(AuthorPrincipal, true); + + Assert.Multiple(() => { + Assert.That(result, Is.Not.Empty); + Assert.That(result, Has.Count.EqualTo(2)); + Assert.That(result.FirstOrDefault(c => c.Id == PrimaryCategoryId), Is.Not.Null); + Assert.That(result.FirstOrDefault(c => c.Id == SecondaryCategoryId), Is.Not.Null); + }); + } + + #endregion + +} + +[TestFixture, FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +[TestOf(typeof(ApplicationRepository))] +public class ApplicationRepositoryTest_CreateArticle : ApplicationRepositoryTests { + private static ArticleCreateDto GetValidTestArticle(Guid[]? categories = null) { + return new ArticleCreateDto( + "Test Article", + "*Test* Body", + null, null, categories, null); + } + + #region Success Tests + + [Test] + public async Task MinimalArticle_Success() { + var article = GetValidTestArticle(); + + var view = await Repository.CreateArticleAsync(article, AuthorPrincipal); + + await using var context = GetContext(); + Assert.Multiple(() => { + Assert.That(context.Set
().IgnoreQueryFilters().ToList(), Has.Count.EqualTo(1)); + Assert.That(context.Set
().IgnoreQueryFilters().First().Id, Is.EqualTo(view.Id)); + Assert.That(view.Status, Is.EqualTo(ArticleStatus.Draft)); + Assert.That(view.BodyHtml, Is.Not.Null); + Assert.That(view.BodyPlain, Is.Not.Null); + Assert.That(view.Slug, Is.EqualTo("test-article")); + }); + } + + [Test] + public async Task WithCategories_Success() { + var article = GetValidTestArticle([PrimaryCategoryId]); + var view = await Repository.CreateArticleAsync(article, AuthorPrincipal); + + await using var context = GetContext(); + Assert.Multiple(() => { + Assert.That(view.Categories, Has.Count.EqualTo(1)); + Assert.That(context.Set
().IgnoreQueryFilters() + .Include(a => a.Categories) + .First(a => a.Id == view.Id).Categories.First().Id, Is.EqualTo(PrimaryCategoryId)); + }); + } + + #endregion + + #region Permission Tests + + [Test] + public void RegularUser_ThrowsMissingPermissions() { + var article = GetValidTestArticle(); + + Assert.ThrowsAsync( + async () => await Repository.CreateArticleAsync(article, UserPrincipal)); + } + + [Test] + public void AnonymousUser_ThrowsMissingPermissions() { + var article = GetValidTestArticle(); + + Assert.ThrowsAsync( + async () => await Repository.CreateArticleAsync(article, AnonymousPrincipal)); + } + + #endregion + + #region Data Validation Tests + + [Test] + public void MissingTitle_ThrowsMalformed() { + var article = new ArticleCreateDto(null!, "test", null, null, null, null); + + Assert.ThrowsAsync( + async () => await Repository.CreateArticleAsync(article, AuthorPrincipal)); + } + + #endregion + +} + +[TestFixture, FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +[TestOf(typeof(ApplicationRepository))] +public class ApplicationRepositoryTest_UpdateArticle : ApplicationRepositoryTests { + private Guid TestArticleId { get; set; } + + private ArticleUpdateDto GetValidTestArticle() => new(TestArticleId); + + private static string StringOfLength(int length) { + var builder = new StringBuilder(); + + for (int i = 0; i < length; i++) { + builder.Append('_'); + } + + return builder.ToString(); + } + + protected override async ValueTask InitializeTestEntities(ApplicationDbContext context) { + var testArticle = new ArticleCreateDto( + "Test Article", + "Test **Article** with *formatting.", + "test-article", + DateTimeOffset.Now.AddHours(-5), + [PrimaryCategoryId], null); + + var view = await Repository.CreateArticleAsync(testArticle, AuthorPrincipal); + TestArticleId = view.Id; + } + + #region Success Tests + + [Test] + public async Task UpdateTitle_Success() { + var update = new ArticleUpdateDto(TestArticleId, "New Title"); + + await Repository.UpdateArticleAsync(update, AuthorPrincipal); + + await using var context = GetContext(); + Assert.Multiple(() => { + Assert.That(context.Set
().IgnoreQueryFilters().First(a => a.Id == TestArticleId).Title, + Is.EqualTo("New Title")); + }); + } + + [Test] + public async Task UpdateBodyUpdatesHtmlAndPlain_Success() { + var update = new ArticleUpdateDto(TestArticleId, body:"Some *new* Body"); + const string expectedHtml = "

Some new Body

"; + const string expectedPlain = "Some new Body"; + + await Repository.UpdateArticleAsync(update, AuthorPrincipal); + + await using var context = GetContext(); + Assert.Multiple(() => { + var article = context.Set
().IgnoreQueryFilters().First(a => a.Id == TestArticleId); + Assert.That(article.BodyHtml, Is.EqualTo(expectedHtml)); + Assert.That(article.BodyPlain, Is.EqualTo(expectedPlain)); + }); + } + + [Test] + public async Task UpdateCategories_Success() { + var update = new ArticleUpdateDto(TestArticleId, categories:[SecondaryCategoryId]); + await Repository.UpdateArticleAsync(update, AuthorPrincipal); + + await using var context = GetContext(); + Assert.Multiple(() => { + var article = context.Set
().IgnoreQueryFilters() + .Include(a => a.Categories).First(a => a.Id == TestArticleId); + Assert.That(article.Categories, Has.Count.EqualTo(1)); + Assert.That(article.Categories.First().Id, Is.EqualTo(SecondaryCategoryId)); + }); + } + + #endregion + + #region Permission Tests + + [Test] + public void AnonymousUser_ThrowsMissingPermissions() { + var update = GetValidTestArticle(); + + Assert.ThrowsAsync( + async () => await Repository.UpdateArticleAsync(update, AnonymousPrincipal)); + } + + [Test] + public void RegularUser_ThrowsMissingPermissions() { + var update = GetValidTestArticle(); + + Assert.ThrowsAsync( + async () => await Repository.UpdateArticleAsync(update, UserPrincipal)); + } + + [Test] + public void UnrelatedAuthor_ThrowsMissingPermissions() { + var update = GetValidTestArticle(); + + Assert.ThrowsAsync( + async () => await Repository.UpdateArticleAsync(update, ReviewerPrincipal)); + } + + #endregion + + #region Data Validation Tests + + [Test] + public void SlugLength65_ThrowsMalformed() { + var update = new ArticleUpdateDto(TestArticleId, slug:StringOfLength(65)); + Assert.ThrowsAsync( + async () => await Repository.UpdateArticleAsync(update, AuthorPrincipal)); + } + + [Test] + public void TitleLength257_ThrowsMalformed() { + var update = new ArticleUpdateDto(TestArticleId, slug:StringOfLength(257)); + Assert.ThrowsAsync( + async () => await Repository.UpdateArticleAsync(update, AuthorPrincipal)); + } + + #endregion + +} \ No newline at end of file diff --git a/Wave.Tests/Data/ArticleTest.cs b/Wave.Tests/Data/ArticleTest.cs index eddb38b..469fcb1 100644 --- a/Wave.Tests/Data/ArticleTest.cs +++ b/Wave.Tests/Data/ArticleTest.cs @@ -22,7 +22,7 @@ public class ArticleTest { public void SlugWithSpecialCharacters() { Article.Title = "Title with, special characters?"; Article.UpdateSlug(); - Assert.That(Article.Slug, Is.EqualTo("title-with%2C-special-characters%3F")); + Assert.That(Article.Slug, Is.EqualTo("title-with-special-characters")); } [Test] @@ -36,14 +36,14 @@ public class ArticleTest { public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterEscapeSize3AtPosition55() { Article.Title = "Auto generating slugs was a mistake I hate this ______ €"; Article.UpdateSlug(); - Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-______-%E2%82%AC")); + Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-______-")); } [Test] public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterEscapeSize2AtPosition56() { Article.Title = "Auto generating slugs was a mistake I hate this _______ üa"; Article.UpdateSlug(); - Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-_______-%C3%BCa")); + Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-_______-a")); } [Test] @@ -57,7 +57,7 @@ public class ArticleTest { public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterEscapeSize2AtPosition57() { Article.Title = "Auto generating slugs was a mistake I hate this ________ üa"; Article.UpdateSlug(); - Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-________-%C3%BCa")); + Assert.That(Article.Slug, Is.EqualTo("auto-generating-slugs-was-a-mistake-i-hate-this-________-a")); } [Test] @@ -71,21 +71,21 @@ public class ArticleTest { public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterAtPosition61() { Article.Title = "Article that ends with a special character and need special cäre"; Article.UpdateSlug(); - Assert.That(Article.Slug, Is.EqualTo("article-that-ends-with-a-special-character-and-need-special-c")); + Assert.That(Article.Slug, Is.EqualTo("article-that-ends-with-a-special-character-and-need-special-cre")); } [Test] public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterAtPosition62() { Article.Title = "Article that ends with a special character and needs special cäre"; Article.UpdateSlug(); - Assert.That(Article.Slug, Is.EqualTo("article-that-ends-with-a-special-character-and-needs-special-c")); + Assert.That(Article.Slug, Is.EqualTo("article-that-ends-with-a-special-character-and-needs-special-cre")); } [Test] public void SlugFromTitleLongerThan64CharacterWithSpecialCharacterAtPosition63() { Article.Title = "Article that ends with a special character and needs special caäre"; Article.UpdateSlug(); - Assert.That(Article.Slug, Is.EqualTo("article-that-ends-with-a-special-character-and-needs-special-ca")); + Assert.That(Article.Slug, Is.EqualTo("article-that-ends-with-a-special-character-and-needs-special-car")); } [Test] diff --git a/Wave.Tests/TestUtilities/DbContextTest.cs b/Wave.Tests/TestUtilities/DbContextTest.cs new file mode 100644 index 0000000..8134fb7 --- /dev/null +++ b/Wave.Tests/TestUtilities/DbContextTest.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore; +using Testcontainers.PostgreSql; +using Wave.Data; + +namespace Wave.Tests.TestUtilities; + +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public abstract class DbContextTest { + private PostgreSqlContainer PostgresContainer { get; } = new PostgreSqlBuilder().WithImage("postgres:16.1-alpine").Build(); + + [SetUp] + public async Task SetUp() { + await PostgresContainer.StartAsync(); + await AndThenSetUp(); + } + protected virtual ValueTask AndThenSetUp() { + return ValueTask.CompletedTask; + } + + [TearDown] + public async Task TearDown() => await PostgresContainer.DisposeAsync(); + + protected ApplicationDbContext GetContext() => + new(new DbContextOptionsBuilder() + .UseNpgsql(PostgresContainer.GetConnectionString()) + .EnableSensitiveDataLogging() + .EnableDetailedErrors() + .EnableThreadSafetyChecks() + .Options); + +} \ No newline at end of file diff --git a/Wave.Tests/Wave.Tests.csproj b/Wave.Tests/Wave.Tests.csproj index a05251b..6d78126 100644 --- a/Wave.Tests/Wave.Tests.csproj +++ b/Wave.Tests/Wave.Tests.csproj @@ -35,6 +35,7 @@ + diff --git a/Wave/Assets/React/ArticleEditor.tsx b/Wave/Assets/React/ArticleEditor.tsx new file mode 100644 index 0000000..9b0a008 --- /dev/null +++ b/Wave/Assets/React/ArticleEditor.tsx @@ -0,0 +1,382 @@ +import React, { useState, useEffect, useRef } from "react"; +import { updateCharactersLeft, insertBeforeSelection, insertBeforeAndAfterSelection } from "../utilities/md_functions"; +import { LabelInput, ToolBarButton } from "./Forms"; +import { CategoryColor, Category, ArticleStatus, ArticleView, ArticleDto } from "../model/Models"; +import { useTranslation } from 'react-i18next'; +import markdownit from "markdown-it"; +import markdownitmark from "markdown-it-mark"; +import "groupby-polyfill/lib/polyfill.js"; + +const nameof = function(name: keyof T) { return name; } + +async function get(url: string): Promise { + let response = await fetch(url, { + method: "GET" + }); + if (!response.ok) + throw new Error(response.statusText); + + return response.json(); +} + +async function post(url: string, data: T) : Promise { + let response = await fetch(url, { + method: "POST", + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json" + } + }); + if (!response.ok) + throw new Error(response.statusText); + + return response.json(); +} +async function put(url: string, data: T) : Promise { + let response = await fetch(url, { + method: "PUT", + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json" + } + }); + if (!response.ok) + throw new Error(response.statusText); + + return response.json(); +} + +function Loading(message: string) { + return
+
+

{message}

+ +
+
+} + +export default function Editor() { + const {t} = useTranslation(); + const [notice, setNotice] = useState(""); + const [dirty, setDirty] = useState(false); + const [isPublished, setIsPublished] = useState(false); + const [article, setArticle] = useState(null); + const [categories, setCategories] = useState([]); + const [model, setModel] = useState({ + body: "", + categories: [], + id: "", + publishDate: new Date(), + slug: "", + title: "" + }); + + const md = markdownit({ + html: false, + linkify: true, + typographer: true, + }) + .use(markdownitmark) + ; + + function onChangeModel(event: React.ChangeEvent) { + const {name, value} = event.target as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement); + if (name == nameof("publishDate")) { + setModel((m : ArticleDto) => ({...m, publishDate: new Date(value)})); + } else if (event.target instanceof HTMLSelectElement) { + const select = event.target as HTMLSelectElement; + setModel((m : ArticleDto) => ({...m, [name]: [...select.selectedOptions].map(o => o.value)})); + } else { + setModel((m : ArticleDto) => ({...m, [name]: value})); + } + setDirty(true); + } + function onSubmit(event: React.FormEvent) { + event.preventDefault(); + + if (!article || !article.id || article.id.length < 1) { + put("/api/article", model) + .then(result => { + // we just reload the editor, to make sure everything is updated properly + window.location.pathname = `/article/${result.id}/edit`; + }).catch(err => setNotice(`Error creating article: ${err}`)); + } else { + post("/api/article", model) + .then(result => { + setDirty(false) + setArticle(result) + + if (model.slug != result.slug) { + setModel(m => ({...m, slug: result.slug})); + } + }) + .catch(err => setNotice(`Error trying to save article: ${err}`)); + } + } + + const location = window.location.pathname; + useEffect(() => { + if (categories.length < 1) { + get("/api/categories?all=true").then(result => { + setCategories(result); + }).catch(error => { + setNotice(`Error loading Categories: ${error.Message}`); + console.log(`Error loading Categories: ${error.message}`); + }); + } + + const id = location.match(/article\/([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\/edit/i); + if (!id) { + const publishDate = new Date(); + publishDate.setDate(publishDate.getDate() + 7); + const article : ArticleView = { + body: "", + id: "", + publishDate: publishDate.toString(), + slug: "", + status: 0, + title: "", + categories: [] + }; + setArticle(article); + setModel(m => ({...m, publishDate: publishDate})); + } else if (!article) { + get(`/api/article/${id[1]}`) + .then(result => { + setNotice(""); + setIsPublished(result.status >= ArticleStatus.Published && (new Date(result.publishDate) <= new Date())); + setModel(m => ({ + ...m, + id: result.id, + title: result.title, + slug: result.slug, + body: result.body, + publishDate: new Date(result.publishDate), + categories: result.categories.map(c => c.id), + })); + setArticle(result); + console.log("Article loaded"); + }) + .catch(error => { + setNotice(`Error loading Article: ${error.message}`); + console.log(`Error loading Article: ${error.message}`); + setArticle(null); + }); + } + }, ([setArticle, setNotice, console, location]) as any[]); + + const markdownArea = useRef(null); + return ( + <> + { + dirty && +
+ + + +

{t("editor.unsaved_changes_notice")}

+
+ } + { + notice.length > 0 && +
+ + + +

{notice}

+
+ } + { + article === null ? + Loading(t("loading.article")) : + <> +
+
    +
  • = ArticleStatus.Draft ? "step-primary" : ""}`}>{t("Draft")}
  • +
  • = ArticleStatus.InReview ? "step-primary" : ""}`}>{t("InReview")}
  • +
  • {t("Published")}
  • +
+ +
+
+ + ) => updateCharactersLeft(event.target as HTMLInputElement)} + placeholder={t("Title_Placeholder")} + name={nameof("title")} value={model.title} onChange={onChangeModel} /> + + + + +
+ + ) => updateCharactersLeft(event.target as HTMLInputElement)} + placeholder={t("Slug_Placeholder")} + name={nameof("slug")} value={model.slug} onChange={onChangeModel} /> + + + ("publishDate")} + defaultValue={(new Date(article.publishDate).toLocaleString("sv", {timeZoneName: "short"}).substring(0, 16))} + onChange={onChangeModel} /> + +
+
+ +
+
+
+
+ insertBeforeSelection(markdownArea.current, "# ", true)}> + {t("Tools.H1_Label")} + + insertBeforeSelection(markdownArea.current, "## ", true)}> + {t("Tools.H2_Label")} + + insertBeforeSelection(markdownArea.current, "### ", true)}> + {t("Tools.H3_Label")} + + insertBeforeSelection(markdownArea.current, "#### ", true)}> + {t("Tools.H4_Label")} + +
+
+ insertBeforeAndAfterSelection(markdownArea.current, "**")}> + B + + insertBeforeAndAfterSelection(markdownArea.current, "*")}> + I + + insertBeforeAndAfterSelection(markdownArea.current, "++")}> + U + + insertBeforeAndAfterSelection(markdownArea.current, "~~")}> + {t("Tools.StrikeThrough_Label")} + + insertBeforeAndAfterSelection(markdownArea.current, "==")}> + {t("Tools.Mark_Label")} + + insertBeforeSelection(markdownArea.current, "> ", true)}> + | {t("Tools.Cite_Label")} + +
+
+ insertBeforeSelection(markdownArea.current, "1. ", true)}> + 1. + + insertBeforeSelection(markdownArea.current, "a. ", true)}> + a. + + insertBeforeSelection(markdownArea.current, "A. ", true)}> + A. + + insertBeforeSelection(markdownArea.current, "i. ", true)}> + i. + + insertBeforeSelection(markdownArea.current, "I. ", true)}> + I. + +
+
+ insertBeforeAndAfterSelection(markdownArea.current, "`")}> + + + + + insertBeforeAndAfterSelection(markdownArea.current, "```")}> + + + + +
+
+ - - -
- - @if (Article.Id != Guid.Empty) { - - @Localizer["ViewArticle_Label"] - - } -
- - - -
- @foreach (var image in Article.Images) { -
- @image.ImageDescription -
- -
- -
- } - -
- -@code { - private const string ImageModal = "AddImage"; - - [Parameter] - public Guid? Id { get; set; } - [Parameter] - public required ApplicationUser User { get; set; } - [Parameter] - public required ClaimsPrincipal ClaimsUser { get; set; } - - [SupplyParameterFromForm] - private InputModel Model { get; set; } = new(); - - private Article Article { get; set; } = default!; - private List Categories { get; set; } = []; - private bool Saving { get; set; } - - protected override void OnInitialized() { - // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - Article ??= new Article {Author = User, Title = "", Body = ""}; - } - - protected override async Task OnInitializedAsync() { - await using var context = await ContextFactory.CreateDbContextAsync(); - if (Categories.Count < 1) - Categories = await context.Set().IgnoreQueryFilters().OrderBy(c => c.Color).ToListAsync(); - - Article? article = null; - if (Id is not null) { - // Ensure we are not double-tracking User on existing articles, since - // a different context loaded this user originally - context.Entry(User).State = EntityState.Unchanged; - - article = await context.Set
() - .IgnoreQueryFilters().Where(a => !a.IsDeleted) - .Include(a => a.Author) - .Include(a => a.Reviewer) - .Include(a => a.Categories) - .Include(a => a.Images) - .FirstAsync(a => a.Id == Id); - if (article is null) throw new ApplicationException("Article not found."); - } - - if (article is not null) { - if (!article.AllowedToEdit(ClaimsUser)) - throw new ApplicationException("You are not allowed to edit this article"); - - Model.Id ??= article.Id; - Model.Title ??= article.Title; - Model.Slug ??= article.Slug; - Model.Body ??= article.Body; - Model.PublishDate ??= article.PublishDate.LocalDateTime; - if (Model.Categories?.Length < 1) { - Model.Categories = article.Categories.Select(c => c.Id).ToArray(); - } - Article = article; - await InvokeAsync(StateHasChanged); - } - } - - private async Task OnValidSubmit() { - if (Article.AllowedToEdit(ClaimsUser) is false) { - Message.ShowError("Permission denied."); - return; - } - try { - Saving = true; - - // Double check user permissions - await HandleRoles(Article, User); - - if (Model.Title is not null) Article.Title = Model.Title; - if (Model.Body is not null) Article.Body = Model.Body; - if (Model.PublishDate is not null && - (Article.Status is not ArticleStatus.Published || Article.PublishDate >= DateTimeOffset.UtcNow)) - Article.PublishDate = Model.PublishDate.Value; - if (Article.Status is ArticleStatus.Published && Article.PublishDate < DateTimeOffset.Now) { - // can't change slugs when the article is public - } else { - Article.UpdateSlug(Model.Slug); - Model.Slug = Article.Slug; - } - - Article.LastModified = DateTimeOffset.UtcNow; - - await using var context = await ContextFactory.CreateDbContextAsync(); - - // Update Newsletter distribution if exists - var newsletter = await context.Set() - .IgnoreQueryFilters().IgnoreAutoIncludes() - .FirstOrDefaultAsync(n => n.Article == Article); - if (newsletter is not null) { - newsletter.DistributionDateTime = Article.PublishDate; - } - - // Avoid unnecessary updates - context.Entry(User).State = EntityState.Unchanged; - Categories.ForEach(c => context.Entry(c).State = EntityState.Unchanged); - await context.Set().IgnoreQueryFilters().IgnoreAutoIncludes() - .Where(ac => ac.Article.Id == Article.Id).LoadAsync(); - - context.Update(Article); - context.RemoveRange(Article.Headings); - Article.UpdateBody(); - - var existingImages = await context.Set
() - .IgnoreQueryFilters().Where(a => a.Id == Article.Id) - .AsNoTrackingWithIdentityResolution() - .SelectMany(a => a.Images).ToListAsync(); - foreach (var image in Article.Images) { - int index = existingImages.FindIndex(a => a.Id == image.Id); - context.Entry(image).State = index > -1 ? EntityState.Modified : EntityState.Added; - if(index > -1) existingImages.RemoveAt(index); - } - foreach (var image in existingImages) { - context.Entry(image).State = EntityState.Deleted; - } - - var relations = await context.Set() - .IgnoreQueryFilters().IgnoreAutoIncludes() - .Where(ac => ac.Article == Article && !Model.Categories.Contains(ac.Category.Id)) - .ToListAsync(); - context.RemoveRange(relations); - - foreach (var category in Model.Categories) { - if (Article.Categories.Any(c => c.Id == category) is not true) { - context.Add(new ArticleCategory { - Article = Article, - Category = Categories.First(c => c.Id == category) - }); - } - } - - await context.SaveChangesAsync(); - foreach (var image in existingImages) { - try { - Images.Delete(image.Id); - } catch (Exception ex) { - Logger.LogWarning(ex, "Failed to delete image: {image}", image.Id); - } - } - Message.ShowSuccess(Localizer["Save_Success"]); - - if (Navigation.Uri.EndsWith("/article/new")) { - Navigation.NavigateTo($"/article/{Article.Id}/edit", false, true); - } - } catch (Exception ex) { - Message.ShowError(Localizer["Save_Error"]); - Logger.LogError(ex, "Failed to save article."); - } finally { - Saving = false; - await InvokeAsync(StateHasChanged); - } - } - - [SuppressMessage("ReSharper", "ConvertIfStatementToSwitchStatement")] - private async Task HandleRoles(Article article, ApplicationUser me) { - // it's our draft - if (article.Status is ArticleStatus.Draft && article.Author.Id == me.Id) return; - - var roles = await UserManager.GetRolesAsync(me); - - // reviewers and admins can review articles - if (article.Status is ArticleStatus.InReview && roles.Any(r => r is "Admin" or "Reviewer")) { - article.Reviewer = me; - return; - } - - // published articles may only be edited my admins or moderators - if (article.Status is ArticleStatus.Published && roles.Any(r => r is "Admin" or "Moderator")) { - article.Reviewer = me; // TODO replace with editor or something? - return; - } - - throw new ApplicationException("You do not have permissions to edit this article"); - } - - private async Task ImageAdded(ArticleImage image) { - Article.Images.Add(image); - await InvokeAsync(StateHasChanged); - } - - private async Task ImageDelete(ArticleImage image) { - Article.Images.Remove(image); - await InvokeAsync(StateHasChanged); - } - - private sealed class InputModel { - public Guid? Id { get; set; } - - [Required(AllowEmptyStrings = false), MaxLength(256)] - public string? Title { get; set; } - [MaxLength(64)] - public string? Slug { get; set; } - [Required(AllowEmptyStrings = false)] - public string? Body { get; set; } - - public Guid[]? Categories { get; set; } = []; - public DateTimeOffset? PublishDate { get; set; } - } - -} \ No newline at end of file diff --git a/Wave/Controllers/ArticleController.cs b/Wave/Controllers/ArticleController.cs new file mode 100644 index 0000000..c2a0842 --- /dev/null +++ b/Wave/Controllers/ArticleController.cs @@ -0,0 +1,107 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using Wave.Data; +using Wave.Data.Transactional; + +namespace Wave.Controllers; + +[ApiController] +[Route("/api/[controller]")] +public class ArticleController(ILogger logger, ApplicationRepository repository) : ControllerBase { + [HttpGet("/api/categories"), AllowAnonymous] + [Produces("application/json")] + public async Task>, + NoContent, + UnauthorizedHttpResult, + ProblemHttpResult>> GetCategories(bool all = false, CancellationToken cancellation = default) { + try { + var categories = await repository.GetCategories(User, all, cancellation); + + if (categories.Count < 1) return TypedResults.NoContent(); + return TypedResults.Ok>(categories); + } catch (ApplicationException) { + logger.LogTrace("Unauthenticated user tried to access all categories. Denied."); + return TypedResults.Unauthorized(); + } catch (Exception ex) { + logger.LogError(ex, "Unexpected error trying to get Categories for user {UserId}.", User.FindFirstValue("Id") ?? "unknown or anonymous"); + return TypedResults.Problem(); + } + } + + [HttpGet("{id:guid}"), AllowAnonymous] + [Produces("application/json")] + public async Task, + NotFound, + UnauthorizedHttpResult, + ProblemHttpResult>> GetArticle(Guid id, CancellationToken cancellation = default) { + try { + return TypedResults.Ok(new ArticleView(await repository.GetArticleAsync(id, User, cancellation))); + } catch (ArticleNotFoundException) { + logger.LogWarning("Failed to look up Article with Id {ArticleId}. Not Found", id); + return TypedResults.NotFound(); + } catch (ArticleMissingPermissionsException) { + logger.LogWarning( + "Failed to look up Article with Id {ArticleId}. User {UserId} Access Denied.", + id, User.FindFirstValue("Id") ?? "unknown or anonymous"); + return TypedResults.Unauthorized(); + } catch (Exception ex) { + logger.LogError(ex, "Unexpected Error."); + return TypedResults.Problem(); + } + } + + [HttpPut(Name = nameof(CreateArticle)), Authorize] + [Produces("application/json")] + public async Task, + BadRequest, + UnauthorizedHttpResult, + ProblemHttpResult>> CreateArticle(ArticleCreateDto input, CancellationToken cancellation = default) { + try { + var article = new ArticleView(await repository.CreateArticleAsync(input, User, cancellation)); + return TypedResults.CreatedAtRoute(article, nameof(CreateArticle), article.Id); + } catch (ArticleMissingPermissionsException) { + logger.LogWarning( + "Unauthorized User with ID {UserId} tried to create an Article.", + User.FindFirstValue("Id") ?? "unknown or anonymous"); + return TypedResults.Unauthorized(); + } catch (ArticleMalformedException ex) { + logger.LogWarning("User with ID {UserId} tried to create an article but submitted bad data.", + User.FindFirstValue("Id") ?? "unknown or anonymous"); + return TypedResults.BadRequest($"Submitted data is not valid: {string.Join(",", ex.Errors.Select(e => e.ErrorMessage))}."); + } catch (Exception ex) { + logger.LogError(ex, "Unexpected Error."); + return TypedResults.Problem(); + } + } + + [HttpPost, Authorize] + [Produces("application/json")] + public async Task, + NotFound, + BadRequest, + UnauthorizedHttpResult, + ProblemHttpResult>> UpdateArticle(ArticleUpdateDto input, CancellationToken cancellation = default) + { + try { + return TypedResults.Ok(new ArticleView(await repository.UpdateArticleAsync(input, User, cancellation))); + } catch (ArticleNotFoundException) { + return TypedResults.NotFound(); + } catch (ArticleMalformedException ex) { + logger.LogWarning("User with ID {UserId} tried to update article with ID {ArticleId} but submitted bad data.", + User.FindFirstValue("Id") ?? "unknown or anonymous", input.Id); + return TypedResults.BadRequest($"Submitted data is not valid: {string.Join(",", ex.Errors.Select(e => e.ErrorMessage))}."); + } catch (ArticleMissingPermissionsException) { + return TypedResults.Unauthorized(); + } catch (ArticleException ex) { + logger.LogError(ex, "Unexpected Article Error."); + return TypedResults.Problem(); + } + } + +} \ No newline at end of file diff --git a/Wave/Controllers/UserController.cs b/Wave/Controllers/UserController.cs index 025a46b..59d42a6 100644 --- a/Wave/Controllers/UserController.cs +++ b/Wave/Controllers/UserController.cs @@ -23,12 +23,12 @@ public class UserController(ImageService imageService, IDbContextFactory u.ProfilePicture).FirstOrDefaultAsync(u => u.Id == userId); if (user is null) return NotFound(); if (user.ProfilePicture is null) { - return Redirect("/img/default_avatar.jpg"); + return Redirect("/dist/img/default_avatar.jpg"); } string? path = ImageService.GetPath(user.ProfilePicture.ImageId); if (path is null) { - return Redirect("/img/default_avatar.jpg"); + return Redirect("/dist/img/default_avatar.jpg"); } if (size < 800) return File(await ImageService.GetResized(path, size), ImageService.ImageMimeType); diff --git a/Wave/Data/ApplicationDbContext.cs b/Wave/Data/ApplicationDbContext.cs index 9d5ad3d..aa5aa5c 100644 --- a/Wave/Data/ApplicationDbContext.cs +++ b/Wave/Data/ApplicationDbContext.cs @@ -1,6 +1,8 @@ +using System.Text; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Wave.Data.Transactional; namespace Wave.Data; @@ -131,4 +133,74 @@ public class ApplicationDbContext(DbContextOptions options key.Ignore(k => k.Claims); }); } + + internal async ValueTask UpdateArticle(ArticleDto dto, Article article, + CancellationToken cancellation) { + if (dto is ArticleCreateDto cDto) { + article.Title = cDto.Title; + article.Body = cDto.Body; + } else if (dto is ArticleUpdateDto uDto) { + if (!string.IsNullOrWhiteSpace(uDto.Title)) article.Title = uDto.Title; + if (!string.IsNullOrWhiteSpace(uDto.Body)) article.Body = uDto.Body; + } + article.LastModified = DateTimeOffset.UtcNow; + article.UpdateBody(); + + // We can't use CanBePublic here since when we create a new article, that isn't initialized yet + if (article.Status != ArticleStatus.Published || article.PublishDate > DateTimeOffset.UtcNow) { + // Update publish date, if it exists and article isn't public yet + if (dto.PublishDate is {} date) article.PublishDate = date; + // Can only change slugs when the article is not public + article.UpdateSlug(dto.Slug); + } + + await UpdateCategories(dto, article, cancellation); + await UpdateImages(dto, article, cancellation); + await UpdateNewsletter(article, cancellation); + } + + private async ValueTask UpdateCategories(ArticleDto dto, Article article, CancellationToken cancellation) { + if (dto.Categories is null) return; + + // Retrieve all existing links between this article and categories + var relationships = await Set() + .IgnoreQueryFilters() + .IgnoreAutoIncludes() + .Include(ac => ac.Category) + .Where(ac => ac.Article == article) + .ToListAsync(cancellation); + + // check which Category is not in the DTO and needs its relationship removed + foreach (var ac in relationships.Where(ac => !dto.Categories.Contains(ac.Category.Id))) { + article.Categories.Remove(ac.Category); + Remove(ac); + } + + // check which Category in the DTO is absent from the article's relationships, and add them + var added = dto.Categories.Where(cId => relationships.All(ac => ac.Category.Id != cId)).ToList(); + if (added.Count > 0) { + var categories = await Set() + .IgnoreAutoIncludes().IgnoreQueryFilters() + .Where(c => added.Contains(c.Id)) + .ToListAsync(cancellation); + + await AddRangeAsync(categories.Select(c => new ArticleCategory { + Article = article, Category = c + }).ToList(), cancellation); + } + } + + private async ValueTask UpdateImages(ArticleDto dto, Article article, CancellationToken cancellation) { + if (dto.Images is null) return; + + // TODO:: implement + } + + private async ValueTask UpdateNewsletter(Article article, CancellationToken cancellation) { + // Update Newsletter distribution if it exists + var newsletter = await Set() + .IgnoreQueryFilters().IgnoreAutoIncludes() + .FirstOrDefaultAsync(n => n.Article == article, cancellation); + if (newsletter is not null) newsletter.DistributionDateTime = article.PublishDate; + } } \ No newline at end of file diff --git a/Wave/Data/ApplicationRepository.cs b/Wave/Data/ApplicationRepository.cs new file mode 100644 index 0000000..9b908f0 --- /dev/null +++ b/Wave/Data/ApplicationRepository.cs @@ -0,0 +1,116 @@ +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.Security.Claims; +using Microsoft.EntityFrameworkCore; +using Wave.Data.Transactional; +using Wave.Utilities; + +namespace Wave.Data; + +public class ArticleException : ApplicationException; +public class ArticleNotFoundException : ArticleException; +public class ArticleMissingPermissionsException : ArticleException; +public class ArticleMalformedException : ArticleException { + public IReadOnlyCollection Errors { get; init; } = []; +} + +/// +/// Adapter for ApplicationDbContext, that enforced valid data and the permission system +/// +/// +public class ApplicationRepository(IDbContextFactory contextFactory) { + private IDbContextFactory ContextFactory { get; } = contextFactory; + + public async ValueTask> GetCategories(ClaimsPrincipal user, bool includeUnusedCategories = false, CancellationToken cancellation = default) { + if (includeUnusedCategories && user.Identity?.IsAuthenticated is not true) + throw new ApplicationException("Trying to access unused articles with unauthenticated user"); + + await using var context = await ContextFactory.CreateDbContextAsync(cancellation); + var categories = await context.Set() + .IgnoreAutoIncludes() + .IgnoreQueryFilters() + .Where(c => includeUnusedCategories || c.Articles.Any()) + .OrderBy(c => c.Color) + .ToListAsync(cancellation); + + return new ReadOnlyCollection(categories); + } + + public async ValueTask
GetArticleAsync(Guid id, ClaimsPrincipal user, CancellationToken cancellation = default) { + await using var context = await ContextFactory.CreateDbContextAsync(cancellation); + var article = await context.Set
() + .IgnoreQueryFilters() + .IgnoreAutoIncludes() + .Include(a => a.Author) + .Include(a => a.Reviewer) + .Include(a => a.Categories) + .FirstOrDefaultAsync(a => a.Id == id, cancellation); + + if (article is null) + throw new ArticleNotFoundException(); + if (article.AllowedToRead(user)) + return article; + + throw new ArticleMissingPermissionsException(); + } + + public async ValueTask
CreateArticleAsync(ArticleCreateDto dto, ClaimsPrincipal user, + CancellationToken cancellation = default) { + if (!Permissions.AllowedToCreate(user)) + throw new ArticleMissingPermissionsException(); + + List results = []; + if (!Validator.TryValidateObject(dto, new ValidationContext(dto), results, true)) { + throw new ArticleMalformedException() { + Errors = results + }; + } + + await using var context = await ContextFactory.CreateDbContextAsync(cancellation); + var appUser = await context.Users.FindAsync([user.FindFirstValue("Id")], cancellation); + + var article = new Article { + Author = appUser ?? throw new ArticleException(), + Title = "", + Body = "" + }; + await context.UpdateArticle(dto, article, cancellation); + await context.Set
().AddAsync(article, cancellation); + await context.SaveChangesAsync(cancellation); + + return article; + } + + public async ValueTask
UpdateArticleAsync(ArticleUpdateDto dto, ClaimsPrincipal user, + CancellationToken cancellation = default) { + List results = []; + if (!Validator.TryValidateObject(dto, new ValidationContext(dto), results, true)) { + throw new ArticleMalformedException() { + Errors = results + }; + } + + await using var context = await ContextFactory.CreateDbContextAsync(cancellation); + var article = await context.Set
() + .IgnoreQueryFilters() + .Include(a => a.Author) + .FirstOrDefaultAsync(a => a.Id == dto.Id, cancellation); + if (article is null) + throw new ArticleNotFoundException(); + if (!article.AllowedToEdit(user)) + throw new ArticleMissingPermissionsException(); + var appUser = await context.Users.FindAsync([user.FindFirstValue("Id")], cancellation); + if (appUser is null) + throw new ArticleException(); + + if (appUser.Id != article.Author.Id) { + article.Reviewer = appUser; + } + + await context.UpdateArticle(dto, article, cancellation); + await context.SaveChangesAsync(cancellation); + + return article; + } + +} \ No newline at end of file diff --git a/Wave/Data/Article.cs b/Wave/Data/Article.cs index 8d46644..bd926d1 100644 --- a/Wave/Data/Article.cs +++ b/Wave/Data/Article.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Text; using System.Text.RegularExpressions; using Wave.Utilities; @@ -60,15 +61,32 @@ public partial class Article : ISoftDelete { public void UpdateSlug(string? potentialNewSlug = null) { if (!string.IsNullOrWhiteSpace(potentialNewSlug) && Uri.IsWellFormedUriString(potentialNewSlug, UriKind.Relative)) { + if (potentialNewSlug.Length > 64) potentialNewSlug = potentialNewSlug[..64]; Slug = potentialNewSlug; return; } - if (string.IsNullOrWhiteSpace(potentialNewSlug) && !string.IsNullOrWhiteSpace(Slug)) return; + // if (string.IsNullOrWhiteSpace(potentialNewSlug) && !string.IsNullOrWhiteSpace(Slug)) return; + + string baseSlug = string.IsNullOrWhiteSpace(potentialNewSlug) ? Title.ToLower() : potentialNewSlug; + + { + baseSlug = Regex.Replace(Uri.EscapeDataString(Encoding.ASCII.GetString( + Encoding.Convert( + Encoding.UTF8, + Encoding.GetEncoding( + Encoding.ASCII.EncodingName, + new EncoderReplacementFallback(string.Empty), + new DecoderExceptionFallback()), + Encoding.UTF8.GetBytes(baseSlug)) + ).Replace("-", "+").Replace(" ", "-")), @"(%[\dA-F]{2})", string.Empty); + if (baseSlug.Length > 64) baseSlug = baseSlug[..64]; + Slug = baseSlug; + return; + } - string baseSlug = potentialNewSlug ?? Title; baseSlug = baseSlug.ToLowerInvariant()[..Math.Min(64, baseSlug.Length)]; - string slug = Uri.EscapeDataString(baseSlug).Replace("-", "+").Replace("%20", "-"); + string slug = Uri.EscapeDataString(baseSlug); // I hate my life int escapeTrimOvershoot = 0; diff --git a/Wave/Data/Transactional/ArticleDto.cs b/Wave/Data/Transactional/ArticleDto.cs new file mode 100644 index 0000000..d5ebc74 --- /dev/null +++ b/Wave/Data/Transactional/ArticleDto.cs @@ -0,0 +1,81 @@ +using System.ComponentModel.DataAnnotations; + +namespace Wave.Data.Transactional; + +public abstract class ArticleDto( + string? slug, + DateTimeOffset? publishDate, + Guid[]? categories, + Guid[]? images) +{ + [MaxLength(64)] + public string? Slug { get; init; } = slug; + + public DateTimeOffset? PublishDate { get; init; } = publishDate; + public Guid[]? Categories { get; init; } = categories; + public Guid[]? Images { get; init; } = images; + + public void Deconstruct( + out string? slug, out DateTimeOffset? publishDate, out Guid[]? categories, out Guid[]? images) { + slug = Slug; + publishDate = PublishDate; + categories = Categories; + images = Images; + } +} + +public class ArticleCreateDto( + string title, + [Required(AllowEmptyStrings = false)] string body, + string? slug, + DateTimeOffset? publishDate, + Guid[]? categories, + Guid[]? images) : ArticleDto(slug, publishDate, categories, images) +{ + [Required(AllowEmptyStrings = false)] + [MaxLength(256)] + public string Title { get; init; } = title; + + public string Body { get; init; } = body; + + public void Deconstruct( + out string title, out string body, out string? slug, out DateTimeOffset? publishDate, out Guid[]? categories, + out Guid[]? images) { + title = Title; + body = Body; + slug = Slug; + publishDate = PublishDate; + categories = Categories; + images = Images; + } +} + +public class ArticleUpdateDto( + Guid id, + string? title = null, + string? body = null, + string? slug = null, + DateTimeOffset? publishDate = null, + Guid[]? categories = null, + Guid[]? images = null) : ArticleDto(slug, publishDate, categories, images) +{ + [Required] + public Guid Id { get; init; } = id; + + [MaxLength(256)] + public string? Title { get; init; } = title; + + public string? Body { get; init; } = body; + + public void Deconstruct( + out Guid id, out string? title, out string? body, out string? slug, out DateTimeOffset? publishDate, + out Guid[]? categories, out Guid[]? images) { + id = Id; + title = Title; + body = Body; + slug = Slug; + publishDate = PublishDate; + categories = Categories; + images = Images; + } +} \ No newline at end of file diff --git a/Wave/Data/Transactional/ArticleView.cs b/Wave/Data/Transactional/ArticleView.cs new file mode 100644 index 0000000..28b75a6 --- /dev/null +++ b/Wave/Data/Transactional/ArticleView.cs @@ -0,0 +1,30 @@ +namespace Wave.Data.Transactional; + +public sealed record CategoryView(Guid id, string Name, CategoryColors Color) { + public CategoryView(Category category) : this( + category.Id, category.Name, category.Color) {} + +} + +public sealed record ArticleView( + Guid Id, + string Title, + string Slug, + string BodyHtml, + string Body, + string BodyPlain, + ArticleStatus Status, + DateTimeOffset PublishDate, + IReadOnlyList Categories) +{ + public ArticleView(Article article) : this( + article.Id, + article.Title, + article.Slug, + article.BodyHtml, + article.Body, + article.BodyPlain, + article.Status, + article.PublishDate, + article.Categories.Select(c => new CategoryView(c)).ToList()) {} +} \ No newline at end of file diff --git a/Wave/Dockerfile b/Wave/Dockerfile index 7a03e28..6510898 100644 --- a/Wave/Dockerfile +++ b/Wave/Dockerfile @@ -13,6 +13,19 @@ EXPOSE 8080 HEALTHCHECK --start-period=5s --start-interval=15s --interval=30s --timeout=30s --retries=3 \ CMD curl --fail http://localhost:8080/health || exit 1 +FROM node:20-alpine AS vite-build +WORKDIR /src +RUN mkdir -p "wwwroot" +COPY ["Wave/package.json", "Wave/package-lock.json", "./"] +RUN npm install +COPY [ \ + "Wave/tsconfig.json", \ + "Wave/tsconfig.node.json", \ + "Wave/*.config.ts", \ + "./"] +COPY ["Wave/Assets/", "./Assets/"] +RUN npx vite build + FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build ARG BUILD_CONFIGURATION=Release ARG VERSION=0.0.1 @@ -38,6 +51,7 @@ RUN dotnet publish "./Wave.csproj" \ FROM base AS final WORKDIR /app +COPY --from=vite-build /src/wwwroot ./wwwroot COPY --from=publish /app/publish . COPY LICENSE . ENTRYPOINT ["dotnet", "Wave.dll"] diff --git a/Wave/Program.cs b/Wave/Program.cs index de94552..2352d80 100644 --- a/Wave/Program.cs +++ b/Wave/Program.cs @@ -26,6 +26,7 @@ using Serilog; using Serilog.Events; using Serilog.Sinks.Grafana.Loki; +using Vite.AspNetCore; using Wave.Utilities.Metrics; #region Version Information @@ -81,6 +82,13 @@ options.OutputFormatters.Add(new SyndicationFeedFormatter()); }); builder.Services.AddOutputCache(); +builder.Services.AddViteServices(options => { + // options.Server.AutoRun = true; + options.Server.ScriptName = "dev"; + options.Server.Https = false; + options.Server.UseReactRefresh = true; + options.Base = "/dist/"; +}); #region Data Protection & Redis @@ -198,6 +206,7 @@ .AddSignInManager() .AddDefaultTokenProviders() .AddClaimsPrincipalFactory(); +builder.Services.AddScoped(); #endregion @@ -334,18 +343,22 @@ } } }); +app.UseAuthentication(); +app.UseAuthorization(); + app.UseAntiforgery(); -app.MapRazorComponents().AddInteractiveServerRenderMode(); - -// Add additional endpoints required by the Identity /Account Razor components. -app.MapAdditionalIdentityEndpoints(); - app.MapHealthChecks("/health"); - +app.MapRazorComponents().AddInteractiveServerRenderMode(); +app.MapAdditionalIdentityEndpoints(); app.MapControllers(); -app.UseOutputCache(); +if (app.Environment.IsDevelopment()) { + //app.UseWebSockets(); + app.UseViteDevelopmentServer(true); +} + +app.UseOutputCache(); app.UseRequestLocalization(); { diff --git a/Wave/Utilities/Permissions.cs b/Wave/Utilities/Permissions.cs index 6114edc..c8cad1e 100644 --- a/Wave/Utilities/Permissions.cs +++ b/Wave/Utilities/Permissions.cs @@ -15,7 +15,9 @@ public static class Permissions { if (article.Status >= ArticleStatus.Published && article.PublishDate <= DateTimeOffset.UtcNow) { return true; } - + + if (principal.Identity?.IsAuthenticated is false) return false; + // Admins always get access if (principal.IsInRole("Admin")) { return true; @@ -33,9 +35,26 @@ public static class Permissions { return false; } + + public static bool AllowedToCreate(ClaimsPrincipal principal) { + if (principal.Identity?.IsAuthenticated is false) return false; + + // Admins always get access + if (principal.IsInRole("Admin")) { + return true; + } + + // Authors can author articles (duh) + if (principal.IsInRole("Author")) { + return true; + } + + return false; + } public static bool AllowedToEdit(this Article? article, ClaimsPrincipal principal) { if (article is null || article.IsDeleted) return false; + if (principal.Identity?.IsAuthenticated is false) return false; // anon users can't edit ever if (article.Author is null) throw new ArgumentException("Checking permissions without loading related Author."); // Admins always can edit articles @@ -70,12 +89,14 @@ public static class Permissions { } public static bool AllowedToRejectReview(this Article? article, ClaimsPrincipal principal) { + if (principal.Identity?.IsAuthenticated is false) return false; // anon users can't review ever // if you can publish it, you can reject it return article?.Status is ArticleStatus.InReview && article.AllowedToPublish(principal); } public static bool AllowedToSubmitForReview(this Article? article, ClaimsPrincipal principal) { if (article is null || article.IsDeleted) return false; + if (principal.Identity?.IsAuthenticated is false) return false; // anon users can't edit ever if (article.Author is null) throw new ArgumentException("Checking permissions without loading related Author."); // Draft articles can be submitted by their authors (admins can publish them anyway, no need to submit) @@ -88,6 +109,7 @@ public static class Permissions { public static bool AllowedToPublish(this Article? article, ClaimsPrincipal principal) { if (article is null || article.IsDeleted) return false; + if (principal.Identity?.IsAuthenticated is false) return false; // anon users can't publish ever if (article.Author is null) throw new ArgumentException("Checking permissions without loading related Author."); // Admins can skip review and directly publish draft articles @@ -111,6 +133,7 @@ public static class Permissions { public static bool AllowedToDelete(this Article? article, ClaimsPrincipal principal) { if (article is null || article.IsDeleted) return false; + if (principal.Identity?.IsAuthenticated is false) return false; // anon users can't delete ever if (article.Author is null) throw new ArgumentException("Checking permissions without loading related Author."); // Admins can delete articles whenever diff --git a/Wave/Wave.csproj b/Wave/Wave.csproj index 97de6c4..79c7f58 100644 --- a/Wave/Wave.csproj +++ b/Wave/Wave.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -9,25 +9,29 @@ ..\docker-compose.dcproj + + + + - + - - - + + + - + - + - - + + @@ -42,10 +46,7 @@ - - - - + diff --git a/Wave/appsettings.Development.json b/Wave/appsettings.Development.json index 7a73a41..0c7a32a 100644 --- a/Wave/appsettings.Development.json +++ b/Wave/appsettings.Development.json @@ -1,2 +1,3 @@ { + "DetailedErrors": true } \ No newline at end of file diff --git a/Wave/package-lock.json b/Wave/package-lock.json index c348a2b..9c19520 100644 Binary files a/Wave/package-lock.json and b/Wave/package-lock.json differ diff --git a/Wave/package.json b/Wave/package.json index 0fba23e..afd4350 100644 --- a/Wave/package.json +++ b/Wave/package.json @@ -1,18 +1,38 @@ { - "name": "wave", - "version": "0.0.1", - "scripts": { - "css:build": "postcss -o wwwroot/css/main.min.css wwwroot/css/main.css" - }, - "author": "Mia Rose Winter", - "license": "MIT", - "devDependencies": { - "@tailwindcss/typography": "^0.5.10", - "autoprefixer": "^10.4.16", - "cssnano": "^6.0.3", - "daisyui": "^4.6.0", - "postcss": "^8.4.33", - "postcss-cli": "^11.0.0", - "tailwindcss": "^3.4.1" - } + "name": "wave", + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "check": "tsc", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "author": "Mia Rose Winter", + "license": "MIT", + "devDependencies": { + "@fontsource/noto-sans-display": "^5.0.20", + "@fontsource/nunito-sans": "^5.0.13", + "@tailwindcss/typography": "^0.5.10", + "@types/node": "^20.14.2", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react-swc": "^3.7.0", + "autoprefixer": "^10.4.16", + "cssnano": "^6.0.3", + "daisyui": "^4.6.0", + "i18next": "^23.11.5", + "markdown-it": "^14.1.0", + "markdown-it-mark": "^4.0.0", + "postcss": "^8.4.33", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^14.1.2", + "tailwindcss": "^3.4.1", + "typescript": "^5.4.5", + "vite": "^5.2.13" + }, + "dependencies": { + "groupby-polyfill": "^1.0.0" + } } diff --git a/Wave/postcss.config.js b/Wave/postcss.config.js deleted file mode 100644 index b6d05b3..0000000 --- a/Wave/postcss.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - cssnano: { - preset: "default" - } - } -} diff --git a/Wave/tailwind.config.js b/Wave/tailwind.config.ts similarity index 97% rename from Wave/tailwind.config.js rename to Wave/tailwind.config.ts index 8c204ab..c2bd463 100644 --- a/Wave/tailwind.config.js +++ b/Wave/tailwind.config.ts @@ -1,9 +1,8 @@ +const defaultTheme = require('tailwindcss/defaultTheme'); /** @type {import('tailwindcss').Config} */ -const defaultTheme = require('tailwindcss/defaultTheme') - -module.exports = { - content: ["Pages/**/*.cshtml", "Components/**/*.razor"], +export default { + content: ["Assets/**/*.{ts,tsx}", "Pages/**/*.cshtml", "Components/**/*.razor"], safelist: ["youtube"], theme: { extend: { diff --git a/Wave/tsconfig.json b/Wave/tsconfig.json new file mode 100644 index 0000000..ea94063 --- /dev/null +++ b/Wave/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": [ "ES2020", "ES2020.Promise", "DOM", "DOM.Iterable" ], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "typeRoots": [ "node_modules/@types" ], + + "strict": true + }, + "include": [ + "Assets/**/*.ts", "Assets/**/*.tsx" + ], + "references": [ + { "path": "./tsconfig.node.json" } + ] +} \ No newline at end of file diff --git a/Wave/tsconfig.node.json b/Wave/tsconfig.node.json new file mode 100644 index 0000000..32a7def --- /dev/null +++ b/Wave/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/Wave/vite.config.ts b/Wave/vite.config.ts new file mode 100644 index 0000000..28b70b2 --- /dev/null +++ b/Wave/vite.config.ts @@ -0,0 +1,52 @@ +import { defineConfig } from "vite"; +import tailwindcss from "tailwindcss"; +import autoprefixer from "autoprefixer"; +import cssnano from "cssnano"; +import react from "@vitejs/plugin-react-swc" + +export default defineConfig({ + appType: "custom", + base: "/dist/", + root: "Assets", + publicDir: "public", + build: { + emptyOutDir: true, + manifest: true, + outDir: "../wwwroot/dist", + assetsDir: "", + rollupOptions: { + input: [ + "Assets/css/main.css", + "Assets/main.tsx" + ], + output: { + entryFileNames: "js/[name].[hash].js", + chunkFileNames: "js/[name]-chunk.js", + assetFileNames: (info) => { + if (info.name) { + if (/\.css$/.test(info.name)) { + return "css/[name].[hash][extname]"; + } + + return "[name][extname]"; + } else { + return "[name][extname]"; + } + } + } + }, + }, + optimizeDeps: { + include: [] + }, + plugins: [react()], + css: { + postcss: { + plugins: [ + tailwindcss(), + autoprefixer(), + cssnano({preset: "default"}), + ] + } + } +}); \ No newline at end of file diff --git a/Wave/wwwroot/css/main.css b/Wave/wwwroot/css/main.css deleted file mode 100644 index 85a0d80..0000000 --- a/Wave/wwwroot/css/main.css +++ /dev/null @@ -1,58 +0,0 @@ -@font-face { - font-display: swap; - font-family: 'Nunito Sans'; - font-style: normal; - font-weight: 700; - src: url('../fonts/nunito-sans-v15-latin-700.woff2') format('woff2'); -} -@font-face { - font-display: swap; - font-family: 'Noto Sans Display'; - font-style: normal; - font-weight: 400; - src: url('../fonts/noto-sans-display-v26-latin-regular.woff2') format('woff2'); -} - -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - body { - @apply font-body; - } - h1, h2, h3, h4, h5, h6 { - @apply font-heading tracking-tight; - } - hyphens-auto { - hyphenate-limit-chars: 5 3; - } -} -@layer components { - .youtube { - @apply rounded p-2 bg-base-200; - } - - input.narrow-reading-toggle { - display: none; - } - body:has(label[for=narrow-reading-toggle]) input.narrow-reading-toggle:checked + .reading-toggle-target { - @apply max-w-3xl; - } - - .fade-away { - -webkit-mask-image: linear-gradient(black, black 80%, rgba(0, 0, 0, 0.5) 85%, transparent 100%); - mask-image: linear-gradient(black, black 80%, rgba(0, 0, 0, 0.5) 85%, transparent 100%); - } - - .prose div > pre, .prose > pre { - @apply bg-inherit text-inherit rounded-none; - } - .prose pre:has(code) { - @apply border-2 border-current; - } - - .characters-left { - @apply absolute right-6 bottom-6 select-none pointer-events-none; - } -} diff --git a/Wave/wwwroot/css/main.min.css b/Wave/wwwroot/css/main.min.css deleted file mode 100644 index 6a4bc3b..0000000 --- a/Wave/wwwroot/css/main.min.css +++ /dev/null @@ -1,3 +0,0 @@ -@font-face{font-display:swap;font-family:Nunito Sans;font-style:normal;font-weight:700;src:url(../fonts/nunito-sans-v15-latin-700.woff2) format("woff2")}@font-face{font-display:swap;font-family:Noto Sans Display;font-style:normal;font-weight:400;src:url(../fonts/noto-sans-display-v26-latin-regular.woff2) format("woff2")} - -/*! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;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;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}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-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-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{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}:root,[data-theme]{background-color:var(--fallback-b1,oklch(var(--b1)/1));color:var(--fallback-bc,oklch(var(--bc)/1))}@supports not (color:oklch(0 0 0)){:root{color-scheme:light;--fallback-p:#491eff;--fallback-pc:#d4dbff;--fallback-s:#ff41c7;--fallback-sc:#fff9fc;--fallback-a:#00cfbd;--fallback-ac:#00100d;--fallback-n:#2b3440;--fallback-nc:#d7dde4;--fallback-b1:#fff;--fallback-b2:#e5e6e6;--fallback-b3:#e5e6e6;--fallback-bc:#1f2937;--fallback-in:#00b3f0;--fallback-inc:#000;--fallback-su:#00ca92;--fallback-suc:#000;--fallback-wa:#ffc22d;--fallback-wac:#000;--fallback-er:#ff6f70;--fallback-erc:#000}@media (prefers-color-scheme:dark){:root{color-scheme:dark;--fallback-p:#7582ff;--fallback-pc:#050617;--fallback-s:#ff71cf;--fallback-sc:#190211;--fallback-a:#00c7b5;--fallback-ac:#000e0c;--fallback-n:#2a323c;--fallback-nc:#a6adbb;--fallback-b1:#1d232a;--fallback-b2:#191e24;--fallback-b3:#15191e;--fallback-bc:#a6adbb;--fallback-in:#00b3f0;--fallback-inc:#000;--fallback-su:#00ca92;--fallback-suc:#000;--fallback-wa:#ffc22d;--fallback-wac:#000;--fallback-er:#ff6f70;--fallback-erc:#000}}}html{-webkit-tap-highlight-color:transparent}:root{--wave-heading-font:Nunito Sans;--pc:0.180887 0.036283 105.052616;--ac:0.164005 0.020628 219.654428;--nc:0.136569 0.027089 105.04608;--inc:0.130481 0.033356 249.181093;--suc:0.155205 0.051559 141.441374;--wac:0.187418 0.039766 105.010469;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--wave-body-font:Noto Sans Display;--p:0.904433 0.181415 105.052616;--s:0.844147 0.091903 1.145315;--sc:0.159066 0 0;--a:0.820026 0.103138 219.654428;--n:0.682843 0.135444 105.04608;--b1:0.979009 0.008213 91.481779;--b2:0.829552 0.021252 91.613791;--b3:0.657213 0.03418 91.110572;--bc:0 0 0;--in:0.652403 0.166781 249.181093;--su:0.776026 0.257795 141.441374;--wa:0.937091 0.198832 105.010469;--er:0.48269 0.196179 27.62926;--erc:1 0 0;--rounded-box:0.2rem;--rounded-btn:0.2rem;--rounded-badge:0.2rem}@media (prefers-color-scheme:dark){:root{--wave-heading-font:Nunito Sans;--pc:0.188058 0.037706 104.932014;--ac:0.170394 0.0215 220.006723;--nc:0.136569 0.027089 105.04608;--inc:0.118647 0.036441 252.872278;--suc:0.110432 0.036439 141.138418;--wac:0.170992 0.036177 104.408029;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--wave-body-font:Noto Sans Display;--p:0.940289 0.188531 104.932014;--s:0.754158 0.080841 1.060979;--sc:0 0 0;--a:0.851972 0.107498 220.006723;--n:0.682843 0.135444 105.04608;--b1:0.223295 0.035604 1.967265;--b2:0.160428 0.018448 359.8262;--b3:0.149116 0.005595 355.678569;--bc:0.940422 0.025515 288.31809;--in:0.593236 0.182207 252.872278;--su:0.55216 0.182195 141.138418;--wa:0.854959 0.180883 104.408029;--er:0.310766 0.125346 26.370357;--erc:1 0 0;--rounded-box:0.2rem;--rounded-btn:0.2rem;--rounded-badge:0.2rem}}[data-theme=wave-light]{--wave-heading-font:Nunito Sans;--pc:0.180887 0.036283 105.052616;--ac:0.164005 0.020628 219.654428;--nc:0.136569 0.027089 105.04608;--inc:0.130481 0.033356 249.181093;--suc:0.155205 0.051559 141.441374;--wac:0.187418 0.039766 105.010469;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--wave-body-font:Noto Sans Display;--p:0.904433 0.181415 105.052616;--s:0.844147 0.091903 1.145315;--sc:0.159066 0 0;--a:0.820026 0.103138 219.654428;--n:0.682843 0.135444 105.04608;--b1:0.979009 0.008213 91.481779;--b2:0.829552 0.021252 91.613791;--b3:0.657213 0.03418 91.110572;--bc:0 0 0;--in:0.652403 0.166781 249.181093;--su:0.776026 0.257795 141.441374;--wa:0.937091 0.198832 105.010469;--er:0.48269 0.196179 27.62926;--erc:1 0 0;--rounded-box:0.2rem;--rounded-btn:0.2rem;--rounded-badge:0.2rem}[data-theme=wave-dark]{--wave-heading-font:Nunito Sans;--pc:0.188058 0.037706 104.932014;--ac:0.170394 0.0215 220.006723;--nc:0.136569 0.027089 105.04608;--inc:0.118647 0.036441 252.872278;--suc:0.110432 0.036439 141.138418;--wac:0.170992 0.036177 104.408029;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--wave-body-font:Noto Sans Display;--p:0.940289 0.188531 104.932014;--s:0.754158 0.080841 1.060979;--sc:0 0 0;--a:0.851972 0.107498 220.006723;--n:0.682843 0.135444 105.04608;--b1:0.223295 0.035604 1.967265;--b2:0.160428 0.018448 359.8262;--b3:0.149116 0.005595 355.678569;--bc:0.940422 0.025515 288.31809;--in:0.593236 0.182207 252.872278;--su:0.55216 0.182195 141.138418;--wa:0.854959 0.180883 104.408029;--er:0.310766 0.125346 26.370357;--erc:1 0 0;--rounded-box:0.2rem;--rounded-btn:0.2rem;--rounded-badge:0.2rem}[data-theme=modern-light]{--wave-heading-font:Tahoma;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--wave-body-font:ui-sans-serif;--p:0.379059 0.137761 265.522188;--pc:1 0 0;--s:0.985104 0 0;--sc:0.379059 0.137761 265.522188;--a:0.809069 0.095598 251.812784;--ac:0 0 0;--n:0.424445 0.180869 265.63771;--nc:1 0 0;--b1:1 0 0;--b2:0.966956 0.002874 264.541934;--b3:0.927582 0.005814 264.531291;--bc:0 0 0;--in:0.684687 0.147869 237.322518;--inc:1 0 0;--su:0.722746 0.192007 149.57933;--suc:1 0 0;--wa:0.76859 0.164659 70.08039;--wac:0 0 0;--er:0.50542 0.190493 27.518103;--erc:1 0 0;--rounded-box:0.2rem;--rounded-btn:0.2rem;--rounded-badge:0.2rem}[data-theme=modern-dark]{--wave-heading-font:Tahoma;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--wave-body-font:ui-sans-serif;--p:0.71374 0.143381 254.624021;--pc:0 0 0;--s:0.372927 0.03062 259.732849;--sc:0.71374 0.143381 254.624021;--a:0.809069 0.095598 251.812784;--ac:0 0 0;--n:0.54615 0.215208 262.880917;--nc:1 0 0;--b1:0.446112 0.026312 256.801751;--b2:0.278078 0.029596 256.847952;--b3:0.210084 0.031763 264.664526;--bc:1 0 0;--in:0.684687 0.147869 237.322518;--inc:1 0 0;--su:0.722746 0.192007 149.57933;--suc:1 0 0;--wa:0.76859 0.164659 70.08039;--wac:0 0 0;--er:0.50542 0.190493 27.518103;--erc:1 0 0;--rounded-box:0.2rem;--rounded-btn:0.2rem;--rounded-badge:0.2rem}body{font-family:var(--wave-body-font),ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"}h1,h2,h3,h4,h5,h6{font-family:var(--wave-heading-font),ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";letter-spacing:-.025em}hyphens-auto{hyphenate-limit-chars:5 3}*,: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: }::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: }.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}}@media (min-width:1792px){.container{max-width:1792px}}@media (min-width:2048px){.container{max-width:2048px}}@media (min-width:2304px){.container{max-width:2304px}}@media (min-width:2560px){.container{max-width:2560px}}@media (min-width:3072px){.container{max-width:3072px}}.alert{align-content:flex-start;align-items:center;border-radius:var(--rounded-box,1rem);border-width:1px;display:grid;gap:1rem;grid-auto-flow:row;justify-items:center;text-align:center;width:100%;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));padding:1rem;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--alert-bg:var(--fallback-b2,oklch(var(--b2)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1));background-color:var(--alert-bg)}@media (min-width:640px){.alert{grid-auto-flow:column;grid-template-columns:auto minmax(auto,1fr);justify-items:start;text-align:start}}.avatar{display:inline-flex;position:relative}.avatar>div{aspect-ratio:1/1;display:block;overflow:hidden}.avatar img{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.avatar.placeholder>div{display:flex}.avatar.placeholder>div,.badge{align-items:center;justify-content:center}.badge{border-radius:var(--rounded-badge,1.9rem);border-width:1px;display:inline-flex;font-size:.875rem;height:1.25rem;line-height:1.25rem;padding-left:.563rem;padding-right:.563rem;transition-duration:.2s;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-timing-function:cubic-bezier(0,0,.2,1);width:-moz-fit-content;width:fit-content;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}@media (hover:hover){.label a:hover{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.menu li>:not(ul):not(.menu-title):not(details).active,.menu li>:not(ul):not(.menu-title):not(details):active,.menu li>details>summary:active{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.table tr.hover:hover,.table tr.hover:nth-child(2n):hover{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.table-zebra tr.hover:hover,.table-zebra tr.hover:nth-child(2n):hover{--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}}.btn{align-items:center;animation:button-pop var(--animation-btn,.25s) ease-out;border-color:transparent;border-color:oklch(var(--btn-color,var(--b2))/var(--tw-border-opacity));border-radius:var(--rounded-btn,.5rem);border-width:var(--border-btn,1px);cursor:pointer;display:inline-flex;flex-shrink:0;flex-wrap:wrap;font-size:.875rem;font-weight:600;gap:.5rem;height:3rem;justify-content:center;line-height:1em;min-height:3rem;padding-left:1rem;padding-right:1rem;text-align:center;text-decoration-line:none;transition-duration:.2s;transition-property:color,background-color,border-color,opacity,box-shadow,transform;transition-timing-function:cubic-bezier(0,0,.2,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);background-color:oklch(var(--btn-color,var(--b2))/var(--tw-bg-opacity));box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:var(--fallback-bc,oklch(var(--bc)/1));--tw-bg-opacity:1;--tw-border-opacity:1}.btn-disabled,.btn:disabled,.btn[disabled]{pointer-events:none}.btn-circle,.btn-square{height:3rem;padding:0;width:3rem}.btn-circle{border-radius:9999px}:where(.btn:is(input[type=checkbox])),:where(.btn:is(input[type=radio])){-webkit-appearance:none;-moz-appearance:none;appearance:none;width:auto}.btn:is(input[type=checkbox]):after,.btn:is(input[type=radio]):after{--tw-content:attr(aria-label);content:var(--tw-content)}.card{border-radius:var(--rounded-box,1rem);display:flex;flex-direction:column;position:relative}.card:focus{outline:2px solid transparent;outline-offset:2px}.card-body{display:flex;flex:1 1 auto;flex-direction:column;gap:.5rem;padding:var(--padding-card,2rem)}.card-body :where(p){flex-grow:1}.card-actions{align-items:flex-start;display:flex;flex-wrap:wrap;gap:.5rem}.card figure{align-items:center;display:flex;justify-content:center}.card.image-full{display:grid}.card.image-full:before{border-radius:var(--rounded-box,1rem);content:"";position:relative;z-index:10;--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));opacity:.75}.card.image-full:before,.card.image-full>*{grid-column-start:1;grid-row-start:1}.card.image-full>figure img{height:100%;-o-object-fit:cover;object-fit:cover}.card.image-full>.card-body{position:relative;z-index:20;--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.checkbox{flex-shrink:0;--chkbg:var(--fallback-bc,oklch(var(--bc)/1));--chkfg:var(--fallback-b1,oklch(var(--b1)/1));-webkit-appearance:none;-moz-appearance:none;appearance:none;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));border-radius:var(--rounded-btn,.5rem);border-width:1px;cursor:pointer;height:1.5rem;width:1.5rem;--tw-border-opacity:0.2}.drawer{display:grid;grid-auto-columns:max-content auto;position:relative;width:100%}.drawer-content{grid-column-start:2;min-width:0}.drawer-content,.drawer-side{grid-row-start:1}.drawer-side{align-items:flex-start;display:grid;grid-column-start:1;grid-template-columns:repeat(1,minmax(0,1fr));grid-template-rows:repeat(1,minmax(0,1fr));height:100vh;height:100dvh;inset-inline-start:0;justify-items:start;overflow-y:auto;overscroll-behavior:contain;pointer-events:none;position:fixed;top:0;width:100%}.drawer-side>.drawer-overlay{background-color:transparent;cursor:pointer;place-self:stretch;position:sticky;top:0;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1)}.drawer-side>*{grid-column-start:1;grid-row-start:1}.drawer-side>:not(.drawer-overlay){transform:translateX(-100%);transition-duration:.3s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1);will-change:transform}[dir=rtl] .drawer-side>:not(.drawer-overlay){transform:translateX(100%)}.drawer-toggle{-webkit-appearance:none;-moz-appearance:none;appearance:none;height:0;opacity:0;position:fixed;width:0}.drawer-toggle:checked~.drawer-side{pointer-events:auto;visibility:visible}.drawer-toggle:checked~.drawer-side>:not(.drawer-overlay){transform:translateX(0)}.drawer-end{grid-auto-columns:auto max-content}.drawer-end .drawer-toggle~.drawer-content{grid-column-start:1}.drawer-end .drawer-toggle~.drawer-side{grid-column-start:2;justify-items:end}.drawer-end .drawer-toggle~.drawer-side>:not(.drawer-overlay){transform:translateX(100%)}[dir=rtl] .drawer-end .drawer-toggle~.drawer-side>:not(.drawer-overlay){transform:translateX(-100%)}.drawer-end .drawer-toggle:checked~.drawer-side>:not(.drawer-overlay){transform:translateX(0)}.dropdown{display:inline-block;position:relative}.dropdown>:not(summary):focus{outline:2px solid transparent;outline-offset:2px}.dropdown .dropdown-content{position:absolute}.dropdown:is(:not(details)) .dropdown-content{opacity:0;transform-origin:top;visibility:hidden;--tw-scale-x:.95;--tw-scale-y:.95;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));transition-duration:.2s;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-timing-function:cubic-bezier(0,0,.2,1)}.dropdown-end .dropdown-content{inset-inline-end:0}.dropdown-left .dropdown-content{bottom:auto;inset-inline-end:100%;top:0;transform-origin:right}.dropdown-right .dropdown-content{bottom:auto;inset-inline-start:100%;top:0;transform-origin:left}.dropdown-bottom .dropdown-content{bottom:auto;top:100%;transform-origin:top}.dropdown-top .dropdown-content{bottom:100%;top:auto;transform-origin:bottom}.dropdown-end.dropdown-left .dropdown-content,.dropdown-end.dropdown-right .dropdown-content{bottom:0;top:auto}.dropdown.dropdown-open .dropdown-content,.dropdown:focus-within .dropdown-content,.dropdown:not(.dropdown-hover):focus .dropdown-content{opacity:1;visibility:visible}@media (hover:hover){.dropdown.dropdown-hover:hover .dropdown-content{opacity:1;visibility:visible}.btm-nav>.disabled:hover,.btm-nav>[disabled]:hover{pointer-events:none;--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.btn:hover{--tw-border-opacity:1;border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn:hover{background-color:color-mix(in oklab,oklch(var(--btn-color,var(--b2))/var(--tw-bg-opacity,1)) 90%,#000);border-color:color-mix(in oklab,oklch(var(--btn-color,var(--b2))/var(--tw-border-opacity,1)) 90%,#000)}}@supports not (color:oklch(0 0 0)){.btn:hover{background-color:var(--btn-color,var(--fallback-b2));border-color:var(--btn-color,var(--fallback-b2))}}.btn.glass:hover{--glass-opacity:25%;--glass-border-opacity:15%}.btn-ghost:hover{border-color:transparent}@supports (color:oklch(0 0 0)){.btn-ghost:hover{background-color:var(--fallback-bc,oklch(var(--bc)/.2))}}.btn-link:hover{background-color:transparent;border-color:transparent;text-decoration-line:underline}.btn-outline.btn-primary:hover{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-primary:hover{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000)}}.btn-outline.btn-secondary:hover{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-secondary:hover{background-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000)}}.btn-outline.btn-accent:hover{--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-accent:hover{background-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000)}}.btn-outline.btn-info:hover{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-info:hover{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000)}}.btn-outline.btn-error:hover{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-error:hover{background-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000)}}.btn-disabled:hover,.btn:disabled:hover,.btn[disabled]:hover{--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}@supports (color:color-mix(in oklab,black,black)){.btn:is(input[type=checkbox]:checked):hover,.btn:is(input[type=radio]:checked):hover{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000)}}.dropdown.dropdown-hover:hover .dropdown-content{--tw-scale-x:1;--tw-scale-y:1;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))}:where(.menu li:not(.menu-title):not(.disabled)>:not(ul):not(details):not(.menu-title)):not(.active):hover,:where(.menu li:not(.menu-title):not(.disabled)>details>summary:not(.menu-title)):not(.active):hover{cursor:pointer;outline:2px solid transparent;outline-offset:2px}@supports (color:oklch(0 0 0)){:where(.menu li:not(.menu-title):not(.disabled)>:not(ul):not(details):not(.menu-title)):not(.active):hover,:where(.menu li:not(.menu-title):not(.disabled)>details>summary:not(.menu-title)):not(.active):hover{background-color:var(--fallback-bc,oklch(var(--bc)/.1))}}}.dropdown:is(details) summary::-webkit-details-marker{display:none}.file-input{border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));border-radius:var(--rounded-btn,.5rem);border-width:1px;flex-shrink:1;font-size:1rem;height:3rem;line-height:2;line-height:1.5rem;overflow:hidden;padding-inline-end:1rem;--tw-border-opacity:0;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.file-input::file-selector-button{align-items:center;border-style:solid;cursor:pointer;display:inline-flex;flex-shrink:0;flex-wrap:wrap;font-size:.875rem;height:100%;justify-content:center;line-height:1.25rem;line-height:1em;margin-inline-end:1rem;padding-left:1rem;padding-right:1rem;text-align:center;transition-duration:.2s;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-timing-function:cubic-bezier(0,0,.2,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;--tw-border-opacity:1;border-color:var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));font-weight:600;text-transform:uppercase;--tw-text-opacity:1;animation:button-pop var(--animation-btn,.25s) ease-out;border-width:var(--border-btn,1px);color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));text-decoration-line:none}.footer{-moz-column-gap:1rem;column-gap:1rem;font-size:.875rem;grid-auto-flow:row;line-height:1.25rem;row-gap:2.5rem;width:100%}.footer,.footer>*{display:grid;place-items:start}.footer>*{gap:.5rem}@media (min-width:48rem){.footer{grid-auto-flow:column}.footer-center{grid-auto-flow:row dense}}.form-control{flex-direction:column}.form-control,.label{display:flex}.label{align-items:center;justify-content:space-between;padding:.5rem .25rem;-webkit-user-select:none;-moz-user-select:none;user-select:none}.input{-webkit-appearance:none;-moz-appearance:none;appearance:none;border-color:transparent;border-radius:var(--rounded-btn,.5rem);border-width:1px;flex-shrink:1;font-size:1rem;height:3rem;line-height:2;line-height:1.5rem;padding-left:1rem;padding-right:1rem;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.join{align-items:stretch;border-radius:var(--rounded-btn,.5rem);display:inline-flex}.join :where(.join-item){border-end-end-radius:0;border-end-start-radius:0;border-start-end-radius:0;border-start-start-radius:0}.join .join-item:not(:first-child):not(:last-child),.join :not(:first-child):not(:last-child) .join-item{border-end-end-radius:0;border-end-start-radius:0;border-start-end-radius:0;border-start-start-radius:0}.join .join-item:first-child:not(:last-child),.join :first-child:not(:last-child) .join-item{border-end-end-radius:0;border-start-end-radius:0}.join .dropdown .join-item:first-child:not(:last-child),.join :first-child:not(:last-child) .dropdown .join-item{border-end-end-radius:inherit;border-start-end-radius:inherit}.join :where(.join-item:first-child:not(:last-child)),.join :where(:first-child:not(:last-child) .join-item){border-end-start-radius:inherit;border-start-start-radius:inherit}.join .join-item:last-child:not(:first-child),.join :last-child:not(:first-child) .join-item{border-end-start-radius:0;border-start-start-radius:0}.join :where(.join-item:last-child:not(:first-child)),.join :where(:last-child:not(:first-child) .join-item){border-end-end-radius:inherit;border-start-end-radius:inherit}@supports not selector(:has(*)){:where(.join *){border-radius:inherit}}@supports selector(:has(*)){:where(.join :has(.join-item)){border-radius:inherit}}.\!link{cursor:pointer!important;text-decoration-line:underline!important}.link{cursor:pointer;text-decoration-line:underline}.menu{display:flex;flex-direction:column;flex-wrap:wrap;font-size:.875rem;line-height:1.25rem;padding:.5rem}.menu :where(li ul){margin-inline-start:1rem;padding-inline-start:.5rem;position:relative;white-space:nowrap}.menu :where(li:not(.menu-title)>:not(ul):not(details):not(.menu-title)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){align-content:flex-start;align-items:center;display:grid;gap:.5rem;grid-auto-columns:minmax(auto,max-content) auto max-content;grid-auto-flow:column;-webkit-user-select:none;-moz-user-select:none;user-select:none}.menu li.disabled{color:var(--fallback-bc,oklch(var(--bc)/.3));cursor:not-allowed;-webkit-user-select:none;-moz-user-select:none;user-select:none}.menu :where(li>.menu-dropdown:not(.menu-dropdown-show)){display:none}:where(.menu li){align-items:stretch;display:flex;flex-direction:column;flex-shrink:0;flex-wrap:wrap;position:relative}:where(.menu li) .badge{justify-self:end}.modal{background-color:transparent;color:inherit;display:grid;height:100%;inset:0;justify-items:center;margin:0;max-height:none;max-width:none;opacity:0;overflow-y:hidden;overscroll-behavior:contain;padding:0;pointer-events:none;position:fixed;transition-duration:.2s;transition-property:transform,opacity,visibility;transition-timing-function:cubic-bezier(0,0,.2,1);width:100%;z-index:999}:where(.modal){align-items:center}.modal-box{grid-column-start:1;grid-row-start:1;max-height:calc(100vh - 5em);max-width:32rem;width:91.666667%;--tw-scale-x:.9;--tw-scale-y:.9;border-bottom-left-radius:var(--rounded-box,1rem);border-bottom-right-radius:var(--rounded-box,1rem);border-top-left-radius:var(--rounded-box,1rem);border-top-right-radius:var(--rounded-box,1rem);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));--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));box-shadow:0 25px 50px -12px rgba(0,0,0,.25);overflow-y:auto;overscroll-behavior:contain;padding:1.5rem;transition-duration:.2s;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-timing-function:cubic-bezier(0,0,.2,1)}.modal-open,.modal-toggle:checked+.modal,.modal:target,.modal[open]{opacity:1;pointer-events:auto;visibility:visible}.modal-action{display:flex;justify-content:flex-end;margin-top:1.5rem}:root:has(:is(.modal-open,.modal:target,.modal-toggle:checked+.modal,.modal[open])){overflow:hidden}.progress{background-color:var(--fallback-bc,oklch(var(--bc)/.2));border-radius:var(--rounded-box,1rem);height:.5rem;overflow:hidden;position:relative;width:100%}.progress,.select{-webkit-appearance:none;-moz-appearance:none;appearance:none}.select{border-color:transparent;border-radius:var(--rounded-btn,.5rem);border-width:1px;cursor:pointer;display:inline-flex;font-size:.875rem;height:3rem;line-height:1.25rem;line-height:2;min-height:3rem;padding-left:1rem;padding-right:2.5rem;-webkit-user-select:none;-moz-user-select:none;user-select:none;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));background-image:linear-gradient(45deg,transparent 50%,currentColor 0),linear-gradient(135deg,currentColor 50%,transparent 0);background-position:calc(100% - 20px) calc(1px + 50%),calc(100% - 16.1px) calc(1px + 50%);background-repeat:no-repeat;background-size:4px 4px,4px 4px}.select[multiple]{height:auto}.steps{counter-reset:step;display:inline-grid;grid-auto-columns:1fr;grid-auto-flow:column;overflow:hidden;overflow-x:auto}.steps .step{display:grid;grid-template-columns:repeat(1,minmax(0,1fr));grid-template-columns:auto;grid-template-rows:repeat(2,minmax(0,1fr));grid-template-rows:40px 1fr;min-width:4rem;place-items:center;text-align:center}.swap{cursor:pointer;display:inline-grid;place-content:center;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.swap>*{grid-column-start:1;grid-row-start:1;transition-duration:.3s;transition-property:transform,opacity;transition-timing-function:cubic-bezier(0,0,.2,1)}.swap input{-webkit-appearance:none;-moz-appearance:none;appearance:none}.swap .swap-indeterminate,.swap .swap-on,.swap input:indeterminate~.swap-on{opacity:0}.swap input:checked~.swap-off,.swap input:indeterminate~.swap-off,.swap-active .swap-off{opacity:0}.swap input:checked~.swap-on,.swap input:indeterminate~.swap-indeterminate,.swap-active .swap-on{opacity:1}.table{border-radius:var(--rounded-box,1rem);font-size:.875rem;line-height:1.25rem;position:relative;text-align:left;width:100%}.table :where(.table-pin-rows thead tr){position:sticky;top:0;z-index:1;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.table :where(.table-pin-rows tfoot tr){bottom:0;position:sticky;z-index:1;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.table :where(.table-pin-cols tr th){left:0;position:sticky;right:0;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.table-zebra tbody tr:nth-child(2n) :where(.table-pin-cols tr th){--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.textarea{border-color:transparent;border-radius:var(--rounded-btn,.5rem);border-width:1px;flex-shrink:1;font-size:.875rem;line-height:1.25rem;line-height:2;min-height:3rem;padding:.5rem 1rem;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.toast{display:flex;flex-direction:column;gap:.5rem;min-width:-moz-fit-content;min-width:fit-content;padding:1rem;position:fixed;white-space:nowrap}.toggle{flex-shrink:0;--tglbg:var(--fallback-b1,oklch(var(--b1)/1));--handleoffset:1.5rem;--handleoffsetcalculator:calc(var(--handleoffset)*-1);--togglehandleborder:0 0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:currentColor;border-color:currentColor;border-radius:var(--rounded-badge,1.9rem);border-width:1px;box-shadow:var(--handleoffsetcalculator) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset,var(--togglehandleborder);color:var(--fallback-bc,oklch(var(--bc)/.5));cursor:pointer;height:1.5rem;transition:background,box-shadow var(--animation-input,.2s) ease-out;width:3rem}.alert-info{border-color:var(--fallback-in,oklch(var(--in)/.2));--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));--alert-bg:var(--fallback-in,oklch(var(--in)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1))}.alert-success{border-color:var(--fallback-su,oklch(var(--su)/.2));--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)));--alert-bg:var(--fallback-su,oklch(var(--su)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1))}.alert-warning{border-color:var(--fallback-wa,oklch(var(--wa)/.2));--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)));--alert-bg:var(--fallback-wa,oklch(var(--wa)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1))}.alert-error{border-color:var(--fallback-er,oklch(var(--er)/.2));--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));--alert-bg:var(--fallback-er,oklch(var(--er)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1))}.avatar-group :where(.avatar){border-radius:9999px;border-width:4px;overflow:hidden;--tw-border-opacity:1;border-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-border-opacity)))}.badge-neutral{background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));border-color:var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity)));color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.badge-neutral,.badge-primary{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1}.badge-primary{background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.badge-secondary{background-color:var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity)));border-color:var(--fallback-s,oklch(var(--s)/var(--tw-border-opacity)));color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.badge-accent,.badge-secondary{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1}.badge-accent{background-color:var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity)));border-color:var(--fallback-a,oklch(var(--a)/var(--tw-border-opacity)));color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.badge-info{background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)));color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.badge-info,.badge-warning{border-color:transparent;--tw-bg-opacity:1;--tw-text-opacity:1}.badge-warning{background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)));color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.badge-error{border-color:transparent;--tw-bg-opacity:1;background-color:var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.badge-outline{border-color:currentColor;--tw-border-opacity:0.5;background-color:transparent;color:currentColor}.badge-outline.badge-neutral{--tw-text-opacity:1;color:var(--fallback-n,oklch(var(--n)/var(--tw-text-opacity)))}.badge-outline.badge-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.badge-outline.badge-secondary{--tw-text-opacity:1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)))}.badge-outline.badge-accent{--tw-text-opacity:1;color:var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)))}.badge-outline.badge-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.badge-outline.badge-success{--tw-text-opacity:1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.badge-outline.badge-warning{--tw-text-opacity:1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.badge-outline.badge-error{--tw-text-opacity:1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.btm-nav>.disabled,.btm-nav>[disabled]{pointer-events:none;--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.btm-nav>* .label{font-size:1rem;line-height:1.5rem}.btn:active:focus,.btn:active:hover{animation:button-pop 0s ease-out;transform:scale(var(--btn-focus-scale,.97))}@supports not (color:oklch(0 0 0)){.btn{background-color:var(--btn-color,var(--fallback-b2));border-color:var(--btn-color,var(--fallback-b2))}.btn-primary{--btn-color:var(--fallback-p)}.btn-secondary{--btn-color:var(--fallback-s)}.btn-accent{--btn-color:var(--fallback-a)}.btn-neutral{--btn-color:var(--fallback-n)}.btn-info{--btn-color:var(--fallback-in)}.btn-error{--btn-color:var(--fallback-er)}.prose :where(code):not(:where([class~=not-prose] *,pre *)){background-color:var(--fallback-b3,oklch(var(--b3)/1))}}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-primary.btn-active{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000)}.btn-outline.btn-secondary.btn-active{background-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000)}.btn-outline.btn-accent.btn-active{background-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000)}.btn-outline.btn-info.btn-active{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000)}.btn-outline.btn-error.btn-active{background-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000)}}.btn:focus-visible{outline-offset:2px;outline-style:solid;outline-width:2px}.btn-primary{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));outline-color:var(--fallback-p,oklch(var(--p)/1))}@supports (color:oklch(0 0 0)){.btn-primary{--btn-color:var(--p)}.btn-secondary{--btn-color:var(--s)}.btn-accent{--btn-color:var(--a)}.btn-neutral{--btn-color:var(--n)}.btn-info{--btn-color:var(--in)}.btn-error{--btn-color:var(--er)}}.btn-secondary{color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)));outline-color:var(--fallback-s,oklch(var(--s)/1))}.btn-accent,.btn-secondary{--tw-text-opacity:1}.btn-accent{color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)));outline-color:var(--fallback-a,oklch(var(--a)/1))}.btn-neutral{color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));outline-color:var(--fallback-n,oklch(var(--n)/1))}.btn-info,.btn-neutral{--tw-text-opacity:1}.btn-info{color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));outline-color:var(--fallback-in,oklch(var(--in)/1))}.btn-error{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));outline-color:var(--fallback-er,oklch(var(--er)/1))}.btn.glass{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:currentColor}.btn.glass.btn-active{--glass-opacity:25%;--glass-border-opacity:15%}.btn-ghost{background-color:transparent;border-width:1px;color:currentColor;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:currentColor}.btn-ghost,.btn-ghost.btn-active{border-color:transparent}.btn-ghost.btn-active{background-color:var(--fallback-bc,oklch(var(--bc)/.2))}.btn-link{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)));--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:currentColor}.btn-link,.btn-link.btn-active{background-color:transparent;border-color:transparent;text-decoration-line:underline}.btn-outline.btn-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.btn-outline.btn-primary.btn-active{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn-outline.btn-secondary{--tw-text-opacity:1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)))}.btn-outline.btn-secondary.btn-active{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.btn-outline.btn-accent{--tw-text-opacity:1;color:var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)))}.btn-outline.btn-accent.btn-active{--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.btn-outline.btn-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.btn-outline.btn-info.btn-active{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.btn-outline.btn-error{--tw-text-opacity:1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.btn-outline.btn-error.btn-active{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.btn.btn-disabled,.btn:disabled,.btn[disabled]{--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.btn:is(input[type=checkbox]:checked),.btn:is(input[type=radio]:checked){--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn:is(input[type=checkbox]:checked):focus-visible,.btn:is(input[type=radio]:checked):focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}@keyframes button-pop{0%{transform:scale(var(--btn-focus-scale,.98))}40%{transform:scale(1.02)}to{transform:scale(1)}}.card :where(figure:first-child){border-end-end-radius:unset;border-end-start-radius:unset;border-start-end-radius:inherit;border-start-start-radius:inherit;overflow:hidden}.card :where(figure:last-child){border-end-end-radius:inherit;border-end-start-radius:inherit;border-start-end-radius:unset;border-start-start-radius:unset;overflow:hidden}.card:focus-visible{outline:2px solid currentColor;outline-offset:2px}.card.bordered{border-width:1px;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.card.compact .card-body{font-size:.875rem;line-height:1.25rem;padding:1rem}.card-title{align-items:center;display:flex;font-size:1.25rem;font-weight:600;gap:.5rem;line-height:1.75rem}.card.image-full :where(figure){border-radius:inherit;overflow:hidden}.checkbox:focus{box-shadow:none}.checkbox:focus-visible{outline-color:var(--fallback-bc,oklch(var(--bc)/1));outline-offset:2px;outline-style:solid;outline-width:2px}.checkbox:checked,.checkbox[aria-checked=true],.checkbox[checked=true]{animation:checkmark var(--animation-input,.2s) ease-out;background-color:var(--chkbg);background-image:linear-gradient(-45deg,transparent 65%,var(--chkbg) 65.99%),linear-gradient(45deg,transparent 75%,var(--chkbg) 75.99%),linear-gradient(-45deg,var(--chkbg) 40%,transparent 40.99%),linear-gradient(45deg,var(--chkbg) 30%,var(--chkfg) 30.99%,var(--chkfg) 40%,transparent 40.99%),linear-gradient(-45deg,var(--chkfg) 50%,var(--chkbg) 50.99%);background-repeat:no-repeat}.checkbox:indeterminate{--tw-bg-opacity:1;animation:checkmark var(--animation-input,.2s) ease-out;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));background-image:linear-gradient(90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(-90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(0deg,var(--chkbg) 43%,var(--chkfg) 43%,var(--chkfg) 57%,var(--chkbg) 57%);background-repeat:no-repeat}.checkbox:disabled{border-color:transparent;cursor:not-allowed;--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));opacity:.2}@keyframes checkmark{0%{background-position-y:5px}50%{background-position-y:-2px}to{background-position-y:0}}.drawer-toggle:checked~.drawer-side>.drawer-overlay{background-color:#0006}.drawer-toggle:focus-visible~.drawer-content label.drawer-button{outline-offset:2px;outline-style:solid;outline-width:2px}.dropdown.dropdown-open .dropdown-content,.dropdown:focus .dropdown-content,.dropdown:focus-within .dropdown-content{--tw-scale-x:1;--tw-scale-y:1;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))}.file-input-bordered{--tw-border-opacity:0.2}.file-input:focus{outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.file-input-disabled,.file-input[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));--tw-text-opacity:0.2}.file-input-disabled::-moz-placeholder,.file-input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.file-input-disabled::placeholder,.file-input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.file-input-disabled::file-selector-button,.file-input[disabled]::file-selector-button{--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.label-text{font-size:.875rem;line-height:1.25rem}.label-text,.label-text-alt{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.label-text-alt{font-size:.75rem;line-height:1rem}.input input:focus{outline:2px solid transparent;outline-offset:2px}.input[list]::-webkit-calendar-picker-indicator{line-height:1em}.input-bordered{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.input:focus,.input:focus-within{border-color:var(--fallback-bc,oklch(var(--bc)/.2));box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.input-disabled,.input:disabled,.input[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4))}.input-disabled::-moz-placeholder,.input:disabled::-moz-placeholder,.input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.input-disabled::placeholder,.input:disabled::placeholder,.input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.input::-webkit-date-and-time-value{text-align:inherit}.join>:where(:not(:first-child)){margin-bottom:0;margin-top:0;margin-inline-start:-1px}.join-item:focus{isolation:isolate}.link-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){@media (hover:hover){.link-primary:hover{color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 80%,#000)}}}.\!link:focus{outline:2px solid transparent!important;outline-offset:2px!important}.link:focus{outline:2px solid transparent;outline-offset:2px}.\!link:focus-visible{outline:2px solid currentColor!important;outline-offset:2px!important}.link:focus-visible{outline:2px solid currentColor;outline-offset:2px}.loading{aspect-ratio:1/1;background-color:currentColor;display:inline-block;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:100%;mask-size:100%;pointer-events:none;width:1.5rem}.loading,.loading-spinner{-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='%23000'%3E%3Cstyle%3E@keyframes spinner_zKoa{to{transform:rotate(360deg)}}@keyframes spinner_YpZS{0%25{stroke-dasharray:0 150;stroke-dashoffset:0}47.5%25{stroke-dasharray:42 150;stroke-dashoffset:-16}95%25,to{stroke-dasharray:42 150;stroke-dashoffset:-59}}%3C/style%3E%3Cg style='transform-origin:center;animation:spinner_zKoa 2s linear infinite'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' class='spinner_V8m1' style='stroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite'/%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='%23000'%3E%3Cstyle%3E@keyframes spinner_zKoa{to{transform:rotate(360deg)}}@keyframes spinner_YpZS{0%25{stroke-dasharray:0 150;stroke-dashoffset:0}47.5%25{stroke-dasharray:42 150;stroke-dashoffset:-16}95%25,to{stroke-dasharray:42 150;stroke-dashoffset:-59}}%3C/style%3E%3Cg style='transform-origin:center;animation:spinner_zKoa 2s linear infinite'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' class='spinner_V8m1' style='stroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite'/%3E%3C/g%3E%3C/svg%3E")}.loading-lg{width:2.5rem}:where(.menu li:empty){--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));height:1px;margin:.5rem 1rem;opacity:.1}.menu :where(li ul):before{bottom:.75rem;inset-inline-start:0;position:absolute;top:.75rem;width:1px;--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));content:"";opacity:.1}.menu :where(li:not(.menu-title)>:not(ul):not(details):not(.menu-title)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--rounded-btn,.5rem);padding:.5rem 1rem;text-align:start;transition-duration:.2s;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-timing-function:cubic-bezier(0,0,.2,1);text-wrap:balance}:where(.menu li:not(.menu-title):not(.disabled)>:not(ul):not(details):not(.menu-title)):is(summary):not(.active):focus-visible,:where(.menu li:not(.menu-title):not(.disabled)>:not(ul):not(details):not(.menu-title)):not(summary):not(.active).focus,:where(.menu li:not(.menu-title):not(.disabled)>:not(ul):not(details):not(.menu-title)):not(summary):not(.active):focus,:where(.menu li:not(.menu-title):not(.disabled)>details>summary:not(.menu-title)):is(summary):not(.active):focus-visible,:where(.menu li:not(.menu-title):not(.disabled)>details>summary:not(.menu-title)):not(summary):not(.active).focus,:where(.menu li:not(.menu-title):not(.disabled)>details>summary:not(.menu-title)):not(summary):not(.active):focus{background-color:var(--fallback-bc,oklch(var(--bc)/.1));cursor:pointer;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));outline:2px solid transparent;outline-offset:2px}.menu li>:not(ul):not(.menu-title):not(details).active,.menu li>:not(ul):not(.menu-title):not(details):active,.menu li>details>summary:active{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.menu :where(li>details>summary)::-webkit-details-marker{display:none}.menu :where(li>.menu-dropdown-toggle):after,.menu :where(li>details>summary):after{box-shadow:2px 2px;content:"";display:block;height:.5rem;justify-self:end;margin-top:-.5rem;pointer-events:none;transform:rotate(45deg);transform-origin:75% 75%;transition-duration:.3s;transition-property:transform,margin-top;transition-timing-function:cubic-bezier(.4,0,.2,1);width:.5rem}.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after,.menu :where(li>details[open]>summary):after{margin-top:0;transform:rotate(225deg)}.mockup-phone .display{border-radius:40px;margin-top:-25px;overflow:hidden}.mockup-browser .mockup-browser-toolbar .input{display:block;height:1.75rem;margin-left:auto;margin-right:auto;overflow:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:24rem;--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));direction:ltr;padding-left:2rem}.mockup-browser .mockup-browser-toolbar .input:before{aspect-ratio:1/1;height:.75rem;left:.5rem;--tw-translate-y:-50%;border-color:currentColor;border-radius:9999px;border-width:2px}.mockup-browser .mockup-browser-toolbar .input:after,.mockup-browser .mockup-browser-toolbar .input:before{content:"";opacity:.6;position:absolute;top: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))}.mockup-browser .mockup-browser-toolbar .input:after{height:.5rem;left:1.25rem;--tw-translate-y:25%;--tw-rotate:-45deg;border-color:currentColor;border-radius:9999px;border-width:1px}.modal::backdrop,.modal:not(dialog:not(.modal-open)){animation:modal-pop .2s ease-out;background-color:#0006}.modal-open .modal-box,.modal-toggle:checked+.modal .modal-box,.modal:target .modal-box,.modal[open] .modal-box{--tw-translate-y:0px;--tw-scale-x:1;--tw-scale-y:1;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))}.modal-action>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}@keyframes modal-pop{0%{opacity:0}}.progress::-moz-progress-bar{background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)))}.progress-primary::-moz-progress-bar,.progress::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1}.progress-primary::-moz-progress-bar{background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)))}.progress:indeterminate{--progress-color:var(--fallback-bc,oklch(var(--bc)/1));animation:progress-loading 5s ease-in-out infinite;background-image:repeating-linear-gradient(90deg,var(--progress-color) -1%,var(--progress-color) 10%,transparent 10%,transparent 90%);background-position-x:15%;background-size:200%}.progress-primary:indeterminate{--progress-color:var(--fallback-p,oklch(var(--p)/1))}.progress::-webkit-progress-bar{background-color:transparent;border-radius:var(--rounded-box,1rem)}.progress::-webkit-progress-value{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)))}.progress-primary::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)))}.progress:indeterminate::-moz-progress-bar{animation:progress-loading 5s ease-in-out infinite;background-color:transparent;background-image:repeating-linear-gradient(90deg,var(--progress-color) -1%,var(--progress-color) 10%,transparent 10%,transparent 90%);background-position-x:15%;background-size:200%}@keyframes progress-loading{50%{background-position-x:-115%}}@keyframes radiomark{0%{box-shadow:0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset}50%{box-shadow:0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset}to{box-shadow:0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset}}@keyframes rating-pop{0%{transform:translateY(-.125em)}40%{transform:translateY(-.125em)}to{transform:translateY(0)}}.select-bordered,.select:focus{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.select:focus{box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.select-primary,.select-primary:focus{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)))}.select-primary:focus{outline-color:var(--fallback-p,oklch(var(--p)/1))}.select-disabled,.select:disabled,.select[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));--tw-text-opacity:0.2}.select-disabled::-moz-placeholder,.select:disabled::-moz-placeholder,.select[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.select-disabled::placeholder,.select:disabled::placeholder,.select[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.select-multiple,.select[multiple],.select[size].select:not([size="1"]){background-image:none;padding-right:1rem}[dir=rtl] .select{background-position:12px calc(1px + 50%),16px calc(1px + 50%)}.skeleton{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;animation:skeleton 1.8s ease-in-out infinite;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));background-image:linear-gradient(105deg,transparent 0,transparent 40%,var(--fallback-b1,oklch(var(--b1)/1)) 50%,transparent 60%,transparent 100%);background-position-x:-50%;background-repeat:no-repeat;background-size:200% auto;will-change:background-position}@media (prefers-reduced-motion){.skeleton{animation-duration:15s}}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}.steps .step:before{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));content:"";height:.5rem;margin-inline-start:-100%;top: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));width:100%}.steps .step:after,.steps .step:before{grid-column-start:1;grid-row-start:1;--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));--tw-text-opacity:1}.steps .step:after{border-radius:9999px;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));content:counter(step);counter-increment:step;display:grid;height:2rem;place-items:center;place-self:center;position:relative;width:2rem;z-index:1}.steps .step:first-child:before{content:none}.steps .step[data-content]:after{content:attr(data-content)}.steps .step-neutral+.step-neutral:before,.steps .step-neutral:after{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.steps .step-primary+.step-primary:before,.steps .step-primary:after{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.steps .step-secondary+.step-secondary:before,.steps .step-secondary:after{--tw-bg-opacity:1;background-color:var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.steps .step-accent+.step-accent:before,.steps .step-accent:after{--tw-bg-opacity:1;background-color:var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.steps .step-info+.step-info:before,.steps .step-info:after{--tw-bg-opacity:1;background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)))}.steps .step-info:after{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.steps .step-success+.step-success:before,.steps .step-success:after{--tw-bg-opacity:1;background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)))}.steps .step-success:after{--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}.steps .step-warning+.step-warning:before,.steps .step-warning:after{--tw-bg-opacity:1;background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)))}.steps .step-warning:after{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.steps .step-error+.step-error:before,.steps .step-error:after{--tw-bg-opacity:1;background-color:var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)))}.steps .step-error:after{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.swap-rotate .swap-indeterminate,.swap-rotate .swap-on,.swap-rotate input:indeterminate~.swap-on{--tw-rotate:45deg;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))}.swap-active:where(.swap-rotate) .swap-off,.swap-rotate input:checked~.swap-off,.swap-rotate input:indeterminate~.swap-off{--tw-rotate:-45deg;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))}.swap-active:where(.swap-rotate) .swap-on,.swap-rotate input:checked~.swap-on,.swap-rotate input:indeterminate~.swap-indeterminate{--tw-rotate:0deg;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))}.swap-flip .swap-indeterminate,.swap-flip .swap-on,.swap-flip input:indeterminate~.swap-on{backface-visibility:hidden;opacity:1;transform:rotateY(180deg)}.swap-active:where(.swap-flip) .swap-off,.swap-flip input:checked~.swap-off,.swap-flip input:indeterminate~.swap-off{backface-visibility:hidden;opacity:1;transform:rotateY(-180deg)}.swap-active:where(.swap-flip) .swap-on,.swap-flip input:checked~.swap-on,.swap-flip input:indeterminate~.swap-indeterminate{transform:rotateY(0deg)}:is([dir=rtl] .table){text-align:right}.table :where(th,td){padding:.75rem 1rem;vertical-align:middle}.table tr.active,.table tr.active:nth-child(2n),.table-zebra tbody tr:nth-child(2n){--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.table-zebra tr.active,.table-zebra tr.active:nth-child(2n),.table-zebra-zebra tbody tr:nth-child(2n){--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}.table :where(thead,tbody) :where(tr:first-child:last-child),.table :where(thead,tbody) :where(tr:not(:last-child)){border-bottom-width:1px;--tw-border-opacity:1;border-bottom-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.table :where(thead,tfoot){color:var(--fallback-bc,oklch(var(--bc)/.6));font-size:.75rem;font-weight:700;line-height:1rem;white-space:nowrap}.textarea-bordered,.textarea:focus{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.textarea:focus{box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.textarea-disabled,.textarea:disabled,.textarea[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));--tw-text-opacity:0.2}.textarea-disabled::-moz-placeholder,.textarea:disabled::-moz-placeholder,.textarea[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.textarea-disabled::placeholder,.textarea:disabled::placeholder,.textarea[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.toast>*{animation:toast-pop .25s ease-out}@keyframes toast-pop{0%{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}[dir=rtl] .toggle{--handleoffsetcalculator:calc(var(--handleoffset)*1)}.toggle:focus-visible{outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.toggle:hover{background-color:currentColor}.toggle:checked,.toggle[aria-checked=true],.toggle[checked=true]{background-image:none;--handleoffsetcalculator:var(--handleoffset);--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}[dir=rtl] .toggle:checked,[dir=rtl] .toggle[aria-checked=true],[dir=rtl] .toggle[checked=true]{--handleoffsetcalculator:calc(var(--handleoffset)*-1)}.toggle:indeterminate{--tw-text-opacity:1;box-shadow:calc(var(--handleoffset)/2) 0 0 2px var(--tglbg) inset,calc(var(--handleoffset)/-2) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}[dir=rtl] .toggle:indeterminate{box-shadow:calc(var(--handleoffset)/2) 0 0 2px var(--tglbg) inset,calc(var(--handleoffset)/-2) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset}.toggle:disabled{cursor:not-allowed;--tw-border-opacity:1;background-color:transparent;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));opacity:.3;--togglehandleborder:0 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset,var(--handleoffsetcalculator) 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset}:root .prose{--tw-prose-body:var(--fallback-bc,oklch(var(--bc)/0.8));--tw-prose-headings:var(--fallback-bc,oklch(var(--bc)/1));--tw-prose-lead:var(--fallback-bc,oklch(var(--bc)/1));--tw-prose-links:var(--fallback-bc,oklch(var(--bc)/1));--tw-prose-bold:var(--fallback-bc,oklch(var(--bc)/1));--tw-prose-counters:var(--fallback-bc,oklch(var(--bc)/1));--tw-prose-bullets:var(--fallback-bc,oklch(var(--bc)/0.5));--tw-prose-hr:var(--fallback-bc,oklch(var(--bc)/0.2));--tw-prose-quotes:var(--fallback-bc,oklch(var(--bc)/1));--tw-prose-quote-borders:var(--fallback-bc,oklch(var(--bc)/0.2));--tw-prose-captions:var(--fallback-bc,oklch(var(--bc)/0.5));--tw-prose-code:var(--fallback-bc,oklch(var(--bc)/1));--tw-prose-pre-code:var(--fallback-nc,oklch(var(--nc)/1));--tw-prose-pre-bg:var(--fallback-n,oklch(var(--n)/1));--tw-prose-th-borders:var(--fallback-bc,oklch(var(--bc)/0.5));--tw-prose-td-borders:var(--fallback-bc,oklch(var(--bc)/0.2))}.prose :where(code):not(:where([class~=not-prose] *,pre *)){background-color:var(--fallback-bc,oklch(var(--bc)/.1));border-radius:var(--rounded-badge);font-weight:400;padding:1px 8px}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{display:none}.prose pre code{border-radius:0;padding:0}.prose :where(tbody tr,thead):not(:where([class~=not-prose] *)){border-bottom-color:var(--fallback-bc,oklch(var(--bc)/.2))}.no-animation{--btn-focus-scale:1;--animation-btn:0;--animation-input:0}.badge-sm{height:1rem;line-height:1rem;padding-left:.438rem;padding-right:.438rem}.badge-sm,.btn-xs{font-size:.75rem}.btn-xs{height:1.5rem;min-height:1.5rem;padding-left:.5rem;padding-right:.5rem}.btn-sm{font-size:.875rem;height:2rem;min-height:2rem;padding-left:.75rem;padding-right:.75rem}.btn-lg{font-size:1.125rem;height:4rem;min-height:4rem;padding-left:1.5rem;padding-right:1.5rem}.btn-wide{width:16rem}.btn-square:where(.btn-xs){height:1.5rem;padding:0;width:1.5rem}.btn-square:where(.btn-sm){height:2rem;padding:0;width:2rem}.btn-square:where(.btn-md){height:3rem;padding:0;width:3rem}.btn-square:where(.btn-lg){height:4rem;padding:0;width:4rem}.btn-circle:where(.btn-xs){border-radius:9999px;height:1.5rem;padding:0;width:1.5rem}.btn-circle:where(.btn-sm){border-radius:9999px;height:2rem;padding:0;width:2rem}.btn-circle:where(.btn-md){border-radius:9999px;height:3rem;padding:0;width:3rem}.btn-circle:where(.btn-lg){border-radius:9999px;height:4rem;padding:0;width:4rem}.card-side{align-items:stretch;flex-direction:row}.card-side :where(figure:first-child){border-end-end-radius:unset;border-end-start-radius:inherit;border-start-end-radius:unset;border-start-start-radius:inherit;overflow:hidden}.card-side :where(figure:last-child){border-end-end-radius:inherit;border-end-start-radius:unset;border-start-end-radius:inherit;border-start-start-radius:unset;overflow:hidden}.card-side figure>*{max-width:unset}:where(.card-side figure>*){height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.drawer-open>.drawer-toggle{display:none}.drawer-open>.drawer-toggle~.drawer-side{display:block;overscroll-behavior:auto;pointer-events:auto;position:sticky;visibility:visible;width:auto}.drawer-open>.drawer-toggle~.drawer-side>:not(.drawer-overlay),[dir=rtl] .drawer-open>.drawer-toggle~.drawer-side>:not(.drawer-overlay){transform:translateX(0)}.drawer-open>.drawer-toggle:checked~.drawer-side{pointer-events:auto;visibility:visible}.join.join-vertical{flex-direction:column}.join.join-vertical .join-item:first-child:not(:last-child),.join.join-vertical :first-child:not(:last-child) .join-item{border-end-end-radius:0;border-end-start-radius:0;border-start-end-radius:inherit;border-start-start-radius:inherit}.join.join-vertical .join-item:last-child:not(:first-child),.join.join-vertical :last-child:not(:first-child) .join-item{border-end-end-radius:inherit;border-end-start-radius:inherit;border-start-end-radius:0;border-start-start-radius:0}.join.join-horizontal{flex-direction:row}.join.join-horizontal .join-item:first-child:not(:last-child),.join.join-horizontal :first-child:not(:last-child) .join-item{border-end-end-radius:0;border-end-start-radius:inherit;border-start-end-radius:0;border-start-start-radius:inherit}.join.join-horizontal .join-item:last-child:not(:first-child),.join.join-horizontal :last-child:not(:first-child) .join-item{border-end-end-radius:inherit;border-end-start-radius:0;border-start-end-radius:inherit;border-start-start-radius:0}.select-sm{font-size:.875rem;height:2rem;line-height:2rem;min-height:2rem;padding-left:.75rem;padding-right:2rem}[dir=rtl] .select-sm{padding-left:2rem;padding-right:.75rem}.steps-horizontal .step{display:grid;grid-template-columns:repeat(1,minmax(0,1fr));grid-template-rows:repeat(2,minmax(0,1fr));place-items:center;text-align:center}.steps-vertical .step{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-template-rows:repeat(1,minmax(0,1fr))}:where(.toast){bottom:0;inset-inline-end:0;inset-inline-start:auto;top:auto;--tw-translate-x:0px;--tw-translate-y: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))}.toast:where(.toast-start){inset-inline-end:auto;inset-inline-start: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))}.toast:where(.toast-center){inset-inline-end:50%;inset-inline-start:50%;--tw-translate-x:-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))}:is([dir=rtl] .toast:where(.toast-center)){--tw-translate-x: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))}.toast:where(.toast-end){inset-inline-end:0;inset-inline-start:auto;--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))}.toast:where(.toast-bottom){bottom:0;top:auto;--tw-translate-y: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))}.toast:where(.toast-middle){bottom:auto;top:50%;--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))}.toast:where(.toast-top){bottom:auto;top:0;--tw-translate-y: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))}.avatar.online:before{background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)))}.avatar.offline:before,.avatar.online:before{border-radius:9999px;content:"";display:block;position:absolute;z-index:10;--tw-bg-opacity:1;height:15%;outline-color:var(--fallback-b1,oklch(var(--b1)/1));outline-style:solid;outline-width:2px;right:7%;top:7%;width:15%}.avatar.offline:before{background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}.card-compact .card-body{font-size:.875rem;line-height:1.25rem;padding:1rem}.card-compact .card-title{margin-bottom:.25rem}.card-normal .card-body{font-size:1rem;line-height:1.5rem;padding:var(--padding-card,2rem)}.card-normal .card-title{margin-bottom:.75rem}.drawer-open>.drawer-toggle~.drawer-side>.drawer-overlay{background-color:transparent;cursor:default}.join.join-vertical>:where(:not(:first-child)){margin-left:0;margin-right:0;margin-top:-1px}.join.join-horizontal>:where(:not(:first-child)){margin-bottom:0;margin-top:0;margin-inline-start:-1px}.modal-top :where(.modal-box){max-width:none;width:100%;--tw-translate-y:-2.5rem;--tw-scale-x:1;--tw-scale-y:1;border-bottom-left-radius:var(--rounded-box,1rem);border-bottom-right-radius:var(--rounded-box,1rem);border-top-left-radius:0;border-top-right-radius: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))}.modal-middle :where(.modal-box){max-width:32rem;width:91.666667%;--tw-translate-y:0px;--tw-scale-x:.9;--tw-scale-y:.9;border-bottom-left-radius:var(--rounded-box,1rem);border-bottom-right-radius:var(--rounded-box,1rem);border-top-left-radius:var(--rounded-box,1rem);border-top-right-radius:var(--rounded-box,1rem);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))}.modal-bottom :where(.modal-box){max-width:none;width:100%;--tw-translate-y:2.5rem;--tw-scale-x:1;--tw-scale-y:1;border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:var(--rounded-box,1rem);border-top-right-radius:var(--rounded-box,1rem);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))}.steps-horizontal .step{grid-template-columns:auto;grid-template-rows:40px 1fr;min-width:4rem}.steps-horizontal .step:before{height:.5rem;width:100%;--tw-translate-x:0px;--tw-translate-y:0px;content:"";margin-inline-start:-100%;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))}:is([dir=rtl] .steps-horizontal .step):before{--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))}.steps-vertical .step{gap:.5rem;grid-template-columns:40px 1fr;grid-template-rows:auto;justify-items:start;min-height:4rem}.steps-vertical .step:before{height:100%;width:.5rem;--tw-translate-x:-50%;--tw-translate-y:-50%;margin-inline-start: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))}:is([dir=rtl] .steps-vertical .step):before{--tw-translate-x: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))}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-bottom:1.2em;margin-top:1.2em}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-bottom:1.25em;margin-top:1.25em;padding-left:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-bottom:1.25em;margin-top:1.25em;padding-left:1.625em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-bottom:3em;margin-top:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){border-left-color:var(--tw-prose-quote-borders);border-left-width:.25rem;color:var(--tw-prose-quotes);font-style:italic;font-weight:500;margin-bottom:1.6em;margin-top:1.6em;padding-left:1em;quotes:"\201C""\201D""\2018""\2019"}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-size:2.25em;font-weight:800;line-height:1.1111111;margin-bottom:.8888889em;margin-top:0}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.5em;font-weight:700;line-height:1.3333333;margin-bottom:1em;margin-top:2em}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.25em;font-weight:600;line-height:1.6;margin-bottom:.6em;margin-top:1.6em}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;line-height:1.5;margin-bottom:.5em;margin-top:1.5em}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-bottom:2em;margin-top:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){border-radius:.3125rem;box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows)/10%),0 3px 0 rgb(var(--tw-prose-kbd-shadows)/10%);color:var(--tw-prose-kbd);font-family:inherit;font-size:.875em;font-weight:500;padding:.1875em .375em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:var(--tw-prose-pre-bg);border-radius:.375rem;color:var(--tw-prose-pre-code);font-size:.875em;font-weight:400;line-height:1.7142857;margin-bottom:1.7142857em;margin-top:1.7142857em;overflow-x:auto;padding:.8571429em 1.1428571em}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:transparent;border-radius:0;border-width:0;color:inherit;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.875em;line-height:1.7142857;margin-bottom:2em;margin-top:2em;table-layout:auto;text-align:left;width:100%}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-color:var(--tw-prose-th-borders);border-bottom-width:1px}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;padding-bottom:.5714286em;padding-left:.5714286em;padding-right:.5714286em;vertical-align:bottom}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-color:var(--tw-prose-td-borders);border-bottom-width:1px}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-color:var(--tw-prose-th-borders);border-top-width:1px}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0;margin-top:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-kbd:#111827;--tw-prose-kbd-shadows:17 24 39;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:255 255 255;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:rgba(0,0,0,.5);--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0;margin-top:0}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.5em;margin-top:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.375em}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(.prose>ul>li>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-left:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-right:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-right:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.prose-sm{font-size:.875rem;line-height:1.7142857}.prose-sm :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.1428571em;margin-top:1.1428571em}.prose-sm :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.2857143em;line-height:1.5555556;margin-bottom:.8888889em;margin-top:.8888889em}.prose-sm :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.3333333em;margin-top:1.3333333em;padding-left:1.1111111em}.prose-sm :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:2.1428571em;line-height:1.2;margin-bottom:.8em;margin-top:0}.prose-sm :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.4285714em;line-height:1.4;margin-bottom:.8em;margin-top:1.6em}.prose-sm :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.2857143em;line-height:1.5555556;margin-bottom:.4444444em;margin-top:1.5555556em}.prose-sm :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){line-height:1.4285714;margin-bottom:.5714286em;margin-top:1.4285714em}.prose-sm :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.7142857em;margin-top:1.7142857em}.prose-sm :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.7142857em;margin-top:1.7142857em}.prose-sm :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0;margin-top:0}.prose-sm :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.7142857em;margin-top:1.7142857em}.prose-sm :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){border-radius:.3125rem;font-size:.8571429em;padding:.1428571em .3571429em}.prose-sm :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em}.prose-sm :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.9em}.prose-sm :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8888889em}.prose-sm :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){border-radius:.25rem;font-size:.8571429em;line-height:1.6666667;margin-bottom:1.6666667em;margin-top:1.6666667em;padding:.6666667em 1em}.prose-sm :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.1428571em;margin-top:1.1428571em;padding-left:1.5714286em}.prose-sm :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.1428571em;margin-top:1.1428571em;padding-left:1.5714286em}.prose-sm :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.2857143em;margin-top:.2857143em}.prose-sm :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.4285714em}.prose-sm :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.4285714em}.prose-sm :where(.prose-sm>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.5714286em;margin-top:.5714286em}.prose-sm :where(.prose-sm>ul>li>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose-sm>ul>li>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(.prose-sm>ol>li>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose-sm>ol>li>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.5714286em;margin-top:.5714286em}.prose-sm :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.1428571em;margin-top:1.1428571em}.prose-sm :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.2857143em;padding-left:1.5714286em}.prose-sm :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2.8571429em;margin-top:2.8571429em}.prose-sm :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;line-height:1.5}.prose-sm :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){padding-bottom:.6666667em;padding-left:1em;padding-right:1em}.prose-sm :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:0}.prose-sm :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-right:0}.prose-sm :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding:.6666667em 1em}.prose-sm :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:0}.prose-sm :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-right:0}.prose-sm :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.7142857em;margin-top:1.7142857em}.prose-sm :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0;margin-top:0}.prose-sm :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;line-height:1.3333333;margin-top:.6666667em}.prose-sm :where(.prose-sm>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(.prose-sm>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.prose-neutral{--tw-prose-body:#404040;--tw-prose-headings:#171717;--tw-prose-lead:#525252;--tw-prose-links:#171717;--tw-prose-bold:#171717;--tw-prose-counters:#737373;--tw-prose-bullets:#d4d4d4;--tw-prose-hr:#e5e5e5;--tw-prose-quotes:#171717;--tw-prose-quote-borders:#e5e5e5;--tw-prose-captions:#737373;--tw-prose-kbd:#171717;--tw-prose-kbd-shadows:23 23 23;--tw-prose-code:#171717;--tw-prose-pre-code:#e5e5e5;--tw-prose-pre-bg:#262626;--tw-prose-th-borders:#d4d4d4;--tw-prose-td-borders:#e5e5e5;--tw-prose-invert-body:#d4d4d4;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#a3a3a3;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#a3a3a3;--tw-prose-invert-bullets:#525252;--tw-prose-invert-hr:#404040;--tw-prose-invert-quotes:#f5f5f5;--tw-prose-invert-quote-borders:#404040;--tw-prose-invert-captions:#a3a3a3;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:255 255 255;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d4d4d4;--tw-prose-invert-pre-bg:rgba(0,0,0,.5);--tw-prose-invert-th-borders:#525252;--tw-prose-invert-td-borders:#404040}.youtube{border-radius:.1rem;--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));padding:.5rem}input.narrow-reading-toggle{display:none}body:has(label[for=narrow-reading-toggle]) input.narrow-reading-toggle:checked+.reading-toggle-target{max-width:48rem}.fade-away{-webkit-mask-image:linear-gradient(#000,#000 80%,rgba(0,0,0,.5) 85%,transparent);mask-image:linear-gradient(#000,#000 80%,rgba(0,0,0,.5) 85%,transparent)}.prose div>pre,.prose>pre{background-color:inherit;border-radius:0;color:inherit}.prose pre:has(code){border-color:currentColor;border-width:2px}.characters-left{bottom:1.5rem;pointer-events:none;position:absolute;right:1.5rem;-webkit-user-select:none;-moz-user-select:none;user-select:none}.static{position:static}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-8{inset:2rem}.bottom-2{bottom:.5rem}.left-2{left:.5rem}.right-0{right:0}.right-2{right:.5rem}.top-0{top:0}.top-2{top:.5rem}.z-10{z-index:10}.z-50{z-index:50}.z-\[1\]{z-index:1}.row-span-2{grid-row:span 2/span 2}.row-span-3{grid-row:span 3/span 3}.float-start{float:inline-start}.float-left{float:left}.mx-auto{margin-left:auto;margin-right:auto}.my-3{margin-bottom:.75rem;margin-top:.75rem}.my-6{margin-bottom:1.5rem;margin-top:1.5rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-6{margin-bottom:1.5rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.mt-3{margin-top:.75rem}.line-clamp-1{-webkit-line-clamp:1}.line-clamp-1,.line-clamp-2{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-3{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical;-webkit-line-clamp:3}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1/1}.h-12{height:3rem}.h-128{height:32rem}.h-16{height:4rem}.h-24{height:6rem}.h-32{height:8rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-full{max-height:100%}.min-h-24{min-height:6rem}.min-h-56{min-height:14rem}.min-h-96{min-height:24rem}.min-h-\[2\.8em\]{min-height:2.8em}.w-0{width:0}.w-1\/3{width:33.333333%}.w-16{width:4rem}.w-24{width:6rem}.w-4{width:1rem}.w-40{width:10rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-52{width:13rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-80{width:20rem}.w-full{width:100%}.min-w-0{min-width:0}.max-w-full{max-width:100%}.max-w-none{max-width:none}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.grow-0{flex-grow:0}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-content-center{place-content:center}.content-center{align-content:center}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.gap-y-3{row-gap:.75rem}.gap-y-4{row-gap:1rem}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hyphens-auto{-webkit-hyphens:auto;hyphens:auto}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.1rem}.rounded-box{border-radius:var(--rounded-box,1rem)}.rounded-md{border-radius:.2rem}.border-2{border-width:2px}.border-b-2{border-bottom-width:2px}.border-current{border-color:currentColor}.bg-accent{background-color:var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity)))}.bg-accent,.bg-base-100{--tw-bg-opacity:1}.bg-base-100{background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.bg-base-200{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.bg-base-300{background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}.bg-base-300,.bg-error{--tw-bg-opacity:1}.bg-error{background-color:var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)))}.bg-info{background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)))}.bg-info,.bg-orange-500{--tw-bg-opacity:1}.bg-orange-500{background-color:rgb(249 115 22/var(--tw-bg-opacity))}.bg-primary{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)))}.bg-secondary{--tw-bg-opacity:1;background-color:var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity)))}.bg-warning{--tw-bg-opacity:1;background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)))}.fill-current{fill:currentColor}.stroke-current{stroke:currentColor}.object-contain{-o-object-fit:contain;object-fit:contain}.object-left{-o-object-position:left;object-position:left}.p-0{padding:0}.p-2{padding:.5rem}.p-4{padding:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-6{padding-bottom:1.5rem;padding-top:1.5rem}.py-8{padding-bottom:2rem;padding-top:2rem}.text-center{text-align:center}.text-justify{text-align:justify}.text-2xl{font-size:1.777rem}.text-3xl{font-size:2.369rem}.text-sm{font-size:.75rem}.text-xl{font-size:1.333rem}.font-bold{font-weight:700}.font-normal{font-weight:400}.not-italic{font-style:normal}.text-accent-content{--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.text-base-content{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.text-error{--tw-text-opacity:1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.text-error-content{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.text-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.text-info-content{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.text-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.text-primary-content{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.text-secondary-content{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.text-slate-400{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity))}.text-slate-50{--tw-text-opacity:1;color:rgb(248 250 252/var(--tw-text-opacity))}.text-success{--tw-text-opacity:1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.text-warning-content{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-\[4px_4px_0_0_currentColor\]{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[4px_4px_0_0_currentColor\]{--tw-shadow:4px 4px 0 0 currentColor;--tw-shadow-colored:4px 4px 0 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)}.outline-none{outline:2px solid transparent;outline-offset:2px}.transition{transition-duration:.15s;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)}.hover\:link:hover{cursor:pointer;text-decoration-line:underline}.hover\:badge-outline:hover{border-color:currentColor;--tw-border-opacity:0.5;background-color:transparent;color:currentColor}.hover\:badge-outline:hover.badge-neutral{--tw-text-opacity:1;color:var(--fallback-n,oklch(var(--n)/var(--tw-text-opacity)))}.hover\:badge-outline:hover.badge-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.hover\:badge-outline:hover.badge-secondary{--tw-text-opacity:1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)))}.hover\:badge-outline:hover.badge-accent{--tw-text-opacity:1;color:var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)))}.hover\:badge-outline:hover.badge-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.hover\:badge-outline:hover.badge-success{--tw-text-opacity:1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.hover\:badge-outline:hover.badge-warning{--tw-text-opacity:1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.hover\:badge-outline:hover.badge-error{--tw-text-opacity:1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.hover\:link:hover:focus{outline:2px solid transparent;outline-offset:2px}.hover\:link:hover:focus-visible{outline:2px solid currentColor;outline-offset:2px}@media not all and (min-width:1024px){.max-lg\:dropdown-bottom .dropdown-content{bottom:auto;top:100%;transform-origin:top}.max-lg\:dropdown-end .dropdown-content{inset-inline-end:0}.max-lg\:dropdown-end.dropdown-left .dropdown-content,.max-lg\:dropdown-end.dropdown-right .dropdown-content{bottom:0;top:auto}}@media (min-width:640px){.sm\:btn-wide{width:16rem}.sm\:card-side{align-items:stretch;flex-direction:row}.sm\:card-side :where(figure:first-child){border-end-end-radius:unset;border-end-start-radius:inherit;border-start-end-radius:unset;border-start-start-radius:inherit;overflow:hidden}.sm\:card-side :where(figure:last-child){border-end-end-radius:inherit;border-end-start-radius:unset;border-start-end-radius:inherit;border-start-start-radius:unset;overflow:hidden}.sm\:card-side figure>*{max-width:unset}:where(.sm\:card-side figure>*){height:100%;-o-object-fit:cover;object-fit:cover;width:100%}}@media (min-width:768px){.md\:btn-wide{width:16rem}.md\:drawer-open>.drawer-toggle{display:none}.md\:drawer-open>.drawer-toggle~.drawer-side{display:block;overscroll-behavior:auto;pointer-events:auto;position:sticky;visibility:visible;width:auto}.md\:drawer-open>.drawer-toggle~.drawer-side>:not(.drawer-overlay),[dir=rtl] .md\:drawer-open>.drawer-toggle~.drawer-side>:not(.drawer-overlay){transform:translateX(0)}.md\:drawer-open>.drawer-toggle:checked~.drawer-side{pointer-events:auto;visibility:visible}.md\:join-horizontal.join{flex-direction:row}.md\:join-horizontal .join :first-child:not(:last-child) .join-item,.md\:join-horizontal.join .join-item:first-child:not(:last-child){border-end-end-radius:0;border-end-start-radius:inherit;border-start-end-radius:0;border-start-start-radius:inherit}.md\:join-horizontal .join :last-child:not(:first-child) .join-item,.md\:join-horizontal.join .join-item:last-child:not(:first-child){border-end-end-radius:inherit;border-end-start-radius:0;border-start-end-radius:inherit;border-start-start-radius:0}.md\:drawer-open>.drawer-toggle~.drawer-side>.drawer-overlay{background-color:transparent;cursor:default}.md\:join-horizontal.join>:where(:not(:first-child)){margin-bottom:0;margin-top:0;margin-inline-start:-1px}}@media (min-width:1024px){.lg\:dropdown-right .dropdown-content{bottom:auto;inset-inline-start:100%;top:0;transform-origin:left}.lg\:dropdown-right.dropdown-end .dropdown-content{bottom:0;top:auto}.lg\:prose-base{font-size:1rem;line-height:1.75}.lg\:prose-base :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em}.lg\:prose-base :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.25em;line-height:1.6;margin-bottom:1.2em;margin-top:1.2em}.lg\:prose-base :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.6em;margin-top:1.6em;padding-left:1em}.lg\:prose-base :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:2.25em;line-height:1.1111111;margin-bottom:.8888889em;margin-top:0}.lg\:prose-base :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.5em;line-height:1.3333333;margin-bottom:1em;margin-top:2em}.lg\:prose-base :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.25em;line-height:1.6;margin-bottom:.6em;margin-top:1.6em}.lg\:prose-base :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){line-height:1.5;margin-bottom:.5em;margin-top:1.5em}.lg\:prose-base :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.lg\:prose-base :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.lg\:prose-base :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0;margin-top:0}.lg\:prose-base :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.lg\:prose-base :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){border-radius:.3125rem;font-size:.875em;padding:.1875em .375em}.lg\:prose-base :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.875em}.lg\:prose-base :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.875em}.lg\:prose-base :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.9em}.lg\:prose-base :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){border-radius:.375rem;font-size:.875em;line-height:1.7142857;margin-bottom:1.7142857em;margin-top:1.7142857em;padding:.8571429em 1.1428571em}.lg\:prose-base :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em;padding-left:1.625em}.lg\:prose-base :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em;padding-left:1.625em}.lg\:prose-base :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.5em;margin-top:.5em}.lg\:prose-base :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.375em}.lg\:prose-base :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.375em}.lg\:prose-base :where(.lg\:prose-base>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.lg\:prose-base :where(.lg\:prose-base>ul>li>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.lg\:prose-base :where(.lg\:prose-base>ul>li>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.lg\:prose-base :where(.lg\:prose-base>ol>li>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.lg\:prose-base :where(.lg\:prose-base>ol>li>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.lg\:prose-base :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.lg\:prose-base :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em}.lg\:prose-base :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.lg\:prose-base :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-left:1.625em}.lg\:prose-base :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:3em;margin-top:3em}.lg\:prose-base :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.lg\:prose-base :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.lg\:prose-base :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.lg\:prose-base :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.lg\:prose-base :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.875em;line-height:1.7142857}.lg\:prose-base :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){padding-bottom:.5714286em;padding-left:.5714286em;padding-right:.5714286em}.lg\:prose-base :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:0}.lg\:prose-base :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-right:0}.lg\:prose-base :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding:.5714286em}.lg\:prose-base :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:0}.lg\:prose-base :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-right:0}.lg\:prose-base :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.lg\:prose-base :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0;margin-top:0}.lg\:prose-base :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.lg\:prose-base :where(.lg\:prose-base>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.lg\:prose-base :where(.lg\:prose-base>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}}@media not all and (min-width:1024px){.max-lg\:hidden{display:none}}@media not all and (min-width:768px){.max-md\:\!hidden{display:none!important}.max-md\:hidden{display:none}}@media not all and (min-width:640px){.max-sm\:w-full{width:100%}}@media (min-width:640px){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:col-span-4{grid-column:span 4/span 4}.sm\:block{display:block}.sm\:max-h-56{max-height:14rem}.sm\:w-56{width:14rem}.sm\:max-w-44{max-width:11rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:justify-start{justify-content:flex-start}.sm\:border-l-2{border-left-width:2px}}@media (min-width:768px){.md\:block{display:block}.md\:hidden{display:none}.md\:w-40{width:10rem}.md\:w-48{width:12rem}.md\:w-8{width:2rem}.md\:max-w-xs{max-width:20rem}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-\[fit-content\(20rem\)_1fr\]{grid-template-columns:fit-content(20rem) 1fr}.md\:flex-row{flex-direction:row}.md\:justify-start{justify-content:flex-start}.md\:px-12{padding-left:3rem;padding-right:3rem}.md\:text-left{text-align:left}.md\:text-center{text-align:center}}@media (min-width:1024px){.lg\:w-56{width:14rem}.lg\:w-64{width:16rem}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:hyphens-auto{-webkit-hyphens:auto;hyphens:auto}.lg\:text-2xl{font-size:1.777rem}.lg\:text-3xl{font-size:2.369rem}.lg\:text-4xl{font-size:3.158rem}.lg\:text-5xl{font-size:4.21rem}}@media (min-width:1280px){.xl\:order-first{order:-9999}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}}@media (prefers-color-scheme:dark){.dark\:bg-base-200{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}}.\[\&_li\>\*\]\:rounded-none li>*{border-radius:0} \ No newline at end of file diff --git a/Wave/wwwroot/fonts/noto-sans-display-v26-latin-regular.woff2 b/Wave/wwwroot/fonts/noto-sans-display-v26-latin-regular.woff2 deleted file mode 100644 index b278720..0000000 Binary files a/Wave/wwwroot/fonts/noto-sans-display-v26-latin-regular.woff2 and /dev/null differ diff --git a/Wave/wwwroot/fonts/nunito-sans-v15-latin-700.woff2 b/Wave/wwwroot/fonts/nunito-sans-v15-latin-700.woff2 deleted file mode 100644 index 9a8b29b..0000000 Binary files a/Wave/wwwroot/fonts/nunito-sans-v15-latin-700.woff2 and /dev/null differ diff --git a/docker-compose.override.yml b/docker-compose.override.yml index f4cb04f..5dcec09 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -3,7 +3,7 @@ version: '3.4' services: web: environment: - - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_HTTP_PORTS=8080 + - ASPNETCORE_ENVIRONMENT=Development volumes: - ${APPDATA}/Microsoft/UserSecrets:/home/app/.microsoft/usersecrets:ro \ No newline at end of file diff --git a/launchSettings.json b/launchSettings.json index cc19705..8282b22 100644 --- a/launchSettings.json +++ b/launchSettings.json @@ -1,23 +1,42 @@ { - "profiles": { - "Docker Compose": { - "commandName": "DockerCompose", - "commandVersion": "1.0", - "serviceActions": { - "web": "StartDebugging", - "database": "StartWithoutDebugging", - "mailhog": "DoNotStart", - "redis": "StartWithoutDebugging" - } - }, - "SMTP Debugging": { - "commandName": "DockerCompose", - "commandVersion": "1.0", - "composeProfile": { - "includes": [ - "smtp-debug" - ] - } - } - } + "profiles": { + "Docker Compose": { + "commandName": "DockerCompose", + "commandVersion": "1.0", + "serviceActions": { + "web": "StartDebugging", + "database": "StartWithoutDebugging", + "mailhog": "DoNotStart", + "redis": "StartWithoutDebugging", + "grafana": "StartWithoutDebugging", + "loki": "StartWithoutDebugging", + "prometheus": "StartWithoutDebugging" + } + }, + "SMTP Debugging": { + "commandName": "DockerCompose", + "commandVersion": "1.0", + "composeProfile": { + "includes": [ + "smtp-debug" + ] + } + }, + "Production": { + "commandName": "DockerCompose", + "commandVersion": "1.0", + "serviceActions": { + "database": "StartWithoutDebugging", + "grafana": "DoNotStart", + "loki": "DoNotStart", + "mailhog": "DoNotStart", + "prometheus": "DoNotStart", + "redis": "StartWithoutDebugging", + "web": "StartDebugging" + }, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production" + } + } + } } \ No newline at end of file