@model TimelineViewModel
@{
    ViewData["Title"] = "Timeline";
    ViewData["ShellClass"] = "plotline-shell-wide";
    ViewData["ProjectSection"] = "Timeline";
    var selectedBook = Model.Books.FirstOrDefault(x => x.Book.BookID == Model.SelectedBookID)?.Book;
    var hasActiveFilter = Model.MatchingSceneIDs.Count != Model.OrderedScenes.Count || !string.IsNullOrWhiteSpace(Model.FocusLabel);
    var warningScenes = Model.OrderedScenes.Where(x => x.WarningCount > 0).ToList();
    var timelineColumns = new List<(BookTimelineViewModel Book, ChapterTimelineViewModel? Chapter, Scene? Scene, bool IsPlaceholder, string PlaceholderLabel)>();
    foreach (var book in Model.Books)
    {
        if (!book.Chapters.Any())
        {
            timelineColumns.Add((book, null, null, true, "No chapters"));
            continue;
        }

        foreach (var chapter in book.Chapters)
        {
            if (!chapter.Scenes.Any())
            {
                timelineColumns.Add((book, chapter, null, true, "No scenes"));
                continue;
            }

            foreach (var scene in chapter.Scenes)
            {
                timelineColumns.Add((book, chapter, scene, false, string.Empty));
            }
        }
    }
    var timelineMetricColumns = timelineColumns.Select(column => new
    {
        sceneId = column.Scene?.SceneID,
        isPlaceholder = column.Scene is null,
        axisLabel = column.Scene is null ? string.Empty : $"Ch {column.Chapter?.Chapter.ChapterNumber:g}.{column.Scene.SceneNumber:g}",
        chapterLabel = column.Chapter is null ? column.Book.Book.BookDisplayTitle : $"Chapter {column.Chapter.Chapter.ChapterNumber:g}",
        sceneNumber = column.Scene?.SceneNumber.ToString("g"),
        sceneTitle = column.Scene?.SceneTitle ?? column.PlaceholderLabel,
        bookTitle = column.Book.Book.BookDisplayTitle,
        timelineUrl = column.Scene is null ? null : Url.Action("Index", "Timeline", new { projectId = Model.Project.ProjectID, bookId = column.Book.Book.BookID, selectedSceneId = column.Scene.SceneID })
    }).ToList();
    var timelineMetricShapePayload = new
    {
        scenes = timelineMetricColumns,
        metrics = Model.MetricGraphs.Select(graph => new
        {
            metricTypeId = graph.MetricTypeID,
            metricName = graph.MetricName,
            minValue = graph.MinValue,
            maxValue = graph.MaxValue,
            points = graph.Points.Select(point => new { sceneId = point.SceneID, value = point.Value })
        })
    };
    int ChapterSceneColumns(ChapterTimelineViewModel chapter) => Math.Max(chapter.Scenes.Count, 1);
    int BookSceneColumns(BookTimelineViewModel book) => Math.Max(book.Chapters.Any() ? book.Chapters.Sum(ChapterSceneColumns) : 1, 1);
    var hasSelectedScene = Model.SelectedScene is not null;
    int CharacterAppearanceCount(CharacterLaneViewModel lane) => lane.SceneSlots.Sum(slot => slot.Appearances.Count);
    int HiddenCount<T>(IReadOnlyList<T> items, int visibleCount) => Math.Max(0, items.Count - visibleCount);
    string PlotMarkerKind(string? plotEventTypeName) => (plotEventTypeName ?? "Progress").Trim().ToLowerInvariant() switch
    {
        "start" => "start",
        "reveal" => "reveal",
        "twist" => "twist",
        "branch" => "branch",
        "split" => "split",
        "merge" => "merge",
        "resolve" => "resolve",
        "abandon" => "abandon",
        _ => "progress"
    };
    string PlotMarkerIcon(string? plotEventTypeName) => PlotMarkerKind(plotEventTypeName) switch
    {
        "start" => ">",
        "progress" => "+",
        "reveal" => "o",
        "twist" => "!",
        "branch" => "B",
        "split" => "Y",
        "merge" => "M",
        "resolve" => "OK",
        "abandon" => "x",
        _ => "+"
    };
    string ImportanceLabel(int importance) => importance switch
    {
        >= 8 => $"High ({importance})",
        >= 4 => $"Medium ({importance})",
        > 0 => $"Low ({importance})",
        _ => "Not set"
    };
    string ThreadEventTooltip(ThreadEvent threadEvent, BookTimelineViewModel book, ChapterTimelineViewModel? chapter, Scene scene)
    {
        var lines = new List<string>
        {
            $"Thread: {threadEvent.ThreadTitle}",
            $"Event: {threadEvent.PlotEventTypeName}",
            $"Book: {book.Book.BookDisplayTitle}",
            $"Chapter: {(chapter is null ? "No chapter" : $"{chapter.Chapter.ChapterNumber:g}: {chapter.Chapter.ChapterTitle}")}",
            $"Scene: {scene.SceneNumber:g}: {scene.SceneTitle}",
            $"Importance: {ImportanceLabel(threadEvent.Importance)}",
            $"Status: {(string.IsNullOrWhiteSpace(threadEvent.ThreadStatusName) ? "Unknown" : threadEvent.ThreadStatusName)}"
        };
        if (!string.IsNullOrWhiteSpace(threadEvent.EventTitle))
        {
            lines.Add($"Marker: {threadEvent.EventTitle}");
        }
        if (!string.IsNullOrWhiteSpace(threadEvent.EventDescription))
        {
            lines.Add($"Summary: {threadEvent.EventDescription}");
        }
        return string.Join(Environment.NewLine, lines);
    }
    string SuggestedWarningAction(ContinuityWarning warning)
    {
        var warningText = $"{warning.WarningTypeName} {warning.Message} {warning.Details}".ToLowerInvariant();
        if (warningText.Contains("dormant") || warningText.Contains("no activity") || warningText.Contains("neglected"))
        {
            return "Add a Progress, Reveal, Twist, Resolve or Abandon event, or confirm the thread is intentionally quiet.";
        }
        if (warningText.Contains("unresolved") || warningText.Contains("missing closure") || warningText.Contains("closure"))
        {
            return "Review the ending scenes and add a Resolve or Abandon event if the thread is finished.";
        }
        if (warningText.Contains("contradict") || warningText.Contains("conflict"))
        {
            return "Review the related scene details and update the conflicting story state.";
        }
        if (warningText.Contains("dependency"))
        {
            return "Review the linked scene dependency and adjust the ordering or dependency status.";
        }
        return "Open the warning dashboard or scene inspector and review the affected story detail.";
    }
    string WarningTooltip(Scene scene)
    {
        if (!Model.WarningsByScene.TryGetValue(scene.SceneID, out var sceneWarnings) || sceneWarnings.Count == 0)
        {
            return $"{scene.WarningCount} warning{(scene.WarningCount == 1 ? "" : "s")} on Scene {scene.SceneNumber:g}: {scene.SceneTitle}";
        }

        var lines = new List<string>
        {
            $"{sceneWarnings.Count} warning{(sceneWarnings.Count == 1 ? "" : "s")} on Scene {scene.SceneNumber:g}: {scene.SceneTitle}"
        };
        foreach (var warning in sceneWarnings.OrderByDescending(x => x.SeverityName).ThenBy(x => x.WarningTypeName))
        {
            lines.Add(string.Empty);
            lines.Add($"Warning: {warning.WarningTypeName}");
            lines.Add($"Reason: {(string.IsNullOrWhiteSpace(warning.Details) ? warning.Message : warning.Details)}");
            lines.Add($"Suggested Action: {SuggestedWarningAction(warning)}");
        }
        return string.Join(Environment.NewLine, lines);
    }
    string TrimMarkerLabel(string value)
        => string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().Length <= 5 ? value.Trim() : value.Trim()[..5];
    IReadOnlyList<ContinuityWarning> EntityWarnings(Scene scene, string entityType, int entityId)
        => Model.WarningsByScene.TryGetValue(scene.SceneID, out var sceneWarnings)
            ? sceneWarnings
                .Where(warning => string.Equals(warning.EntityType, entityType, StringComparison.OrdinalIgnoreCase)
                    && warning.EntityID == entityId)
                .OrderByDescending(warning => warning.SeverityName)
                .ThenBy(warning => warning.WarningTypeName)
                .ToList()
            : [];
    bool HasStoryState(SceneCharacter appearance)
        => !string.IsNullOrWhiteSpace(appearance.EmotionalState)
            || !string.IsNullOrWhiteSpace(appearance.PhysicalCondition)
            || !string.IsNullOrWhiteSpace(appearance.KnowledgeNotes);
    string AssetMarkerLabel(AssetEvent assetEvent)
    {
        var typeName = assetEvent.AssetEventTypeName?.Trim() ?? string.Empty;
        return typeName.ToLowerInvariant() switch
        {
            "first mentioned" => "First",
            "mentioned" => "Ref",
            "found" => "Find",
            "lost" => "Lost",
            "given" => "Give",
            "taken" => "Take",
            "stolen" => "Stol",
            "hidden" => "Hide",
            "moved" or "location changed" => "Move",
            "used" => "Use",
            "opened" => "Open",
            "destroyed" => "X",
            "revealed" => "Show",
            "investigated" => "?",
            "resolved" => "Done",
            "state changed" => "State",
            _ => string.IsNullOrWhiteSpace(typeName) ? "Asset" : TrimMarkerLabel(typeName)
        };
    }
    string AssetMarkerMeaning(AssetEvent assetEvent)
    {
        var typeName = assetEvent.AssetEventTypeName?.Trim() ?? string.Empty;
        return typeName.ToLowerInvariant() switch
        {
            "first mentioned" => "Asset is first mentioned in this scene.",
            "mentioned" => "Asset is mentioned in this scene.",
            "found" => "Asset is found in this scene.",
            "lost" => "Asset is lost in this scene.",
            "given" => "Asset changes hands in this scene.",
            "taken" => "Asset is taken in this scene.",
            "stolen" => "Asset is stolen in this scene.",
            "hidden" => "Asset is hidden in this scene.",
            "moved" or "location changed" => "Asset changes location in this scene.",
            "used" => "Asset is used in this scene.",
            "opened" => "Asset is opened in this scene.",
            "destroyed" => "Asset is destroyed in this scene.",
            "revealed" => "Asset is revealed in this scene.",
            "investigated" => "Asset is investigated in this scene.",
            "resolved" => "Asset thread is resolved in this scene.",
            "state changed" => "Asset state changes in this scene.",
            _ => string.IsNullOrWhiteSpace(typeName) ? "Asset event occurs in this scene." : $"Asset event: {typeName}."
        };
    }
    string AssetStorySentence(AssetEvent assetEvent)
    {
        var title = assetEvent.EventTitle?.Trim();
        if (!string.IsNullOrWhiteSpace(title) && !string.Equals(title, assetEvent.AssetEventTypeName, StringComparison.OrdinalIgnoreCase))
        {
            return $"{assetEvent.AssetName}: {title}.";
        }

        return AssetMarkerMeaning(assetEvent);
    }
    string AssetMarkerClass(AssetEvent assetEvent, StoryAsset asset, IReadOnlyList<ContinuityWarning> assetWarnings)
    {
        var classes = new List<string>();
        var typeName = assetEvent.AssetEventTypeName?.Trim().ToLowerInvariant() ?? string.Empty;
        if (assetWarnings.Any())
        {
            classes.Add("timeline-marker-has-warning");
        }
        if (asset.Importance >= 8)
        {
            classes.Add("timeline-marker-important");
        }
        if (!string.IsNullOrWhiteSpace(assetEvent.ToStateName))
        {
            classes.Add("timeline-marker-state-change");
        }
        if (typeName is "found" or "revealed" or "destroyed" or "resolved" or "state changed")
        {
            classes.Add("timeline-marker-story-turn");
        }
        if (typeName is "given" or "taken" or "stolen" or "moved" or "location changed")
        {
            classes.Add("timeline-marker-transfer");
        }
        return string.Join(' ', classes);
    }
    string AssetMarkerTooltip(AssetEvent assetEvent, StoryAsset asset, Scene scene, IReadOnlyList<ContinuityWarning> assetWarnings)
    {
        var lines = new List<string>
        {
            AssetStorySentence(assetEvent),
            $"Asset: {assetEvent.AssetName}",
            $"Event type: {(string.IsNullOrWhiteSpace(assetEvent.AssetEventTypeName) ? "Not set" : assetEvent.AssetEventTypeName)}",
            $"Scene: {scene.SceneNumber:g}: {scene.SceneTitle}"
        };
        if (asset.Importance >= 8)
        {
            lines.Add("Significance: High-importance story asset.");
        }
        if (!string.IsNullOrWhiteSpace(assetEvent.FromStateName) || !string.IsNullOrWhiteSpace(assetEvent.ToStateName))
        {
            lines.Add($"State change: {(string.IsNullOrWhiteSpace(assetEvent.FromStateName) ? "Unknown" : assetEvent.FromStateName)} -> {(string.IsNullOrWhiteSpace(assetEvent.ToStateName) ? "Unknown" : assetEvent.ToStateName)}");
        }
        if (!string.IsNullOrWhiteSpace(assetEvent.ToStateName))
        {
            lines.Add($"Current state after this event: {assetEvent.ToStateName}");
        }
        if (!string.IsNullOrWhiteSpace(assetEvent.EventDescription))
        {
            lines.Add($"Summary: {assetEvent.EventDescription}");
        }
        foreach (var warning in assetWarnings.Take(2))
        {
            lines.Add($"Continuity: {warning.WarningTypeName} - {(string.IsNullOrWhiteSpace(warning.Details) ? warning.Message : warning.Details)}");
        }
        return string.Join(Environment.NewLine, lines);
    }
    string CharacterMarkerLabel(SceneCharacter appearance, SceneCharacter? firstAppearance, SceneCharacter? lastAppearance)
    {
        if (firstAppearance?.SceneCharacterID == appearance.SceneCharacterID)
        {
            return "Intro";
        }
        if (lastAppearance?.SceneCharacterID == appearance.SceneCharacterID
            && string.Equals(appearance.PresenceTypeName, "Leaves", StringComparison.OrdinalIgnoreCase))
        {
            return "Exit";
        }
        if (string.Equals(appearance.RoleInSceneTypeName, "POV Character", StringComparison.OrdinalIgnoreCase))
        {
            return "POV";
        }
        if (!string.IsNullOrWhiteSpace(appearance.KnowledgeNotes))
        {
            return "Know";
        }
        if (HasStoryState(appearance))
        {
            return "State";
        }
        var presence = appearance.PresenceTypeName?.Trim() ?? string.Empty;
        return presence.ToLowerInvariant() switch
        {
            "present" => "In",
            "arrives" => "In+",
            "leaves" => "Out",
            "nearby" => "Near",
            "hidden" => "Hide",
            "overhearing" => "Hear",
            "mentioned only" => "Ref",
            "background" => "Bg",
            "off-screen" => "Off",
            _ => "Char"
        };
    }
    string CharacterMarkerMeaning(SceneCharacter appearance, SceneCharacter? firstAppearance, SceneCharacter? lastAppearance)
    {
        if (firstAppearance?.SceneCharacterID == appearance.SceneCharacterID)
        {
            return $"{appearance.CharacterName} enters this story lane in this scene.";
        }
        if (lastAppearance?.SceneCharacterID == appearance.SceneCharacterID
            && string.Equals(appearance.PresenceTypeName, "Leaves", StringComparison.OrdinalIgnoreCase))
        {
            return $"{appearance.CharacterName} exits this story lane in this scene.";
        }
        if (!string.IsNullOrWhiteSpace(appearance.KnowledgeNotes))
        {
            return $"{appearance.CharacterName}'s knowledge changes in this scene.";
        }
        if (!string.IsNullOrWhiteSpace(appearance.EmotionalState) || !string.IsNullOrWhiteSpace(appearance.PhysicalCondition))
        {
            return $"{appearance.CharacterName}'s story state changes in this scene.";
        }
        var presence = appearance.PresenceTypeName?.Trim() ?? string.Empty;
        return presence.ToLowerInvariant() switch
        {
            "present" => "Character appears in this scene.",
            "arrives" => "Character arrives in this scene.",
            "leaves" => "Character leaves in this scene.",
            "nearby" => "Character is nearby in this scene.",
            "hidden" => "Character is hidden in this scene.",
            "overhearing" => "Character overhears this scene.",
            "mentioned only" => "Character is mentioned in this scene.",
            "background" => "Character is in the background of this scene.",
            "off-screen" => "Character is off-screen in this scene.",
            _ => "Character appears in this scene."
        };
    }
    string CharacterMarkerClass(SceneCharacter appearance, SceneCharacter? firstAppearance, SceneCharacter? lastAppearance, IReadOnlyList<ContinuityWarning> characterWarnings)
    {
        var classes = new List<string>();
        if (characterWarnings.Any())
        {
            classes.Add("timeline-marker-has-warning");
        }
        if (firstAppearance?.SceneCharacterID == appearance.SceneCharacterID)
        {
            classes.Add("timeline-marker-story-turn");
        }
        if (lastAppearance?.SceneCharacterID == appearance.SceneCharacterID
            && string.Equals(appearance.PresenceTypeName, "Leaves", StringComparison.OrdinalIgnoreCase))
        {
            classes.Add("timeline-marker-exit");
        }
        if (string.Equals(appearance.RoleInSceneTypeName, "POV Character", StringComparison.OrdinalIgnoreCase)
            || string.Equals(appearance.RoleInSceneTypeName, "Main Participant", StringComparison.OrdinalIgnoreCase))
        {
            classes.Add("timeline-marker-important");
        }
        if (HasStoryState(appearance))
        {
            classes.Add("timeline-marker-state-change");
        }
        return string.Join(' ', classes);
    }
    string CharacterMarkerTooltip(SceneCharacter appearance, Scene scene, SceneCharacter? firstAppearance, SceneCharacter? lastAppearance, IReadOnlyList<ContinuityWarning> characterWarnings)
    {
        var lines = new List<string>
        {
            CharacterMarkerMeaning(appearance, firstAppearance, lastAppearance),
            $"Character: {appearance.CharacterName}",
            $"Role: {(string.IsNullOrWhiteSpace(appearance.RoleInSceneTypeName) ? "Not set" : appearance.RoleInSceneTypeName)}",
            $"Presence: {(string.IsNullOrWhiteSpace(appearance.PresenceTypeName) ? "Not set" : appearance.PresenceTypeName)}",
            $"Scene: {scene.SceneNumber:g}: {scene.SceneTitle}"
        };
        if (!string.IsNullOrWhiteSpace(appearance.LocationPath) || !string.IsNullOrWhiteSpace(appearance.LocationName))
        {
            lines.Add($"Location: {appearance.LocationPath ?? appearance.LocationName}");
        }
        if (!string.IsNullOrWhiteSpace(appearance.EntryLocationName))
        {
            lines.Add($"Entry: {appearance.EntryLocationName}");
        }
        if (!string.IsNullOrWhiteSpace(appearance.ExitLocationName))
        {
            lines.Add($"Exit: {appearance.ExitLocationName}");
        }
        if (!string.IsNullOrWhiteSpace(appearance.EmotionalState))
        {
            lines.Add($"Emotional state: {appearance.EmotionalState}");
        }
        if (!string.IsNullOrWhiteSpace(appearance.PhysicalCondition))
        {
            lines.Add($"Physical condition: {appearance.PhysicalCondition}");
        }
        if (!string.IsNullOrWhiteSpace(appearance.KnowledgeNotes))
        {
            lines.Add($"Knowledge: {appearance.KnowledgeNotes}");
        }
        if (!string.IsNullOrWhiteSpace(appearance.AppearanceNotes))
        {
            lines.Add($"Summary: {appearance.AppearanceNotes}");
        }
        foreach (var warning in characterWarnings.Take(2))
        {
            lines.Add($"Continuity: {warning.WarningTypeName} - {(string.IsNullOrWhiteSpace(warning.Details) ? warning.Message : warning.Details)}");
        }
        return string.Join(Environment.NewLine, lines);
    }
    bool HasLeadSceneRole(CharacterLaneViewModel lane) => lane.SceneSlots
        .SelectMany(slot => slot.Appearances)
        .Any(appearance =>
            string.Equals(appearance.RoleInSceneTypeName, "POV Character", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(appearance.RoleInSceneTypeName, "Main Participant", StringComparison.OrdinalIgnoreCase));
    string CharacterGroupName(CharacterLaneViewModel lane)
    {
        var description = lane.Character.DefaultDescription ?? string.Empty;

        // Timeline grouping prefers the editable CharacterImportance field; imported
        // classification metadata remains a fallback for older imported projects.
        if (lane.Character.CharacterImportance >= 8)
        {
            return "Major characters";
        }

        if (lane.Character.CharacterImportance >= 4)
        {
            return "Secondary / supporting characters";
        }

        if (lane.Character.CharacterImportance.HasValue)
        {
            return "Minor / mentioned characters";
        }

        if (description.Contains("Imported character classification: Major", StringComparison.OrdinalIgnoreCase))
        {
            return "Major characters";
        }

        if (description.Contains("Imported character classification: Supporting", StringComparison.OrdinalIgnoreCase))
        {
            return "Secondary / supporting characters";
        }

        if (description.Contains("Imported character classification: Minor", StringComparison.OrdinalIgnoreCase))
        {
            return "Minor / mentioned characters";
        }

        if (HasLeadSceneRole(lane) || CharacterAppearanceCount(lane) >= 6)
        {
            return "Major characters";
        }

        return CharacterAppearanceCount(lane) <= 1 ? "Minor / mentioned characters" : "Secondary / supporting characters";
    }
    int CharacterGroupOrder(string groupName) => groupName switch
    {
        "Major characters" => 0,
        "Secondary / supporting characters" => 1,
        _ => 2
    };
}

<nav class="breadcrumb-trail" aria-label="Breadcrumb">
    <a asp-controller="Projects" asp-action="Index">Projects</a>
    <a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
    @if (selectedBook is not null)
    {
        <span>@selectedBook.BookDisplayTitle</span>
    }
    <span>Timeline</span>
</nav>

<partial name="_ProjectSectionNav" model="Model.Project" />

<div class="workspace-heading">
    <div>
        <p class="eyebrow">Story timeline</p>
        <h1>@Model.Project.ProjectName <help-icon key="timeline.overview" /></h1>
    </div>
    <form method="get" class="timeline-filter">
        <input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
        <select name="BookID" asp-items="Model.BookOptions" class="form-select" aria-label="Book filter">
            <option value="">All books</option>
        </select>
        <help-icon key="timeline.fields.bookFilter" mode="inline" />
        <button class="btn btn-outline-primary" type="submit">View</button>
    </form>
    <div class="button-row">
        <form asp-controller="Warnings" asp-action="ValidateProject" method="post">
            <input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
            <input type="hidden" name="bookId" value="@Model.SelectedBookID" />
            <button class="btn btn-outline-primary" type="submit">Validate project</button>
        </form>
        @if (Model.SelectedBookID.HasValue)
        {
            <form asp-controller="Warnings" asp-action="ValidateBook" method="post">
                <input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
                <input type="hidden" name="bookId" value="@Model.SelectedBookID" />
                <button class="btn btn-outline-primary" type="submit">Validate book</button>
            </form>
        }
        <a class="btn btn-outline-secondary" asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Warnings</a>
        <a class="btn btn-outline-secondary" asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Story health</a>
        <a class="btn btn-outline-secondary" asp-controller="Timeline" asp-action="Settings" asp-route-projectId="@Model.Project.ProjectID">Timeline Settings</a>
        <a class="btn btn-outline-primary" asp-controller="Scenarios" asp-action="Create" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Create Scenario</a>
        <a class="btn btn-outline-secondary" asp-controller="Archives" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Archived Items</a>
    </div>
</div>

<section class="timeline-control-panel" data-timeline-controls>
    <details class="timeline-filter-drawer">
        <summary>Filters and focus</summary>
        <help-icon key="timeline.filters" mode="inline" />
        <form method="get" class="timeline-filter-grid">
            <input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
            <div>
                <label class="form-label">Book <help-icon key="timeline.fields.bookFilter" mode="inline" /></label>
                <select name="BookID" asp-items="Model.BookOptions" class="form-select form-select-sm">
                    <option value="">All books</option>
                </select>
            </div>
            <div>
                <label class="form-label">From chapter <help-icon key="timeline.fields.chapterRange" mode="inline" /></label>
                <select name="ChapterFromID" asp-items="Model.ChapterFromOptions" class="form-select form-select-sm">
                    <option value="">Start</option>
                </select>
            </div>
            <div>
                <label class="form-label">To chapter <help-icon key="timeline.fields.chapterRange" mode="inline" /></label>
                <select name="ChapterToID" asp-items="Model.ChapterToOptions" class="form-select form-select-sm">
                    <option value="">End</option>
                </select>
            </div>
            <div>
                <label class="form-label">From scene <help-icon key="timeline.fields.sceneRange" mode="inline" /></label>
                <select name="SceneFromID" asp-items="Model.SceneFromOptions" class="form-select form-select-sm">
                    <option value="">Start</option>
                </select>
            </div>
            <div>
                <label class="form-label">To scene <help-icon key="timeline.fields.sceneRange" mode="inline" /></label>
                <select name="SceneToID" asp-items="Model.SceneToOptions" class="form-select form-select-sm">
                    <option value="">End</option>
                </select>
            </div>
            <div>
                <label class="form-label">Plot line <help-icon key="timeline.plotThreadLanes" mode="inline" /></label>
                <select name="PlotLineID" asp-items="Model.PlotLineOptions" class="form-select form-select-sm"><option value="">Any</option></select>
            </div>
            <div>
                <label class="form-label">Plot thread <help-icon key="timeline.plotThreadLanes" mode="inline" /></label>
                <select name="PlotThreadID" asp-items="Model.PlotThreadOptions" class="form-select form-select-sm"><option value="">Any</option></select>
            </div>
            <div>
                <label class="form-label">Story asset <help-icon key="timeline.assetEvents" mode="inline" /></label>
                <select name="StoryAssetID" asp-items="Model.AssetOptions" class="form-select form-select-sm"><option value="">Any</option></select>
            </div>
            <div>
                <label class="form-label">Character <help-icon key="timeline.characterAppearances" mode="inline" /></label>
                <select name="CharacterID" asp-items="Model.CharacterOptions" class="form-select form-select-sm"><option value="">Any</option></select>
            </div>
            <div>
                <label class="form-label">Location <help-icon key="timeline.fields.locationFilter" mode="inline" /></label>
                <select name="LocationID" asp-items="Model.LocationOptions" class="form-select form-select-sm"><option value="">Any</option></select>
            </div>
            <div>
                <label class="form-label">Warning severity <help-icon key="timeline.fields.warningSeverity" mode="inline" /></label>
                <select name="WarningSeverityID" asp-items="Model.WarningSeverityOptions" class="form-select form-select-sm"><option value="">Any</option></select>
            </div>
            <div>
                <label class="form-label">Warning type <help-icon key="timeline.fields.warningType" mode="inline" /></label>
                <select name="WarningTypeID" asp-items="Model.WarningTypeOptions" class="form-select form-select-sm"><option value="">Any</option></select>
            </div>
            <div>
                <label class="form-label">Revision status <help-icon key="timeline.fields.revisionStatus" mode="inline" /></label>
                <select name="RevisionStatusID" asp-items="Model.RevisionStatusOptions" class="form-select form-select-sm"><option value="">Any</option></select>
            </div>
            <div>
                <label class="form-label">Scene purpose <help-icon key="timeline.fields.scenePurpose" mode="inline" /></label>
                <select name="ScenePurposeTypeID" asp-items="Model.ScenePurposeOptions" class="form-select form-select-sm"><option value="">Any</option></select>
            </div>
            <div>
                <label class="form-label">POV character <help-icon key="timeline.fields.povCharacter" mode="inline" /></label>
                <select name="POVCharacterID" asp-items="Model.PovCharacterOptions" class="form-select form-select-sm"><option value="">Any</option></select>
            </div>
            <div>
                <label class="form-label">Focus type <help-icon key="timeline.fields.focusType" mode="inline" /></label>
                <select name="FocusType" class="form-select form-select-sm">
                    <option value="">No focus</option>
                    <option value="character" selected="@(Model.Filter.FocusType == "character")">Character</option>
                    <option value="asset" selected="@(Model.Filter.FocusType == "asset")">Story asset</option>
                    <option value="plotline" selected="@(Model.Filter.FocusType == "plotline")">Plot line</option>
                    <option value="plotthread" selected="@(Model.Filter.FocusType == "plotthread")">Plot thread</option>
                    <option value="location" selected="@(Model.Filter.FocusType == "location")">Location</option>
                    <option value="scene" selected="@(Model.Filter.FocusType == "scene")">Scene</option>
                </select>
            </div>
            <div>
                <label class="form-label">Focus ID <help-icon key="timeline.fields.focusId" mode="inline" /></label>
                <input name="FocusID" value="@Model.Filter.FocusID" class="form-control form-control-sm" placeholder="Entity ID" />
            </div>
            <div class="timeline-filter-checks">
                <label><input type="checkbox" name="ShowWarningsOnly" value="true" checked="@Model.Filter.ShowWarningsOnly" /> Warnings only <help-icon key="timeline.fields.warningOptions" mode="inline" /></label>
                <label><input type="checkbox" name="IncludeDismissedWarnings" value="true" checked="@Model.Filter.IncludeDismissedWarnings" /> Include dismissed <help-icon key="timeline.fields.warningOptions" mode="inline" /></label>
                <label><input type="checkbox" name="IncludeIntentionalWarnings" value="true" checked="@Model.Filter.IncludeIntentionalWarnings" /> Include intentional <help-icon key="timeline.fields.warningOptions" mode="inline" /></label>
            </div>
            <div class="timeline-filter-actions">
                <button class="btn btn-outline-primary btn-sm" type="submit">Apply filters</button>
                <a class="btn btn-outline-secondary btn-sm" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Clear filters</a>
            </div>
        </form>
    </details>

    <details class="timeline-control-group">
        <summary>View</summary>
        <div class="timeline-visibility-panel">
        <label class="timeline-control-select">Density <help-icon key="timeline.fields.density" mode="inline" />
            <select class="form-select form-select-sm" data-timeline-density>
                <option value="compact">Compact</option>
                <option value="comfortable">Comfortable</option>
                <option value="expanded">Expanded</option>
            </select>
        </label>
        <label class="timeline-control-select">Zoom <help-icon key="timeline.fields.zoom" mode="inline" />
            <select class="form-select form-select-sm" data-timeline-zoom>
                <option value="book">Book</option>
                <option value="chapter">Chapter</option>
                <option value="scene-detail">Scene Detail</option>
                <option value="scene-overview">Scene Overview</option>
            </select>
        </label>
        @if (Model.Settings.ShowSceneCards) { <label><input type="checkbox" data-timeline-toggle="scene-cards" checked /> Scene cards</label> }
        @if (Model.Settings.ShowPlotLines) { <label><input type="checkbox" data-timeline-toggle="plot-lines" checked /> Plot lines</label> }
        @if (Model.Settings.ShowStoryAssets) { <label><input type="checkbox" data-timeline-toggle="assets" checked /> Story assets</label> }
        @if (Model.Settings.ShowCharacterAppearances) { <label><input type="checkbox" data-timeline-toggle="characters" checked /> Characters</label> }
        @if (Model.Settings.ShowWarnings) { <label><input type="checkbox" data-timeline-toggle="warnings" checked /> Warnings</label> }
        @if (Model.Settings.ShowMetricShape) { <label><input type="checkbox" data-timeline-toggle="metrics" checked /> Metrics</label> }
        </div>
    </details>

    <details class="timeline-control-group">
        <summary>Jump</summary>
        <div class="timeline-jump-panel">
        <input class="form-control form-control-sm" type="search" placeholder="Search timeline..." data-timeline-search />
        <help-icon key="timeline.fields.jumpControls" mode="inline" />
        <select class="form-select form-select-sm" data-timeline-jump>
            <option value="">Jump to scene</option>
            @foreach (var book in Model.Books)
            {
                @foreach (var chapter in book.Chapters)
                {
                    @foreach (var scene in chapter.Scenes)
                    {
                        <option value="@scene.SceneID">Ch @chapter.Chapter.ChapterNumber / Scene @scene.SceneNumber: @scene.SceneTitle</option>
                    }
                }
            }
        </select>
        <select class="form-select form-select-sm" data-timeline-warning-jump>
            <option value="">Jump to warning</option>
            @foreach (var scene in warningScenes)
            {
                <option value="@scene.SceneID">@scene.WarningCount warning@(scene.WarningCount == 1 ? "" : "s") - Scene @scene.SceneNumber: @scene.SceneTitle</option>
            }
        </select>
        <select class="form-select form-select-sm" data-timeline-entity-jump>
            <option value="">Jump to lane item</option>
            @foreach (var lane in Model.PlotLanes)
            {
                var firstScene = lane.SceneSlots.FirstOrDefault(x => x.Events.Any())?.Scene;
                if (firstScene is not null)
                {
                    <option value="@firstScene.SceneID">Plot line: @lane.PlotLine.PlotLineName</option>
                }
            }
            @foreach (var lane in Model.AssetLanes)
            {
                var firstScene = lane.SceneSlots.FirstOrDefault(x => x.Events.Any())?.Scene;
                if (firstScene is not null)
                {
                    <option value="@firstScene.SceneID">Asset: @lane.Asset.AssetName</option>
                }
            }
            @foreach (var lane in Model.CharacterLanes)
            {
                var firstScene = lane.SceneSlots.FirstOrDefault(x => x.Appearances.Any())?.Scene;
                if (firstScene is not null)
                {
                    <option value="@firstScene.SceneID">Character: @lane.Character.CharacterName</option>
                }
            }
            @foreach (var locationScene in Model.OrderedScenes.Where(x => !string.IsNullOrWhiteSpace(x.PrimaryLocationName)).GroupBy(x => x.PrimaryLocationName).Select(x => x.First()))
            {
                <option value="@locationScene.SceneID">Location: @locationScene.PrimaryLocationName</option>
            }
        </select>
        <span class="timeline-shortcut-hint">Shortcuts: Shift + scroll moves sideways, arrows scroll, +/- zoom, Esc clears focus</span>
        </div>
    </details>

    <details class="timeline-control-group">
        <summary>Saved Views</summary>
        <help-icon key="timeline.savedViews" mode="inline" />
        <div class="timeline-preset-panel">
        <form asp-action="SavePreset" method="post" data-timeline-preset-form>
            <input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
            <input type="hidden" name="BookID" value="@Model.SelectedBookID" />
            <input type="hidden" name="VisibilityJson" value="{}" data-visibility-json />
            <input type="hidden" name="Filter.ProjectID" value="@Model.Project.ProjectID" />
            <input type="hidden" name="Filter.BookID" value="@Model.Filter.BookID" />
            <input type="hidden" name="Filter.SelectedSceneID" value="@Model.Filter.SelectedSceneID" />
            <input type="hidden" name="Filter.ChapterFromID" value="@Model.Filter.ChapterFromID" />
            <input type="hidden" name="Filter.ChapterToID" value="@Model.Filter.ChapterToID" />
            <input type="hidden" name="Filter.SceneFromID" value="@Model.Filter.SceneFromID" />
            <input type="hidden" name="Filter.SceneToID" value="@Model.Filter.SceneToID" />
            <input type="hidden" name="Filter.PlotLineID" value="@Model.Filter.PlotLineID" />
            <input type="hidden" name="Filter.PlotThreadID" value="@Model.Filter.PlotThreadID" />
            <input type="hidden" name="Filter.StoryAssetID" value="@Model.Filter.StoryAssetID" />
            <input type="hidden" name="Filter.CharacterID" value="@Model.Filter.CharacterID" />
            <input type="hidden" name="Filter.LocationID" value="@Model.Filter.LocationID" />
            <input type="hidden" name="Filter.WarningSeverityID" value="@Model.Filter.WarningSeverityID" />
            <input type="hidden" name="Filter.WarningTypeID" value="@Model.Filter.WarningTypeID" />
            <input type="hidden" name="Filter.RevisionStatusID" value="@Model.Filter.RevisionStatusID" />
            <input type="hidden" name="Filter.ScenePurposeTypeID" value="@Model.Filter.ScenePurposeTypeID" />
            <input type="hidden" name="Filter.POVCharacterID" value="@Model.Filter.POVCharacterID" />
            <input type="hidden" name="Filter.FocusType" value="@Model.Filter.FocusType" />
            <input type="hidden" name="Filter.FocusID" value="@Model.Filter.FocusID" />
            <input type="hidden" name="Filter.ShowWarningsOnly" value="@Model.Filter.ShowWarningsOnly.ToString().ToLowerInvariant()" />
            <input type="hidden" name="Filter.IncludeDismissedWarnings" value="@Model.Filter.IncludeDismissedWarnings.ToString().ToLowerInvariant()" />
            <input type="hidden" name="Filter.IncludeIntentionalWarnings" value="@Model.Filter.IncludeIntentionalWarnings.ToString().ToLowerInvariant()" />
            <label class="visually-hidden" for="TimelinePresetName">View name <help-icon key="timeline.fields.savedViewName" mode="inline" /></label>
            <input class="form-control form-control-sm" id="TimelinePresetName" name="PresetName" placeholder="View name" />
            <button class="btn btn-outline-primary btn-sm" type="submit">Save view</button>
        </form>
        @if (Model.ViewPresets.Any())
        {
            <div class="preset-chip-row">
                @foreach (var preset in Model.ViewPresets)
                {
                    <span class="preset-chip">
                        <a asp-action="LoadPreset" asp-route-id="@preset.TimelineViewPresetID">@preset.PresetName</a>
                        <form asp-action="DeletePreset" method="post">
                            <input type="hidden" name="id" value="@preset.TimelineViewPresetID" />
                            <input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
                            <input type="hidden" name="bookId" value="@Model.SelectedBookID" />
                            <button type="submit" title="Delete view">x</button>
                        </form>
                    </span>
                }
            </div>
        }
        </div>
    </details>
</section>

@if (!string.IsNullOrWhiteSpace(Model.FocusLabel))
{
    <section class="timeline-focus-banner">
        <strong>Focus: @Model.FocusLabel</strong>
        <span>@Model.FocusSceneIDs.Count matching scene@(Model.FocusSceneIDs.Count == 1 ? "" : "s")</span>
        <a class="btn btn-outline-secondary btn-sm" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Clear focus</a>
    </section>
}

@if (TempData["TimelineDragMessage"] is string dragMessage)
{
    <section class="timeline-focus-banner drag-message">
        <strong>@dragMessage</strong>
        <a class="btn btn-outline-secondary btn-sm" asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Review warnings</a>
    </section>
}

<details class="timeline-drag-palette" data-drag-palette>
    <summary>Drag Sources</summary>
    <div class="drag-chip-group">
        @if (!Model.QuickAddCharacters.Any() && !Model.QuickAddAssets.Any() && !Model.QuickAddLocations.Any())
        {
            <span class="muted">No Quick Add items yet. Tick “Show in Quick Add Bar” on characters, story assets or locations to add shortcuts here.</span>
        }
        @foreach (var character in Model.QuickAddCharacters)
        {
            <span class="drag-chip character" draggable="true" data-drag-type="character" data-drag-id="@character.CharacterID" data-drag-label="@character.CharacterName">
                <partial name="_Avatar" model="@(new AvatarViewModel { DisplayName = character.CharacterName, ImagePath = character.AvatarImagePath ?? character.ImagePath, ThumbnailPath = character.AvatarThumbnailPath ?? character.ThumbnailPath, Size = "sm" })" />
                @character.CharacterName
            </span>
        }
        @foreach (var asset in Model.QuickAddAssets)
        {
            <span class="drag-chip asset" draggable="true" data-drag-type="asset" data-drag-id="@asset.StoryAssetID" data-drag-label="@asset.AssetName">@asset.AssetName</span>
        }
        @foreach (var location in Model.QuickAddLocations)
        {
            <span class="drag-chip location" draggable="true" data-drag-type="location" data-drag-id="@location.LocationID" data-drag-label="@location.LocationName" title="@location.LocationPath">@location.LocationName</span>
        }
    </div>
</details>

@if (Model.Books.Any())
{
    <section class="timeline-minimap" data-timeline-minimap>
        @foreach (var book in Model.Books)
        {
            @foreach (var chapter in book.Chapters)
            {
                var chapterWarnings = chapter.Scenes.Sum(x => x.WarningCount);
                var isActiveChapter = chapter.Scenes.Any(scene => scene.SceneID == Model.SelectedSceneID);
                <button type="button"
                        class="@(isActiveChapter ? "is-active" : "")"
                        data-active-chapter="@(isActiveChapter ? "true" : "false")"
                        data-jump-scene="@chapter.Scenes.FirstOrDefault()?.SceneID"
                        style="--chapter-scenes:@Math.Max(chapter.Scenes.Count, 1); --chapter-warnings:@Math.Min(chapterWarnings, 8)"
                        title="Chapter @chapter.Chapter.ChapterNumber: @chapter.Chapter.ChapterTitle (@chapter.Scenes.Count scenes, @chapterWarnings warnings)">
                    <span>Ch @chapter.Chapter.ChapterNumber</span>
                </button>
            }
        }
    </section>
}

<section class="drag-drop-panel d-none" data-drag-drop-panel>
    <div class="drag-drop-panel-header">
        <div>
            <p class="eyebrow" data-drag-panel-kicker>Drag action</p>
            <h2 data-drag-panel-title>Confirm action</h2>
        </div>
        <button type="button" class="btn btn-outline-secondary btn-sm" data-drag-cancel>Cancel</button>
    </div>
    <p class="muted" data-drag-panel-summary></p>

    <form asp-controller="Timeline" asp-action="DropCharacter" method="post" class="drag-drop-form d-none" data-drag-form="character">
        <input type="hidden" name="SceneID" />
        <input type="hidden" name="CharacterID" />
        <input type="hidden" name="ReturnProjectID" value="@Model.Project.ProjectID" />
        <input type="hidden" name="ReturnBookID" value="@Model.SelectedBookID" />
        <div class="row g-2">
            <div class="col-6"><label class="form-label">Role in scene <help-icon key="timeline.fields.dragCharacterRole" mode="inline" /></label><select class="form-select form-select-sm" name="RoleInSceneTypeID" asp-items="Model.CharacterRoleOptions"><option value="">Not set</option></select></div>
            <div class="col-6"><label class="form-label">Presence type <help-icon key="timeline.fields.dragCharacterRole" mode="inline" /></label><select class="form-select form-select-sm" name="PresenceTypeID" asp-items="Model.PresenceTypeOptions"><option value="">Not set</option></select></div>
            <div class="col-12"><label class="form-label">Current location <help-icon key="sceneInspector.fields.characterLocation" mode="inline" /></label><select class="form-select form-select-sm" name="LocationID" asp-items="Model.LocationOptions"><option value="">Optional</option></select></div>
        </div>
        <div class="button-row mt-2">
            <button class="btn btn-primary btn-sm" type="submit">Add Character</button>
            <button class="btn btn-outline-secondary btn-sm" type="button" data-drag-cancel>Cancel</button>
        </div>
    </form>

    <form asp-controller="Timeline" asp-action="DropAssetEvent" method="post" class="drag-drop-form d-none" data-drag-form="asset-event">
        <input type="hidden" name="SceneID" />
        <input type="hidden" name="StoryAssetID" />
        <input type="hidden" name="ReturnProjectID" value="@Model.Project.ProjectID" />
        <input type="hidden" name="ReturnBookID" value="@Model.SelectedBookID" />
        <div class="row g-2">
            <div class="col-6"><label class="form-label">Event type <help-icon key="timeline.fields.dragAssetEvent" mode="inline" /></label><select class="form-select form-select-sm" name="AssetEventTypeID" asp-items="Model.AssetEventTypeOptions"></select></div>
            <div class="col-6"><label class="form-label">State transition <help-icon key="timeline.fields.dragAssetEvent" mode="inline" /></label><select class="form-select form-select-sm" name="ToStateID" asp-items="Model.AssetStateOptions"><option value="">Optional</option></select></div>
            <div class="col-12"><label class="form-label">Location <help-icon key="sceneInspector.fields.assetLocation" mode="inline" /></label><select class="form-select form-select-sm" name="LocationID" asp-items="Model.LocationOptions"><option value="">Optional</option></select></div>
            <div class="col-12"><label class="form-label">Event title</label><input class="form-control form-control-sm" name="EventTitle" /></div>
            <div class="col-12"><label class="form-label">Custody note</label><textarea class="form-control form-control-sm" name="CustodyNote" rows="2"></textarea></div>
        </div>
        <div class="button-row mt-2">
            <button class="btn btn-primary btn-sm" type="submit">Add Asset Event</button>
            <button class="btn btn-outline-secondary btn-sm" type="button" data-drag-cancel>Cancel</button>
        </div>
    </form>

    <form asp-controller="Timeline" asp-action="DropAssetDependency" method="post" class="drag-drop-form d-none" data-drag-form="asset-dependency">
        <input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
        <input type="hidden" name="ReturnProjectID" value="@Model.Project.ProjectID" />
        <input type="hidden" name="ReturnBookID" value="@Model.SelectedBookID" />
        <input type="hidden" name="SourceAssetID" />
        <input type="hidden" name="TargetAssetID" />
        <div class="row g-2">
            <div class="col-12"><label class="form-label">Dependency type <help-icon key="timeline.fields.dragAssetDependency" mode="inline" /></label><select class="form-select form-select-sm" name="AssetDependencyTypeID" asp-items="Model.AssetDependencyTypeOptions"></select></div>
            <div class="col-6"><label class="form-label">Source required state <help-icon key="timeline.fields.dragAssetDependency" mode="inline" /></label><select class="form-select form-select-sm" name="SourceRequiredStateID" asp-items="Model.AssetStateOptions"><option value="">Optional</option></select></div>
            <div class="col-6"><label class="form-label">Target blocked state <help-icon key="timeline.fields.dragAssetDependency" mode="inline" /></label><select class="form-select form-select-sm" name="TargetBlockedStateID" asp-items="Model.AssetStateOptions"><option value="">Optional</option></select></div>
            <div class="col-12"><label class="form-label">Notes</label><textarea class="form-control form-control-sm" name="Description" rows="2"></textarea></div>
        </div>
        <p class="plain-preview" data-asset-dependency-preview></p>
        <div class="button-row mt-2">
            <button class="btn btn-primary btn-sm" type="submit">Save Dependency</button>
            <button class="btn btn-outline-secondary btn-sm" type="button" data-drag-cancel>Cancel</button>
        </div>
    </form>

    <form asp-controller="Timeline" asp-action="DropLocationRelationship" method="post" class="drag-drop-form d-none" data-drag-form="location-relationship">
        <input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
        <input type="hidden" name="ReturnBookID" value="@Model.SelectedBookID" />
        <input type="hidden" name="FromLocationID" />
        <input type="hidden" name="ToLocationID" />
        <div class="row g-2">
            <div class="col-12"><label class="form-label">Relationship type <help-icon key="timeline.fields.dragLocationRelationship" mode="inline" /></label><select class="form-select form-select-sm" name="LocationRelationshipTypeID" asp-items="Model.LocationRelationshipTypeOptions"></select></div>
            <div class="col-12 drag-check-grid">
                <label><input type="checkbox" name="IsBidirectional" value="true" /> Bidirectional</label>
                <label><input type="checkbox" name="CanHearNormalSpeech" value="true" /> Can hear speech</label>
                <label><input type="checkbox" name="CanHearLoudNoise" value="true" /> Can hear loud noise</label>
                <label><input type="checkbox" name="CanSeeMovement" value="true" /> Can see movement</label>
                <label><input type="checkbox" name="CanSeeClearly" value="true" /> Can see clearly</label>
                <label><input type="checkbox" name="CanTravelDirectly" value="true" /> Can travel directly</label>
            </div>
            <div class="col-12"><label class="form-label">Notes</label><textarea class="form-control form-control-sm" name="Notes" rows="2"></textarea></div>
        </div>
        <div class="button-row mt-2">
            <button class="btn btn-primary btn-sm" type="submit">Save</button>
            <button class="btn btn-outline-secondary btn-sm" type="button" data-drag-cancel>Cancel</button>
        </div>
    </form>
</section>

<div class="modal fade plotline-modal" id="sceneMoveModal" tabindex="-1" aria-labelledby="sceneMoveModalTitle" aria-hidden="true" data-scene-move-modal>
    <div class="modal-dialog modal-dialog-centered">
        <div class="modal-content plotline-confirm-modal scene-move-modal-content">
            <form asp-controller="Timeline" asp-action="DropMoveScene" method="post" data-drag-form="scene">
                <div class="modal-header">
                    <div>
                        <p class="eyebrow mb-1">Scene move</p>
                        <h2 class="modal-title" id="sceneMoveModalTitle" data-scene-move-title>Move Scene?</h2>
                    </div>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cancel move"></button>
                </div>
                <div class="modal-body">
                    <input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
                    <input type="hidden" name="BookID" value="@Model.SelectedBookID" />
                    <input type="hidden" name="SceneID" />
                    <input type="hidden" name="AnchorSceneID" />
                    <input type="hidden" name="TargetChapterID" />
                    <input type="hidden" name="Position" />
                    <input type="hidden" name="ReturnSelectedSceneID" value="@Model.SelectedSceneID" />
                    <p class="scene-move-summary" data-scene-move-summary></p>
                    <div class="focused-validation-summary">
                        <span>After moving, PlotDirector will run continuity validation.</span>
                        <span>Existing links, events, metrics, purposes, characters, assets, warnings and dependencies stay attached to the same SceneID.</span>
                    </div>
                </div>
                <div class="modal-footer">
                    <button class="btn btn-primary" type="submit" data-scene-move-submit>Move Scene</button>
                    <button class="btn btn-outline-secondary" type="button" data-bs-dismiss="modal">Cancel</button>
                </div>
            </form>
        </div>
    </div>
</div>

@if (!Model.Books.Any())
{
    <section class="empty-panel">
        <h2>No timeline yet</h2>
        <p>Add a book, then chapters and scenes, and PlotDirector will arrange them here as your narrative spine.</p>
    </section>
}
else
{
    <div class="workspace-layout timeline-filtered-root @(hasSelectedScene ? "inspector-drawer-open" : "inspector-drawer-closed")" data-timeline-root>
        <main class="timeline-workspace">
            <section class="timeline-navigation-card" data-timeline-navigation-card hidden>
                <div class="timeline-navigation-card-icon" aria-hidden="true">&harr;</div>
                <div class="timeline-navigation-card-copy">
                    <h2>Your timeline continues off-screen</h2>
                    <p>Use Jump to move straight to another chapter.</p>
                    <p>To browse nearby scenes sideways, hold Shift while using your mouse wheel.</p>
                    <p>On a trackpad, hold Shift and use a two-finger scroll gesture.</p>
                </div>
                <div class="timeline-navigation-card-actions">
                    <button type="button" class="btn btn-primary btn-sm" data-timeline-navigation-demo aria-label="Show timeline horizontal scrolling demonstration">Show Me</button>
                    <button type="button" class="btn btn-outline-secondary btn-sm" data-timeline-navigation-dismiss aria-label="Got it, do not show timeline navigation guidance again">Got It</button>
                </div>
            </section>
            <section class="timeline-shell workspace-timeline" data-timeline-section="scene-cards" style="--scene-count:@timelineColumns.Count">
                @if (Model.Settings.ShowSceneCards)
                {
                <div class="timeline-section-header" data-collapse-target="scene-cards">
                    <button type="button" data-collapse-toggle="scene-cards">Scene Cards</button>
                    <help-icon key="timeline.sceneCards" mode="inline" />
                    <span>@Model.OrderedScenes.Count scene@(Model.OrderedScenes.Count == 1 ? "" : "s")</span>
                </div>
                <div data-collapse-body="scene-cards">
                <div class="timeline-series-row">
                    <div class="timeline-project-title">
                        <span>Project / Series</span>
                        <strong>@Model.Project.ProjectName</strong>
                    </div>
                    <div class="timeline-books">
                    @foreach (var book in Model.Books)
                    {
                        <div class="timeline-book" style="--book-scene-count:@BookSceneColumns(book)">
                            <div class="timeline-book-title">
                                <span>Book @book.Book.BookNumber</span>
                                <strong>@book.Book.BookDisplayTitle</strong>
                            </div>
                            <div class="timeline-chapters">
                            @if (!book.Chapters.Any())
                            {
                                <div class="timeline-chapter" style="--chapter-scene-count:1">
                                    <div class="timeline-chapter-title">
                                        <span>No chapters</span>
                                        <strong>Start planning</strong>
                                    </div>
                                    <div class="timeline-scenes">
                                        <div class="timeline-empty-scene timeline-placeholder-scene is-placeholder">No scenes</div>
                                    </div>
                                </div>
                            }
                            else
                            {
                                @foreach (var chapter in book.Chapters)
                                {
                                    <div class="timeline-chapter"
                                         style="--chapter-scene-count:@ChapterSceneColumns(chapter)"
                                         data-chapter-drop-target="true"
                                         data-chapter-id="@chapter.Chapter.ChapterID"
                                         data-chapter-label="Chapter @chapter.Chapter.ChapterNumber: @chapter.Chapter.ChapterTitle">
                                        <div class="timeline-chapter-title">
                                            <span>Chapter @chapter.Chapter.ChapterNumber</span>
                                            <strong>@chapter.Chapter.ChapterTitle</strong>
                                        </div>
                                        <div class="timeline-scenes"
                                             data-chapter-drop-target="true"
                                             data-chapter-id="@chapter.Chapter.ChapterID"
                                             data-chapter-label="Chapter @chapter.Chapter.ChapterNumber: @chapter.Chapter.ChapterTitle">
                                            @if (!chapter.Scenes.Any())
                                            {
                                                <div class="timeline-empty-scene timeline-placeholder-scene is-placeholder" data-empty-chapter-drop-target="true">Drop a scene here</div>
                                            }
                                            else
                                            {
                                                @foreach (var scene in chapter.Scenes)
                                                {
                                                    var selectedClass = scene.SceneID == Model.SelectedSceneID ? "selected" : "";
                                                    var matchClass = Model.MatchingSceneIDs.Contains(scene.SceneID) ? "filter-match" : "filter-dim";
                                                    var focusClass = Model.FocusSceneIDs.Contains(scene.SceneID) ? "focus-match" : (Model.FocusSceneIDs.Any() ? "focus-dim" : "");
                                                    var overview = Model.SceneOverviews.GetValueOrDefault(scene.SceneID) ?? new TimelineSceneOverviewViewModel();
                                                    var povLabel = Model.ScenePovLabels.GetValueOrDefault(scene.SceneID, scene.POVCharacterID.HasValue ? "POV set" : "No POV");
                                                    var metricSummary = string.Join(" / ", Model.MetricGraphs.Take(2).Select(graph => $"{graph.MetricName}: {graph.Points.FirstOrDefault(x => x.SceneID == scene.SceneID)?.Value ?? 0}"));
                                                    var plotSearch = string.Join(' ', Model.PlotLanes
                                                        .SelectMany(x => x.SceneSlots.Where(slot => slot.Scene.SceneID == scene.SceneID).SelectMany(slot => slot.Events.Select(evt => $"{x.PlotLine.PlotLineName} {evt.ThreadTitle} {evt.EventTitle}"))));
                                                    var assetSearch = string.Join(' ', Model.AssetLanes
                                                        .SelectMany(x => x.SceneSlots.Where(slot => slot.Scene.SceneID == scene.SceneID).SelectMany(slot => slot.Events.Select(evt => $"{x.Asset.AssetName} {evt.EventTitle}"))));
                                                    var characterSearch = string.Join(' ', Model.CharacterLanes
                                                        .SelectMany(x => x.SceneSlots.Where(slot => slot.Scene.SceneID == scene.SceneID).SelectMany(slot => slot.Appearances.Select(appearance => $"{x.Character.CharacterName} {appearance.RoleInSceneTypeName} {appearance.PresenceTypeName}"))));
                                                    var warningSearch = scene.WarningCount > 0 ? "warning warnings continuity" : "";
                                                    var searchText = $"{scene.SceneNumber} {scene.SceneTitle} {scene.Summary} {povLabel} {scene.PrimaryLocationName} {string.Join(' ', scene.PurposeLabels.Select(x => x.PurposeName))} {plotSearch} {assetSearch} {characterSearch} {warningSearch}";
                                                    <a class="timeline-scene-card @selectedClass @matchClass @focusClass"
                                                       draggable="true"
                                                       data-scene-id="@scene.SceneID"
                                                       data-drag-type="scene"
                                                       data-drag-id="@scene.SceneID"
                                                       data-drag-label="Scene @scene.SceneNumber: @scene.SceneTitle"
                                                       data-chapter-id="@chapter.Chapter.ChapterID"
                                                       data-chapter-label="Chapter @chapter.Chapter.ChapterNumber: @chapter.Chapter.ChapterTitle"
                                                       data-timeline-search-text="@searchText"
                                                       asp-controller="Timeline"
                                                       asp-action="Index"
                                                       asp-route-ProjectID="@Model.Project.ProjectID"
                                                       asp-route-BookID="@Model.SelectedBookID"
                                                       asp-route-SelectedSceneID="@scene.SceneID"
                                                       asp-route-FocusType="scene"
                                                       asp-route-FocusID="@scene.SceneID"
                                                       title="Scene @scene.SceneNumber: @scene.SceneTitle&#10;POV: @povLabel&#10;Location: @(scene.PrimaryLocationName ?? "No location")&#10;@scene.Summary">
                                                        <span class="scene-number">Scene @scene.SceneNumber</span>
                                                        <strong>@scene.SceneTitle</strong>
                                                        <span class="scene-pov-label">@povLabel</span>
                                                        <div class="scene-badge-row">
                                                            <span class="status-pill">@scene.RevisionStatusName</span>
                                                            @if (Model.Settings.ShowWarnings)
                                                            {
                                                                if (scene.WarningCount > 0)
                                                                {
                                                                    <span class="warning-placeholder active" data-warning-marker title="@WarningTooltip(scene)">
                                                                        <span aria-hidden="true">!</span>
                                                                        <span>@scene.WarningCount warning@(scene.WarningCount == 1 ? "" : "s")</span>
                                                                    </span>
                                                                }
                                                                else
                                                                {
                                                                    <span class="warning-placeholder">Warnings 0</span>
                                                                }
                                                            }
                                                            @if (scene.DependencyCount > 0)
                                                            {
                                                                <span class="dependency-placeholder">@scene.DependencyCount dep@(scene.DependencyCount == 1 ? "" : "s")</span>
                                                            }
                                                        </div>
                                                        <span class="time-label">@scene.TimeLabel</span>
                                                        @if (!string.IsNullOrWhiteSpace(scene.PrimaryLocationName))
                                                        {
                                                            <span class="location-pill" data-location-marker title="@scene.PrimaryLocationPath">@scene.PrimaryLocationName</span>
                                                        }
                                                        @if (!string.IsNullOrWhiteSpace(metricSummary))
                                                        {
                                                            <span class="metric-mini-summary">@metricSummary</span>
                                                        }
                                                        @if (scene.PurposeLabels.Any())
                                                        {
                                                            <div class="purpose-chip-row">
                                                                @foreach (var purpose in scene.PurposeLabels.Take(3))
                                                                {
                                                                    <span class="purpose-chip">@purpose.PurposeName</span>
                                                                }
                                                            </div>
                                                        }
                                                        <span class="scene-summary">@scene.Summary</span>
                                                        <div class="scene-overview-panel">
                                                            <div class="scene-overview-section scene-overview-characters">
                                                                <span class="scene-overview-label">Characters</span>
                                                                <div class="scene-overview-chip-row">
                                                                    @if (overview.Characters.Any())
                                                                    {
                                                                        foreach (var character in overview.Characters.Take(7))
                                                                        {
                                                                            <span class="scene-overview-chip @(character.IsPov ? "is-pov" : "")">@character.CharacterName@(character.IsPov ? " [POV]" : "")</span>
                                                                        }
                                                                        if (HiddenCount(overview.Characters, 7) > 0)
                                                                        {
                                                                            <span class="scene-overview-chip is-more">+@HiddenCount(overview.Characters, 7) more</span>
                                                                        }
                                                                    }
                                                                    else
                                                                    {
                                                                        <span class="scene-overview-empty">No characters linked</span>
                                                                    }
                                                                </div>
                                                            </div>
                                                            <div class="scene-overview-section scene-overview-assets">
                                                                <span class="scene-overview-label">Assets</span>
                                                                <div class="scene-overview-chip-row">
                                                                    @if (overview.Assets.Any())
                                                                    {
                                                                        foreach (var asset in overview.Assets.Take(6))
                                                                        {
                                                                            <span class="scene-overview-chip @(asset.Importance >= 8 ? "is-important" : "")">@asset.AssetName</span>
                                                                        }
                                                                        if (HiddenCount(overview.Assets, 6) > 0)
                                                                        {
                                                                            <span class="scene-overview-chip is-more">+@HiddenCount(overview.Assets, 6) more</span>
                                                                        }
                                                                    }
                                                                    else
                                                                    {
                                                                        <span class="scene-overview-empty">No assets linked</span>
                                                                    }
                                                                </div>
                                                            </div>
                                                            <div class="scene-overview-section scene-overview-locations">
                                                                <span class="scene-overview-label">Locations</span>
                                                                <div class="scene-overview-chip-row">
                                                                    @if (overview.Locations.Any())
                                                                    {
                                                                        foreach (var location in overview.Locations.Take(4))
                                                                        {
                                                                            <span class="scene-overview-chip">@location</span>
                                                                        }
                                                                        if (HiddenCount(overview.Locations, 4) > 0)
                                                                        {
                                                                            <span class="scene-overview-chip is-more">+@HiddenCount(overview.Locations, 4) more</span>
                                                                        }
                                                                    }
                                                                    else
                                                                    {
                                                                        <span class="scene-overview-empty">No locations linked</span>
                                                                    }
                                                                </div>
                                                            </div>
                                                            @if (scene.PurposeLabels.Any() || !string.IsNullOrWhiteSpace(scene.Summary))
                                                            {
                                                                <div class="scene-overview-section scene-overview-purpose">
                                                                    <span class="scene-overview-label">Purpose</span>
                                                                    @if (scene.PurposeLabels.Any())
                                                                    {
                                                                        <div class="scene-overview-chip-row">
                                                                            @foreach (var purpose in scene.PurposeLabels.Take(4))
                                                                            {
                                                                                <span class="scene-overview-chip">@purpose.PurposeName</span>
                                                                            }
                                                                        </div>
                                                                    }
                                                                    @if (!string.IsNullOrWhiteSpace(scene.Summary))
                                                                    {
                                                                        <span class="scene-overview-brief">@scene.Summary</span>
                                                                    }
                                                                </div>
                                                            }
                                                        </div>
                                                    </a>
                                                }
                                            }
                                        </div>
                                    </div>
                                }
                            }
                            </div>
                        </div>
                    }
                    </div>
                </div>
                </div>
                }
                @if (Model.Settings.ShowMetricShape)
                {
                <div class="metric-timeline-board" data-timeline-section="metrics" style="--scene-count:@timelineColumns.Count">
                    <div class="lane-board-heading metric-lane-board-heading">
                        <button type="button" data-collapse-toggle="metrics">Metric Shape</button>
                        <help-icon key="timeline.metrics" mode="inline" />
                        <small>@Model.OrderedScenes.Count scene@(Model.OrderedScenes.Count == 1 ? "" : "s")</small>
                    </div>
                    <div data-collapse-body="metrics">
                    @if (!Model.MetricGraphs.Any() || !Model.OrderedScenes.Any())
                    {
                        <div class="future-lane-placeholder">
                            Add scenes to begin seeing emotional and pacing shape.
                        </div>
                    }
                    else
                    {
                        <div class="metric-shape-chart metric-shape-timeline-chart" data-metric-shape-chart data-timeline-metric-editor data-update-url="@Url.Action("UpdateSceneMetricValue", "SceneMetrics")">
                            <div class="metric-shape-controls">
                                <p class="metric-shape-intro">Compare selected metrics across the same scene timeline. Click a point to focus that scene.</p>
                                <div class="metric-toggle-row" data-metric-toggles></div>
                                <div class="timeline-metric-edit-toolbar" data-timeline-metric-edit-toolbar>
                                    @Html.AntiForgeryToken()
                                    <span class="timeline-metric-edit-status" data-edit-metric-status>Show only one metric line to edit its curve.</span>
                                    <help-icon key="timeline.fields.metricEdit" mode="inline" />
                                </div>
                            </div>
                            <div class="metric-timeline-row">
                                <div class="metric-lane-label">
                                    <strong>Metric Shape</strong>
                                    <span>Scene pacing</span>
                                </div>
                                <div class="metric-chart-frame metric-timeline-frame">
                                    <canvas aria-label="Comparative scene metric line graph"></canvas>
                                    <p class="muted d-none" data-chart-fallback>Metric chart could not load. The raw metric rows are still available below.</p>
                                </div>
                            </div>
                            <script type="application/json">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(timelineMetricShapePayload))</script>
                        </div>
                        <details class="metric-raw-details">
                            <summary>Show raw metric rows</summary>
                            <div class="metric-graph-stack mt-2">
                                @foreach (var graph in Model.MetricGraphs)
                                {
                                    <div class="metric-graph-row" style="--scene-count:@Math.Max(timelineColumns.Count, 1)">
                                        <div class="metric-graph-label">@graph.MetricName</div>
                                        <div class="metric-graph-points" style="--scene-count:@Math.Max(timelineColumns.Count, 1)">
                                            @foreach (var column in timelineColumns)
                                            {
                                                if (column.Scene is null)
                                                {
                                                    <span class="metric-point metric-point-placeholder timeline-placeholder-cell is-placeholder" title="@column.PlaceholderLabel"><span>No value</span></span>
                                                    continue;
                                                }

                                                var point = graph.Points.FirstOrDefault(x => x.SceneID == column.Scene.SceneID);
                                                if (point is null)
                                                {
                                                    <span class="metric-point metric-point-placeholder timeline-placeholder-cell is-placeholder" title="No metric value"><span>No value</span></span>
                                                    continue;
                                                }

                                                <a asp-controller="Timeline"
                                                   asp-action="Index"
                                                   asp-route-projectId="@Model.Project.ProjectID"
                                                   asp-route-bookId="@Model.SelectedBookID"
                                                   asp-route-selectedSceneId="@point.SceneID"
                                                   class="metric-point"
                                                   style="--metric-height:@point.Percent%"
                                                   title="Scene @point.SceneNumber: @point.SceneTitle - @point.Value">
                                                    <span>@point.Value</span>
                                                </a>
                                            }
                                        </div>
                                    </div>
                                }
                            </div>
                        </details>
                    }
                    </div>
                </div>
                }
                @if (Model.Settings.ShowPlotLines)
                {
                    if (Model.PlotLanes.Any() && Model.OrderedScenes.Any())
                    {
                        <div class="plot-lane-board" data-timeline-section="plot-lines" data-plot-line-svg-board style="--scene-count:@timelineColumns.Count">
                        <svg class="plot-lane-svg-overlay" data-plot-line-svg aria-hidden="true"></svg>
                        <svg class="plot-lane-svg-overlay plot-lane-svg-health-overlay" data-plot-line-health-svg aria-hidden="true"></svg>
                        <div class="lane-board-heading plot-lane-board-heading">
                            <span class="timeline-heading-with-help">
                                <button type="button" data-collapse-toggle="plot-lines">Plot line lanes</button>
                                <help-icon key="timeline.plotThreadLanes" mode="inline" />
                                <help-icon key="timeline.threadHealth" mode="inline" />
                            </span>
                            <small>Thread event markers by scene</small>
                        </div>
                        <div data-collapse-body="plot-lines">
                        @foreach (var group in Model.PlotLanes.GroupBy(x => x.PlotLine.PlotLineTypeName).OrderBy(x => x.Key))
                        {
                            <div class="lane-group">
                                <button type="button" class="lane-group-heading" data-collapse-toggle="plot-group-@group.Key.GetHashCode()">@group.Key</button>
                                <div data-collapse-body="plot-group-@group.Key.GetHashCode()">
                        @foreach (var lane in Model.PlotLanes)
                        {
                            if (lane.PlotLine.PlotLineTypeName != group.Key) { continue; }
                            var laneFocusClass = Model.Filter.FocusType == "plotline" && Model.Filter.FocusID.HasValue && Model.Filter.FocusID != lane.PlotLine.PlotLineID ? "lane-dim" : "";
                            <div class="plot-lane-row @laneFocusClass" data-plot-line-row data-plot-line-id="@lane.PlotLine.PlotLineID" data-plot-colour="@lane.PlotLine.Colour" style="--plot-colour:@lane.PlotLine.Colour">
                                <a class="plot-lane-label" asp-controller="PlotLines" asp-action="Edit" asp-route-id="@lane.PlotLine.PlotLineID">
                                    <span class="colour-swatch" style="background:@lane.PlotLine.Colour"></span>
                                    <strong>@lane.PlotLine.PlotLineName</strong>
                                </a>
                                <div class="plot-lane-slots">
                                    @foreach (var column in timelineColumns)
                                    {
                                        if (column.Scene is not Scene realPlotScene)
                                        {
                                            <div class="plot-lane-slot timeline-placeholder-cell is-placeholder" title="@column.PlaceholderLabel"></div>
                                            continue;
                                        }

                                        var slot = lane.SceneSlots.FirstOrDefault(x => x.Scene.SceneID == realPlotScene.SceneID);
                                        <div class="plot-lane-slot" data-plot-line-slot data-scene-id="@realPlotScene.SceneID" title="Scene @realPlotScene.SceneNumber: @realPlotScene.SceneTitle">
                                            @foreach (var threadEvent in slot?.Events ?? Enumerable.Empty<ThreadEvent>())
                                            {
                                                var markerKind = PlotMarkerKind(threadEvent.PlotEventTypeName);
                                                var markerClass = markerKind == "resolve"
                                                    ? "resolved"
                                                    : markerKind == "abandon"
                                                        ? "contradicted"
                                                        : "";
                                                <a class="thread-marker thread-marker-type-@markerKind @markerClass"
                                                   data-plot-event-node
                                                   data-plot-event-type="@threadEvent.PlotEventTypeName"
                                                   data-target-plot-line-id="@threadEvent.TargetPlotLineID"
                                                   data-target-plot-line-ids="@string.Join(",", threadEvent.TargetPlotLineIDs)"
                                                   asp-controller="Timeline"
                                                   asp-action="Index"
                                                   asp-route-ProjectID="@Model.Project.ProjectID"
                                                   asp-route-BookID="@Model.SelectedBookID"
                                                   asp-route-SelectedSceneID="@realPlotScene.SceneID"
                                                   asp-route-FocusType="plotthread"
                                                   asp-route-FocusID="@threadEvent.PlotThreadID"
                                                   title="@ThreadEventTooltip(threadEvent, column.Book, column.Chapter, realPlotScene)">
                                                    <span class="thread-marker-icon" aria-hidden="true">@PlotMarkerIcon(threadEvent.PlotEventTypeName)</span>
                                                    <span class="thread-marker-text">@threadEvent.MarkerText</span>
                                                </a>
                                            }
                                        </div>
                                    }
                                </div>
                            </div>
                        }
                                </div>
                            </div>
                        }
                        </div>
                        </div>
                    }
                    else
                    {
                        <div class="future-lane-placeholder">
                            Add visible plot lines and thread events to see narrative lanes here.
                        </div>
                    }
                }
                @if (Model.Settings.ShowStoryAssets)
                {
                    if (Model.AssetLanes.Any() && Model.OrderedScenes.Any())
                    {
                        <div class="asset-lane-board" data-timeline-section="assets" style="--scene-count:@timelineColumns.Count">
                        <div class="lane-board-heading asset-lane-board-heading">
                            <button type="button" data-collapse-toggle="assets">Story asset lanes</button>
                            <help-icon key="timeline.assetEvents" mode="inline" />
                            <small>Asset event markers by scene</small>
                        </div>
                        <div data-collapse-body="assets">
                        @foreach (var group in Model.AssetLanes.GroupBy(x => x.Asset.Importance >= 8 ? "High importance" : "Other assets"))
                        {
                            <div class="lane-group">
                                <button type="button" class="lane-group-heading" data-collapse-toggle="asset-group-@group.Key.GetHashCode()">@group.Key</button>
                                <div data-collapse-body="asset-group-@group.Key.GetHashCode()">
                        @foreach (var lane in Model.AssetLanes)
                        {
                            if ((lane.Asset.Importance >= 8 ? "High importance" : "Other assets") != group.Key) { continue; }
                            var assetFocusClass = Model.Filter.FocusType == "asset" && Model.Filter.FocusID.HasValue && Model.Filter.FocusID != lane.Asset.StoryAssetID ? "lane-dim" : "";
                            <div class="asset-lane-row @assetFocusClass">
                                <a class="asset-lane-label" draggable="true" data-drag-type="asset" data-drag-id="@lane.Asset.StoryAssetID" data-drag-label="@lane.Asset.AssetName" data-asset-drop-target="true" data-asset-id="@lane.Asset.StoryAssetID" data-asset-label="@lane.Asset.AssetName" asp-controller="StoryAssets" asp-action="Details" asp-route-id="@lane.Asset.StoryAssetID">
                                    <strong>@lane.Asset.AssetName</strong>
                                    <span>@lane.Asset.KindName / @lane.Asset.Importance</span>
                                </a>
                                <div class="asset-lane-slots">
                                    @foreach (var column in timelineColumns)
                                    {
                                        if (column.Scene is not Scene realAssetScene)
                                        {
                                            <div class="asset-lane-slot timeline-placeholder-cell is-placeholder" title="@column.PlaceholderLabel"></div>
                                            continue;
                                        }

                                        var slot = lane.SceneSlots.FirstOrDefault(x => x.Scene.SceneID == realAssetScene.SceneID);
                                        <div class="asset-lane-slot" title="Scene @realAssetScene.SceneNumber: @realAssetScene.SceneTitle">
                                            @foreach (var assetEvent in slot?.Events ?? Enumerable.Empty<AssetEvent>())
                                            {
                                                var assetWarnings = EntityWarnings(realAssetScene, "StoryAsset", assetEvent.StoryAssetID);
                                                var assetMarkerTooltip = AssetMarkerTooltip(assetEvent, lane.Asset, realAssetScene, assetWarnings);
                                                var assetMarkerClass = AssetMarkerClass(assetEvent, lane.Asset, assetWarnings);
                                                <a class="asset-marker @assetMarkerClass"
                                                   asp-controller="Timeline"
                                                   asp-action="Index"
                                                   asp-route-ProjectID="@Model.Project.ProjectID"
                                                   asp-route-BookID="@Model.SelectedBookID"
                                                   asp-route-SelectedSceneID="@realAssetScene.SceneID"
                                                   asp-route-FocusType="asset"
                                                   asp-route-FocusID="@assetEvent.StoryAssetID"
                                                   title="@assetMarkerTooltip"
                                                   aria-label="@assetMarkerTooltip">
                                                    @AssetMarkerLabel(assetEvent)
                                                </a>
                                            }
                                        </div>
                                    }
                                </div>
                            </div>
                        }
                                </div>
                            </div>
                        }
                        </div>
                        </div>
                    }
                    else
                    {
                        <div class="future-lane-placeholder">
                            Add story assets and asset events to see asset lanes here.
                        </div>
                    }
                }
                @if (Model.Settings.ShowCharacterAppearances)
                {
                    if (Model.CharacterLanes.Any() && Model.OrderedScenes.Any())
                    {
                        <div class="character-lane-board" data-timeline-section="characters" style="--scene-count:@timelineColumns.Count">
                        <div class="lane-board-heading character-lane-board-heading">
                            <button type="button" data-collapse-toggle="characters">Character appearances</button>
                            <help-icon key="timeline.characterAppearances" mode="inline" />
                            <small>All project characters, grouped by role and activity</small>
                        </div>
                        <div data-collapse-body="characters">
                        @foreach (var group in Model.CharacterLanes
                            .Select((lane, index) => new { lane, index, group = CharacterGroupName(lane) })
                            .GroupBy(x => x.group)
                            .OrderBy(group => CharacterGroupOrder(group.Key)))
                        {
                            var groupId = $"character-group-{CharacterGroupOrder(group.Key)}";
                            var defaultCollapsed = CharacterGroupOrder(group.Key) > 0 ? "collapsed" : null;
                            <div class="lane-group">
                                <button type="button" class="lane-group-heading" data-collapse-toggle="@groupId" data-collapse-default="@defaultCollapsed">@group.Key <span>@group.Count()</span></button>
                                <div class="character-lane-group-body" data-collapse-body="@groupId">
                        @foreach (var laneItem in group)
                        {
                            var lane = laneItem.lane;
                            var characterFocusClass = Model.Filter.FocusType == "character" && Model.Filter.FocusID.HasValue && Model.Filter.FocusID != lane.Character.CharacterID ? "lane-dim" : "";
                            var orderedLaneAppearances = lane.SceneSlots
                                .SelectMany(slot => slot.Appearances)
                                .OrderBy(appearance => timelineColumns.FindIndex(column => column.Scene?.SceneID == appearance.SceneID))
                                .ThenBy(appearance => appearance.SceneCharacterID)
                                .ToList();
                            var firstAppearance = orderedLaneAppearances.FirstOrDefault();
                            var lastAppearance = orderedLaneAppearances.LastOrDefault();
                            <div class="character-lane-row @characterFocusClass">
                                <a class="character-lane-label" asp-controller="Characters" asp-action="Details" asp-route-id="@lane.Character.CharacterID">
                                    <partial name="_Avatar" model="@(new AvatarViewModel { DisplayName = lane.Character.CharacterName, ImagePath = lane.Character.AvatarImagePath ?? lane.Character.ImagePath, ThumbnailPath = lane.Character.AvatarThumbnailPath ?? lane.Character.ThumbnailPath, Size = "sm" })" />
                                    <span>
                                        <strong>@lane.Character.CharacterName</strong>
                                        <small>@(lane.Character.ShortName ?? "Character")</small>
                                    </span>
                                </a>
                                <div class="character-lane-slots">
                                    @foreach (var column in timelineColumns)
                                    {
                                        if (column.Scene is not Scene realCharacterScene)
                                        {
                                            <div class="character-lane-slot timeline-placeholder-cell is-placeholder" title="@column.PlaceholderLabel"></div>
                                            continue;
                                        }

                                        var slot = lane.SceneSlots.FirstOrDefault(x => x.Scene.SceneID == realCharacterScene.SceneID);
                                        <div class="character-lane-slot" title="Scene @realCharacterScene.SceneNumber: @realCharacterScene.SceneTitle">
                                            @foreach (var appearance in slot?.Appearances ?? Enumerable.Empty<SceneCharacter>())
                                            {
                                                var characterWarnings = EntityWarnings(realCharacterScene, "Character", appearance.CharacterID);
                                                var characterMarkerTooltip = CharacterMarkerTooltip(appearance, realCharacterScene, firstAppearance, lastAppearance, characterWarnings);
                                                var characterMarkerClass = CharacterMarkerClass(appearance, firstAppearance, lastAppearance, characterWarnings);
                                                <a class="character-marker @characterMarkerClass"
                                                   asp-controller="Timeline"
                                                   asp-action="Index"
                                                   asp-route-ProjectID="@Model.Project.ProjectID"
                                                   asp-route-BookID="@Model.SelectedBookID"
                                                   asp-route-SelectedSceneID="@realCharacterScene.SceneID"
                                                   asp-route-FocusType="character"
                                                   asp-route-FocusID="@appearance.CharacterID"
                                                   title="@characterMarkerTooltip"
                                                   aria-label="@characterMarkerTooltip">
                                                    @if (!string.IsNullOrWhiteSpace(appearance.CharacterThumbnailPath) || !string.IsNullOrWhiteSpace(appearance.CharacterImagePath))
                                                    {
                                                        <img src="@(appearance.CharacterThumbnailPath ?? appearance.CharacterImagePath)" alt="" loading="lazy" />
                                                    }
                                                    else
                                                    {
                                                        @CharacterMarkerLabel(appearance, firstAppearance, lastAppearance)
                                                    }
                                                </a>
                                            }
                                        </div>
                                    }
                                </div>
                            </div>
                        }
                                </div>
                            </div>
                        }
                        </div>
                        </div>
                    }
                    else
                    {
                        <div class="future-lane-placeholder">
                            Add characters to scenes to see character appearance lanes here.
                        </div>
                    }
                }
            </section>
        </main>

        @if (Model.SelectedScene is not null)
        {
            <div class="timeline-inspector-resizer" data-inspector-resizer role="separator" aria-orientation="vertical" aria-label="Resize scene inspector" tabindex="0"></div>
            <aside class="scene-inspector-panel" data-inspector-drawer>
                <div class="inspector-header">
                    <div>
                        <p class="eyebrow">Scene inspector</p>
                        <h2>@Model.SelectedScene.SceneTitle</h2>
                    </div>
                    <a class="btn btn-outline-secondary btn-sm" href="@Url.Action("Index", "Timeline", new { projectId = Model.Project.ProjectID, bookId = Model.SelectedBookID })" data-inspector-close>Close</a>
                </div>
                <div class="scene-inspector-body">
                    <partial name="~/Views/Scenes/_SceneInspector.cshtml" model="Model.SelectedScene" />
                </div>
            </aside>
        }
    </div>
}

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
