diff --git a/internal/api/model/status.go b/internal/api/model/status.go
index 00be868f1..a9c668565 100644
--- a/internal/api/model/status.go
+++ b/internal/api/model/status.go
@@ -126,8 +126,17 @@ type WebStatus struct {
 	// display this status in the web view.
 	Indent int
 
-	// This status is the first status after
-	// the "main" thread, so it and everything
+	// This status is the last visible status
+	// in the main thread, so everything below
+	// can be considered "replies".
+	ThreadLastMain bool
+
+	// This status is the one around which
+	// the thread context was constructed.
+	ThreadContextStatus bool
+
+	// This status is the first visibile status
+	// after the "main" thread, so it and everything
 	// below it can be considered "replies".
 	ThreadFirstReply bool
 }
diff --git a/internal/api/model/statuscontext.go b/internal/api/model/statuscontext.go
index 205672dc8..f2519c0a3 100644
--- a/internal/api/model/statuscontext.go
+++ b/internal/api/model/statuscontext.go
@@ -29,15 +29,16 @@ type ThreadContext struct {
 }
 
 type WebThreadContext struct {
-	// Parents in the thread.
-	Ancestors []*WebStatus `json:"ancestors"`
+	// Status around which this
+	// thread ctx was constructed.
+	Status *WebStatus
 
-	// Children in the thread.
-	Descendants []*WebStatus `json:"descendants"`
-
-	// The status around which the ancestors
-	// + descendants context was constructed.
-	Status *WebStatus `json:"-"`
+	// Ordered slice of statuses
+	// for rendering in template.
+	//
+	// Includes ancestors, target
+	// status, and descendants.
+	Statuses []*WebStatus
 
 	// Total length of
 	// the main thread.
diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go
index 4271bd233..013cf4827 100644
--- a/internal/processing/status/context.go
+++ b/internal/processing/status/context.go
@@ -400,8 +400,7 @@ func (p *Processor) WebContextGet(
 
 	// Start preparing web context.
 	wCtx := &apimodel.WebThreadContext{
-		Ancestors:   make([]*apimodel.WebStatus, 0, len(iCtx.ancestors)),
-		Descendants: make([]*apimodel.WebStatus, 0, len(iCtx.descendants)),
+		Statuses: make([]*apimodel.WebStatus, 0, len(wholeThread)),
 	}
 
 	var (
@@ -415,72 +414,70 @@ func (p *Processor) WebContextGet(
 		// ie., who created first post in the thread.
 		contextAcctID = wholeThread[0].AccountID
 
-		// Position of target status in wholeThread,
-		// we put it on top of ancestors.
-		targetStatusIdx = len(iCtx.ancestors)
-
-		// Position from which we should add
-		// to descendants and not to ancestors.
-		descendantsIdx = targetStatusIdx + 1
-
 		// Whether we've reached end of "main"
 		// thread and are now looking at replies.
 		inReplies bool
 
-		// Index in wholeThread where
-		// the "main" thread ends.
+		// Index in wholeThread
+		// where replies begin.
 		firstReplyIdx int
 
 		// We should mark the next **VISIBLE**
 		// reply as the first reply.
-		markNextVisibleAsReply bool
+		markNextVisibleAsFirstReply bool
 	)
 
 	for idx, status := range wholeThread {
 		if !inReplies {
-			// Haven't reached end
-			// of "main" thread yet.
-			//
-			// First post in wholeThread can't
-			// be a self reply, so ignore it.
-			//
-			// That aside, first non-self-reply
-			// in wholeThread means the "main"
-			// thread is now over.
-			if idx != 0 && !isSelfReply(status, contextAcctID) {
-				// Jot some stuff down.
-				firstReplyIdx = idx
+			// Check if we've reached replies
+			// by looking for the first status
+			// that's not a self-reply, ie.,
+			// not a post in the "main" thread.
+			switch {
+			case idx == 0:
+				// First post in wholeThread can't
+				// be a self reply anyway because
+				// it (very likely) doesn't reply
+				// to anything, so ignore it.
+
+			case !isSelfReply(status, contextAcctID):
+				// This is not a self-reply, which
+				// means it's a reply from another
+				// account. So, replies start here.
 				inReplies = true
-				markNextVisibleAsReply = true
+				firstReplyIdx = idx
+				markNextVisibleAsFirstReply = true
 			}
 		}
 
 		// Ensure status is actually
-		// visible to just anyone.
+		// visible to just anyone, and
+		// hide / don't include it if not.
 		v, err := p.filter.StatusVisible(ctx, nil, status)
 		if err != nil || !v {
-			// Skip this one.
 			if !inReplies {
+				// Main thread entry hidden.
 				wCtx.ThreadHidden++
 			} else {
+				// Reply hidden.
 				wCtx.ThreadRepliesHidden++
 			}
 			continue
 		}
 
-		// Prepare status to add to thread context.
-		apiStatus, err := p.converter.StatusToWebStatus(ctx, status)
+		// Prepare visible status to add to thread context.
+		webStatus, err := p.converter.StatusToWebStatus(ctx, status)
 		if err != nil {
 			continue
 		}
 
-		if markNextVisibleAsReply {
+		if markNextVisibleAsFirstReply {
 			// This is the first visible
 			// "reply / comment", so the
 			// little "x amount of replies"
 			// header should go above this.
-			apiStatus.ThreadFirstReply = true
-			markNextVisibleAsReply = false
+			webStatus.ThreadFirstReply = true
+			markNextVisibleAsFirstReply = false
 		}
 
 		// If this is a reply, work out the indent of
@@ -491,59 +488,47 @@ func (p *Processor) WebContextGet(
 			case !ok:
 				// No parent with
 				// indent, start at 0.
-				apiStatus.Indent = 0
+				webStatus.Indent = 0
 
 			case isSelfReply(status, status.AccountID):
 				// Self reply, so indent at same
 				// level as own replied-to status.
-				apiStatus.Indent = parentIndent
+				webStatus.Indent = parentIndent
 
 			case parentIndent == 5:
 				// Already indented as far as we
 				// can go to keep things readable
 				// on thin screens, so just keep
 				// parent's indent.
-				apiStatus.Indent = parentIndent
+				webStatus.Indent = parentIndent
 
 			default:
 				// Reply to someone else who's
 				// indented, but not to TO THE MAX.
 				// Indent by another one.
-				apiStatus.Indent = parentIndent + 1
+				webStatus.Indent = parentIndent + 1
 			}
 
 			// Store the indent for this status.
-			statusIndents[status.ID] = apiStatus.Indent
+			statusIndents[status.ID] = webStatus.Indent
 		}
 
-		switch {
-		case idx == targetStatusIdx:
-			// This is the target status itself.
-			wCtx.Status = apiStatus
-
-		case idx < descendantsIdx:
-			// Haven't reached descendants yet,
-			// so this must be an ancestor.
-			wCtx.Ancestors = append(
-				wCtx.Ancestors,
-				apiStatus,
-			)
-
-		default:
-			// We're in descendants town now.
-			wCtx.Descendants = append(
-				wCtx.Descendants,
-				apiStatus,
-			)
+		if webStatus.ID == targetStatusID {
+			// This is the og
+			// thread context status.
+			webStatus.ThreadContextStatus = true
+			wCtx.Status = webStatus
 		}
+
+		wCtx.Statuses = append(wCtx.Statuses, webStatus)
 	}
 
 	// Now we've gone through the whole
 	// thread, we can add some additional info.
 
 	// Length of the "main" thread. If there are
-	// replies then it's up to where the replies
-	// start, otherwise it's the whole thing.
+	// visible replies then it's up to where the
+	// replies start, else it's the whole thing.
 	if inReplies {
 		wCtx.ThreadLength = firstReplyIdx
 	} else {
@@ -553,6 +538,9 @@ func (p *Processor) WebContextGet(
 	// Jot down number of hidden posts so template doesn't have to do it.
 	wCtx.ThreadShown = wCtx.ThreadLength - wCtx.ThreadHidden
 
+	// Mark the last "main" visible status.
+	wCtx.Statuses[wCtx.ThreadShown-1].ThreadLastMain = true
+
 	// Number of replies is equal to number
 	// of statuses in the thread that aren't
 	// part of the "main" thread.
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
index 9ad5d2c08..7bbca8ae7 100644
--- a/internal/typeutils/internaltofrontend_test.go
+++ b/internal/typeutils/internaltofrontend_test.go
@@ -1014,6 +1014,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
   "PollOptions": null,
   "Local": false,
   "Indent": 0,
+  "ThreadLastMain": false,
+  "ThreadContextStatus": false,
   "ThreadFirstReply": false
 }`, string(b))
 }
diff --git a/web/source/css/thread.css b/web/source/css/thread.css
index 4f4e3e938..e2e6ae6b7 100644
--- a/web/source/css/thread.css
+++ b/web/source/css/thread.css
@@ -42,6 +42,15 @@
 		h2 {
 			margin-right: auto;
 		}
+
+		&.replies.hidden-only {
+			/*
+				No visible replies below this column
+				header, so round off the bottom.
+			*/
+			border-bottom-left-radius: $br;
+			border-bottom-right-radius: $br;
+		}
 	}
 
 	.status {
diff --git a/web/template/thread.tmpl b/web/template/thread.tmpl
index 8b4aa2248..0cf9ecb05 100644
--- a/web/template/thread.tmpl
+++ b/web/template/thread.tmpl
@@ -20,7 +20,7 @@
 {{- define "repliesSummary" -}}
     {{- if .context.ThreadRepliesShown -}}
         {{- if .context.ThreadRepliesHidden -}}
-            {{- if eq .context.ThreadReplies 1 -}}
+            {{- if eq .context.ThreadRepliesShown 1 -}}
                 {{- /* Some replies are hidden. */ -}}
                 {{ .context.ThreadRepliesShown }} visible reply
             {{- else if gt .context.ThreadRepliesShown 1 -}}
@@ -35,6 +35,8 @@
                 {{ .context.ThreadReplies }} replies
             {{- end -}}
         {{- end -}}
+    {{- else -}}
+        {{- .context.ThreadRepliesHidden }} {{ if eq .context.ThreadRepliesHidden 1 }}reply{{ else }}replies{{ end }} hidden or not public
     {{- end -}}
 {{- end -}}
 
@@ -60,7 +62,7 @@
 {{- with . }}
 </section>
 <section class="thread thread-replies" aria-labelledby="replies" open>
-    <div class="col-header replies">
+    <div class="col-header replies{{- if not .context.ThreadRepliesShown }} hidden-only{{- end -}}">
         <h2 id="replies">{{- template "repliesSummary" . -}}</h2>
         <a href="#thread-summary">back to top</a>
     </div>
@@ -77,41 +79,18 @@
             {{- end }}
         </div>
 
-        {{- range $thisStatus := .context.Ancestors }}
-        {{- if $thisStatus.ThreadFirstReply }}
+        {{- range $status := .context.Statuses }}
+        <article
+            class="status{{- if $status.ThreadContextStatus }} expanded{{- end -}}{{- if $status.Indent }} indent-{{ $status.Indent }}{{- end -}}"
+            {{- includeAttr "status_attributes.tmpl" $status | indentAttr 3 }}
+        >
+            {{- include "status.tmpl" $status | indent 3 }}
+        </article>
+        {{- if and $status.ThreadLastMain $.context.ThreadReplies }}
         {{- include "repliesStart" $ | indent 1 }}
         {{- end }}
-        <article
-            class="status{{- if $thisStatus.Indent }} indent-{{ $thisStatus.Indent }}{{- end -}}"
-            {{- includeAttr "status_attributes.tmpl" $thisStatus | indentAttr 3 }}
-        >
-            {{- include "status.tmpl" $thisStatus | indent 3 }}
-        </article>
         {{- end }}
 
-        {{- with $thisStatus := .context.Status }}
-        {{- if $thisStatus.ThreadFirstReply }}
-        {{- include "repliesStart" $ | indent 1 }}
-        {{- end }}
-        <article
-            class="status expanded{{- if $thisStatus.Indent }} indent-{{ $thisStatus.Indent }}{{- end -}}"
-            {{- includeAttr "status_attributes.tmpl" $thisStatus | indentAttr 3  }}
-        >
-            {{- include "status.tmpl" $thisStatus | indent 3 }}
-        </article>
-        {{- end }}
-
-        {{- range $thisStatus := .context.Descendants }}
-        {{- if $thisStatus.ThreadFirstReply }}
-        {{- include "repliesStart" $ | indent 1 }}
-        {{- end }}
-        <article
-            class="status{{- if $thisStatus.Indent }} indent-{{ $thisStatus.Indent }}{{- end -}}"
-            {{- includeAttr "status_attributes.tmpl" $thisStatus | indentAttr 3 }}
-        >
-            {{- include "status.tmpl" $thisStatus | indent 3 }}
-        </article>
-        {{- end }}
     {{- if .context.ThreadReplies }}
     </section>
     {{- end }}