Implemented the Projects listing redesign.

This commit is contained in:
Nick Beckley 2026-06-22 20:58:20 +01:00
parent c9a9605cd2
commit 54fdc034ea
6 changed files with 618 additions and 77 deletions

View File

@ -51,6 +51,7 @@ public interface IProjectActivityRepository
public interface IBookRepository
{
Task<IReadOnlyList<Book>> ListByProjectAsync(int projectId);
Task<IReadOnlyList<Book>> ListByProjectsAsync(IEnumerable<int> projectIds);
Task<Book?> GetAsync(int bookId);
Task<Book?> GetCoverPathsAsync(int bookId);
Task<int> SaveAsync(Book book);
@ -2374,6 +2375,67 @@ public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IB
return rows.ToList();
}
public async Task<IReadOnlyList<Book>> ListByProjectsAsync(IEnumerable<int> projectIds)
{
var ids = projectIds.Distinct().ToArray();
if (ids.Length == 0)
{
return [];
}
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Book>(
"""
SELECT
b.BookID,
b.ProjectID,
b.BookTitle,
b.Subtitle,
b.Tagline,
b.ShortDescription,
b.Status,
b.BookNumber,
b.Description,
b.TargetWordCount,
b.WritingStartDate,
b.TargetCompletionDate,
b.PublicationDate,
b.CoverThumbnailPath,
b.CoverStandardPath,
ISNULL(wordCounts.CurrentWordCount, 0) AS CurrentWordCount,
ISNULL(counts.ChapterCount, 0) AS ChapterCount,
ISNULL(counts.SceneCount, 0) AS SceneCount,
b.SortOrder,
b.CreatedDate,
b.UpdatedDate,
b.IsArchived,
b.ArchivedDate,
b.ArchivedReason
FROM dbo.Books b
OUTER APPLY
(
SELECT
COUNT(DISTINCT c.ChapterID) AS ChapterCount,
COUNT(s.SceneID) AS SceneCount
FROM dbo.Chapters c
LEFT JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
WHERE c.BookID = b.BookID AND c.IsArchived = 0
) counts
OUTER APPLY
(
SELECT SUM(ISNULL(sw.ActualWordCount, 0)) AS CurrentWordCount
FROM dbo.Chapters c
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
WHERE c.BookID = b.BookID AND c.IsArchived = 0
) wordCounts
WHERE b.ProjectID IN @ProjectIDs AND b.IsArchived = 0
ORDER BY b.ProjectID, b.SortOrder, b.BookNumber, b.BookTitle;
""",
new { ProjectIDs = ids });
return rows.ToList();
}
public async Task<Book?> GetAsync(int bookId)
{
using var connection = connectionFactory.CreateConnection();

View File

@ -411,14 +411,50 @@ public sealed class ProjectService(
return new ProjectIndexViewModel();
}
var activeProjects = await projects.ListActiveForUserAsync(currentUser.UserId.Value);
var archivedProjects = await projects.ListArchivedForUserAsync(currentUser.UserId.Value);
var projectBooks = await books.ListByProjectsAsync(activeProjects.Concat(archivedProjects).Select(project => project.ProjectID));
var booksByProject = projectBooks
.GroupBy(book => book.ProjectID)
.ToDictionary(group => group.Key, group => group.ToList());
return new ProjectIndexViewModel
{
ActiveProjects = await projects.ListActiveForUserAsync(currentUser.UserId.Value),
ArchivedProjects = await projects.ListArchivedForUserAsync(currentUser.UserId.Value),
ActiveProjects = activeProjects.Select(project => ToIndexCard(project, booksByProject)).ToList(),
ArchivedProjects = archivedProjects.Select(project => ToIndexCard(project, booksByProject)).ToList(),
Trial = TrialVisibilityViewModel.FromSubscription(await subscriptions.GetCurrentSubscriptionAsync(currentUser.UserId.Value), DateTime.UtcNow)
};
}
private static ProjectIndexCardViewModel ToIndexCard(Project project, IReadOnlyDictionary<int, List<Book>> booksByProject)
{
booksByProject.TryGetValue(project.ProjectID, out var projectBooks);
projectBooks ??= [];
return new ProjectIndexCardViewModel
{
ProjectID = project.ProjectID,
ProjectName = project.ProjectName,
Description = project.Description,
CreatedDate = project.CreatedDate,
UpdatedDate = project.UpdatedDate,
IsArchived = project.IsArchived,
ArchivedDate = project.ArchivedDate,
AccessRole = project.AccessRole,
BookCount = projectBooks.Count,
TotalWords = projectBooks.Sum(book => book.CurrentWordCount),
LastBookUpdatedDate = projectBooks.Count == 0 ? null : projectBooks.Max(book => book.UpdatedDate),
CoverThumbnailPaths = projectBooks
.Where(book => !string.IsNullOrWhiteSpace(book.CoverThumbnailPath))
.OrderBy(book => book.SortOrder)
.ThenBy(book => book.BookNumber)
.ThenBy(book => book.BookTitle)
.Select(book => book.CoverThumbnailPath!)
.Take(3)
.ToList()
};
}
public async Task<ProjectDetailViewModel?> GetDetailAsync(int projectId)
{
if (!currentUser.UserId.HasValue)

View File

@ -23,11 +23,28 @@ public sealed class ProjectEditViewModel
public sealed class ProjectIndexViewModel
{
public IReadOnlyList<Project> ActiveProjects { get; init; } = [];
public IReadOnlyList<Project> ArchivedProjects { get; init; } = [];
public IReadOnlyList<ProjectIndexCardViewModel> ActiveProjects { get; init; } = [];
public IReadOnlyList<ProjectIndexCardViewModel> ArchivedProjects { get; init; } = [];
public TrialVisibilityViewModel? Trial { get; init; }
}
public sealed class ProjectIndexCardViewModel
{
public int ProjectID { get; set; }
public string ProjectName { get; set; } = string.Empty;
public string? Description { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime UpdatedDate { get; set; }
public bool IsArchived { get; set; }
public DateTime? ArchivedDate { get; set; }
public string AccessRole { get; set; } = "Owner";
public bool IsOwner => string.Equals(AccessRole, "Owner", StringComparison.OrdinalIgnoreCase);
public int BookCount { get; set; }
public int TotalWords { get; set; }
public DateTime? LastBookUpdatedDate { get; set; }
public IReadOnlyList<string> CoverThumbnailPaths { get; set; } = [];
}
public sealed class ProjectDetailViewModel
{
public Project Project { get; set; } = new();

View File

@ -56,82 +56,195 @@
}
else
{
<section class="list-section">
<h2>Active Projects</h2>
@if (!Model.ActiveProjects.Any())
{
<p class="muted">No active projects.</p>
}
else
{
<div class="content-grid">
@foreach (var project in Model.ActiveProjects)
{
<article class="plain-card">
<h3>@project.ProjectName</h3>
<p class="eyebrow">@project.AccessRole</p>
<p>@(string.IsNullOrWhiteSpace(project.Description) ? "No description yet." : project.Description)</p>
<div class="button-row">
<a class="btn btn-outline-primary btn-sm" asp-action="Details" asp-route-id="@project.ProjectID">Open</a>
<a class="btn btn-outline-secondary btn-sm" asp-action="Edit" asp-route-id="@project.ProjectID">Edit</a>
@if (project.IsOwner)
{
<a class="btn btn-outline-secondary btn-sm" asp-controller="Exports" asp-action="ProjectBackup" asp-route-projectId="@project.ProjectID">Download backup</a>
<form asp-action="Archive" method="post" class="archive-button-form" data-confirm-message="Archive this project?&#10;&#10;Archived projects are hidden from your active workspace but remain stored within PlotDirector. Export your project first if you wish to maintain an external backup.">
<input type="hidden" name="id" value="@project.ProjectID" />
<button class="btn btn-outline-danger btn-sm" type="submit">Archive</button>
</form>
}
</div>
</article>
}
</div>
}
</section>
<section class="project-gallery" aria-label="Project lists">
<ul class="nav nav-pills project-tabs" id="projectTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active"
id="active-projects-tab"
data-bs-toggle="tab"
data-bs-target="#active-projects-panel"
type="button"
role="tab"
aria-controls="active-projects-panel"
aria-selected="true">
Active Projects (@Model.ActiveProjects.Count)
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link"
id="archived-projects-tab"
data-bs-toggle="tab"
data-bs-target="#archived-projects-panel"
type="button"
role="tab"
aria-controls="archived-projects-panel"
aria-selected="false">
Archived Projects (@Model.ArchivedProjects.Count)
</button>
</li>
</ul>
<section class="list-section">
<h2>Archived Projects</h2>
@if (!Model.ArchivedProjects.Any())
{
<p class="muted">No archived projects.</p>
}
else
{
<div class="content-grid">
@foreach (var project in Model.ArchivedProjects)
<div class="tab-content project-tab-content" id="projectTabsContent">
<div class="tab-pane fade show active"
id="active-projects-panel"
role="tabpanel"
aria-labelledby="active-projects-tab"
tabindex="0">
@if (!Model.ActiveProjects.Any())
{
var archivedDateLabel = project.ArchivedDate?.ToLocalTime().ToString("yyyy-MM-dd");
<article class="plain-card">
<h3>@project.ProjectName</h3>
<p class="eyebrow">
Archived
@if (!string.IsNullOrWhiteSpace(archivedDateLabel))
{
<span> / @archivedDateLabel</span>
}
</p>
<p>@(string.IsNullOrWhiteSpace(project.Description) ? "No description yet." : project.Description)</p>
<div class="button-row">
<a class="btn btn-outline-secondary btn-sm" asp-controller="Exports" asp-action="ProjectBackup" asp-route-projectId="@project.ProjectID">Download backup</a>
<form asp-action="Restore" method="post" class="archive-button-form" data-confirm-message="Restore this project?&#10;&#10;It will return to your active project list.">
<input type="hidden" name="id" value="@project.ProjectID" />
<button class="btn btn-outline-primary btn-sm" type="submit">Restore</button>
</form>
@if (project.IsOwner)
{
<button class="btn btn-outline-danger btn-sm"
type="button"
data-hard-delete-project
data-project-id="@project.ProjectID"
data-project-name="@project.ProjectName">
Delete Permanently
</button>
}
</div>
</article>
<div class="empty-panel project-empty-state">
<h2>No active projects</h2>
<p class="muted mb-0">Restored projects will appear here, ready to open again.</p>
</div>
}
else
{
<div class="project-card-grid">
@foreach (var project in Model.ActiveProjects)
{
<article class="project-card">
<div class="project-card__visual">
@if (project.CoverThumbnailPaths.Any())
{
<div class="project-cover-stack" aria-label="Book covers for @project.ProjectName">
@foreach (var cover in project.CoverThumbnailPaths.Select((path, index) => new { path, index }))
{
<img src="@cover.path"
alt=""
class="project-cover-stack__cover project-cover-stack__cover--@cover.index"
loading="lazy" />
}
</div>
}
else
{
<div class="project-cover-fallback" aria-hidden="true">
<span>@Initials(project.ProjectName)</span>
</div>
}
</div>
<div class="project-card__body">
<div class="project-card__heading">
<div>
<p class="eyebrow mb-1">@project.AccessRole</p>
<h2><a asp-action="Details" asp-route-id="@project.ProjectID">@project.ProjectName</a></h2>
</div>
</div>
<p class="project-card__description">@(string.IsNullOrWhiteSpace(project.Description) ? "No description yet." : project.Description)</p>
<div class="project-card__stats" aria-label="Project summary">
<span><strong>@project.BookCount.ToString("N0")</strong> books</span>
<span><strong>@project.TotalWords.ToString("N0")</strong> words</span>
<span>Modified @FormatDate(ProjectLastUpdated(project))</span>
</div>
</div>
<div class="project-card__actions">
<a class="btn btn-primary btn-sm" asp-action="Details" asp-route-id="@project.ProjectID">Open</a>
<a class="btn btn-outline-secondary btn-sm" asp-action="Edit" asp-route-id="@project.ProjectID">Edit</a>
@if (project.IsOwner)
{
<div class="dropdown pw-actions">
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
More
</button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" asp-controller="Exports" asp-action="ProjectBackup" asp-route-projectId="@project.ProjectID">Download backup</a>
<div class="dropdown-divider"></div>
<form asp-action="Archive" method="post" class="archive-button-form" data-confirm-message="Archive this project?&#10;&#10;Archived projects are hidden from your active workspace but remain stored within PlotDirector. Export your project first if you wish to maintain an external backup.">
<input type="hidden" name="id" value="@project.ProjectID" />
<button class="dropdown-item text-danger" type="submit">Archive</button>
</form>
</div>
</div>
}
</div>
</article>
}
</div>
}
</div>
}
<div class="tab-pane fade"
id="archived-projects-panel"
role="tabpanel"
aria-labelledby="archived-projects-tab"
tabindex="0">
@if (!Model.ArchivedProjects.Any())
{
<div class="empty-panel project-empty-state">
<h2>No archived projects</h2>
<p class="muted mb-0">Archived projects will live here, recoverable but out of your active workspace.</p>
</div>
}
else
{
<div class="project-card-grid">
@foreach (var project in Model.ArchivedProjects)
{
var archivedDateLabel = project.ArchivedDate?.ToLocalTime().ToString("yyyy-MM-dd");
<article class="project-card project-card--archived">
<div class="project-card__visual">
@if (project.CoverThumbnailPaths.Any())
{
<div class="project-cover-stack" aria-label="Book covers for @project.ProjectName">
@foreach (var cover in project.CoverThumbnailPaths.Select((path, index) => new { path, index }))
{
<img src="@cover.path"
alt=""
class="project-cover-stack__cover project-cover-stack__cover--@cover.index"
loading="lazy" />
}
</div>
}
else
{
<div class="project-cover-fallback" aria-hidden="true">
<span>@Initials(project.ProjectName)</span>
</div>
}
</div>
<div class="project-card__body">
<div class="project-card__heading">
<div>
<p class="eyebrow mb-1">
Archived
@if (!string.IsNullOrWhiteSpace(archivedDateLabel))
{
<span> / @archivedDateLabel</span>
}
</p>
<h2>@project.ProjectName</h2>
</div>
</div>
<p class="project-card__description">@(string.IsNullOrWhiteSpace(project.Description) ? "No description yet." : project.Description)</p>
<div class="project-card__stats" aria-label="Project summary">
<span><strong>@project.BookCount.ToString("N0")</strong> books</span>
<span><strong>@project.TotalWords.ToString("N0")</strong> words</span>
<span>Modified @FormatDate(ProjectLastUpdated(project))</span>
</div>
</div>
<div class="project-card__actions">
<a class="btn btn-outline-secondary btn-sm" asp-controller="Exports" asp-action="ProjectBackup" asp-route-projectId="@project.ProjectID">Download backup</a>
<form asp-action="Restore" method="post" class="archive-button-form" data-confirm-message="Restore this project?&#10;&#10;It will return to your active project list.">
<input type="hidden" name="id" value="@project.ProjectID" />
<button class="btn btn-outline-primary btn-sm" type="submit">Restore</button>
</form>
@if (project.IsOwner)
{
<button class="btn btn-outline-danger btn-sm"
type="button"
data-hard-delete-project
data-project-id="@project.ProjectID"
data-project-name="@project.ProjectName">
Delete Permanently
</button>
}
</div>
</article>
}
</div>
}
</div>
</div>
</section>
<div class="modal fade" id="hardDeleteProjectModal" tabindex="-1" aria-labelledby="hardDeleteProjectModalTitle" aria-hidden="true">
@ -165,6 +278,20 @@ else
@functions {
private static string FormatDate(DateTime value) => value.ToLocalTime().ToString("yyyy-MM-dd");
private static DateTime ProjectLastUpdated(ProjectIndexCardViewModel project) =>
new[] { project.UpdatedDate, project.LastBookUpdatedDate ?? project.UpdatedDate }.Max();
private static string Initials(string value)
{
var words = value
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Take(2)
.Select(word => char.ToUpperInvariant(word[0]));
var initials = string.Concat(words);
return string.IsNullOrWhiteSpace(initials) ? "PD" : initials;
}
}
@section Scripts {

View File

@ -420,6 +420,217 @@ a:hover {
background: var(--plotline-danger);
}
.project-gallery {
display: grid;
gap: 1rem;
}
.project-tabs {
gap: 0.45rem;
padding: 0.35rem;
border: 1px solid var(--plotline-border-soft);
border-radius: 999px;
background: rgba(255, 250, 241, 0.72);
box-shadow: var(--plotline-shadow-subtle);
width: fit-content;
max-width: 100%;
}
.project-tabs .nav-link {
border-radius: 999px;
color: var(--plotline-text-muted);
font-weight: 800;
padding: 0.48rem 0.9rem;
}
.project-tabs .nav-link:hover,
.project-tabs .nav-link:focus {
color: var(--plotline-accent-dark);
background: rgba(122, 77, 44, 0.1);
}
.project-tabs .nav-link.active {
color: #fff8ec;
background: linear-gradient(180deg, var(--plotline-accent), var(--plotline-accent-dark));
box-shadow: 0 8px 16px rgba(62, 45, 28, 0.14);
}
.project-tab-content {
min-width: 0;
}
.project-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 22rem), 1fr));
gap: 1rem;
}
.project-card {
display: grid;
grid-template-rows: auto 1fr auto;
overflow: hidden;
min-height: 100%;
border: 1px solid var(--plotline-border-soft);
border-radius: var(--plotline-radius);
background: var(--plotline-surface);
box-shadow: var(--plotline-shadow-subtle);
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
.project-card:hover {
border-color: rgba(122, 77, 44, 0.24);
box-shadow: var(--plotline-shadow-card);
transform: translateY(-1px);
}
.project-card--archived {
opacity: 0.88;
}
.project-card__visual {
position: relative;
display: grid;
place-items: center;
min-height: 11rem;
overflow: hidden;
background:
radial-gradient(circle at 26% 22%, rgba(255, 255, 255, 0.64), transparent 11rem),
linear-gradient(135deg, rgba(242, 226, 207, 0.94), rgba(223, 236, 239, 0.78));
}
.project-card--archived .project-card__visual {
filter: saturate(0.55);
}
.project-card__body {
display: grid;
gap: 0.75rem;
padding: 1rem 1rem 0.85rem;
}
.project-card__heading {
display: flex;
justify-content: space-between;
gap: 0.8rem;
}
.project-card__heading h2 {
margin: 0;
font-size: 1.35rem;
line-height: 1.1;
}
.project-card__heading h2 a {
color: var(--plotline-text);
text-decoration-color: rgba(122, 77, 44, 0.28);
}
.project-card__description {
display: -webkit-box;
min-height: 3.2rem;
margin: 0;
overflow: hidden;
color: var(--plotline-text-muted);
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.project-card__stats {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.project-card__stats span {
display: inline-flex;
align-items: baseline;
gap: 0.25rem;
border: 1px solid rgba(84, 68, 48, 0.12);
border-radius: 999px;
padding: 0.28rem 0.6rem;
color: var(--plotline-text-muted);
background: rgba(255, 250, 241, 0.74);
font-size: 0.82rem;
font-weight: 700;
}
.project-card__stats strong {
color: var(--plotline-accent-dark);
}
.project-card__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.45rem;
border-top: 1px solid rgba(84, 68, 48, 0.1);
padding: 0.85rem 1rem 1rem;
}
.project-card__actions form {
display: inline-flex;
margin: 0;
}
.project-cover-stack {
position: relative;
width: min(11.5rem, 70%);
height: 9.4rem;
}
.project-cover-stack__cover {
position: absolute;
top: 50%;
left: 50%;
width: 4.65rem;
aspect-ratio: 2 / 3;
border: 3px solid rgba(255, 253, 248, 0.86);
border-radius: 0.45rem;
background: var(--plotline-soft);
box-shadow: 0 12px 28px rgba(62, 45, 28, 0.22);
object-fit: cover;
}
.project-cover-stack__cover--0 {
z-index: 3;
transform: translate(-50%, -50%) rotate(-1deg) scale(1.08);
}
.project-cover-stack__cover--1 {
z-index: 2;
transform: translate(-88%, -47%) rotate(-8deg) scale(0.94);
}
.project-cover-stack__cover--2 {
z-index: 1;
transform: translate(-12%, -47%) rotate(8deg) scale(0.94);
}
.project-cover-fallback {
display: grid;
place-items: center;
width: min(9.5rem, 62%);
aspect-ratio: 2 / 3;
border: 1px solid rgba(84, 68, 48, 0.18);
border-radius: 0.8rem;
background:
linear-gradient(180deg, rgba(255, 253, 248, 0.34), transparent 32%),
linear-gradient(135deg, var(--plotline-accent), var(--plotline-accent-dark));
box-shadow: 0 14px 30px rgba(62, 45, 28, 0.2);
color: #fff8ec;
}
.project-cover-fallback span {
font-family: Georgia, "Times New Roman", serif;
font-size: clamp(2.2rem, 7vw, 3.2rem);
font-weight: 700;
letter-spacing: 0.02em;
}
.project-empty-state {
margin: 0;
}
@media (max-width: 767.98px) {
.pw-page-heading {
align-items: stretch;
@ -433,6 +644,21 @@ a:hover {
.pw-project-nav {
padding: 0.65rem;
}
.project-tabs {
width: 100%;
border-radius: var(--plotline-radius);
}
.project-tabs .nav-item,
.project-tabs .nav-link {
flex: 1 1 auto;
}
.project-card__actions .btn,
.project-card__actions .dropdown {
flex: 1 1 auto;
}
}
.timeline-workspace {
@ -1830,6 +2056,79 @@ a:focus-visible {
color: #ffb2aa !important;
}
[data-theme="dark"] .project-tabs {
border-color: rgba(241, 195, 143, 0.18);
background: rgba(35, 29, 25, 0.88);
}
[data-theme="dark"] .project-tabs .nav-link {
color: #ead8c4;
}
[data-theme="dark"] .project-tabs .nav-link:hover,
[data-theme="dark"] .project-tabs .nav-link:focus {
color: #ffd5a6;
background: rgba(241, 195, 143, 0.12);
}
[data-theme="dark"] .project-tabs .nav-link.active {
color: #21160f;
background: linear-gradient(180deg, #f1c38f, var(--plotline-accent));
}
[data-theme="dark"] .project-card {
color: var(--app-text);
border-color: var(--app-border);
background: var(--app-surface);
}
[data-theme="dark"] .project-card:hover {
border-color: rgba(241, 195, 143, 0.34);
}
[data-theme="dark"] .project-card__visual {
background:
radial-gradient(circle at 28% 24%, rgba(241, 195, 143, 0.22), transparent 11rem),
linear-gradient(135deg, rgba(58, 42, 32, 0.96), rgba(33, 56, 61, 0.72));
}
[data-theme="dark"] .project-card__heading h2,
[data-theme="dark"] .project-card__heading h2 a {
color: var(--app-text);
}
[data-theme="dark"] .project-card__description {
color: var(--app-text-muted);
}
[data-theme="dark"] .project-card__stats span {
color: #ead8c4;
border-color: rgba(231, 214, 190, 0.2);
background: rgba(231, 214, 190, 0.08);
}
[data-theme="dark"] .project-card__stats strong {
color: #ffd5a6;
}
[data-theme="dark"] .project-card__actions {
border-top-color: rgba(231, 214, 190, 0.14);
}
[data-theme="dark"] .project-cover-stack__cover {
border-color: rgba(42, 35, 30, 0.92);
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.38);
}
[data-theme="dark"] .project-cover-fallback {
color: #21160f;
border-color: rgba(241, 195, 143, 0.34);
background:
linear-gradient(180deg, rgba(255, 244, 232, 0.3), transparent 32%),
linear-gradient(135deg, #f1c38f, var(--plotline-accent));
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.34);
}
[data-theme="dark"] .timeline-filter,
[data-theme="dark"] .workspace-heading .button-row,
[data-theme="dark"] .timeline-control-panel {

File diff suppressed because one or more lines are too long