diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7bb3cfd..cda39e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,8 @@ follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
+## 2.16.0 — 2026-10-09
+
### Added
- **A rule or box edge holds several titles.** `Rule.Titles` and `Frame.Titles` add titles beside the
diff --git a/MarkupString.Ansi/MarkupString.Ansi.csproj b/MarkupString.Ansi/MarkupString.Ansi.csproj
index 2a06de6..6e40348 100644
--- a/MarkupString.Ansi/MarkupString.Ansi.csproj
+++ b/MarkupString.Ansi/MarkupString.Ansi.csproj
@@ -12,7 +12,7 @@
each time one goes out; drop it for a release that removes public API on purpose, and put
it back straight after. -->
true
- 2.15.4
+ 2.15.5
Terminal styling for MarkupString: colours (16, 256 and truecolor), text attributes and links, an ansi() code parser, and emitters that fold a run's layers into one SGR sequence, HTML span, Pueblo/MXP tag or BBCode tag.
diff --git a/MarkupString.Html/MarkupString.Html.csproj b/MarkupString.Html/MarkupString.Html.csproj
index bdd3634..4f3b848 100644
--- a/MarkupString.Html/MarkupString.Html.csproj
+++ b/MarkupString.Html/MarkupString.Html.csproj
@@ -12,7 +12,7 @@
each time one goes out; drop it for a release that removes public API on purpose, and put
it back straight after. -->
true
- 2.15.4
+ 2.15.5
Raw HTML tag markup for MarkupString — an anchor, a pre, a span — rendered as itself in the HTML, Pueblo and MXP formats and folded into terminal styling elsewhere, with checked construction and tag policies for untrusted input.
diff --git a/MarkupString.Mxp/MarkupString.Mxp.csproj b/MarkupString.Mxp/MarkupString.Mxp.csproj
index 2179cff..fb94028 100644
--- a/MarkupString.Mxp/MarkupString.Mxp.csproj
+++ b/MarkupString.Mxp/MarkupString.Mxp.csproj
@@ -12,7 +12,7 @@
each time one goes out; drop it for a release that removes public API on purpose, and put
it back straight after. -->
true
- 2.15.4
+ 2.15.5
MXP for MarkupString: writes the shared vocabulary — sounds, music, pictures, panes, gauges, status, variables, link expiry, relocation, login prompts — as MXP tags, holding each to what the client said it supports.
diff --git a/MarkupString.Pueblo/MarkupString.Pueblo.csproj b/MarkupString.Pueblo/MarkupString.Pueblo.csproj
index a045f7f..4876f45 100644
--- a/MarkupString.Pueblo/MarkupString.Pueblo.csproj
+++ b/MarkupString.Pueblo/MarkupString.Pueblo.csproj
@@ -12,7 +12,7 @@
each time one goes out; drop it for a release that removes public API on purpose, and put
it back straight after. -->
true
- 2.15.4
+ 2.15.5
Pueblo for MarkupString: writes the shared vocabulary — sounds, pictures, panes, clearing the screen, prefetching — in the Pueblo client's own HTML extensions.
diff --git a/MarkupString/MarkupString.csproj b/MarkupString/MarkupString.csproj
index 4c64ece..14e0ee3 100644
--- a/MarkupString/MarkupString.csproj
+++ b/MarkupString/MarkupString.csproj
@@ -12,7 +12,7 @@
each time one goes out; drop it for a release that removes public API on purpose, and put
it back straight after. -->
true
- 2.15.4
+ 2.15.5
Immutable styled text: a string plus layered markup runs, rendered to ANSI, HTML, Pueblo, MXP, BBCode or plain text through a registry of emitters, and round-tripped through JSON.
diff --git a/MarkupString/PublicAPI.Shipped.txt b/MarkupString/PublicAPI.Shipped.txt
index 439eda8..08b97e7 100644
--- a/MarkupString/PublicAPI.Shipped.txt
+++ b/MarkupString/PublicAPI.Shipped.txt
@@ -351,6 +351,17 @@ MarkupString.Layout.ColumnFormat.Wrap.init -> void
MarkupString.Layout.CutFrom
MarkupString.Layout.CutFrom.End = 0 -> MarkupString.Layout.CutFrom
MarkupString.Layout.CutFrom.Start = 1 -> MarkupString.Layout.CutFrom
+MarkupString.Layout.EdgeTitle
+MarkupString.Layout.EdgeTitle.$() -> MarkupString.Layout.EdgeTitle!
+MarkupString.Layout.EdgeTitle.Deconstruct(out MarkupString.MarkupText! Text, out MarkupString.Layout.Alignment Side) -> void
+MarkupString.Layout.EdgeTitle.EdgeTitle(MarkupString.MarkupText! Text, MarkupString.Layout.Alignment Side = MarkupString.Layout.Alignment.Center) -> void
+MarkupString.Layout.EdgeTitle.Equals(MarkupString.Layout.EdgeTitle? other) -> bool
+MarkupString.Layout.EdgeTitle.Priority.get -> int?
+MarkupString.Layout.EdgeTitle.Priority.init -> void
+MarkupString.Layout.EdgeTitle.Side.get -> MarkupString.Layout.Alignment
+MarkupString.Layout.EdgeTitle.Side.init -> void
+MarkupString.Layout.EdgeTitle.Text.get -> MarkupString.MarkupText!
+MarkupString.Layout.EdgeTitle.Text.init -> void
MarkupString.Layout.Field
MarkupString.Layout.Field.$() -> MarkupString.Layout.Field!
MarkupString.Layout.Field.Deconstruct(out MarkupString.MarkupText! Label, out MarkupString.Layout.Block! Value) -> void
@@ -429,6 +440,8 @@ MarkupString.Layout.Frame.Body.get -> MarkupString.Layout.Block!
MarkupString.Layout.Frame.Body.init -> void
MarkupString.Layout.Frame.Border.get -> MarkupString.Layout.BorderStyle?
MarkupString.Layout.Frame.Border.init -> void
+MarkupString.Layout.Frame.BottomTitles.get -> System.Collections.Immutable.ImmutableArray
+MarkupString.Layout.Frame.BottomTitles.init -> void
MarkupString.Layout.Frame.Deconstruct(out MarkupString.Layout.Block! Body) -> void
MarkupString.Layout.Frame.Equals(MarkupString.Layout.Frame? other) -> bool
MarkupString.Layout.Frame.Frame(MarkupString.Layout.Block! Body) -> void
@@ -438,6 +451,8 @@ MarkupString.Layout.Frame.Title.get -> MarkupString.MarkupText?
MarkupString.Layout.Frame.Title.init -> void
MarkupString.Layout.Frame.TitleAlignment.get -> MarkupString.Layout.Alignment
MarkupString.Layout.Frame.TitleAlignment.init -> void
+MarkupString.Layout.Frame.Titles.get -> System.Collections.Immutable.ImmutableArray
+MarkupString.Layout.Frame.Titles.init -> void
MarkupString.Layout.Gauge
MarkupString.Layout.Gauge.BarWidth.get -> int
MarkupString.Layout.Gauge.BarWidth.init -> void
@@ -614,6 +629,8 @@ MarkupString.Layout.Rule.Title.get -> MarkupString.MarkupText?
MarkupString.Layout.Rule.Title.init -> void
MarkupString.Layout.Rule.TitleAlignment.get -> MarkupString.Layout.Alignment
MarkupString.Layout.Rule.TitleAlignment.init -> void
+MarkupString.Layout.Rule.Titles.get -> System.Collections.Immutable.ImmutableArray
+MarkupString.Layout.Rule.Titles.init -> void
MarkupString.Layout.SeparatorRows
MarkupString.Layout.SeparatorRows.EveryRow = 0 -> MarkupString.Layout.SeparatorRows
MarkupString.Layout.SeparatorRows.FirstRowOnly = 1 -> MarkupString.Layout.SeparatorRows
@@ -1149,6 +1166,9 @@ override MarkupString.Layout.Colored.ToString() -> string!
override MarkupString.Layout.ColumnFormat.Equals(object? obj) -> bool
override MarkupString.Layout.ColumnFormat.GetHashCode() -> int
override MarkupString.Layout.ColumnFormat.ToString() -> string!
+override MarkupString.Layout.EdgeTitle.Equals(object? obj) -> bool
+override MarkupString.Layout.EdgeTitle.GetHashCode() -> int
+override MarkupString.Layout.EdgeTitle.ToString() -> string!
override MarkupString.Layout.Field.Equals(object? obj) -> bool
override MarkupString.Layout.Field.GetHashCode() -> int
override MarkupString.Layout.Field.ToString() -> string!
@@ -1415,6 +1435,8 @@ static MarkupString.Layout.Colored.operator !=(MarkupString.Layout.Colored? left
static MarkupString.Layout.Colored.operator ==(MarkupString.Layout.Colored? left, MarkupString.Layout.Colored? right) -> bool
static MarkupString.Layout.ColumnFormat.operator !=(MarkupString.Layout.ColumnFormat? left, MarkupString.Layout.ColumnFormat? right) -> bool
static MarkupString.Layout.ColumnFormat.operator ==(MarkupString.Layout.ColumnFormat? left, MarkupString.Layout.ColumnFormat? right) -> bool
+static MarkupString.Layout.EdgeTitle.operator !=(MarkupString.Layout.EdgeTitle? left, MarkupString.Layout.EdgeTitle? right) -> bool
+static MarkupString.Layout.EdgeTitle.operator ==(MarkupString.Layout.EdgeTitle? left, MarkupString.Layout.EdgeTitle? right) -> bool
static MarkupString.Layout.Field.operator !=(MarkupString.Layout.Field? left, MarkupString.Layout.Field? right) -> bool
static MarkupString.Layout.Field.operator ==(MarkupString.Layout.Field? left, MarkupString.Layout.Field? right) -> bool
static MarkupString.Layout.Fields.operator !=(MarkupString.Layout.Fields? left, MarkupString.Layout.Fields? right) -> bool
diff --git a/MarkupString/PublicAPI.Unshipped.txt b/MarkupString/PublicAPI.Unshipped.txt
index a8c0ae1..7dc5c58 100644
--- a/MarkupString/PublicAPI.Unshipped.txt
+++ b/MarkupString/PublicAPI.Unshipped.txt
@@ -1,23 +1 @@
#nullable enable
-MarkupString.Layout.EdgeTitle
-MarkupString.Layout.EdgeTitle.$() -> MarkupString.Layout.EdgeTitle!
-MarkupString.Layout.EdgeTitle.Deconstruct(out MarkupString.MarkupText! Text, out MarkupString.Layout.Alignment Side) -> void
-MarkupString.Layout.EdgeTitle.EdgeTitle(MarkupString.MarkupText! Text, MarkupString.Layout.Alignment Side = MarkupString.Layout.Alignment.Center) -> void
-MarkupString.Layout.EdgeTitle.Equals(MarkupString.Layout.EdgeTitle? other) -> bool
-MarkupString.Layout.EdgeTitle.Side.get -> MarkupString.Layout.Alignment
-MarkupString.Layout.EdgeTitle.Side.init -> void
-MarkupString.Layout.EdgeTitle.Text.get -> MarkupString.MarkupText!
-MarkupString.Layout.EdgeTitle.Text.init -> void
-MarkupString.Layout.Frame.BottomTitles.get -> System.Collections.Immutable.ImmutableArray
-MarkupString.Layout.Frame.BottomTitles.init -> void
-MarkupString.Layout.Frame.Titles.get -> System.Collections.Immutable.ImmutableArray
-MarkupString.Layout.Frame.Titles.init -> void
-MarkupString.Layout.Rule.Titles.get -> System.Collections.Immutable.ImmutableArray
-MarkupString.Layout.Rule.Titles.init -> void
-override MarkupString.Layout.EdgeTitle.Equals(object? obj) -> bool
-override MarkupString.Layout.EdgeTitle.GetHashCode() -> int
-override MarkupString.Layout.EdgeTitle.ToString() -> string!
-static MarkupString.Layout.EdgeTitle.operator !=(MarkupString.Layout.EdgeTitle? left, MarkupString.Layout.EdgeTitle? right) -> bool
-static MarkupString.Layout.EdgeTitle.operator ==(MarkupString.Layout.EdgeTitle? left, MarkupString.Layout.EdgeTitle? right) -> bool
-MarkupString.Layout.EdgeTitle.Priority.get -> int?
-MarkupString.Layout.EdgeTitle.Priority.init -> void