diff --git a/pygitweb/static/board.js b/pygitweb/static/board.js
index bd61fee..1fd5ba7 100644
--- a/pygitweb/static/board.js
+++ b/pygitweb/static/board.js
@@ -4,6 +4,87 @@
 
   var ICONS_BASE = '/static/icons/';
   var ICON_LIST = ['cloud.svg', 'cloud-filled.svg', 'cloud-storm.svg', 'cloud-down.svg', 'cloud-up.svg', 'cloud-exclamation.svg'];
+  var boardMarkdownConverter = null;
+
+  function whenShowdownReady(cb) {
+    if (typeof showdown !== 'undefined') {
+      cb();
+      return;
+    }
+    var script = document.getElementById('Showdown-script');
+    if (!script) return;
+    function run() {
+      if (typeof showdown !== 'undefined') cb();
+    }
+    script.addEventListener('load', run);
+    if (script.getAttribute('data-loaded')) run();
+  }
+
+  function getBoardMarkdownConverter() {
+    if (!boardMarkdownConverter && typeof showdown !== 'undefined') {
+      boardMarkdownConverter = new showdown.Converter({ ghCompatibleHeaderId: true });
+    }
+    return boardMarkdownConverter;
+  }
+
+  function markdownToHtml(source) {
+    var conv = getBoardMarkdownConverter();
+    if (!conv) return null;
+    return conv.makeHtml(source || '');
+  }
+
+  function htmlToPlainPreview(html) {
+    var tmp = document.createElement('div');
+    tmp.innerHTML = html;
+    return (tmp.textContent || '').replace(/\s+/g, ' ').trim();
+  }
+
+  function markdownPlainPreview(source, maxLen) {
+    var md = source || '';
+    var html = markdownToHtml(md);
+    var text = html !== null ? htmlToPlainPreview(html) : md.replace(/\s+/g, ' ').trim();
+    if (maxLen && text.length > maxLen) return text.slice(0, maxLen) + '\u2026';
+    return text;
+  }
+
+  function getMarkdownFromElement(el) {
+    if (!el) return '';
+    if (Object.prototype.hasOwnProperty.call(el.dataset, 'boardMarkdown')) {
+      return el.dataset.boardMarkdown;
+    }
+    return (el.textContent || '').trim();
+  }
+
+  function setMarkdownElement(el, source) {
+    if (!el) return;
+    var md = source || '';
+    el.dataset.boardMarkdown = md;
+    el.classList.add('markdown-body', 'board-card-markdown');
+    function apply() {
+      var html = markdownToHtml(md);
+      if (html !== null) {
+        el.innerHTML = html;
+      } else {
+        el.textContent = md;
+      }
+    }
+    if (typeof showdown !== 'undefined') {
+      apply();
+    } else {
+      el.textContent = md;
+      whenShowdownReady(apply);
+    }
+  }
+
+  function syncCompactDescription(card, source) {
+    var preview = card.querySelector('.board-card-description-preview');
+    var full = card.querySelector('.board-card-description-full');
+    var md = source !== undefined ? source : getMarkdownFromElement(full);
+    if (preview) {
+      preview.textContent = md ? markdownPlainPreview(md, 120) : '';
+      preview.dataset.boardMarkdown = md;
+    }
+  }
 
   function preloadIcons() {
     ICON_LIST.forEach(function(name) {
@@ -58,37 +139,35 @@
     card.dataset.taskStatus = status;
     var titleEl = card.querySelector('.board-card-title');
     if (titleEl) titleEl.textContent = task.title || 'Untitled';
-    var descEl = card.querySelector('.board-card-description');
-    if (descEl) descEl.textContent = task.description || '';
+    var descFull = card.querySelector('.board-card-description-full');
+    if (descFull) setMarkdownElement(descFull, task.description || '');
+    syncCompactDescription(card, task.description || '');
     setCardIcon(card, 'cloud-filled');
     var meta = card.querySelector('.board-card-meta');
     if (meta) {
       var badge = meta.querySelector('.badge');
       var assigneeSpan = meta.querySelectorAll('.text-truncate')[0];
-      var commentsSpan = meta.querySelector('.ms-auto');
+      var commentsSpan = meta.querySelector('.board-card-comments-count-meta') || meta.querySelector('.ms-auto');
       if (badge) {
         badge.textContent = task.priority || 'LOW';
         badge.className = 'badge badge-outline text-' + (task.priority === 'CRITICAL' ? 'danger' : task.priority === 'HIGH' ? 'warning' : 'secondary');
       }
       if (assigneeSpan) assigneeSpan.textContent = task.assignee || 'None';
-      if (commentsSpan) commentsSpan.textContent = (task.comments_count || 0) + ' comment' + ((task.comments_count || 0) !== 1 ? 's' : '');
+      if (commentsSpan) commentsSpan.textContent = commentCountLabel(task.comments_count || 0);
     }
     var details = card.querySelector('.board-card-details');
     if (details) {
-      var fullDesc = details.querySelector('.board-card-description-full');
       var dueDate = details.querySelector('.board-card-due-date');
       var updatedAt = details.querySelector('.board-card-updated-at');
       var statusEl = details.querySelector('.board-card-status');
       var priorityEl = details.querySelector('.board-card-priority');
       var assigneeEl = details.querySelector('.board-card-assignee');
-      var commentsCountEl = details.querySelector('.board-card-comments-count');
-      if (fullDesc) fullDesc.textContent = task.description || '';
       if (dueDate) dueDate.textContent = task.due_date || 'None';
       if (updatedAt) updatedAt.textContent = task.updated_at || 'None';
       if (statusEl) statusEl.textContent = status;
       if (priorityEl) priorityEl.textContent = task.priority || 'LOW';
       if (assigneeEl) assigneeEl.textContent = task.assignee || 'None';
-      if (commentsCountEl) commentsCountEl.textContent = (task.comments_count || 0) + ' comment' + ((task.comments_count || 0) !== 1 ? 's' : '');
+      updateCardCommentCount(card, task.comments_count || 0);
     }
     return clone;
   }
@@ -210,6 +289,166 @@
   var boardColumns = document.querySelector('.board-columns');
   var draggedCard = null;
   var didDrag = false;
+  var boardAuth = { auth_enabled: false, username: null };
+
+  function loadBoardAuthStatus() {
+    return fetch('/auth/status', { credentials: 'same-origin' })
+      .then(function(res) { return res.ok ? res.json() : null; })
+      .then(function(s) {
+        if (!s) return;
+        boardAuth.auth_enabled = !!s.auth_enabled;
+        boardAuth.username = s.username || null;
+      })
+      .catch(function() {});
+  }
+  loadBoardAuthStatus();
+
+  function canAddComment() {
+    return !boardAuth.auth_enabled || !!boardAuth.username;
+  }
+
+  function commentCountLabel(n) {
+    return n + ' comment' + (n !== 1 ? 's' : '');
+  }
+
+  function updateCardCommentCount(card, count) {
+    var n = count || 0;
+    var meta = card.querySelector('.board-card-meta');
+    if (meta) {
+      var commentsSpan = meta.querySelector('.board-card-comments-count-meta') || meta.querySelector('.ms-auto');
+      if (commentsSpan) commentsSpan.textContent = commentCountLabel(n);
+    }
+  }
+
+  function commentAuthorInitials(author) {
+    var parts = (author || '').trim().split(/\s+/).filter(Boolean);
+    if (!parts.length) return '?';
+    if (parts.length === 1) return parts[0].charAt(0).toUpperCase();
+    return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
+  }
+
+  function commentAvatarHtml(comment) {
+    var wrap = document.createElement('span');
+    wrap.className = 'board-card-comment-avatar';
+    if (comment.gravatar_url) {
+      var img = document.createElement('img');
+      img.src = comment.gravatar_url;
+      img.alt = '';
+      img.width = 32;
+      img.height = 32;
+      img.onerror = function() {
+        wrap.replaceChildren();
+        var fb = document.createElement('span');
+        fb.className = 'board-card-comment-avatar-fallback';
+        fb.textContent = commentAuthorInitials(comment.author);
+        wrap.appendChild(fb);
+      };
+      wrap.appendChild(img);
+    } else {
+      var fb = document.createElement('span');
+      fb.className = 'board-card-comment-avatar-fallback';
+      fb.textContent = commentAuthorInitials(comment.author);
+      wrap.appendChild(fb);
+    }
+    return wrap;
+  }
+
+  function renderCommentsList(card, comments) {
+    var list = card.querySelector('.board-card-comments-list');
+    if (!list) return;
+    list.innerHTML = '';
+    if (!Array.isArray(comments) || !comments.length) return;
+    comments.forEach(function(comment) {
+      var row = document.createElement('div');
+      row.className = 'board-card-comment-item';
+      row.appendChild(commentAvatarHtml(comment));
+      var body = document.createElement('div');
+      body.className = 'board-card-comment-body';
+      var authorEl = document.createElement('div');
+      authorEl.className = 'board-card-comment-author';
+      authorEl.textContent = comment.author || 'Unknown';
+      var contentEl = document.createElement('div');
+      contentEl.className = 'board-card-comment-content';
+      setMarkdownElement(contentEl, comment.content || '');
+      body.appendChild(authorEl);
+      body.appendChild(contentEl);
+      row.appendChild(body);
+      list.appendChild(row);
+    });
+  }
+
+  function syncCommentFormVisibility(card) {
+    var form = card.querySelector('.board-card-comment-form');
+    var hint = card.querySelector('.board-card-comment-signin-hint');
+    if (!form || !hint) return;
+    if (canAddComment()) {
+      form.classList.remove('d-none');
+      hint.classList.add('d-none');
+    } else {
+      form.classList.add('d-none');
+      hint.classList.remove('d-none');
+    }
+  }
+
+  function loadCommentsForCard(card) {
+    var taskRef = card.dataset.taskRef;
+    if (!taskRef) return Promise.resolve();
+    var ctx = getBoardContext();
+    if (!ctx.project) return Promise.resolve();
+    var listUrl = '/comments/list?' + new URLSearchParams({
+      project: ctx.project,
+      board: ctx.board,
+      task: taskRef
+    }).toString();
+    return fetch(listUrl, { credentials: 'same-origin' })
+      .then(function(res) { return res.ok ? res.json() : []; })
+      .then(function(comments) {
+        if (!Array.isArray(comments)) comments = [];
+        renderCommentsList(card, comments);
+        updateCardCommentCount(card, comments.length);
+        return comments;
+      })
+      .catch(function() { return []; });
+  }
+
+  function setupCommentForm(popoutCard) {
+    var form = popoutCard.querySelector('.board-card-comment-form');
+    if (!form || form.dataset.bound === '1') return;
+    form.dataset.bound = '1';
+    syncCommentFormVisibility(popoutCard);
+    form.addEventListener('submit', function(e) {
+      e.preventDefault();
+      if (!canAddComment()) return;
+      var taskRef = popoutCard.dataset.taskRef;
+      if (!taskRef) return;
+      var input = form.querySelector('.board-card-comment-input');
+      var content = (input && input.value) ? input.value.trim() : '';
+      if (!content) return;
+      var ctx = getBoardContext();
+      if (!ctx.project) return;
+      var createUrl = '/comments/create?' + new URLSearchParams({
+        project: ctx.project,
+        board: ctx.board,
+        task: taskRef,
+        content: content
+      }).toString();
+      fetch(createUrl, { method: 'POST', credentials: 'same-origin' })
+        .then(function(res) {
+          if (!res.ok) {
+            var msg = (res.status === 401 || res.status === 403)
+              ? 'Not authorized to add comments (sign in again if needed).'
+              : 'Could not post comment.';
+            showBoardError(msg);
+            return null;
+          }
+          if (input) input.value = '';
+          return loadCommentsForCard(popoutCard);
+        })
+        .catch(function() {
+          showBoardError('Could not post comment.');
+        });
+    });
+  }
 
   function findBoardCardByTaskRef(taskRef) {
     if (!taskRef) return null;
@@ -314,7 +553,7 @@
       var priorityDisplay = (pEl && pEl.textContent) ? pEl.textContent.trim() : 'LOW';
       var body = {
         title: (tEl && tEl.textContent) ? tEl.textContent.trim() || 'Untitled' : 'Untitled',
-        description: (dEl && dEl.textContent) ? dEl.textContent.trim() : '',
+        description: getMarkdownFromElement(dEl),
         status: statusDisplay ? statusDisplay.replace(/\s+/g, '_').toUpperCase() : 'TODO',
         priority: priorityDisplay ? priorityDisplay.replace(/\s+/g, '_').toUpperCase() : 'LOW',
         assignee: (aEl && aEl.textContent && aEl.textContent.trim() !== 'None') ? aEl.textContent.trim() : null,
@@ -357,7 +596,8 @@
       var textarea = popoutCard.querySelector('.board-card-description-edit');
       if (!textarea) return;
       var val = (textarea.value || '').trim();
-      descEl.textContent = val || '';
+      setMarkdownElement(descEl, val);
+      syncCompactDescription(popoutCard, val);
       descEl.classList.remove('d-none');
       textarea.remove();
       pushTaskUpdate();
@@ -365,7 +605,7 @@
     if (descEl) {
       descEl.addEventListener('click', function () {
         if (popoutCard.querySelector('.board-card-description-edit')) return;
-        var val = (descEl.textContent || '').trim();
+        var val = getMarkdownFromElement(descEl);
         var textarea = document.createElement('textarea');
         textarea.className = 'form-control form-control-sm board-card-description-edit mt-1';
         textarea.rows = 3;
@@ -471,6 +711,13 @@
     var details = card.querySelector('.board-card-details');
     if (details) details.classList.remove('d-none');
     setupPopoutEditables(card);
+    setupCommentForm(card);
+    var descFull = card.querySelector('.board-card-description-full');
+    if (descFull && !Object.prototype.hasOwnProperty.call(descFull.dataset, 'boardMarkdown')) {
+      setMarkdownElement(descFull, getMarkdownFromElement(descFull));
+      syncCompactDescription(card);
+    }
+    loadCommentsForCard(card);
     var overlay = document.createElement('div');
     overlay.className = 'board-card-popout-overlay';
     overlay.appendChild(card);
diff --git a/pygitweb/static/main.css b/pygitweb/static/main.css
index 0b5bb3f..9428120 100644
--- a/pygitweb/static/main.css
+++ b/pygitweb/static/main.css
@@ -1017,8 +1017,12 @@ pre {
 	.board-card-popout {
 		position: relative;
 		z-index: 1061;
-		max-width: 40rem;
-		width: 100%;
+		width: 70%;
+		max-width: 70%;
+		height: 80%;
+		max-height: 80%;
+		display: flex;
+		flex-direction: column;
 		pointer-events: auto;
 		box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.2);
 	}
@@ -1026,6 +1030,12 @@ pre {
 
 /* Popout card: editable fields (click to edit) */
 .board-card-popout {
+	.card-body {
+		flex: 1;
+		min-height: 0;
+		overflow-y: auto;
+	}
+
 	.board-card-editable,
 	.board-card-title.board-card-editable {
 		cursor: pointer;
@@ -1050,6 +1060,82 @@ pre {
 		min-height: 4rem;
 		resize: vertical;
 	}
+
+	.board-card-comment-item {
+		display: flex;
+		gap: 0.5rem;
+		align-items: flex-start;
+	}
+
+	.board-card-comment-item + .board-card-comment-item {
+		margin-top: 0.75rem;
+	}
+
+	.board-card-comment-avatar {
+		flex-shrink: 0;
+		width: 2rem;
+		height: 2rem;
+		border-radius: 50%;
+		overflow: hidden;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		background-color: var(--pgw-bg-muted);
+	}
+
+	.board-card-comment-avatar img {
+		width: 100%;
+		height: 100%;
+		object-fit: cover;
+	}
+
+	.board-card-comment-avatar-fallback {
+		font-size: 0.65rem;
+		font-weight: 600;
+		color: var(--pgw-text-muted);
+		text-transform: uppercase;
+	}
+
+	.board-card-comment-body {
+		min-width: 0;
+		flex: 1;
+	}
+
+	.board-card-comment-author {
+		font-weight: 500;
+		color: var(--pgw-text);
+	}
+
+	.board-card-description-preview {
+		color: var(--pgw-text);
+	}
+
+	.board-card-description-full.board-card-markdown,
+	.board-card-comment-content.board-card-markdown {
+		color: var(--pgw-text);
+		word-break: break-word;
+	}
+
+	.board-card-markdown.markdown-body {
+		font-size: inherit;
+		line-height: 1.5;
+
+		> :first-child {
+			margin-top: 0;
+		}
+
+		> :last-child {
+			margin-bottom: 0;
+		}
+
+		p {
+			margin-bottom: 0.5rem;
+		}
+
+		pre {
+			margin-bottom: 0.5rem;
+		}
+	}
 }
 
 /* Search page: input, status, and result lines */
diff --git a/pygitweb/tasks.py b/pygitweb/tasks.py
index 51754b0..0fe197d 100644
--- a/pygitweb/tasks.py
+++ b/pygitweb/tasks.py
@@ -6,8 +6,9 @@ Data is stored in the repo ODB/RefDB via pygittools.tasks (Board, Task, Comment)
 from __future__ import annotations
 
 import os
+import re
 import time
-from typing import Any
+from typing import Annotated, Any
 
 import pygit2
 from fastapi import APIRouter, Depends, HTTPException, Query, Request
@@ -24,10 +25,16 @@ from pygittools.tasks import (
 	get_task,
 	get_task_by_oid,
 )
-from pygitweb.auth import require_active_user_if_auth_enabled, require_permission
+from pygitweb.auth import (
+	access_token_from_request,
+	principal_from_session,
+	require_active_user_if_auth_enabled,
+	require_permission,
+)
 from pygitweb.config import settings
 from pygitweb.dependencies import ValidatedQueryProject
-from pygitweb.permissions import Permission
+from pygitweb.gravatar import gravatar_url
+from pygitweb.permissions import Permission, PermissionPrincipal
 
 # Well-known empty tree OID for boards/tasks when repo has no HEAD
 EMPTY_TREE_OID = pygit2.Oid(hex="4b825dc642cb6eb9a060e54bf8d69288fbee4904")
@@ -48,6 +55,58 @@ def _repo_head_or_empty(repo: pygit2.Repository) -> pygit2.Oid | str:
 		return EMPTY_TREE_OID
 
 
+def _sync_board_task_oid(
+	repo: pygit2.Repository,
+	board: str,
+	old_oid: str,
+	new_oid: pygit2.Oid | str,
+	*,
+	move_to_end: bool = False,
+) -> None:
+	"""Replace a task OID on the board after the task tag object was rewritten."""
+	b = get_board(repo, _board_ref(board))
+	if not b or not getattr(b, "tasks", None):
+		return
+	new_oid_str = str(new_oid)
+	tasks_list = [new_oid_str if o == old_oid else o for o in b.tasks]
+	if move_to_end and new_oid_str in tasks_list:
+		tasks_list = [o for o in tasks_list if o != new_oid_str] + [new_oid_str]
+	b.tasks = tasks_list
+	b.update_message()
+	b.write(repo)
+
+
+def _task_from_board_entry(
+	repo: pygit2.Repository,
+	oid_hex: str | pygit2.Oid,
+	*,
+	board: str | None = None,
+) -> Task | None:
+	"""
+	Load a task listed on a board by OID.
+
+	Board entries can lag behind ``refs/tags/tasks/…`` after a task rewrite (e.g. a new
+	comment). When the ref points at a newer tag object, return that version and repair
+	the board list when ``board`` is provided.
+	"""
+	oid = pygit2.Oid(hex=oid_hex) if isinstance(oid_hex, str) else oid_hex
+	t = get_task_by_oid(repo, oid)
+	if not t or not t.name:
+		return t
+	try:
+		if t.name not in repo.references:
+			return t
+		current_oid = repo.references[t.name].resolve().target
+	except KeyError:
+		return t
+	if str(current_oid) == str(oid):
+		return t
+	t_current = get_task(repo, t.name)
+	if t_current and board:
+		_sync_board_task_oid(repo, board, str(oid), current_oid)
+	return t_current or t
+
+
 def create_board_for_project(
 	project: str,
 	name: str = "Tasks",
@@ -178,8 +237,7 @@ def get_board_tasks_grouped(
 	by_status: dict[str, list[dict[str, Any]]] = {s: [] for s in BOARD_STATUS_ORDER}
 	for oid_hex in task_oids:
 		try:
-			oid = pygit2.Oid(hex=oid_hex) if isinstance(oid_hex, str) else oid_hex
-			t = get_task_by_oid(repo, oid)
+			t = _task_from_board_entry(repo, oid_hex, board=board_name)
 			if t:
 				d = _task_to_json(t)
 				status_key = (d.get("status") or "TODO") if d else "TODO"
@@ -217,8 +275,7 @@ def task_list(
 	tasks = []
 	for oid_hex in task_oids:
 		try:
-			oid = pygit2.Oid(hex=oid_hex) if isinstance(oid_hex, str) else oid_hex
-			t = get_task_by_oid(repo, oid)
+			t = _task_from_board_entry(repo, oid_hex, board=board)
 			if t:
 				tasks.append(_task_to_json(t))
 		except (ValueError, KeyError, TypeError):
@@ -323,16 +380,7 @@ async def task_update(
 	old_oid = str(repo.references[ref].resolve().target)
 	t.update_message()
 	new_oid = t.write(repo)
-	board_ref = _board_ref(board)
-	b = get_board(repo, board_ref)
-	if b and getattr(b, "tasks", None):
-		new_oid_str = str(new_oid)
-		tasks_list = [new_oid_str if o == old_oid else o for o in b.tasks]
-		if status_changed and new_oid_str in tasks_list:
-			tasks_list = [o for o in tasks_list if o != new_oid_str] + [new_oid_str]
-		b.tasks = tasks_list
-		b.update_message()
-		b.write(repo)
+	_sync_board_task_oid(repo, board, old_oid, new_oid, move_to_end=status_changed)
 	return JSONResponse(content=_task_to_json(t))
 
 
@@ -370,15 +418,41 @@ def task_delete(
 comment_router = APIRouter(tags=["comments"])
 
 
+def _parse_tagger_display(tagger: str) -> tuple[str, str | None]:
+	s = (tagger or "").strip()
+	if not s:
+		return ("Unknown", None)
+	m = re.match(r"^(.+?)\s*<([^>]+)>", s)
+	if m:
+		name = m.group(1).strip() or "Unknown"
+		return (name, m.group(2).strip())
+	return (s, None)
+
+
+def _tagger_from_principal(principal: PermissionPrincipal | None) -> str:
+	if principal is None:
+		return ""
+	email = (principal.email or "").strip() or f"{principal.username}@local"
+	name = principal.username.strip() or email
+	return f"{name} <{email}>"
+
+
 def _comment_to_json(c: Comment) -> dict[str, Any]:
+	author, email = _parse_tagger_display(c.tagger)
 	return {
 		"content": c.content,
 		"tagger": c.tagger,
+		"author": author,
+		"gravatar_url": gravatar_url(email) if email else None,
 		"created_at": c.created_at.isoformat() if c.created_at else None,
 		"edited_at": c.edited_at.isoformat() if c.edited_at else None,
 	}
 
 
+def _comment_sort_key(entry: dict[str, Any]) -> str:
+	return entry.get("edited_at") or entry.get("created_at") or ""
+
+
 @comment_router.get(
 	"/list",
 	response_class=JSONResponse,
@@ -405,6 +479,7 @@ def comment_list(
 				comments.append(_comment_to_json(c))
 		except (ValueError, KeyError, TypeError):
 			continue
+	comments.sort(key=_comment_sort_key)
 	return JSONResponse(content=comments)
 
 
@@ -418,6 +493,7 @@ def comment_create(
 	board: str = Query(..., description="Board name"),
 	task: str = Query(..., description="Task ref"),
 	content: str = Query(..., description="Comment content"),
+	token: Annotated[str | None, Depends(access_token_from_request)] = None,
 ) -> JSONResponse:
 	"""Create a new comment on a task."""
 	repo = pygit2.Repository(os.path.join(settings.PROJECTROOT, project))
@@ -429,13 +505,22 @@ def comment_create(
 		target_oid = repo.references[task_ref].resolve().target
 	except KeyError as exc:
 		raise HTTPException(status_code=404, detail="Task not found") from exc
-	comment = Comment(target=target_oid, tagger="", content=content or "")
+	tagger = _tagger_from_principal(principal_from_session(token))
+	comment = Comment(target=target_oid, tagger=tagger, content=content or "")
 	comment_oid = comment.write(repo)
 	t.comments = getattr(t, "comments", []) or []
 	t.comments.append(str(comment_oid))
+	old_task_oid = str(target_oid)
 	t.update_message()
-	t.write(repo)
-	return JSONResponse(content={"oid": str(comment_oid), "message": "Comment created"})
+	new_task_oid = t.write(repo)
+	_sync_board_task_oid(repo, board, old_task_oid, new_task_oid)
+	return JSONResponse(
+		content={
+			"oid": str(comment_oid),
+			"message": "Comment created",
+			"comments_count": len(t.comments),
+		}
+	)
 
 
 @comment_router.post(
@@ -471,8 +556,14 @@ async def comment_update(
 	t = get_task(repo, task_ref)
 	if t and getattr(t, "comments", None):
 		t.comments = [str(new_oid) if str(co) == comment_oid else co for co in t.comments]
+		try:
+			old_task_oid = str(repo.references[task_ref].resolve().target)
+		except KeyError:
+			old_task_oid = ""
 		t.update_message()
-		t.write(repo)
+		new_task_oid = t.write(repo)
+		if old_task_oid:
+			_sync_board_task_oid(repo, board, old_task_oid, new_task_oid)
 	return JSONResponse(
 		content={
 			**_comment_to_json(c),
diff --git a/pygitweb/tasks_routes_test.py b/pygitweb/tasks_routes_test.py
index a599045..cd2a1c0 100644
--- a/pygitweb/tasks_routes_test.py
+++ b/pygitweb/tasks_routes_test.py
@@ -146,6 +146,8 @@ class TestTaskRoutes:
 			comment = body[0]
 			assert comment["content"] == "alpha note"
 			assert comment["tagger"] == "alice <alice@example.com>"
+			assert comment["author"] == "alice"
+			assert comment["gravatar_url"] is not None
 			assert comment["created_at"] is not None
 			assert comment["edited_at"] is not None
 
@@ -287,6 +289,81 @@ class TestTaskRoutes:
 			b_oid = str(repo.references[b_ref].resolve().target)
 			assert loaded.tasks.index(b_oid) < loaded.tasks.index(a_oid)
 
+		def test_task_list_resolves_stale_board_task_oid(self, mutable_env: dict[str, str | TestClient]) -> None:
+			"""Board may still list an old task OID after the ref was rewritten."""
+			client = mutable_env["client"]
+			assert isinstance(client, TestClient)
+			repo = pygit2.Repository(str(Path(settings.PROJECTROOT) / str(mutable_env["project"])))
+			board = get_board(repo, f"{BOARD_REF_PREFIX}{mutable_env['board']}")
+			assert board is not None
+			stale_oid = board.tasks[0]
+			task_ref = f"{TASK_REF_PREFIX}mutable_seed"
+			t = Task(
+				EMPTY_TREE_OID,
+				task_ref,
+				tagger="",
+				title="Stale board entry",
+				description="",
+				status=Task.Status.TODO,
+				priority=Task.Priority.LOW,
+			)
+			t.comments = ["abc123"]
+			t.update_message()
+			current_oid = t.write(repo)
+			board.tasks = [stale_oid]
+			board.update_message()
+			board.write(repo)
+
+			list_resp = client.get(
+				"/tasks/list",
+				params={"project": mutable_env["project"], "board": mutable_env["board"]},
+			)
+			assert list_resp.status_code == 200
+			by_ref = {entry["ref"]: entry for entry in list_resp.json()}
+			assert by_ref[task_ref]["comments_count"] == 1
+			reloaded = get_board(repo, f"{BOARD_REF_PREFIX}{mutable_env['board']}")
+			assert reloaded is not None
+			assert str(current_oid) in reloaded.tasks
+
+		def test_comment_create_updates_task_list_comments_count(
+			self, mutable_env: dict[str, str | TestClient]
+		) -> None:
+			client = mutable_env["client"]
+			assert isinstance(client, TestClient)
+			create_resp = client.post(
+				"/tasks/create",
+				params={
+					"project": mutable_env["project"],
+					"board": mutable_env["board"],
+					"title": "Comment count task",
+				},
+			)
+			assert create_resp.status_code == 200
+			task_ref: str = create_resp.json()["ref"]
+			comment_resp = client.post(
+				"/comments/create",
+				params={
+					"project": mutable_env["project"],
+					"board": mutable_env["board"],
+					"task": task_ref,
+					"content": "board count note",
+				},
+			)
+			assert comment_resp.status_code == 200
+			assert comment_resp.json()["comments_count"] == 1
+			list_resp = client.get(
+				"/tasks/list",
+				params={"project": mutable_env["project"], "board": mutable_env["board"]},
+			)
+			assert list_resp.status_code == 200
+			by_ref = {entry["ref"]: entry for entry in list_resp.json()}
+			assert by_ref[task_ref]["comments_count"] == 1
+			repo = pygit2.Repository(str(Path(settings.PROJECTROOT) / str(mutable_env["project"])))
+			board = get_board(repo, f"{BOARD_REF_PREFIX}{mutable_env['board']}")
+			assert board is not None
+			ref_oid = str(repo.references[task_ref].resolve().target)
+			assert ref_oid in board.tasks
+
 		def test_comment_delete_response_format(self, mutable_env: dict[str, str | TestClient]) -> None:
 			client = mutable_env["client"]
 			assert isinstance(client, TestClient)
diff --git a/pygitweb/templates/board_card.html b/pygitweb/templates/board_card.html
index 10faa43..f68ee73 100644
--- a/pygitweb/templates/board_card.html
+++ b/pygitweb/templates/board_card.html
@@ -3,7 +3,7 @@
   <div class="card-body py-2 px-3 position-relative">
     <span class="board-card-icon-wrap position-absolute top-0 end-0 mt-1 me-1" aria-hidden="true"><span class="board-card-icon" data-icon="cloud"></span></span>
     <div class="board-card-title fw-medium text-truncate"{% if task %} title="{{ task.title }}"{% endif %}>{% if task %}{{ task.title or 'Untitled' }}{% endif %}</div>
-    <div class="board-card-description small text-muted mt-1 text-truncate">{% if task and task.description %}{{ task.description }}{% else %}{% endif %}</div>
+    <div class="board-card-description small board-card-description-preview mt-1 text-truncate">{% if task and task.description %}{{ task.description }}{% else %}{% endif %}</div>
     <div class="board-card-meta d-flex align-items-center gap-1 mt-1 small text-muted">
       {% if task %}
         {% if task.priority and task.priority != 'LOW' %}
@@ -16,11 +16,7 @@
         {% else %}
         <span class="text-truncate">None</span>
         {% endif %}
-        {% if task.comments_count %}
-        <span class="ms-auto">{{ task.comments_count }} comment{{ 's' if task.comments_count != 1 else '' }}</span>
-        {% else %}
-        <span class="ms-auto">0 comments</span>
-        {% endif %}
+        <span class="ms-auto board-card-comments-count-meta">{{ task.comments_count | default(0) }} comment{{ 's' if (task.comments_count | default(0)) != 1 else '' }}</span>
       {% else %}
         <span class="badge badge-outline text-secondary">LOW</span>
         <span class="text-truncate">None</span>
@@ -28,13 +24,21 @@
       {% endif %}
     </div>
     <div class="board-card-details d-none mt-2 pt-2 border-top small">
-      <div class="mb-2"><strong class="text-muted">Description</strong><div class="board-card-description-full board-card-editable text-muted mt-1">{% if task and task.description %}{{ task.description }}{% else %}{% endif %}</div></div>
+      <div class="mb-2"><strong class="text-muted">Description</strong><div class="board-card-description-full board-card-editable board-card-markdown mt-1">{% if task and task.description %}{{ task.description }}{% else %}{% endif %}</div></div>
       <div class="mb-2"><strong class="text-muted">Due date</strong><div class="board-card-due-date board-card-editable text-muted mt-1">{% if task and task.due_date %}{{ task.due_date }}{% else %}None{% endif %}</div></div>
       <div class="mb-2"><strong class="text-muted">Last modified</strong><div class="board-card-updated-at text-muted mt-1">{% if task and task.updated_at %}{{ task.updated_at }}{% else %}None{% endif %}</div></div>
       <div class="mb-2"><strong class="text-muted">Status</strong><div class="board-card-status board-card-editable text-muted mt-1">{% if task %}{{ task.status or 'TODO' }}{% else %}TODO{% endif %}</div></div>
       <div class="mb-2"><strong class="text-muted">Priority</strong><div class="board-card-priority board-card-editable text-muted mt-1">{% if task and task.priority %}{{ task.priority }}{% else %}LOW{% endif %}</div></div>
       <div class="mb-2"><strong class="text-muted">Assignee</strong><div class="board-card-assignee text-muted mt-1">{% if task and task.assignee %}{{ task.assignee }}{% else %}None{% endif %}</div></div>
-      <div class="mb-0"><strong class="text-muted">Comments</strong><div class="board-card-comments-count text-muted mt-1">{% if task and task.comments_count %}{{ task.comments_count }} comment{{ 's' if task.comments_count != 1 else '' }}{% else %}0 comments{% endif %}</div></div>
+      <div class="mb-0 board-card-comments-section">
+        <strong class="text-muted">Comments</strong>
+        <div class="board-card-comments-list mt-2"></div>
+        <form class="board-card-comment-form d-none mt-2" action="#" method="post">
+          <textarea class="form-control form-control-sm board-card-comment-input" rows="2" placeholder="Add a comment…" aria-label="Comment"></textarea>
+          <button type="submit" class="btn btn-sm btn-primary mt-1">Post comment</button>
+        </form>
+        <p class="board-card-comment-signin-hint text-muted small mt-1 mb-0 d-none">Sign in to add a comment.</p>
+      </div>
     </div>
   </div>
 </div>
