diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php
index 3336e17e988..7005f8fcf83 100644
--- a/app/Activity/CommentRepo.php
+++ b/app/Activity/CommentRepo.php
@@ -4,6 +4,8 @@
 
 use BookStack\Activity\Models\Comment;
 use BookStack\Entities\Models\Entity;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Exceptions\PrettyException;
 use BookStack\Facades\Activity as ActivityService;
 use BookStack\Util\HtmlDescriptionFilter;
 
@@ -20,7 +22,7 @@ public function getById(int $id): Comment
     /**
      * Create a new comment on an entity.
      */
-    public function create(Entity $entity, string $html, ?int $parent_id): Comment
+    public function create(Entity $entity, string $html, ?int $parentId, string $contentRef): Comment
     {
         $userId = user()->id;
         $comment = new Comment();
@@ -29,7 +31,8 @@ public function create(Entity $entity, string $html, ?int $parent_id): Comment
         $comment->created_by = $userId;
         $comment->updated_by = $userId;
         $comment->local_id = $this->getNextLocalId($entity);
-        $comment->parent_id = $parent_id;
+        $comment->parent_id = $parentId;
+        $comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $contentRef) === 1 ? $contentRef : '';
 
         $entity->comments()->save($comment);
         ActivityService::add(ActivityType::COMMENT_CREATE, $comment);
@@ -52,6 +55,41 @@ public function update(Comment $comment, string $html): Comment
         return $comment;
     }
 
+
+    /**
+     * Archive an existing comment.
+     */
+    public function archive(Comment $comment): Comment
+    {
+        if ($comment->parent_id) {
+            throw new NotifyException('Only top-level comments can be archived.', '/', 400);
+        }
+
+        $comment->archived = true;
+        $comment->save();
+
+        ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
+
+        return $comment;
+    }
+
+    /**
+     * Un-archive an existing comment.
+     */
+    public function unarchive(Comment $comment): Comment
+    {
+        if ($comment->parent_id) {
+            throw new NotifyException('Only top-level comments can be un-archived.', '/', 400);
+        }
+
+        $comment->archived = false;
+        $comment->save();
+
+        ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
+
+        return $comment;
+    }
+
     /**
      * Delete a comment from the system.
      */
diff --git a/app/Activity/Controllers/CommentController.php b/app/Activity/Controllers/CommentController.php
index 52ccc823864..479d57c4db9 100644
--- a/app/Activity/Controllers/CommentController.php
+++ b/app/Activity/Controllers/CommentController.php
@@ -3,6 +3,8 @@
 namespace BookStack\Activity\Controllers;
 
 use BookStack\Activity\CommentRepo;
+use BookStack\Activity\Tools\CommentTree;
+use BookStack\Activity\Tools\CommentTreeNode;
 use BookStack\Entities\Queries\PageQueries;
 use BookStack\Http\Controller;
 use Illuminate\Http\Request;
@@ -26,6 +28,7 @@ public function savePageComment(Request $request, int $pageId)
         $input = $this->validate($request, [
             'html'      => ['required', 'string'],
             'parent_id' => ['nullable', 'integer'],
+            'content_ref' => ['string'],
         ]);
 
         $page = $this->pageQueries->findVisibleById($pageId);
@@ -40,14 +43,12 @@ public function savePageComment(Request $request, int $pageId)
 
         // Create a new comment.
         $this->checkPermission('comment-create-all');
-        $comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null);
+        $contentRef = $input['content_ref'] ?? '';
+        $comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $contentRef);
 
         return view('comments.comment-branch', [
             'readOnly' => false,
-            'branch' => [
-                'comment' => $comment,
-                'children' => [],
-            ]
+            'branch' => new CommentTreeNode($comment, 0, []),
         ]);
     }
 
@@ -74,6 +75,46 @@ public function update(Request $request, int $commentId)
         ]);
     }
 
+    /**
+     * Mark a comment as archived.
+     */
+    public function archive(int $id)
+    {
+        $comment = $this->commentRepo->getById($id);
+        $this->checkOwnablePermission('page-view', $comment->entity);
+        if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) {
+            $this->showPermissionError();
+        }
+
+        $this->commentRepo->archive($comment);
+
+        $tree = new CommentTree($comment->entity);
+        return view('comments.comment-branch', [
+            'readOnly' => false,
+            'branch' => $tree->getCommentNodeForId($id),
+        ]);
+    }
+
+    /**
+     * Unmark a comment as archived.
+     */
+    public function unarchive(int $id)
+    {
+        $comment = $this->commentRepo->getById($id);
+        $this->checkOwnablePermission('page-view', $comment->entity);
+        if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) {
+            $this->showPermissionError();
+        }
+
+        $this->commentRepo->unarchive($comment);
+
+        $tree = new CommentTree($comment->entity);
+        return view('comments.comment-branch', [
+            'readOnly' => false,
+            'branch' => $tree->getCommentNodeForId($id),
+        ]);
+    }
+
     /**
      * Delete a comment from the system.
      */
diff --git a/app/Activity/Models/Comment.php b/app/Activity/Models/Comment.php
index d0385d3962f..91cea4fe0e3 100644
--- a/app/Activity/Models/Comment.php
+++ b/app/Activity/Models/Comment.php
@@ -19,6 +19,8 @@
  * @property int      $entity_id
  * @property int      $created_by
  * @property int      $updated_by
+ * @property string   $content_ref
+ * @property bool     $archived
  */
 class Comment extends Model implements Loggable
 {
diff --git a/app/Activity/Tools/CommentTree.php b/app/Activity/Tools/CommentTree.php
index 16f6804ea42..a05a9d24726 100644
--- a/app/Activity/Tools/CommentTree.php
+++ b/app/Activity/Tools/CommentTree.php
@@ -9,7 +9,7 @@ class CommentTree
 {
     /**
      * The built nested tree structure array.
-     * @var array{comment: Comment, depth: int, children: array}[]
+     * @var CommentTreeNode[]
      */
     protected array $tree;
     protected array $comments;
@@ -28,7 +28,7 @@ public function enabled(): bool
 
     public function empty(): bool
     {
-        return count($this->tree) === 0;
+        return count($this->getActive()) === 0;
     }
 
     public function count(): int
@@ -36,9 +36,35 @@ public function count(): int
         return count($this->comments);
     }
 
-    public function get(): array
+    public function getActive(): array
     {
-        return $this->tree;
+        return array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived);
+    }
+
+    public function activeThreadCount(): int
+    {
+        return count($this->getActive());
+    }
+
+    public function getArchived(): array
+    {
+        return array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived);
+    }
+
+    public function archivedThreadCount(): int
+    {
+        return count($this->getArchived());
+    }
+
+    public function getCommentNodeForId(int $commentId): ?CommentTreeNode
+    {
+        foreach ($this->tree as $node) {
+            if ($node->comment->id === $commentId) {
+                return $node;
+            }
+        }
+
+        return null;
     }
 
     public function canUpdateAny(): bool
@@ -54,6 +80,7 @@ public function canUpdateAny(): bool
 
     /**
      * @param Comment[] $comments
+     * @return CommentTreeNode[]
      */
     protected function createTree(array $comments): array
     {
@@ -77,26 +104,22 @@ protected function createTree(array $comments): array
 
         $tree = [];
         foreach ($childMap[0] ?? [] as $childId) {
-            $tree[] = $this->createTreeForId($childId, 0, $byId, $childMap);
+            $tree[] = $this->createTreeNodeForId($childId, 0, $byId, $childMap);
         }
 
         return $tree;
     }
 
-    protected function createTreeForId(int $id, int $depth, array &$byId, array &$childMap): array
+    protected function createTreeNodeForId(int $id, int $depth, array &$byId, array &$childMap): CommentTreeNode
     {
         $childIds = $childMap[$id] ?? [];
         $children = [];
 
         foreach ($childIds as $childId) {
-            $children[] = $this->createTreeForId($childId, $depth + 1, $byId, $childMap);
+            $children[] = $this->createTreeNodeForId($childId, $depth + 1, $byId, $childMap);
         }
 
-        return [
-            'comment' => $byId[$id],
-            'depth' => $depth,
-            'children' => $children,
-        ];
+        return new CommentTreeNode($byId[$id], $depth, $children);
     }
 
     protected function loadComments(): array
diff --git a/app/Activity/Tools/CommentTreeNode.php b/app/Activity/Tools/CommentTreeNode.php
new file mode 100644
index 00000000000..7b280bd2d95
--- /dev/null
+++ b/app/Activity/Tools/CommentTreeNode.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace BookStack\Activity\Tools;
+
+use BookStack\Activity\Models\Comment;
+
+class CommentTreeNode
+{
+    public Comment $comment;
+    public int $depth;
+
+    /**
+     * @var CommentTreeNode[]
+     */
+    public array $children;
+
+    public function __construct(Comment $comment, int $depth, array $children)
+    {
+        $this->comment = $comment;
+        $this->depth = $depth;
+        $this->children = $children;
+    }
+}
diff --git a/database/factories/Activity/Models/CommentFactory.php b/database/factories/Activity/Models/CommentFactory.php
index efbd183b31d..844bc399381 100644
--- a/database/factories/Activity/Models/CommentFactory.php
+++ b/database/factories/Activity/Models/CommentFactory.php
@@ -27,6 +27,8 @@ public function definition()
             'html'      => $html,
             'parent_id' => null,
             'local_id'  => 1,
+            'content_ref' => '',
+            'archived' => false,
         ];
     }
 }
diff --git a/database/migrations/2025_04_18_215145_add_content_refs_and_archived_to_comments.php b/database/migrations/2025_04_18_215145_add_content_refs_and_archived_to_comments.php
new file mode 100644
index 00000000000..794201dec8a
--- /dev/null
+++ b/database/migrations/2025_04_18_215145_add_content_refs_and_archived_to_comments.php
@@ -0,0 +1,30 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('comments', function (Blueprint $table) {
+            $table->string('content_ref');
+            $table->boolean('archived')->index();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('comments', function (Blueprint $table) {
+            $table->dropColumn('content_ref');
+            $table->dropColumn('archived');
+        });
+    }
+};
diff --git a/lang/en/common.php b/lang/en/common.php
index b05169bb2c4..06a9e855ce3 100644
--- a/lang/en/common.php
+++ b/lang/en/common.php
@@ -30,6 +30,8 @@
     'create' => 'Create',
     'update' => 'Update',
     'edit' => 'Edit',
+    'archive' => 'Archive',
+    'unarchive' => 'Un-Archive',
     'sort' => 'Sort',
     'move' => 'Move',
     'copy' => 'Copy',
diff --git a/lang/en/entities.php b/lang/en/entities.php
index a74785eaacd..6e616ded452 100644
--- a/lang/en/entities.php
+++ b/lang/en/entities.php
@@ -392,8 +392,11 @@
     'comment' => 'Comment',
     'comments' => 'Comments',
     'comment_add' => 'Add Comment',
+    'comment_none' => 'No comments to display',
     'comment_placeholder' => 'Leave a comment here',
-    'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments',
+    'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
+    'comment_archived_count' => ':count Archived',
+    'comment_archived_threads' => 'Archived Threads',
     'comment_save' => 'Save Comment',
     'comment_new' => 'New Comment',
     'comment_created' => 'commented :createDiff',
@@ -402,8 +405,14 @@
     'comment_deleted_success' => 'Comment deleted',
     'comment_created_success' => 'Comment added',
     'comment_updated_success' => 'Comment updated',
+    'comment_archive_success' => 'Comment archived',
+    'comment_unarchive_success' => 'Comment un-archived',
+    'comment_view' => 'View comment',
+    'comment_jump_to_thread' => 'Jump to thread',
     'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
     'comment_in_reply_to' => 'In reply to :commentId',
+    'comment_reference' => 'Reference',
+    'comment_reference_outdated' => '(Outdated)',
     'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',
 
     // Revision
diff --git a/resources/icons/archive.svg b/resources/icons/archive.svg
new file mode 100644
index 00000000000..90a4f35b7e9
--- /dev/null
+++ b/resources/icons/archive.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m480-240 160-160-56-56-64 64v-168h-80v168l-64-64-56 56 160 160ZM200-640v440h560v-440H200Zm0 520q-33 0-56.5-23.5T120-200v-499q0-14 4.5-27t13.5-24l50-61q11-14 27.5-21.5T250-840h460q18 0 34.5 7.5T772-811l50 61q9 11 13.5 24t4.5 27v499q0 33-23.5 56.5T760-120H200Zm16-600h528l-34-40H250l-34 40Zm264 300Z"/></svg>
\ No newline at end of file
diff --git a/resources/icons/bookmark.svg b/resources/icons/bookmark.svg
new file mode 100644
index 00000000000..30e487c5219
--- /dev/null
+++ b/resources/icons/bookmark.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-120v-640q0-33 23.5-56.5T280-840h400q33 0 56.5 23.5T760-760v640L480-240 200-120Zm80-122 200-86 200 86v-518H280v518Zm0-518h400-400Z"/></svg>
\ No newline at end of file
diff --git a/resources/js/components/editor-toolbox.js b/resources/js/components/editor-toolbox.ts
similarity index 57%
rename from resources/js/components/editor-toolbox.js
rename to resources/js/components/editor-toolbox.ts
index ddb4ff39c76..60bdde05efb 100644
--- a/resources/js/components/editor-toolbox.js
+++ b/resources/js/components/editor-toolbox.ts
@@ -1,42 +1,58 @@
 import {Component} from './component';
 
+export interface EditorToolboxChangeEventData {
+    tab: string;
+    open: boolean;
+}
+
 export class EditorToolbox extends Component {
 
+    protected container!: HTMLElement;
+    protected buttons!: HTMLButtonElement[];
+    protected contentElements!: HTMLElement[];
+    protected toggleButton!: HTMLElement;
+    protected editorWrapEl!: HTMLElement;
+
+    protected open: boolean = false;
+    protected tab: string = '';
+
     setup() {
         // Elements
         this.container = this.$el;
-        this.buttons = this.$manyRefs.tabButton;
+        this.buttons = this.$manyRefs.tabButton as HTMLButtonElement[];
         this.contentElements = this.$manyRefs.tabContent;
         this.toggleButton = this.$refs.toggle;
-        this.editorWrapEl = this.container.closest('.page-editor');
+        this.editorWrapEl = this.container.closest('.page-editor') as HTMLElement;
 
         this.setupListeners();
 
         // Set the first tab as active on load
-        this.setActiveTab(this.contentElements[0].dataset.tabContent);
+        this.setActiveTab(this.contentElements[0].dataset.tabContent || '');
     }
 
-    setupListeners() {
+    protected setupListeners(): void {
         // Toolbox toggle button click
         this.toggleButton.addEventListener('click', () => this.toggle());
         // Tab button click
-        this.container.addEventListener('click', event => {
-            const button = event.target.closest('button');
-            if (this.buttons.includes(button)) {
-                const name = button.dataset.tab;
+        this.container.addEventListener('click', (event: MouseEvent) => {
+            const button = (event.target as HTMLElement).closest('button');
+            if (button instanceof HTMLButtonElement && this.buttons.includes(button)) {
+                const name = button.dataset.tab || '';
                 this.setActiveTab(name, true);
             }
         });
     }
 
-    toggle() {
+    protected toggle(): void {
         this.container.classList.toggle('open');
         const isOpen = this.container.classList.contains('open');
         this.toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
         this.editorWrapEl.classList.toggle('toolbox-open', isOpen);
+        this.open = isOpen;
+        this.emitState();
     }
 
-    setActiveTab(tabName, openToolbox = false) {
+    protected setActiveTab(tabName: string, openToolbox: boolean = false): void {
         // Set button visibility
         for (const button of this.buttons) {
             button.classList.remove('active');
@@ -54,6 +70,14 @@ export class EditorToolbox extends Component {
         if (openToolbox && !this.container.classList.contains('open')) {
             this.toggle();
         }
+
+        this.tab = tabName;
+        this.emitState();
+    }
+
+    protected emitState(): void {
+        const data: EditorToolboxChangeEventData = {tab: this.tab, open: this.open};
+        this.$emit('change', data);
     }
 
 }
diff --git a/resources/js/components/index.ts b/resources/js/components/index.ts
index 10b8025db63..63e1ad0dbf7 100644
--- a/resources/js/components/index.ts
+++ b/resources/js/components/index.ts
@@ -36,6 +36,7 @@ export {NewUserPassword} from './new-user-password';
 export {Notification} from './notification';
 export {OptionalInput} from './optional-input';
 export {PageComment} from './page-comment';
+export {PageCommentReference} from './page-comment-reference';
 export {PageComments} from './page-comments';
 export {PageDisplay} from './page-display';
 export {PageEditor} from './page-editor';
diff --git a/resources/js/components/page-comment-reference.ts b/resources/js/components/page-comment-reference.ts
new file mode 100644
index 00000000000..009e806c104
--- /dev/null
+++ b/resources/js/components/page-comment-reference.ts
@@ -0,0 +1,251 @@
+import {Component} from "./component";
+import {findTargetNodeAndOffset, hashElement} from "../services/dom";
+import {el} from "../wysiwyg/utils/dom";
+import commentIcon from "@icons/comment.svg";
+import closeIcon from "@icons/close.svg";
+import {debounce, scrollAndHighlightElement} from "../services/util";
+import {EditorToolboxChangeEventData} from "./editor-toolbox";
+import {TabsChangeEvent} from "./tabs";
+
+/**
+ * Track the close function for the current open marker so it can be closed
+ * when another is opened so we only show one marker comment thread at one time.
+ */
+let openMarkerClose: Function|null = null;
+
+export class PageCommentReference extends Component {
+    protected link!: HTMLLinkElement;
+    protected reference!: string;
+    protected markerWrap: HTMLElement|null = null;
+
+    protected viewCommentText!: string;
+    protected jumpToThreadText!: string;
+    protected closeText!: string;
+
+    setup() {
+        this.link = this.$el as HTMLLinkElement;
+        this.reference = this.$opts.reference;
+        this.viewCommentText = this.$opts.viewCommentText;
+        this.jumpToThreadText = this.$opts.jumpToThreadText;
+        this.closeText = this.$opts.closeText;
+
+        // Show within page display area if seen
+        this.showForDisplay();
+
+        // Handle editor view to show on comments toolbox view
+        window.addEventListener('editor-toolbox-change', ((event: CustomEvent<EditorToolboxChangeEventData>) => {
+            const tabName: string = event.detail.tab;
+            const isOpen = event.detail.open;
+            if (tabName === 'comments' && isOpen && this.link.checkVisibility()) {
+                this.showForEditor();
+            } else {
+                this.hideMarker();
+            }
+        }) as EventListener);
+
+        // Handle visibility changes within editor toolbox archived details dropdown
+        window.addEventListener('toggle', event => {
+            if (event.target instanceof HTMLElement && event.target.contains(this.link)) {
+                window.requestAnimationFrame(() => {
+                    if (this.link.checkVisibility()) {
+                        this.showForEditor();
+                    } else {
+                        this.hideMarker();
+                    }
+                });
+            }
+        }, {capture: true});
+
+        // Handle comments tab changes to hide/show markers & indicators
+        window.addEventListener('tabs-change', ((event: CustomEvent<TabsChangeEvent>) => {
+            const sectionId = event.detail.showing;
+            if (!sectionId.startsWith('comment-tab-panel')) {
+                return;
+            }
+
+            const panel = document.getElementById(sectionId);
+            if (panel?.contains(this.link)) {
+                this.showForDisplay();
+            } else {
+                this.hideMarker();
+            }
+        }) as EventListener);
+    }
+
+    public showForDisplay() {
+        const pageContentArea = document.querySelector('.page-content');
+        if (pageContentArea instanceof HTMLElement && this.link.checkVisibility()) {
+            this.updateMarker(pageContentArea);
+        }
+    }
+
+    protected showForEditor() {
+        const contentWrap = document.querySelector('.editor-content-wrap');
+        if (contentWrap instanceof HTMLElement) {
+            this.updateMarker(contentWrap);
+        }
+
+        const onChange = () => {
+            this.hideMarker();
+            setTimeout(() => {
+                window.$events.remove('editor-html-change', onChange);
+            }, 1);
+        };
+
+        window.$events.listen('editor-html-change', onChange);
+    }
+
+    protected updateMarker(contentContainer: HTMLElement) {
+        // Reset link and existing marker
+        this.link.classList.remove('outdated', 'missing');
+        if (this.markerWrap) {
+            this.markerWrap.remove();
+        }
+
+        const [refId, refHash, refRange] = this.reference.split(':');
+        const refEl = document.getElementById(refId);
+        if (!refEl) {
+            this.link.classList.add('outdated', 'missing');
+            return;
+        }
+
+        const actualHash = hashElement(refEl);
+        if (actualHash !== refHash) {
+            this.link.classList.add('outdated');
+        }
+
+        const marker = el('button', {
+            type: 'button',
+            class: 'content-comment-marker',
+            title: this.viewCommentText,
+        });
+        marker.innerHTML = <string>commentIcon;
+        marker.addEventListener('click', event => {
+            this.showCommentAtMarker(marker);
+        });
+
+        this.markerWrap = el('div', {
+            class: 'content-comment-highlight',
+        }, [marker]);
+
+        contentContainer.append(this.markerWrap);
+        this.positionMarker(refEl, refRange);
+
+        this.link.href = `#${refEl.id}`;
+        this.link.addEventListener('click', (event: MouseEvent) => {
+            event.preventDefault();
+            scrollAndHighlightElement(refEl);
+        });
+
+        const debouncedReposition = debounce(() => {
+            this.positionMarker(refEl, refRange);
+        }, 50, false).bind(this);
+        window.addEventListener('resize', debouncedReposition);
+    }
+
+    protected positionMarker(targetEl: HTMLElement, range: string) {
+        if (!this.markerWrap) {
+            return;
+        }
+
+        const markerParent = this.markerWrap.parentElement as HTMLElement;
+        const parentBounds = markerParent.getBoundingClientRect();
+        let targetBounds = targetEl.getBoundingClientRect();
+        const [rangeStart, rangeEnd] = range.split('-');
+        if (rangeStart && rangeEnd) {
+            const range = new Range();
+            const relStart = findTargetNodeAndOffset(targetEl, Number(rangeStart));
+            const relEnd = findTargetNodeAndOffset(targetEl, Number(rangeEnd));
+            if (relStart && relEnd) {
+                range.setStart(relStart.node, relStart.offset);
+                range.setEnd(relEnd.node, relEnd.offset);
+                targetBounds = range.getBoundingClientRect();
+            }
+        }
+
+        const relLeft = targetBounds.left - parentBounds.left;
+        const relTop = (targetBounds.top - parentBounds.top) + markerParent.scrollTop;
+
+        this.markerWrap.style.left = `${relLeft}px`;
+        this.markerWrap.style.top = `${relTop}px`;
+        this.markerWrap.style.width = `${targetBounds.width}px`;
+        this.markerWrap.style.height = `${targetBounds.height}px`;
+    }
+
+    public hideMarker() {
+        // Hide marker and close existing marker windows
+        if (openMarkerClose) {
+            openMarkerClose();
+        }
+        this.markerWrap?.remove();
+        this.markerWrap = null;
+    }
+
+    protected showCommentAtMarker(marker: HTMLElement): void {
+        // Hide marker and close existing marker windows
+        if (openMarkerClose) {
+            openMarkerClose();
+        }
+        marker.hidden = true;
+
+        // Locate relevant comment
+        const commentBox = this.link.closest('.comment-box') as HTMLElement;
+
+        // Build comment window
+        const readClone = (commentBox.closest('.comment-branch') as HTMLElement).cloneNode(true) as HTMLElement;
+        const toRemove = readClone.querySelectorAll('.actions, form');
+        for (const el of toRemove) {
+            el.remove();
+        }
+
+        const close = el('button', {type: 'button', title: this.closeText});
+        close.innerHTML = (closeIcon as string);
+        const jump = el('button', {type: 'button', 'data-action': 'jump'}, [this.jumpToThreadText]);
+
+        const commentWindow = el('div', {
+            class: 'content-comment-window'
+        }, [
+            el('div', {
+                class: 'content-comment-window-actions',
+            }, [jump, close]),
+            el('div', {
+                class: 'content-comment-window-content comment-container-compact comment-container-super-compact',
+            }, [readClone]),
+        ]);
+
+        marker.parentElement?.append(commentWindow);
+
+        // Handle interaction within window
+        const closeAction = () => {
+            commentWindow.remove();
+            marker.hidden = false;
+            window.removeEventListener('click', windowCloseAction);
+            openMarkerClose = null;
+        };
+
+        const windowCloseAction = (event: MouseEvent) => {
+            if (!(marker.parentElement as HTMLElement).contains(event.target as HTMLElement)) {
+                closeAction();
+            }
+        };
+        window.addEventListener('click', windowCloseAction);
+
+        openMarkerClose = closeAction;
+        close.addEventListener('click', closeAction.bind(this));
+        jump.addEventListener('click', () => {
+            closeAction();
+            commentBox.scrollIntoView({behavior: 'smooth'});
+            const highlightTarget = commentBox.querySelector('.header') as HTMLElement;
+            highlightTarget.classList.add('anim-highlight');
+            highlightTarget.addEventListener('animationend', () => highlightTarget.classList.remove('anim-highlight'))
+        });
+
+        // Position window within bounds
+        const commentWindowBounds = commentWindow.getBoundingClientRect();
+        const contentBounds = document.querySelector('.page-content')?.getBoundingClientRect();
+        if (contentBounds && commentWindowBounds.right > contentBounds.right) {
+            const diff = commentWindowBounds.right - contentBounds.right;
+            commentWindow.style.left = `-${diff}px`;
+        }
+    }
+}
\ No newline at end of file
diff --git a/resources/js/components/page-comment.js b/resources/js/components/page-comment.js
deleted file mode 100644
index 8c0a8b33e54..00000000000
--- a/resources/js/components/page-comment.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import {Component} from './component';
-import {getLoading, htmlToDom} from '../services/dom.ts';
-import {buildForInput} from '../wysiwyg-tinymce/config';
-
-export class PageComment extends Component {
-
-    setup() {
-        // Options
-        this.commentId = this.$opts.commentId;
-        this.commentLocalId = this.$opts.commentLocalId;
-        this.commentParentId = this.$opts.commentParentId;
-        this.deletedText = this.$opts.deletedText;
-        this.updatedText = this.$opts.updatedText;
-
-        // Editor reference and text options
-        this.wysiwygEditor = null;
-        this.wysiwygLanguage = this.$opts.wysiwygLanguage;
-        this.wysiwygTextDirection = this.$opts.wysiwygTextDirection;
-
-        // Element references
-        this.container = this.$el;
-        this.contentContainer = this.$refs.contentContainer;
-        this.form = this.$refs.form;
-        this.formCancel = this.$refs.formCancel;
-        this.editButton = this.$refs.editButton;
-        this.deleteButton = this.$refs.deleteButton;
-        this.replyButton = this.$refs.replyButton;
-        this.input = this.$refs.input;
-
-        this.setupListeners();
-    }
-
-    setupListeners() {
-        if (this.replyButton) {
-            this.replyButton.addEventListener('click', () => this.$emit('reply', {
-                id: this.commentLocalId,
-                element: this.container,
-            }));
-        }
-
-        if (this.editButton) {
-            this.editButton.addEventListener('click', this.startEdit.bind(this));
-            this.form.addEventListener('submit', this.update.bind(this));
-            this.formCancel.addEventListener('click', () => this.toggleEditMode(false));
-        }
-
-        if (this.deleteButton) {
-            this.deleteButton.addEventListener('click', this.delete.bind(this));
-        }
-    }
-
-    toggleEditMode(show) {
-        this.contentContainer.toggleAttribute('hidden', show);
-        this.form.toggleAttribute('hidden', !show);
-    }
-
-    startEdit() {
-        this.toggleEditMode(true);
-
-        if (this.wysiwygEditor) {
-            this.wysiwygEditor.focus();
-            return;
-        }
-
-        const config = buildForInput({
-            language: this.wysiwygLanguage,
-            containerElement: this.input,
-            darkMode: document.documentElement.classList.contains('dark-mode'),
-            textDirection: this.wysiwygTextDirection,
-            translations: {},
-            translationMap: window.editor_translations,
-        });
-
-        window.tinymce.init(config).then(editors => {
-            this.wysiwygEditor = editors[0];
-            setTimeout(() => this.wysiwygEditor.focus(), 50);
-        });
-    }
-
-    async update(event) {
-        event.preventDefault();
-        const loading = this.showLoading();
-        this.form.toggleAttribute('hidden', true);
-
-        const reqData = {
-            html: this.wysiwygEditor.getContent(),
-            parent_id: this.parentId || null,
-        };
-
-        try {
-            const resp = await window.$http.put(`/comment/${this.commentId}`, reqData);
-            const newComment = htmlToDom(resp.data);
-            this.container.replaceWith(newComment);
-            window.$events.success(this.updatedText);
-        } catch (err) {
-            console.error(err);
-            window.$events.showValidationErrors(err);
-            this.form.toggleAttribute('hidden', false);
-            loading.remove();
-        }
-    }
-
-    async delete() {
-        this.showLoading();
-
-        await window.$http.delete(`/comment/${this.commentId}`);
-        this.$emit('delete');
-        this.container.closest('.comment-branch').remove();
-        window.$events.success(this.deletedText);
-    }
-
-    showLoading() {
-        const loading = getLoading();
-        loading.classList.add('px-l');
-        this.container.append(loading);
-        return loading;
-    }
-
-}
diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts
new file mode 100644
index 00000000000..0c3e19f4b1a
--- /dev/null
+++ b/resources/js/components/page-comment.ts
@@ -0,0 +1,184 @@
+import {Component} from './component';
+import {getLoading, htmlToDom} from '../services/dom';
+import {buildForInput} from '../wysiwyg-tinymce/config';
+import {PageCommentReference} from "./page-comment-reference";
+import {HttpError} from "../services/http";
+
+export interface PageCommentReplyEventData {
+    id: string; // ID of comment being replied to
+    element: HTMLElement; // Container for comment replied to
+}
+
+export interface PageCommentArchiveEventData {
+    new_thread_dom: HTMLElement;
+}
+
+export class PageComment extends Component {
+
+    protected commentId!: string;
+    protected commentLocalId!: string;
+    protected deletedText!: string;
+    protected updatedText!: string;
+    protected archiveText!: string;
+
+    protected wysiwygEditor: any = null;
+    protected wysiwygLanguage!: string;
+    protected wysiwygTextDirection!: string;
+
+    protected container!: HTMLElement;
+    protected contentContainer!: HTMLElement;
+    protected form!: HTMLFormElement;
+    protected formCancel!: HTMLElement;
+    protected editButton!: HTMLElement;
+    protected deleteButton!: HTMLElement;
+    protected replyButton!: HTMLElement;
+    protected archiveButton!: HTMLElement;
+    protected input!: HTMLInputElement;
+
+    setup() {
+        // Options
+        this.commentId = this.$opts.commentId;
+        this.commentLocalId = this.$opts.commentLocalId;
+        this.deletedText = this.$opts.deletedText;
+        this.deletedText = this.$opts.deletedText;
+        this.archiveText = this.$opts.archiveText;
+
+        // Editor reference and text options
+        this.wysiwygLanguage = this.$opts.wysiwygLanguage;
+        this.wysiwygTextDirection = this.$opts.wysiwygTextDirection;
+
+        // Element references
+        this.container = this.$el;
+        this.contentContainer = this.$refs.contentContainer;
+        this.form = this.$refs.form as HTMLFormElement;
+        this.formCancel = this.$refs.formCancel;
+        this.editButton = this.$refs.editButton;
+        this.deleteButton = this.$refs.deleteButton;
+        this.replyButton = this.$refs.replyButton;
+        this.archiveButton = this.$refs.archiveButton;
+        this.input = this.$refs.input as HTMLInputElement;
+
+        this.setupListeners();
+    }
+
+    protected setupListeners(): void {
+        if (this.replyButton) {
+            const data: PageCommentReplyEventData = {
+                id: this.commentLocalId,
+                element: this.container,
+            };
+            this.replyButton.addEventListener('click', () => this.$emit('reply', data));
+        }
+
+        if (this.editButton) {
+            this.editButton.addEventListener('click', this.startEdit.bind(this));
+            this.form.addEventListener('submit', this.update.bind(this));
+            this.formCancel.addEventListener('click', () => this.toggleEditMode(false));
+        }
+
+        if (this.deleteButton) {
+            this.deleteButton.addEventListener('click', this.delete.bind(this));
+        }
+
+        if (this.archiveButton) {
+            this.archiveButton.addEventListener('click', this.archive.bind(this));
+        }
+    }
+
+    protected toggleEditMode(show: boolean) : void {
+        this.contentContainer.toggleAttribute('hidden', show);
+        this.form.toggleAttribute('hidden', !show);
+    }
+
+    protected startEdit() : void {
+        this.toggleEditMode(true);
+
+        if (this.wysiwygEditor) {
+            this.wysiwygEditor.focus();
+            return;
+        }
+
+        const config = buildForInput({
+            language: this.wysiwygLanguage,
+            containerElement: this.input,
+            darkMode: document.documentElement.classList.contains('dark-mode'),
+            textDirection: this.wysiwygTextDirection,
+            drawioUrl: '',
+            pageId: 0,
+            translations: {},
+            translationMap: (window as unknown as Record<string, Object>).editor_translations,
+        });
+
+        (window as unknown as {tinymce: {init: (arg0: Object) => Promise<any>}}).tinymce.init(config).then(editors => {
+            this.wysiwygEditor = editors[0];
+            setTimeout(() => this.wysiwygEditor.focus(), 50);
+        });
+    }
+
+    protected async update(event: Event): Promise<void> {
+        event.preventDefault();
+        const loading = this.showLoading();
+        this.form.toggleAttribute('hidden', true);
+
+        const reqData = {
+            html: this.wysiwygEditor.getContent(),
+        };
+
+        try {
+            const resp = await window.$http.put(`/comment/${this.commentId}`, reqData);
+            const newComment = htmlToDom(resp.data as string);
+            this.container.replaceWith(newComment);
+            window.$events.success(this.updatedText);
+        } catch (err) {
+            console.error(err);
+            if (err instanceof HttpError) {
+                window.$events.showValidationErrors(err);
+            }
+            this.form.toggleAttribute('hidden', false);
+            loading.remove();
+        }
+    }
+
+    protected async delete(): Promise<void> {
+        this.showLoading();
+
+        await window.$http.delete(`/comment/${this.commentId}`);
+        this.$emit('delete');
+
+        const branch = this.container.closest('.comment-branch');
+        if (branch instanceof HTMLElement) {
+            const refs = window.$components.allWithinElement<PageCommentReference>(branch, 'page-comment-reference');
+            for (const ref of refs) {
+                ref.hideMarker();
+            }
+            branch.remove();
+        }
+
+        window.$events.success(this.deletedText);
+    }
+
+    protected async archive(): Promise<void> {
+        this.showLoading();
+        const isArchived = this.archiveButton.dataset.isArchived === 'true';
+        const action = isArchived ? 'unarchive' : 'archive';
+
+        const response = await window.$http.put(`/comment/${this.commentId}/${action}`);
+        window.$events.success(this.archiveText);
+        const eventData: PageCommentArchiveEventData = {new_thread_dom: htmlToDom(response.data as string)};
+        this.$emit(action, eventData);
+
+        const branch = this.container.closest('.comment-branch') as HTMLElement;
+        const references = window.$components.allWithinElement<PageCommentReference>(branch, 'page-comment-reference');
+        for (const reference of references) {
+            reference.hideMarker();
+        }
+        branch.remove();
+    }
+
+    protected showLoading(): HTMLElement {
+        const loading = getLoading();
+        loading.classList.add('px-l');
+        this.container.append(loading);
+        return loading;
+    }
+}
diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.js
deleted file mode 100644
index 8f023836b09..00000000000
--- a/resources/js/components/page-comments.js
+++ /dev/null
@@ -1,175 +0,0 @@
-import {Component} from './component';
-import {getLoading, htmlToDom} from '../services/dom.ts';
-import {buildForInput} from '../wysiwyg-tinymce/config';
-
-export class PageComments extends Component {
-
-    setup() {
-        this.elem = this.$el;
-        this.pageId = Number(this.$opts.pageId);
-
-        // Element references
-        this.container = this.$refs.commentContainer;
-        this.commentCountBar = this.$refs.commentCountBar;
-        this.commentsTitle = this.$refs.commentsTitle;
-        this.addButtonContainer = this.$refs.addButtonContainer;
-        this.replyToRow = this.$refs.replyToRow;
-        this.formContainer = this.$refs.formContainer;
-        this.form = this.$refs.form;
-        this.formInput = this.$refs.formInput;
-        this.formReplyLink = this.$refs.formReplyLink;
-        this.addCommentButton = this.$refs.addCommentButton;
-        this.hideFormButton = this.$refs.hideFormButton;
-        this.removeReplyToButton = this.$refs.removeReplyToButton;
-
-        // WYSIWYG options
-        this.wysiwygLanguage = this.$opts.wysiwygLanguage;
-        this.wysiwygTextDirection = this.$opts.wysiwygTextDirection;
-        this.wysiwygEditor = null;
-
-        // Translations
-        this.createdText = this.$opts.createdText;
-        this.countText = this.$opts.countText;
-
-        // Internal State
-        this.parentId = null;
-        this.formReplyText = this.formReplyLink?.textContent || '';
-
-        this.setupListeners();
-    }
-
-    setupListeners() {
-        this.elem.addEventListener('page-comment-delete', () => {
-            setTimeout(() => this.updateCount(), 1);
-            this.hideForm();
-        });
-
-        this.elem.addEventListener('page-comment-reply', event => {
-            this.setReply(event.detail.id, event.detail.element);
-        });
-
-        if (this.form) {
-            this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this));
-            this.hideFormButton.addEventListener('click', this.hideForm.bind(this));
-            this.addCommentButton.addEventListener('click', this.showForm.bind(this));
-            this.form.addEventListener('submit', this.saveComment.bind(this));
-        }
-    }
-
-    saveComment(event) {
-        event.preventDefault();
-        event.stopPropagation();
-
-        const loading = getLoading();
-        loading.classList.add('px-l');
-        this.form.after(loading);
-        this.form.toggleAttribute('hidden', true);
-
-        const reqData = {
-            html: this.wysiwygEditor.getContent(),
-            parent_id: this.parentId || null,
-        };
-
-        window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
-            const newElem = htmlToDom(resp.data);
-
-            if (reqData.parent_id) {
-                this.formContainer.after(newElem);
-            } else {
-                this.container.append(newElem);
-            }
-
-            window.$events.success(this.createdText);
-            this.hideForm();
-            this.updateCount();
-        }).catch(err => {
-            this.form.toggleAttribute('hidden', false);
-            window.$events.showValidationErrors(err);
-        });
-
-        this.form.toggleAttribute('hidden', false);
-        loading.remove();
-    }
-
-    updateCount() {
-        const count = this.getCommentCount();
-        this.commentsTitle.textContent = window.$trans.choice(this.countText, count, {count});
-    }
-
-    resetForm() {
-        this.removeEditor();
-        this.formInput.value = '';
-        this.parentId = null;
-        this.replyToRow.toggleAttribute('hidden', true);
-        this.container.append(this.formContainer);
-    }
-
-    showForm() {
-        this.removeEditor();
-        this.formContainer.toggleAttribute('hidden', false);
-        this.addButtonContainer.toggleAttribute('hidden', true);
-        this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'});
-        this.loadEditor();
-    }
-
-    hideForm() {
-        this.resetForm();
-        this.formContainer.toggleAttribute('hidden', true);
-        if (this.getCommentCount() > 0) {
-            this.elem.append(this.addButtonContainer);
-        } else {
-            this.commentCountBar.append(this.addButtonContainer);
-        }
-        this.addButtonContainer.toggleAttribute('hidden', false);
-    }
-
-    loadEditor() {
-        if (this.wysiwygEditor) {
-            this.wysiwygEditor.focus();
-            return;
-        }
-
-        const config = buildForInput({
-            language: this.wysiwygLanguage,
-            containerElement: this.formInput,
-            darkMode: document.documentElement.classList.contains('dark-mode'),
-            textDirection: this.wysiwygTextDirection,
-            translations: {},
-            translationMap: window.editor_translations,
-        });
-
-        window.tinymce.init(config).then(editors => {
-            this.wysiwygEditor = editors[0];
-            setTimeout(() => this.wysiwygEditor.focus(), 50);
-        });
-    }
-
-    removeEditor() {
-        if (this.wysiwygEditor) {
-            this.wysiwygEditor.remove();
-            this.wysiwygEditor = null;
-        }
-    }
-
-    getCommentCount() {
-        return this.container.querySelectorAll('[component="page-comment"]').length;
-    }
-
-    setReply(commentLocalId, commentElement) {
-        const targetFormLocation = commentElement.closest('.comment-branch').querySelector('.comment-branch-children');
-        targetFormLocation.append(this.formContainer);
-        this.showForm();
-        this.parentId = commentLocalId;
-        this.replyToRow.toggleAttribute('hidden', false);
-        this.formReplyLink.textContent = this.formReplyText.replace('1234', this.parentId);
-        this.formReplyLink.href = `#comment${this.parentId}`;
-    }
-
-    removeReplyTo() {
-        this.parentId = null;
-        this.replyToRow.toggleAttribute('hidden', true);
-        this.container.append(this.formContainer);
-        this.showForm();
-    }
-
-}
diff --git a/resources/js/components/page-comments.ts b/resources/js/components/page-comments.ts
new file mode 100644
index 00000000000..94f5ab3bb8e
--- /dev/null
+++ b/resources/js/components/page-comments.ts
@@ -0,0 +1,260 @@
+import {Component} from './component';
+import {getLoading, htmlToDom} from '../services/dom';
+import {buildForInput} from '../wysiwyg-tinymce/config';
+import {Tabs} from "./tabs";
+import {PageCommentReference} from "./page-comment-reference";
+import {scrollAndHighlightElement} from "../services/util";
+import {PageCommentArchiveEventData, PageCommentReplyEventData} from "./page-comment";
+
+export class PageComments extends Component {
+
+    private elem!: HTMLElement;
+    private pageId!: number;
+    private container!: HTMLElement;
+    private commentCountBar!: HTMLElement;
+    private activeTab!: HTMLElement;
+    private archivedTab!: HTMLElement;
+    private addButtonContainer!: HTMLElement;
+    private archiveContainer!: HTMLElement;
+    private replyToRow!: HTMLElement;
+    private referenceRow!: HTMLElement;
+    private formContainer!: HTMLElement;
+    private form!: HTMLFormElement;
+    private formInput!: HTMLInputElement;
+    private formReplyLink!: HTMLAnchorElement;
+    private formReferenceLink!: HTMLAnchorElement;
+    private addCommentButton!: HTMLElement;
+    private hideFormButton!: HTMLElement;
+    private removeReplyToButton!: HTMLElement;
+    private removeReferenceButton!: HTMLElement;
+    private wysiwygLanguage!: string;
+    private wysiwygTextDirection!: string;
+    private wysiwygEditor: any = null;
+    private createdText!: string;
+    private countText!: string;
+    private archivedCountText!: string;
+    private parentId: number | null = null;
+    private contentReference: string = '';
+    private formReplyText: string = '';
+
+    setup() {
+        this.elem = this.$el;
+        this.pageId = Number(this.$opts.pageId);
+
+        // Element references
+        this.container = this.$refs.commentContainer;
+        this.commentCountBar = this.$refs.commentCountBar;
+        this.activeTab = this.$refs.activeTab;
+        this.archivedTab = this.$refs.archivedTab;
+        this.addButtonContainer = this.$refs.addButtonContainer;
+        this.archiveContainer = this.$refs.archiveContainer;
+        this.replyToRow = this.$refs.replyToRow;
+        this.referenceRow = this.$refs.referenceRow;
+        this.formContainer = this.$refs.formContainer;
+        this.form = this.$refs.form as HTMLFormElement;
+        this.formInput = this.$refs.formInput as HTMLInputElement;
+        this.formReplyLink = this.$refs.formReplyLink as HTMLAnchorElement;
+        this.formReferenceLink = this.$refs.formReferenceLink as HTMLAnchorElement;
+        this.addCommentButton = this.$refs.addCommentButton;
+        this.hideFormButton = this.$refs.hideFormButton;
+        this.removeReplyToButton = this.$refs.removeReplyToButton;
+        this.removeReferenceButton = this.$refs.removeReferenceButton;
+
+        // WYSIWYG options
+        this.wysiwygLanguage = this.$opts.wysiwygLanguage;
+        this.wysiwygTextDirection = this.$opts.wysiwygTextDirection;
+
+        // Translations
+        this.createdText = this.$opts.createdText;
+        this.countText = this.$opts.countText;
+        this.archivedCountText = this.$opts.archivedCountText;
+
+        this.formReplyText = this.formReplyLink?.textContent || '';
+
+        this.setupListeners();
+    }
+
+    protected setupListeners(): void {
+        this.elem.addEventListener('page-comment-delete', () => {
+            setTimeout(() => this.updateCount(), 1);
+            this.hideForm();
+        });
+
+        this.elem.addEventListener('page-comment-reply', ((event: CustomEvent<PageCommentReplyEventData>) => {
+            this.setReply(event.detail.id, event.detail.element);
+        }) as EventListener);
+
+        this.elem.addEventListener('page-comment-archive', ((event: CustomEvent<PageCommentArchiveEventData>) => {
+            this.archiveContainer.append(event.detail.new_thread_dom);
+            setTimeout(() => this.updateCount(), 1);
+        }) as EventListener);
+
+        this.elem.addEventListener('page-comment-unarchive', ((event: CustomEvent<PageCommentArchiveEventData>) => {
+            this.container.append(event.detail.new_thread_dom);
+            setTimeout(() => this.updateCount(), 1);
+        }) as EventListener);
+
+        if (this.form) {
+            this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this));
+            this.removeReferenceButton.addEventListener('click', () => this.setContentReference(''));
+            this.hideFormButton.addEventListener('click', this.hideForm.bind(this));
+            this.addCommentButton.addEventListener('click', this.showForm.bind(this));
+            this.form.addEventListener('submit', this.saveComment.bind(this));
+        }
+    }
+
+    protected saveComment(event: SubmitEvent): void {
+        event.preventDefault();
+        event.stopPropagation();
+
+        const loading = getLoading();
+        loading.classList.add('px-l');
+        this.form.after(loading);
+        this.form.toggleAttribute('hidden', true);
+
+        const reqData = {
+            html: this.wysiwygEditor.getContent(),
+            parent_id: this.parentId || null,
+            content_ref: this.contentReference,
+        };
+
+        window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
+            const newElem = htmlToDom(resp.data as string);
+
+            if (reqData.parent_id) {
+                this.formContainer.after(newElem);
+            } else {
+                this.container.append(newElem);
+            }
+
+            const refs = window.$components.allWithinElement<PageCommentReference>(newElem, 'page-comment-reference');
+            for (const ref of refs) {
+                ref.showForDisplay();
+            }
+
+            window.$events.success(this.createdText);
+            this.hideForm();
+            this.updateCount();
+        }).catch(err => {
+            this.form.toggleAttribute('hidden', false);
+            window.$events.showValidationErrors(err);
+        });
+
+        this.form.toggleAttribute('hidden', false);
+        loading.remove();
+    }
+
+    protected updateCount(): void {
+        const activeCount = this.getActiveThreadCount();
+        this.activeTab.textContent = window.$trans.choice(this.countText, activeCount);
+        const archivedCount = this.getArchivedThreadCount();
+        this.archivedTab.textContent = window.$trans.choice(this.archivedCountText, archivedCount);
+    }
+
+    protected resetForm(): void {
+        this.removeEditor();
+        this.formInput.value = '';
+        this.setContentReference('');
+        this.removeReplyTo();
+    }
+
+    protected showForm(): void {
+        this.removeEditor();
+        this.formContainer.toggleAttribute('hidden', false);
+        this.addButtonContainer.toggleAttribute('hidden', true);
+        this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'});
+        this.loadEditor();
+
+        // Ensure the active comments tab is displaying
+        const tabs = window.$components.firstOnElement(this.elem, 'tabs');
+        if (tabs instanceof Tabs) {
+            tabs.show('comment-tab-panel-active');
+        }
+    }
+
+    protected hideForm(): void {
+        this.resetForm();
+        this.formContainer.toggleAttribute('hidden', true);
+        if (this.getActiveThreadCount() > 0) {
+            this.elem.append(this.addButtonContainer);
+        } else {
+            this.commentCountBar.append(this.addButtonContainer);
+        }
+        this.addButtonContainer.toggleAttribute('hidden', false);
+    }
+
+    protected loadEditor(): void {
+        if (this.wysiwygEditor) {
+            this.wysiwygEditor.focus();
+            return;
+        }
+
+        const config = buildForInput({
+            language: this.wysiwygLanguage,
+            containerElement: this.formInput,
+            darkMode: document.documentElement.classList.contains('dark-mode'),
+            textDirection: this.wysiwygTextDirection,
+            drawioUrl: '',
+            pageId: 0,
+            translations: {},
+            translationMap: (window as unknown as Record<string, Object>).editor_translations,
+        });
+
+        (window as unknown as {tinymce: {init: (arg0: Object) => Promise<any>}}).tinymce.init(config).then(editors => {
+            this.wysiwygEditor = editors[0];
+            setTimeout(() => this.wysiwygEditor.focus(), 50);
+        });
+    }
+
+    protected removeEditor(): void {
+        if (this.wysiwygEditor) {
+            this.wysiwygEditor.remove();
+            this.wysiwygEditor = null;
+        }
+    }
+
+    protected getActiveThreadCount(): number {
+        return this.container.querySelectorAll(':scope > .comment-branch:not([hidden])').length;
+    }
+
+    protected getArchivedThreadCount(): number {
+        return this.archiveContainer.querySelectorAll(':scope > .comment-branch').length;
+    }
+
+    protected setReply(commentLocalId: string, commentElement: HTMLElement): void {
+        const targetFormLocation = (commentElement.closest('.comment-branch') as HTMLElement).querySelector('.comment-branch-children') as HTMLElement;
+        targetFormLocation.append(this.formContainer);
+        this.showForm();
+        this.parentId = Number(commentLocalId);
+        this.replyToRow.toggleAttribute('hidden', false);
+        this.formReplyLink.textContent = this.formReplyText.replace('1234', String(this.parentId));
+        this.formReplyLink.href = `#comment${this.parentId}`;
+    }
+
+    protected removeReplyTo(): void {
+        this.parentId = null;
+        this.replyToRow.toggleAttribute('hidden', true);
+        this.container.append(this.formContainer);
+        this.showForm();
+    }
+
+    public startNewComment(contentReference: string): void {
+        this.removeReplyTo();
+        this.setContentReference(contentReference);
+    }
+
+    protected setContentReference(reference: string): void {
+        this.contentReference = reference;
+        this.referenceRow.toggleAttribute('hidden', !Boolean(reference));
+        const [id] = reference.split(':');
+        this.formReferenceLink.href = `#${id}`;
+        this.formReferenceLink.onclick = function(event) {
+            event.preventDefault();
+            const el = document.getElementById(id);
+            if (el) {
+                scrollAndHighlightElement(el);
+            }
+        };
+    }
+
+}
diff --git a/resources/js/components/pointer.js b/resources/js/components/pointer.ts
similarity index 50%
rename from resources/js/components/pointer.js
rename to resources/js/components/pointer.ts
index 292b923e551..4b927045aae 100644
--- a/resources/js/components/pointer.js
+++ b/resources/js/components/pointer.ts
@@ -1,25 +1,39 @@
-import * as DOM from '../services/dom.ts';
+import * as DOM from '../services/dom';
 import {Component} from './component';
-import {copyTextToClipboard} from '../services/clipboard.ts';
+import {copyTextToClipboard} from '../services/clipboard';
+import {hashElement, normalizeNodeTextOffsetToParent} from "../services/dom";
+import {PageComments} from "./page-comments";
 
 export class Pointer extends Component {
 
+    protected showing: boolean = false;
+    protected isMakingSelection: boolean = false;
+    protected targetElement: HTMLElement|null = null;
+    protected targetSelectionRange: Range|null = null;
+
+    protected pointer!: HTMLElement;
+    protected linkInput!: HTMLInputElement;
+    protected linkButton!: HTMLElement;
+    protected includeInput!: HTMLInputElement;
+    protected includeButton!: HTMLElement;
+    protected sectionModeButton!: HTMLElement;
+    protected commentButton!: HTMLElement;
+    protected modeToggles!: HTMLElement[];
+    protected modeSections!: HTMLElement[];
+    protected pageId!: string;
+
     setup() {
-        this.container = this.$el;
         this.pointer = this.$refs.pointer;
-        this.linkInput = this.$refs.linkInput;
+        this.linkInput = this.$refs.linkInput as HTMLInputElement;
         this.linkButton = this.$refs.linkButton;
-        this.includeInput = this.$refs.includeInput;
+        this.includeInput = this.$refs.includeInput as HTMLInputElement;
         this.includeButton = this.$refs.includeButton;
         this.sectionModeButton = this.$refs.sectionModeButton;
+        this.commentButton = this.$refs.commentButton;
         this.modeToggles = this.$manyRefs.modeToggle;
         this.modeSections = this.$manyRefs.modeSection;
         this.pageId = this.$opts.pageId;
 
-        // Instance variables
-        this.showing = false;
-        this.isSelection = false;
-
         this.setupListeners();
     }
 
@@ -30,7 +44,7 @@ export class Pointer extends Component {
 
         // Select all contents on input click
         DOM.onSelect([this.includeInput, this.linkInput], event => {
-            event.target.select();
+            (event.target as HTMLInputElement).select();
             event.stopPropagation();
         });
 
@@ -41,7 +55,7 @@ export class Pointer extends Component {
 
         // Hide pointer when clicking away
         DOM.onEvents(document.body, ['click', 'focus'], () => {
-            if (!this.showing || this.isSelection) return;
+            if (!this.showing || this.isMakingSelection) return;
             this.hidePointer();
         });
 
@@ -52,9 +66,10 @@ export class Pointer extends Component {
         const pageContent = document.querySelector('.page-content');
         DOM.onEvents(pageContent, ['mouseup', 'keyup'], event => {
             event.stopPropagation();
-            const targetEl = event.target.closest('[id^="bkmrk"]');
-            if (targetEl && window.getSelection().toString().length > 0) {
-                this.showPointerAtTarget(targetEl, event.pageX, false);
+            const targetEl = (event.target as HTMLElement).closest('[id^="bkmrk"]');
+            if (targetEl instanceof HTMLElement && (window.getSelection() || '').toString().length > 0) {
+                const xPos = (event instanceof MouseEvent) ? event.pageX : 0;
+                this.showPointerAtTarget(targetEl, xPos, false);
             }
         });
 
@@ -63,28 +78,35 @@ export class Pointer extends Component {
 
         // Toggle between pointer modes
         DOM.onSelect(this.modeToggles, event => {
+            const targetToggle = (event.target as HTMLElement);
             for (const section of this.modeSections) {
-                const show = !section.contains(event.target);
+                const show = !section.contains(targetToggle);
                 section.toggleAttribute('hidden', !show);
             }
 
-            this.modeToggles.find(b => b !== event.target).focus();
+            const otherToggle = this.modeToggles.find(b => b !== targetToggle);
+            otherToggle && otherToggle.focus();
         });
+
+        if (this.commentButton) {
+            DOM.onSelect(this.commentButton, this.createCommentAtPointer.bind(this));
+        }
     }
 
     hidePointer() {
-        this.pointer.style.display = null;
+        this.pointer.style.removeProperty('display');
         this.showing = false;
+        this.targetElement = null;
+        this.targetSelectionRange = null;
     }
 
     /**
      * Move and display the pointer at the given element, targeting the given screen x-position if possible.
-     * @param {Element} element
-     * @param {Number} xPosition
-     * @param {Boolean} keyboardMode
      */
-    showPointerAtTarget(element, xPosition, keyboardMode) {
-        this.updateForTarget(element);
+    showPointerAtTarget(element: HTMLElement, xPosition: number, keyboardMode: boolean) {
+        this.targetElement = element;
+        this.targetSelectionRange = window.getSelection()?.getRangeAt(0) || null;
+        this.updateDomForTarget(element);
 
         this.pointer.style.display = 'block';
         const targetBounds = element.getBoundingClientRect();
@@ -98,18 +120,18 @@ export class Pointer extends Component {
         this.pointer.style.top = `${yOffset}px`;
 
         this.showing = true;
-        this.isSelection = true;
+        this.isMakingSelection = true;
 
         setTimeout(() => {
-            this.isSelection = false;
+            this.isMakingSelection = false;
         }, 100);
 
         const scrollListener = () => {
             this.hidePointer();
-            window.removeEventListener('scroll', scrollListener, {passive: true});
+            window.removeEventListener('scroll', scrollListener);
         };
 
-        element.parentElement.insertBefore(this.pointer, element);
+        element.parentElement?.insertBefore(this.pointer, element);
         if (!keyboardMode) {
             window.addEventListener('scroll', scrollListener, {passive: true});
         }
@@ -117,9 +139,8 @@ export class Pointer extends Component {
 
     /**
      * Update the pointer inputs/content for the given target element.
-     * @param {?Element} element
      */
-    updateForTarget(element) {
+    updateDomForTarget(element: HTMLElement) {
         const permaLink = window.baseUrl(`/link/${this.pageId}#${element.id}`);
         const includeTag = `{{@${this.pageId}#${element.id}}}`;
 
@@ -128,18 +149,18 @@ export class Pointer extends Component {
 
         // Update anchor if present
         const editAnchor = this.pointer.querySelector('#pointer-edit');
-        if (editAnchor && element) {
+        if (editAnchor instanceof HTMLAnchorElement && element) {
             const {editHref} = editAnchor.dataset;
             const elementId = element.id;
 
             // Get the first 50 characters.
-            const queryContent = element.textContent && element.textContent.substring(0, 50);
+            const queryContent = (element.textContent || '').substring(0, 50);
             editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
         }
     }
 
     enterSectionSelectMode() {
-        const sections = Array.from(document.querySelectorAll('.page-content [id^="bkmrk"]'));
+        const sections = Array.from(document.querySelectorAll('.page-content [id^="bkmrk"]')) as HTMLElement[];
         for (const section of sections) {
             section.setAttribute('tabindex', '0');
         }
@@ -147,9 +168,39 @@ export class Pointer extends Component {
         sections[0].focus();
 
         DOM.onEnterPress(sections, event => {
-            this.showPointerAtTarget(event.target, 0, true);
+            this.showPointerAtTarget(event.target as HTMLElement, 0, true);
             this.pointer.focus();
         });
     }
 
+    createCommentAtPointer() {
+        if (!this.targetElement) {
+            return;
+        }
+
+        const refId = this.targetElement.id;
+        const hash = hashElement(this.targetElement);
+        let range = '';
+        if (this.targetSelectionRange) {
+            const commonContainer = this.targetSelectionRange.commonAncestorContainer;
+            if (this.targetElement.contains(commonContainer)) {
+                const start = normalizeNodeTextOffsetToParent(
+                    this.targetSelectionRange.startContainer,
+                    this.targetSelectionRange.startOffset,
+                    this.targetElement
+                );
+                const end = normalizeNodeTextOffsetToParent(
+                    this.targetSelectionRange.endContainer,
+                    this.targetSelectionRange.endOffset,
+                    this.targetElement
+                );
+                range = `${start}-${end}`;
+            }
+        }
+
+        const reference = `${refId}:${hash}:${range}`;
+        const pageComments = window.$components.first('page-comments') as PageComments;
+        pageComments.startNewComment(reference);
+    }
+
 }
diff --git a/resources/js/components/tabs.js b/resources/js/components/tabs.ts
similarity index 74%
rename from resources/js/components/tabs.js
rename to resources/js/components/tabs.ts
index f0fc058ced7..a03d37cd48c 100644
--- a/resources/js/components/tabs.js
+++ b/resources/js/components/tabs.ts
@@ -1,5 +1,9 @@
 import {Component} from './component';
 
+export interface TabsChangeEvent {
+    showing: string;
+}
+
 /**
  * Tabs
  * Uses accessible attributes to drive its functionality.
@@ -19,18 +23,25 @@ import {Component} from './component';
  */
 export class Tabs extends Component {
 
+    protected container!: HTMLElement;
+    protected tabList!: HTMLElement;
+    protected tabs!: HTMLElement[];
+    protected panels!: HTMLElement[];
+
+    protected activeUnder!: number;
+    protected active: null|boolean = null;
+
     setup() {
         this.container = this.$el;
-        this.tabList = this.container.querySelector('[role="tablist"]');
+        this.tabList = this.container.querySelector('[role="tablist"]') as HTMLElement;
         this.tabs = Array.from(this.tabList.querySelectorAll('[role="tab"]'));
         this.panels = Array.from(this.container.querySelectorAll(':scope > [role="tabpanel"], :scope > * > [role="tabpanel"]'));
         this.activeUnder = this.$opts.activeUnder ? Number(this.$opts.activeUnder) : 10000;
-        this.active = null;
 
         this.container.addEventListener('click', event => {
-            const tab = event.target.closest('[role="tab"]');
-            if (tab && this.tabs.includes(tab)) {
-                this.show(tab.getAttribute('aria-controls'));
+            const tab = (event.target as HTMLElement).closest('[role="tab"]');
+            if (tab instanceof HTMLElement && this.tabs.includes(tab)) {
+                this.show(tab.getAttribute('aria-controls') || '');
             }
         });
 
@@ -40,7 +51,7 @@ export class Tabs extends Component {
         this.updateActiveState();
     }
 
-    show(sectionId) {
+    public show(sectionId: string): void {
         for (const panel of this.panels) {
             panel.toggleAttribute('hidden', panel.id !== sectionId);
         }
@@ -51,10 +62,11 @@ export class Tabs extends Component {
             tab.setAttribute('aria-selected', selected ? 'true' : 'false');
         }
 
-        this.$emit('change', {showing: sectionId});
+        const data: TabsChangeEvent = {showing: sectionId};
+        this.$emit('change', data);
     }
 
-    updateActiveState() {
+    protected updateActiveState(): void {
         const active = window.innerWidth < this.activeUnder;
         if (active === this.active) {
             return;
@@ -69,13 +81,13 @@ export class Tabs extends Component {
         this.active = active;
     }
 
-    activate() {
+    protected activate(): void {
         const panelToShow = this.panels.find(p => !p.hasAttribute('hidden')) || this.panels[0];
         this.show(panelToShow.id);
         this.tabList.toggleAttribute('hidden', false);
     }
 
-    deactivate() {
+    protected deactivate(): void {
         for (const panel of this.panels) {
             panel.removeAttribute('hidden');
         }
diff --git a/resources/js/services/__tests__/translations.test.ts b/resources/js/services/__tests__/translations.test.ts
index 043f1745ff6..5014051ab04 100644
--- a/resources/js/services/__tests__/translations.test.ts
+++ b/resources/js/services/__tests__/translations.test.ts
@@ -58,6 +58,11 @@ describe('Translations Service', () => {
             expect(caseB).toEqual('an orange angry big dinosaur');
         });
 
+        test('it provides count as a replacement by default', () => {
+            const caseA = $trans.choice(`:count cats|:count dogs`, 4);
+            expect(caseA).toEqual('4 dogs');
+        });
+
         test('not provided replacements are left as-is', () => {
             const caseA = $trans.choice(`An :a dog`, 5, {});
             expect(caseA).toEqual('An :a dog');
diff --git a/resources/js/services/components.ts b/resources/js/services/components.ts
index c19939e92a9..0e13cd0a09f 100644
--- a/resources/js/services/components.ts
+++ b/resources/js/services/components.ts
@@ -139,8 +139,8 @@ export class ComponentStore {
     /**
      * Get all the components of the given name.
      */
-    public get(name: string): Component[] {
-        return this.components[name] || [];
+    public get<T extends Component>(name: string): T[] {
+        return (this.components[name] || []) as T[];
     }
 
     /**
@@ -150,4 +150,9 @@ export class ComponentStore {
         const elComponents = this.elementComponentMap.get(element) || {};
         return elComponents[name] || null;
     }
+
+    public allWithinElement<T extends Component>(element: HTMLElement, name: string): T[] {
+        const components = this.get<T>(name);
+        return components.filter(c => element.contains(c.$el));
+    }
 }
diff --git a/resources/js/services/dom.ts b/resources/js/services/dom.ts
index c88827bac40..c3817536c85 100644
--- a/resources/js/services/dom.ts
+++ b/resources/js/services/dom.ts
@@ -1,3 +1,5 @@
+import {cyrb53} from "./util";
+
 /**
  * Check if the given param is a HTMLElement
  */
@@ -44,9 +46,11 @@ export function forEach(selector: string, callback: (el: Element) => any) {
 /**
  * Helper to listen to multiple DOM events
  */
-export function onEvents(listenerElement: Element, events: string[], callback: (e: Event) => any): void {
-    for (const eventName of events) {
-        listenerElement.addEventListener(eventName, callback);
+export function onEvents(listenerElement: Element|null, events: string[], callback: (e: Event) => any): void {
+    if (listenerElement) {
+        for (const eventName of events) {
+            listenerElement.addEventListener(eventName, callback);
+        }
     }
 }
 
@@ -178,3 +182,78 @@ export function htmlToDom(html: string): HTMLElement {
 
     return firstChild;
 }
+
+/**
+ * For the given node and offset, return an adjusted offset that's relative to the given parent element.
+ */
+export function normalizeNodeTextOffsetToParent(node: Node, offset: number, parentElement: HTMLElement): number {
+    if (!parentElement.contains(node)) {
+        throw new Error('ParentElement must be a prent of element');
+    }
+
+    let normalizedOffset = offset;
+    let currentNode: Node|null = node.nodeType === Node.TEXT_NODE ?
+        node : node.childNodes[offset];
+
+    while (currentNode !== parentElement && currentNode) {
+        if (currentNode.previousSibling) {
+            currentNode = currentNode.previousSibling;
+            normalizedOffset += (currentNode.textContent?.length || 0);
+        } else {
+            currentNode = currentNode.parentNode;
+        }
+    }
+
+    return normalizedOffset;
+}
+
+/**
+ * Find the target child node and adjusted offset based on a parent node and text offset.
+ * Returns null if offset not found within the given parent node.
+ */
+export function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number): ({node: Node, offset: number}|null) {
+    if (offset === 0) {
+        return { node: parentNode, offset: 0 };
+    }
+
+    let currentOffset = 0;
+    let currentNode = null;
+
+    for (let i = 0; i < parentNode.childNodes.length; i++) {
+        currentNode = parentNode.childNodes[i];
+
+        if (currentNode.nodeType === Node.TEXT_NODE) {
+            // For text nodes, count the length of their content
+            // Returns if within range
+            const textLength = (currentNode.textContent || '').length;
+            if (currentOffset + textLength >= offset) {
+                return {
+                    node: currentNode,
+                    offset: offset - currentOffset
+                };
+            }
+
+            currentOffset += textLength;
+        } else if (currentNode.nodeType === Node.ELEMENT_NODE) {
+            // Otherwise, if an element, track the text length and search within
+            // if in range for the target offset
+            const elementTextLength = (currentNode.textContent || '').length;
+            if (currentOffset + elementTextLength >= offset) {
+                return findTargetNodeAndOffset(currentNode as HTMLElement, offset - currentOffset);
+            }
+
+            currentOffset += elementTextLength;
+        }
+    }
+
+    // Return null if not found within range
+    return null;
+}
+
+/**
+ * Create a hash for the given HTML element content.
+ */
+export function hashElement(element: HTMLElement): string {
+    const normalisedElemText = (element.textContent || '').replace(/\s{2,}/g, '');
+    return cyrb53(normalisedElemText);
+}
\ No newline at end of file
diff --git a/resources/js/services/events.ts b/resources/js/services/events.ts
index be9fba7eca5..6045d51f823 100644
--- a/resources/js/services/events.ts
+++ b/resources/js/services/events.ts
@@ -1,7 +1,9 @@
 import {HttpError} from "./http";
 
+type Listener = (data: any) => void;
+
 export class EventManager {
-    protected listeners: Record<string, ((data: any) => void)[]> = {};
+    protected listeners: Record<string, Listener[]> = {};
     protected stack: {name: string, data: {}}[] = [];
 
     /**
@@ -24,6 +26,17 @@ export class EventManager {
         this.listeners[eventName].push(callback);
     }
 
+    /**
+     * Remove an event listener which is using the given callback for the given event name.
+     */
+    remove(eventName: string, callback: Listener): void {
+        const listeners = this.listeners[eventName] || [];
+        const index = listeners.indexOf(callback);
+        if (index !== -1) {
+            listeners.splice(index, 1);
+        }
+    }
+
     /**
      * Emit an event for public use.
      * Sends the event via the native DOM event handling system.
@@ -53,8 +66,7 @@ export class EventManager {
     /**
      * Notify of standard server-provided validation errors.
      */
-    showValidationErrors(responseErr: {status?: number, data?: object}): void {
-        if (!responseErr.status) return;
+    showValidationErrors(responseErr: HttpError): void {
         if (responseErr.status === 422 && responseErr.data) {
             const message = Object.values(responseErr.data).flat().join('\n');
             this.error(message);
diff --git a/resources/js/services/translations.ts b/resources/js/services/translations.ts
index b37dbdfb074..f548a51d1d1 100644
--- a/resources/js/services/translations.ts
+++ b/resources/js/services/translations.ts
@@ -10,6 +10,7 @@ export class Translator {
      * to use. Similar format at Laravel's 'trans_choice' helper.
      */
     choice(translation: string, count: number, replacements: Record<string, string> = {}): string {
+        replacements = Object.assign({}, {count: String(count)}, replacements);
         const splitText = translation.split('|');
         const exactCountRegex = /^{([0-9]+)}/;
         const rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
diff --git a/resources/js/services/util.ts b/resources/js/services/util.ts
index c5a5d2db804..61a02a3d24d 100644
--- a/resources/js/services/util.ts
+++ b/resources/js/services/util.ts
@@ -144,4 +144,25 @@ function getVersion(): string {
 export function importVersioned(moduleName: string): Promise<object> {
     const importPath = window.baseUrl(`dist/${moduleName}.js?version=${getVersion()}`);
     return import(importPath);
+}
+
+/*
+    cyrb53 (c) 2018 bryc (github.com/bryc)
+    License: Public domain (or MIT if needed). Attribution appreciated.
+    A fast and simple 53-bit string hash function with decent collision resistance.
+    Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
+    Taken from: https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
+*/
+export function cyrb53(str: string, seed: number = 0): string {
+    let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
+    for(let i = 0, ch; i < str.length; i++) {
+        ch = str.charCodeAt(i);
+        h1 = Math.imul(h1 ^ ch, 2654435761);
+        h2 = Math.imul(h2 ^ ch, 1597334677);
+    }
+    h1  = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
+    h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
+    h2  = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
+    h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
+    return String((4294967296 * (2097151 & h2) + (h1 >>> 0)));
 }
\ No newline at end of file
diff --git a/resources/sass/_animations.scss b/resources/sass/_animations.scss
index f1aa3139b8e..ccbe36161b6 100644
--- a/resources/sass/_animations.scss
+++ b/resources/sass/_animations.scss
@@ -67,4 +67,26 @@
   animation-duration: 180ms;
   animation-delay: 0s;
   animation-timing-function: cubic-bezier(.62, .28, .23, .99);
+}
+
+@keyframes highlight {
+  0% {
+    background-color: var(--color-primary-light);
+  }
+  33% {
+    background-color: transparent;
+  }
+  66% {
+    background-color: var(--color-primary-light);
+  }
+  100% {
+    background-color: transparent;
+  }
+}
+
+.anim-highlight {
+  animation-name: highlight;
+  animation-duration: 2s;
+  animation-delay: 0s;
+  animation-timing-function: linear;
 }
\ No newline at end of file
diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss
index 58d39d3ee6e..9e96b39fbb4 100644
--- a/resources/sass/_components.scss
+++ b/resources/sass/_components.scss
@@ -569,6 +569,9 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   border-bottom: 0;
   padding: 0 vars.$xs;
 }
+.tab-container [role="tabpanel"].no-outline:focus {
+  outline: none;
+}
 
 .image-picker .none {
   display: none;
@@ -746,6 +749,52 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   height: calc(100% - vars.$m);
 }
 
+.comment-reference-indicator-wrap a {
+  float: left;
+  margin-top: vars.$xs;
+  font-size: 12px;
+  display: inline-block;
+  font-weight: bold;
+  position: relative;
+  border-radius: 4px;
+  overflow: hidden;
+  padding: 2px 6px 2px 0;
+  margin-inline-end: vars.$xs;
+  color: var(--color-link);
+  span {
+    display: none;
+  }
+  &.outdated span {
+    display: inline;
+  }
+  &.outdated.missing {
+    color: var(--color-warning);
+    pointer-events: none;
+  }
+  svg {
+    width: 24px;
+    margin-inline-end: 0;
+  }
+  &:after {
+    background-color: currentColor;
+    content: '';
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    left: 0;
+    top: 0;
+    opacity: 0.15;
+  }
+  &[href="#"] {
+    color: #444;
+    pointer-events: none;
+  }
+}
+
+.comment-branch .comment-box {
+  margin-bottom: vars.$m;
+}
+
 .comment-branch .comment-branch .comment-branch .comment-branch .comment-thread-indicator {
   display: none;
 }
@@ -760,7 +809,15 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   display: block;
 }
 
+.comment-container .empty-state {
+  display: none;
+}
+.comment-container:not(:has([component="page-comment"])) .empty-state {
+  display: block;
+}
+
 .comment-container-compact .comment-box {
+  margin-bottom: vars.$xs;
   .meta {
     font-size: 0.8rem;
   }
@@ -778,6 +835,29 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   width: vars.$m;
 }
 
+.comment-container-super-compact .comment-box {
+  .meta {
+    font-size: 12px;
+  }
+  .avatar {
+    width: 22px;
+    height: 22px;
+    margin-inline-end: 2px !important;
+  }
+  .content {
+    padding: vars.$xxs vars.$s;
+    line-height: 1.2;
+  }
+  .content p {
+    font-size: 12px;
+  }
+}
+
+.comment-container-super-compact .comment-thread-indicator {
+  width: (vars.$xs + 3px);
+  margin-inline-start: 3px;
+}
+
 #tag-manager .drag-card {
   max-width: 500px;
 }
@@ -1127,4 +1207,21 @@ input.scroll-box-search, .scroll-box-header-item {
 }
 .scroll-box > li.empty-state:last-child {
   display: list-item;
+}
+
+details.section-expander summary {
+  border-top: 1px solid #DDD;
+  @include mixins.lightDark(border-color, #DDD, #000);
+  font-weight: bold;
+  font-size: 12px;
+  color: #888;
+  cursor: pointer;
+  padding-block: vars.$xs;
+}
+details.section-expander:open summary {
+  margin-bottom: vars.$s;
+}
+details.section-expander {
+  border-bottom: 1px solid #DDD;
+  @include mixins.lightDark(border-color, #DDD, #000);
 }
\ No newline at end of file
diff --git a/resources/sass/_content.scss b/resources/sass/_content.scss
index b0176d64ef1..aba1556a983 100644
--- a/resources/sass/_content.scss
+++ b/resources/sass/_content.scss
@@ -11,6 +11,7 @@
   max-width: 840px;
   margin: 0 auto;
   overflow-wrap: break-word;
+  position: relative;
   .align-left {
     text-align: left;
   }
diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss
index 45e58ffc865..83aec46f093 100755
--- a/resources/sass/_pages.scss
+++ b/resources/sass/_pages.scss
@@ -158,11 +158,7 @@ body.tox-fullscreen, body.markdown-fullscreen {
   border-radius: 4px;
   box-shadow: 0 0 12px 1px rgba(0, 0, 0, 0.1);
   @include mixins.lightDark(background-color, #fff, #333);
-  width: 275px;
-
-  &.is-page-editable {
-    width: 328px;
-  }
+  width: 328px;
 
   &:before {
     position: absolute;
@@ -183,7 +179,6 @@ body.tox-fullscreen, body.markdown-fullscreen {
   }
   input, button, a {
     position: relative;
-    border-radius: 0;
     height: 28px;
     font-size: 12px;
     vertical-align: top;
@@ -194,17 +189,21 @@ body.tox-fullscreen, body.markdown-fullscreen {
     border: 1px solid #DDD;
     @include mixins.lightDark(border-color, #ddd, #000);
     color: #666;
-    width: 160px;
-    z-index: 40;
-    padding: 5px 10px;
+    width: auto;
+    flex: 1;
+    z-index: 58;
+    padding: 5px;
+    border-radius: 0;
   }
   .text-button {
     @include mixins.lightDark(color, #444, #AAA);
   }
   .input-group .button {
     line-height: 1;
-    margin: 0 0 0 -4px;
+    margin-inline-start: -1px;
+    margin-block: 0;
     box-shadow: none;
+    border-radius: 0;
   }
   a.button {
     margin: 0;
@@ -218,6 +217,97 @@ body.tox-fullscreen, body.markdown-fullscreen {
   }
 }
 
+// Page inline comments
+.content-comment-highlight {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 0;
+  height: 0;
+  user-select: none;
+  pointer-events: none;
+  &:after {
+    content: '';
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    background-color: var(--color-primary);
+    opacity: 0.25;
+  }
+}
+.content-comment-window {
+  font-size: vars.$fs-m;
+  line-height: 1.4;
+  position: absolute;
+  top: calc(100% + 3px);
+  left: 0;
+  z-index: 92;
+  pointer-events: all;
+  min-width: min(340px, 80vw);
+  @include mixins.lightDark(background-color, #FFF, #222);
+  box-shadow: vars.$bs-hover;
+  border-radius: 4px;
+  overflow: hidden;
+}
+.content-comment-window-actions {
+  background-color: var(--color-primary);
+  color: #FFF;
+  display: flex;
+  align-items: center;
+  justify-content: end;
+  gap: vars.$xs;
+  button {
+    color: #FFF;
+    font-size: 12px;
+    padding: vars.$xs;
+    line-height: 1;
+    cursor: pointer;
+  }
+  button[data-action="jump"] {
+    text-decoration: underline;
+  }
+  svg {
+    fill: currentColor;
+    width: 12px;
+  }
+}
+.content-comment-window-content {
+  padding: vars.$xs vars.$s vars.$xs vars.$xs;
+  max-height: 200px;
+  overflow-y: scroll;
+}
+.content-comment-window-content .comment-reference-indicator-wrap {
+  display: none;
+}
+.content-comment-marker {
+  position: absolute;
+  right: -16px;
+  top: -16px;
+  pointer-events: all;
+  width: min(1.5em, 32px);
+  height: min(1.5em, 32px);
+  border-radius: min(calc(1.5em / 2), 32px);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: var(--color-primary);
+  box-shadow: vars.$bs-hover;
+  color: #FFF;
+  cursor: pointer;
+  z-index: 90;
+  transform: scale(1);
+  transition: transform ease-in-out 120ms;
+  svg {
+    fill: #FFF;
+    width: 80%;
+  }
+}
+.page-content [id^="bkmrk-"]:hover .content-comment-marker {
+  transform: scale(1.15);
+}
+
 // Page editor sidebar toolbox
 .floating-toolbox {
   @include mixins.lightDark(background-color, #FFF, #222);
diff --git a/resources/views/comments/comment-branch.blade.php b/resources/views/comments/comment-branch.blade.php
index 78d19ac3ea4..658c33219c3 100644
--- a/resources/views/comments/comment-branch.blade.php
+++ b/resources/views/comments/comment-branch.blade.php
@@ -1,13 +1,16 @@
+{{--
+$branch CommentTreeNode
+--}}
 <div class="comment-branch">
-    <div class="mb-m">
-        @include('comments.comment', ['comment' => $branch['comment']])
+    <div>
+        @include('comments.comment', ['comment' => $branch->comment])
     </div>
     <div class="flex-container-row">
         <div class="comment-thread-indicator-parent">
             <div class="comment-thread-indicator"></div>
         </div>
         <div class="comment-branch-children flex">
-            @foreach($branch['children'] as $childBranch)
+            @foreach($branch->children as $childBranch)
                 @include('comments.comment-branch', ['branch' => $childBranch])
             @endforeach
         </div>
diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php
index 2bf89d6832d..eadf3518722 100644
--- a/resources/views/comments/comment.blade.php
+++ b/resources/views/comments/comment.blade.php
@@ -4,9 +4,9 @@
 <div component="{{ $readOnly ? '' : 'page-comment' }}"
      option:page-comment:comment-id="{{ $comment->id }}"
      option:page-comment:comment-local-id="{{ $comment->local_id }}"
-     option:page-comment:comment-parent-id="{{ $comment->parent_id }}"
      option:page-comment:updated-text="{{ trans('entities.comment_updated_success') }}"
      option:page-comment:deleted-text="{{ trans('entities.comment_deleted_success') }}"
+     option:page-comment:archive-text="{{ $comment->archived ? trans('entities.comment_unarchive_success') : trans('entities.comment_archive_success') }}"
      option:page-comment:wysiwyg-language="{{ $locale->htmlLang() }}"
      option:page-comment:wysiwyg-text-direction="{{ $locale->htmlDirection() }}"
      id="comment{{$comment->local_id}}"
@@ -38,6 +38,12 @@ class="comment-box">
                     @if(userCan('comment-create-all'))
                         <button refs="page-comment@reply-button" type="button" class="text-button text-muted hover-underline text-small p-xs">@icon('reply') {{ trans('common.reply') }}</button>
                     @endif
+                    @if(!$comment->parent_id && (userCan('comment-update', $comment) || userCan('comment-delete', $comment)))
+                        <button refs="page-comment@archive-button"
+                                type="button"
+                                data-is-archived="{{ $comment->archived ? 'true' : 'false' }}"
+                                class="text-button text-muted hover-underline text-small p-xs">@icon('archive') {{ trans('common.' . ($comment->archived ? 'unarchive' : 'archive')) }}</button>
+                    @endif
                     @if(userCan('comment-update', $comment))
                         <button refs="page-comment@edit-button" type="button" class="text-button text-muted hover-underline text-small p-xs">@icon('edit') {{ trans('common.edit') }}</button>
                     @endif
@@ -74,6 +80,16 @@ class="comment-box">
                 <a class="text-muted text-small" href="#comment{{ $comment->parent_id }}">@icon('reply'){{ trans('entities.comment_in_reply_to', ['commentId' => '#' . $comment->parent_id]) }}</a>
             </p>
         @endif
+        @if($comment->content_ref)
+            <div class="comment-reference-indicator-wrap">
+                <a component="page-comment-reference"
+                   option:page-comment-reference:reference="{{ $comment->content_ref }}"
+                   option:page-comment-reference:view-comment-text="{{ trans('entities.comment_view') }}"
+                   option:page-comment-reference:jump-to-thread-text="{{ trans('entities.comment_jump_to_thread') }}"
+                   option:page-comment-reference:close-text="{{ trans('common.close') }}"
+                   href="#">@icon('bookmark'){{ trans('entities.comment_reference') }} <span>{{ trans('entities.comment_reference_outdated') }}</span></a>
+            </div>
+        @endif
         {!! $commentHtml  !!}
     </div>
 
diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php
index 48bf885fff5..51c08d69a85 100644
--- a/resources/views/comments/comments.blade.php
+++ b/resources/views/comments/comments.blade.php
@@ -1,40 +1,75 @@
-<section component="page-comments"
+<section components="page-comments tabs"
          option:page-comments:page-id="{{ $page->id }}"
          option:page-comments:created-text="{{ trans('entities.comment_created_success') }}"
-         option:page-comments:count-text="{{ trans('entities.comment_count') }}"
+         option:page-comments:count-text="{{ trans('entities.comment_thread_count') }}"
+         option:page-comments:archived-count-text="{{ trans('entities.comment_archived_count') }}"
          option:page-comments:wysiwyg-language="{{ $locale->htmlLang() }}"
          option:page-comments:wysiwyg-text-direction="{{ $locale->htmlDirection() }}"
-         class="comments-list"
+         class="comments-list tab-container"
          aria-label="{{ trans('entities.comments') }}">
 
-    <div refs="page-comments@comment-count-bar" class="grid half left-focus v-center no-row-gap">
-        <h5 refs="page-comments@comments-title">{{ trans_choice('entities.comment_count', $commentTree->count(), ['count' => $commentTree->count()]) }}</h5>
+    <div refs="page-comments@comment-count-bar" class="flex-container-row items-center">
+        <div role="tablist" class="flex">
+            <button type="button"
+                    role="tab"
+                    id="comment-tab-active"
+                    aria-controls="comment-tab-panel-active"
+                    refs="page-comments@active-tab"
+                    aria-selected="true">{{ trans_choice('entities.comment_thread_count', $commentTree->activeThreadCount()) }}</button>
+            <button type="button"
+                    role="tab"
+                    id="comment-tab-archived"
+                    aria-controls="comment-tab-panel-archived"
+                    refs="page-comments@archived-tab"
+                    aria-selected="false">{{ trans_choice('entities.comment_archived_count', count($commentTree->getArchived())) }}</button>
+        </div>
         @if ($commentTree->empty() && userCan('comment-create-all'))
-            <div class="text-m-right" refs="page-comments@add-button-container">
+            <div class="ml-m" refs="page-comments@add-button-container">
                 <button type="button"
                         refs="page-comments@add-comment-button"
-                        class="button outline">{{ trans('entities.comment_add') }}</button>
+                        class="button outline mb-m">{{ trans('entities.comment_add') }}</button>
             </div>
         @endif
     </div>
 
-    <div refs="page-comments@commentContainer" class="comment-container">
-        @foreach($commentTree->get() as $branch)
+    <div id="comment-tab-panel-active"
+         tabindex="0"
+         role="tabpanel"
+         aria-labelledby="comment-tab-active"
+         class="comment-container no-outline">
+        <div refs="page-comments@comment-container">
+            @foreach($commentTree->getActive() as $branch)
+                @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])
+            @endforeach
+        </div>
+
+        <p class="text-center text-muted italic empty-state">{{ trans('entities.comment_none') }}</p>
+
+        @if(userCan('comment-create-all'))
+            @include('comments.create')
+            @if (!$commentTree->empty())
+                <div refs="page-comments@addButtonContainer" class="flex-container-row">
+                    <button type="button"
+                            refs="page-comments@add-comment-button"
+                            class="button outline ml-auto">{{ trans('entities.comment_add') }}</button>
+                </div>
+            @endif
+        @endif
+    </div>
+
+    <div refs="page-comments@archive-container"
+         id="comment-tab-panel-archived"
+         tabindex="0"
+         role="tabpanel"
+         aria-labelledby="comment-tab-archived"
+         hidden="hidden"
+         class="comment-container no-outline">
+        @foreach($commentTree->getArchived() as $branch)
             @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])
         @endforeach
+            <p class="text-center text-muted italic empty-state">{{ trans('entities.comment_none') }}</p>
     </div>
 
-    @if(userCan('comment-create-all'))
-        @include('comments.create')
-        @if (!$commentTree->empty())
-            <div refs="page-comments@addButtonContainer" class="text-right">
-                <button type="button"
-                        refs="page-comments@add-comment-button"
-                        class="button outline">{{ trans('entities.comment_add') }}</button>
-            </div>
-        @endif
-    @endif
-
     @if(userCan('comment-create-all') || $commentTree->canUpdateAny())
         @push('body-end')
             <script src="{{ versioned_asset('libs/tinymce/tinymce.min.js') }}" nonce="{{ $cspNonce }}" defer></script>
diff --git a/resources/views/comments/create.blade.php b/resources/views/comments/create.blade.php
index 417f0c60602..134ed516425 100644
--- a/resources/views/comments/create.blade.php
+++ b/resources/views/comments/create.blade.php
@@ -12,6 +12,16 @@
                 </div>
             </div>
         </div>
+        <div refs="page-comments@reference-row" hidden class="primary-background-light text-muted px-s py-xs">
+            <div class="grid left-focus v-center">
+                <div>
+                    <a refs="page-comments@formReferenceLink" href="#">{{ trans('entities.comment_reference') }}</a>
+                </div>
+                <div class="text-right">
+                    <button refs="page-comments@remove-reference-button" class="text-button">{{ trans('common.remove') }}</button>
+                </div>
+            </div>
+        </div>
 
         <div class="content px-s pt-s">
             <form refs="page-comments@form" novalidate>
diff --git a/resources/views/pages/parts/pointer.blade.php b/resources/views/pages/parts/pointer.blade.php
index 56f36cb75f3..f6487b66600 100644
--- a/resources/views/pages/parts/pointer.blade.php
+++ b/resources/views/pages/parts/pointer.blade.php
@@ -6,29 +6,36 @@
          tabindex="-1"
          aria-label="{{ trans('entities.pages_pointer_label') }}"
          class="pointer-container">
-        <div class="pointer flex-container-row items-center justify-space-between p-s anim {{ userCan('page-update', $page) ? 'is-page-editable' : ''}}" >
-            <div refs="pointer@mode-section" class="flex-container-row items-center gap-s">
+        <div class="pointer flex-container-row items-center justify-space-between gap-xs p-xs anim" >
+            <div refs="pointer@mode-section" class="flex flex-container-row items-center gap-xs">
                 <button refs="pointer@mode-toggle"
                         title="{{ trans('entities.pages_pointer_toggle_link') }}"
                         class="text-button icon px-xs">@icon('link')</button>
-                <div class="input-group">
+                <div class="input-group flex flex-container-row items-center">
                     <input refs="pointer@link-input" aria-label="{{ trans('entities.pages_pointer_permalink') }}" readonly="readonly" type="text" id="pointer-url" placeholder="url">
-                    <button refs="pointer@link-button" class="button outline icon" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
+                    <button refs="pointer@link-button" class="button outline icon px-xs" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
                 </div>
             </div>
-            <div refs="pointer@mode-section" hidden class="flex-container-row items-center gap-s">
+            <div refs="pointer@mode-section" hidden class="flex flex-container-row items-center gap-xs">
                 <button refs="pointer@mode-toggle"
                         title="{{ trans('entities.pages_pointer_toggle_include') }}"
                         class="text-button icon px-xs">@icon('include')</button>
-                <div class="input-group">
+                <div class="input-group flex flex-container-row items-center">
                     <input refs="pointer@include-input" aria-label="{{ trans('entities.pages_pointer_include_tag') }}" readonly="readonly" type="text" id="pointer-include" placeholder="include">
-                    <button refs="pointer@include-button" class="button outline icon" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
+                    <button refs="pointer@include-button" class="button outline icon px-xs" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
                 </div>
             </div>
-            @if(userCan('page-update', $page))
-                <a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
-                   class="button primary outline icon heading-edit-icon ml-s px-s" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
-            @endif
+            <div>
+                @if(userCan('page-update', $page))
+                    <a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
+                       class="button primary outline icon heading-edit-icon px-xs" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
+                @endif
+                @if($commentTree->enabled() && userCan('comment-create-all'))
+                    <button type="button"
+                            refs="pointer@comment-button"
+                            class="button primary outline icon px-xs m-none" title="{{ trans('entities.comment_add')}}">@icon('comment')</button>
+                @endif
+            </div>
         </div>
     </div>
 
diff --git a/resources/views/pages/parts/toolbox-comments.blade.php b/resources/views/pages/parts/toolbox-comments.blade.php
index d632b85c689..72958a2fede 100644
--- a/resources/views/pages/parts/toolbox-comments.blade.php
+++ b/resources/views/pages/parts/toolbox-comments.blade.php
@@ -1,3 +1,6 @@
+{{--
+$comments - CommentTree
+--}}
 <div refs="editor-toolbox@tab-content" data-tab-content="comments" class="toolbox-tab-content">
     <h4>{{ trans('entities.comments') }}</h4>
 
@@ -5,11 +8,19 @@
         <p class="text-muted small mb-m">
             {{ trans('entities.comment_editor_explain') }}
         </p>
-        @foreach($comments->get() as $branch)
+        @foreach($comments->getActive() as $branch)
             @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => true])
         @endforeach
         @if($comments->empty())
-            <p class="italic text-muted">{{ trans('common.no_items') }}</p>
+            <p class="italic text-muted">{{ trans('entities.comment_none') }}</p>
+        @endif
+        @if($comments->archivedThreadCount() > 0)
+            <details class="section-expander mt-s">
+                <summary>{{ trans('entities.comment_archived_threads') }}</summary>
+                @foreach($comments->getArchived() as $branch)
+                    @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => true])
+                @endforeach
+            </details>
         @endif
     </div>
 </div>
\ No newline at end of file
diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php
index e3a31dd5ebf..137d43bdb1a 100644
--- a/resources/views/pages/show.blade.php
+++ b/resources/views/pages/show.blade.php
@@ -28,12 +28,6 @@ class="page-content clearfix">
     @include('entities.sibling-navigation', ['next' => $next, 'previous' => $previous])
 
     @if ($commentTree->enabled())
-        @if(($previous || $next))
-            <div class="px-xl print-hidden">
-                <hr class="darker">
-            </div>
-        @endif
-
         <div class="comments-container mb-l print-hidden">
             @include('comments.comments', ['commentTree' => $commentTree, 'page' => $page])
             <div class="clearfix"></div>
diff --git a/routes/web.php b/routes/web.php
index 8184725834c..ea3efe1ac77 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -179,6 +179,8 @@
 
     // Comments
     Route::post('/comment/{pageId}', [ActivityControllers\CommentController::class, 'savePageComment']);
+    Route::put('/comment/{id}/archive', [ActivityControllers\CommentController::class, 'archive']);
+    Route::put('/comment/{id}/unarchive', [ActivityControllers\CommentController::class, 'unarchive']);
     Route::put('/comment/{id}', [ActivityControllers\CommentController::class, 'update']);
     Route::delete('/comment/{id}', [ActivityControllers\CommentController::class, 'destroy']);
 
diff --git a/tests/Entity/CommentDisplayTest.php b/tests/Entity/CommentDisplayTest.php
new file mode 100644
index 00000000000..4e9640baeed
--- /dev/null
+++ b/tests/Entity/CommentDisplayTest.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace Entity;
+
+use BookStack\Activity\ActivityType;
+use BookStack\Activity\Models\Comment;
+use BookStack\Entities\Models\Page;
+use Tests\TestCase;
+
+class CommentDisplayTest extends TestCase
+{
+    public function test_reply_comments_are_nested()
+    {
+        $this->asAdmin();
+        $page = $this->entities->page();
+
+        $this->postJson("/comment/$page->id", ['html' => '<p>My new comment</p>']);
+        $this->postJson("/comment/$page->id", ['html' => '<p>My new comment</p>']);
+
+        $respHtml = $this->withHtml($this->get($page->getUrl()));
+        $respHtml->assertElementCount('.comment-branch', 3);
+        $respHtml->assertElementNotExists('.comment-branch .comment-branch');
+
+        $comment = $page->comments()->first();
+        $resp = $this->postJson("/comment/$page->id", [
+            'html' => '<p>My nested comment</p>', 'parent_id' => $comment->local_id
+        ]);
+        $resp->assertStatus(200);
+
+        $respHtml = $this->withHtml($this->get($page->getUrl()));
+        $respHtml->assertElementCount('.comment-branch', 4);
+        $respHtml->assertElementContains('.comment-branch .comment-branch', 'My nested comment');
+    }
+
+    public function test_comments_are_visible_in_the_page_editor()
+    {
+        $page = $this->entities->page();
+
+        $this->asAdmin()->postJson("/comment/$page->id", ['html' => '<p>My great comment to see in the editor</p>']);
+
+        $respHtml = $this->withHtml($this->get($page->getUrl('/edit')));
+        $respHtml->assertElementContains('.comment-box .content', 'My great comment to see in the editor');
+    }
+
+    public function test_comment_creator_name_truncated()
+    {
+        [$longNamedUser] = $this->users->newUserWithRole(['name' => 'Wolfeschlegelsteinhausenbergerdorff'], ['comment-create-all', 'page-view-all']);
+        $page = $this->entities->page();
+
+        $comment = Comment::factory()->make();
+        $this->actingAs($longNamedUser)->postJson("/comment/$page->id", $comment->getAttributes());
+
+        $pageResp = $this->asAdmin()->get($page->getUrl());
+        $pageResp->assertSee('Wolfeschlegels…');
+    }
+
+    public function test_comment_editor_js_loaded_with_create_or_edit_permissions()
+    {
+        $editor = $this->users->editor();
+        $page = $this->entities->page();
+
+        $resp = $this->actingAs($editor)->get($page->getUrl());
+        $resp->assertSee('tinymce.min.js?', false);
+        $resp->assertSee('window.editor_translations', false);
+        $resp->assertSee('component="entity-selector"', false);
+
+        $this->permissions->removeUserRolePermissions($editor, ['comment-create-all']);
+        $this->permissions->grantUserRolePermissions($editor, ['comment-update-own']);
+
+        $resp = $this->actingAs($editor)->get($page->getUrl());
+        $resp->assertDontSee('tinymce.min.js?', false);
+        $resp->assertDontSee('window.editor_translations', false);
+        $resp->assertDontSee('component="entity-selector"', false);
+
+        Comment::factory()->create([
+            'created_by'  => $editor->id,
+            'entity_type' => 'page',
+            'entity_id'   => $page->id,
+        ]);
+
+        $resp = $this->actingAs($editor)->get($page->getUrl());
+        $resp->assertSee('tinymce.min.js?', false);
+        $resp->assertSee('window.editor_translations', false);
+        $resp->assertSee('component="entity-selector"', false);
+    }
+
+    public function test_comment_displays_relative_times()
+    {
+        $page = $this->entities->page();
+        $comment = Comment::factory()->create(['entity_id' => $page->id, 'entity_type' => $page->getMorphClass()]);
+        $comment->created_at = now()->subWeek();
+        $comment->updated_at = now()->subDay();
+        $comment->save();
+
+        $pageResp = $this->asAdmin()->get($page->getUrl());
+        $html = $this->withHtml($pageResp);
+
+        // Create date shows relative time as text to user
+        $html->assertElementContains('.comment-box', 'commented 1 week ago');
+        // Updated indicator has full time as title
+        $html->assertElementContains('.comment-box span[title^="Updated ' . $comment->updated_at->format('Y-m-d') .  '"]', 'Updated');
+    }
+
+    public function test_comment_displays_reference_if_set()
+    {
+        $page = $this->entities->page();
+        $comment = Comment::factory()->make([
+            'content_ref' => 'bkmrk-a:abc:4-1',
+            'local_id'   =>  10,
+        ]);
+        $page->comments()->save($comment);
+
+        $html = $this->withHtml($this->asEditor()->get($page->getUrl()));
+        $html->assertElementExists('#comment10 .comment-reference-indicator-wrap a');
+    }
+
+    public function test_archived_comments_are_shown_in_their_own_container()
+    {
+        $page = $this->entities->page();
+        $comment = Comment::factory()->make(['local_id' => 44]);
+        $page->comments()->save($comment);
+
+        $html = $this->withHtml($this->asEditor()->get($page->getUrl()));
+        $html->assertElementExists('#comment-tab-panel-active #comment44');
+        $html->assertElementNotExists('#comment-tab-panel-archived .comment-box');
+
+        $comment->archived = true;
+        $comment->save();
+
+        $html = $this->withHtml($this->asEditor()->get($page->getUrl()));
+        $html->assertElementExists('#comment-tab-panel-archived #comment44.comment-box');
+        $html->assertElementNotExists('#comment-tab-panel-active #comment44');
+    }
+}
diff --git a/tests/Entity/CommentTest.php b/tests/Entity/CommentStoreTest.php
similarity index 56%
rename from tests/Entity/CommentTest.php
rename to tests/Entity/CommentStoreTest.php
index 9e019e3d148..8b8a5d488b8 100644
--- a/tests/Entity/CommentTest.php
+++ b/tests/Entity/CommentStoreTest.php
@@ -7,7 +7,7 @@
 use BookStack\Entities\Models\Page;
 use Tests\TestCase;
 
-class CommentTest extends TestCase
+class CommentStoreTest extends TestCase
 {
     public function test_add_comment()
     {
@@ -33,6 +33,32 @@ public function test_add_comment()
 
         $this->assertActivityExists(ActivityType::COMMENT_CREATE);
     }
+    public function test_add_comment_stores_content_reference_only_if_format_valid()
+    {
+        $validityByRefs = [
+            'bkmrk-my-title:4589284922:4-3' => true,
+            'bkmrk-my-title:4589284922:' => true,
+            'bkmrk-my-title:4589284922:abc' => false,
+            'my-title:4589284922:' => false,
+            'bkmrk-my-title-4589284922:' => false,
+        ];
+
+        $page = $this->entities->page();
+
+        foreach ($validityByRefs as $ref => $valid) {
+            $this->asAdmin()->postJson("/comment/$page->id", [
+                'html' => '<p>My comment</p>',
+                'parent_id' => null,
+                'content_ref' => $ref,
+            ]);
+
+            if ($valid) {
+                $this->assertDatabaseHas('comments', ['entity_id' => $page->id, 'content_ref' => $ref]);
+            } else {
+                $this->assertDatabaseMissing('comments', ['entity_id' => $page->id, 'content_ref' => $ref]);
+            }
+        }
+    }
 
     public function test_comment_edit()
     {
@@ -80,6 +106,89 @@ public function test_comment_delete()
         $this->assertActivityExists(ActivityType::COMMENT_DELETE);
     }
 
+    public function test_comment_archive_and_unarchive()
+    {
+        $this->asAdmin();
+        $page = $this->entities->page();
+
+        $comment = Comment::factory()->make();
+        $page->comments()->save($comment);
+        $comment->refresh();
+
+        $this->put("/comment/$comment->id/archive");
+
+        $this->assertDatabaseHas('comments', [
+            'id' => $comment->id,
+            'archived' => true,
+        ]);
+
+        $this->assertActivityExists(ActivityType::COMMENT_UPDATE);
+
+        $this->put("/comment/$comment->id/unarchive");
+
+        $this->assertDatabaseHas('comments', [
+            'id' => $comment->id,
+            'archived' => false,
+        ]);
+
+        $this->assertActivityExists(ActivityType::COMMENT_UPDATE);
+    }
+
+    public function test_archive_endpoints_require_delete_or_edit_permissions()
+    {
+        $viewer = $this->users->viewer();
+        $page = $this->entities->page();
+
+        $comment = Comment::factory()->make();
+        $page->comments()->save($comment);
+        $comment->refresh();
+
+        $endpoints = ["/comment/$comment->id/archive", "/comment/$comment->id/unarchive"];
+
+        foreach ($endpoints as $endpoint) {
+            $resp = $this->actingAs($viewer)->put($endpoint);
+            $this->assertPermissionError($resp);
+        }
+
+        $this->permissions->grantUserRolePermissions($viewer, ['comment-delete-all']);
+
+        foreach ($endpoints as $endpoint) {
+            $resp = $this->actingAs($viewer)->put($endpoint);
+            $resp->assertOk();
+        }
+
+        $this->permissions->removeUserRolePermissions($viewer, ['comment-delete-all']);
+        $this->permissions->grantUserRolePermissions($viewer, ['comment-update-all']);
+
+        foreach ($endpoints as $endpoint) {
+            $resp = $this->actingAs($viewer)->put($endpoint);
+            $resp->assertOk();
+        }
+    }
+
+    public function test_non_top_level_comments_cant_be_archived_or_unarchived()
+    {
+        $this->asAdmin();
+        $page = $this->entities->page();
+
+        $comment = Comment::factory()->make();
+        $page->comments()->save($comment);
+        $subComment = Comment::factory()->make(['parent_id' => $comment->id]);
+        $page->comments()->save($subComment);
+        $subComment->refresh();
+
+        $resp = $this->putJson("/comment/$subComment->id/archive");
+        $resp->assertStatus(400);
+
+        $this->assertDatabaseHas('comments', [
+            'id' => $subComment->id,
+            'archived' => false,
+        ]);
+
+        $resp = $this->putJson("/comment/$subComment->id/unarchive");
+        $resp->assertStatus(400);
+    }
+
     public function test_scripts_cannot_be_injected_via_comment_html()
     {
         $page = $this->entities->page();
@@ -139,96 +248,4 @@ public function test_comment_html_is_limited()
             'html' => $expected,
         ]);
     }
-
-    public function test_reply_comments_are_nested()
-    {
-        $this->asAdmin();
-        $page = $this->entities->page();
-
-        $this->postJson("/comment/$page->id", ['html' => '<p>My new comment</p>']);
-        $this->postJson("/comment/$page->id", ['html' => '<p>My new comment</p>']);
-
-        $respHtml = $this->withHtml($this->get($page->getUrl()));
-        $respHtml->assertElementCount('.comment-branch', 3);
-        $respHtml->assertElementNotExists('.comment-branch .comment-branch');
-
-        $comment = $page->comments()->first();
-        $resp = $this->postJson("/comment/$page->id", [
-            'html' => '<p>My nested comment</p>', 'parent_id' => $comment->local_id
-        ]);
-        $resp->assertStatus(200);
-
-        $respHtml = $this->withHtml($this->get($page->getUrl()));
-        $respHtml->assertElementCount('.comment-branch', 4);
-        $respHtml->assertElementContains('.comment-branch .comment-branch', 'My nested comment');
-    }
-
-    public function test_comments_are_visible_in_the_page_editor()
-    {
-        $page = $this->entities->page();
-
-        $this->asAdmin()->postJson("/comment/$page->id", ['html' => '<p>My great comment to see in the editor</p>']);
-
-        $respHtml = $this->withHtml($this->get($page->getUrl('/edit')));
-        $respHtml->assertElementContains('.comment-box .content', 'My great comment to see in the editor');
-    }
-
-    public function test_comment_creator_name_truncated()
-    {
-        [$longNamedUser] = $this->users->newUserWithRole(['name' => 'Wolfeschlegelsteinhausenbergerdorff'], ['comment-create-all', 'page-view-all']);
-        $page = $this->entities->page();
-
-        $comment = Comment::factory()->make();
-        $this->actingAs($longNamedUser)->postJson("/comment/$page->id", $comment->getAttributes());
-
-        $pageResp = $this->asAdmin()->get($page->getUrl());
-        $pageResp->assertSee('Wolfeschlegels…');
-    }
-
-    public function test_comment_editor_js_loaded_with_create_or_edit_permissions()
-    {
-        $editor = $this->users->editor();
-        $page = $this->entities->page();
-
-        $resp = $this->actingAs($editor)->get($page->getUrl());
-        $resp->assertSee('tinymce.min.js?', false);
-        $resp->assertSee('window.editor_translations', false);
-        $resp->assertSee('component="entity-selector"', false);
-
-        $this->permissions->removeUserRolePermissions($editor, ['comment-create-all']);
-        $this->permissions->grantUserRolePermissions($editor, ['comment-update-own']);
-
-        $resp = $this->actingAs($editor)->get($page->getUrl());
-        $resp->assertDontSee('tinymce.min.js?', false);
-        $resp->assertDontSee('window.editor_translations', false);
-        $resp->assertDontSee('component="entity-selector"', false);
-
-        Comment::factory()->create([
-            'created_by'  => $editor->id,
-            'entity_type' => 'page',
-            'entity_id'   => $page->id,
-        ]);
-
-        $resp = $this->actingAs($editor)->get($page->getUrl());
-        $resp->assertSee('tinymce.min.js?', false);
-        $resp->assertSee('window.editor_translations', false);
-        $resp->assertSee('component="entity-selector"', false);
-    }
-
-    public function test_comment_displays_relative_times()
-    {
-        $page = $this->entities->page();
-        $comment = Comment::factory()->create(['entity_id' => $page->id, 'entity_type' => $page->getMorphClass()]);
-        $comment->created_at = now()->subWeek();
-        $comment->updated_at = now()->subDay();
-        $comment->save();
-
-        $pageResp = $this->asAdmin()->get($page->getUrl());
-        $html = $this->withHtml($pageResp);
-
-        // Create date shows relative time as text to user
-        $html->assertElementContains('.comment-box', 'commented 1 week ago');
-        // Updated indicator has full time as title
-        $html->assertElementContains('.comment-box span[title^="Updated ' . $comment->updated_at->format('Y-m-d') .  '"]', 'Updated');
-    }
 }