diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 87baaab1..1e1e88a3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,7 +23,7 @@ jobs:
     permissions:
       contents: none
     name: CI
-    needs: [test, msrv, lockfile, docs, rustfmt, clippy]
+    needs: [test, msrv, lockfile, docs, rustfmt, clippy, minimal-versions]
     runs-on: ubuntu-latest
     if: "always()"
     steps:
@@ -34,7 +34,7 @@ jobs:
     name: Test
     strategy:
       matrix:
-        os: ["ubuntu-latest", "windows-latest", "macos-14"]
+        os: ["ubuntu-latest", "windows-latest", "macos-latest"]
         rust: ["stable"]
     continue-on-error: ${{ matrix.rust != 'stable' }}
     runs-on: ${{ matrix.os }}
@@ -65,6 +65,24 @@ jobs:
     - uses: taiki-e/install-action@cargo-hack
     - name: Default features
       run: cargo hack check --feature-powerset --locked --rust-version --ignore-private --workspace --lib --bins
+  minimal-versions:
+    name: Minimal versions
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v4
+    - name: Install stable Rust
+      uses: dtolnay/rust-toolchain@stable
+      with:
+        toolchain: stable
+    - name: Install nightly Rust
+      uses: dtolnay/rust-toolchain@stable
+      with:
+        toolchain: nightly
+    - name: Downgrade dependencies to minimal versions
+      run: cargo +nightly generate-lockfile -Z minimal-versions
+    - name: Compile with minimal versions
+      run: cargo +stable check --workspace --all-features --locked
   lockfile:
     runs-on: ubuntu-latest
     steps:
@@ -138,3 +156,22 @@ jobs:
         wait-for-processing: true
     - name: Report status
       run: cargo clippy --workspace --all-features --all-targets -- -D warnings --allow deprecated
+  coverage:
+    name: Coverage
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v4
+    - name: Install Rust
+      uses: dtolnay/rust-toolchain@stable
+      with:
+        toolchain: stable
+    - uses: Swatinem/rust-cache@v2
+    - name: Install cargo-tarpaulin
+      run: cargo install cargo-tarpaulin
+    - name: Gather coverage
+      run: cargo tarpaulin --output-dir coverage --out lcov --timeout 120
+    - name: Publish to Coveralls
+      uses: coverallsapp/github-action@master
+      with:
+        github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index 3d04055c..491030a1 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -24,4 +24,6 @@ jobs:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/setup-python@v5
+      with:
+        python-version: '3.x'
     - uses: pre-commit/action@v3.0.1
diff --git a/.github/workflows/rust-next.yml b/.github/workflows/rust-next.yml
index ab499633..e98386c4 100644
--- a/.github/workflows/rust-next.yml
+++ b/.github/workflows/rust-next.yml
@@ -21,7 +21,7 @@ jobs:
     name: Test
     strategy:
       matrix:
-        os: ["ubuntu-latest", "windows-latest", "macos-latest", "macos-14"]
+        os: ["ubuntu-latest", "windows-latest", "macos-latest"]
         rust: ["stable", "beta"]
         include:
         - os: ubuntu-latest
diff --git a/Cargo.toml b/Cargo.toml
index 42ae1b7c..879ce002 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,58 +1,23 @@
-[package]
-name = "annotate-snippets"
-version = "0.11.4"
-edition = "2021"
-rust-version = "1.65"  # MSRV
-authors = ["Zibi Braniecki <gandalf@mozilla.com>"]
-description = "Library for building code annotations"
-license = "Apache-2.0/MIT"
-repository = "https://github.com/rust-lang/annotate-snippets-rs"
-readme = "README.md"
-keywords = ["code", "analysis", "ascii", "errors", "debug"]
+[workspace]
+resolver = "2"
 
-[package.metadata.release]
-tag-name = "{{version}}"
-pre-release-replacements = [
-  {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1},
-  {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1},
-  {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1},
-  {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## [Unreleased] - ReleaseDate\n", exactly=1},
-  {file="CHANGELOG.md", search="<!-- next-url -->", replace="<!-- next-url -->\n[Unreleased]: https://github.com/rust-lang/annotate-snippets-rs/compare/{{tag_name}}...HEAD", exactly=1},
+[workspace.package]
+repository = "https://github.com/rust-lang/annotate-snippets-rs"
+license = "MIT OR Apache-2.0"
+edition = "2021"
+rust-version = "1.65.0"  # MSRV
+include = [
+  "build.rs",
+  "src/**/*",
+  "Cargo.toml",
+  "Cargo.lock",
+  "LICENSE*",
+  "README.md",
+  "benches/**/*",
+  "examples/**/*"
 ]
 
-[badges]
-maintenance = { status = "actively-developed" }
-
-[dependencies]
-anstyle = "1.0.4"
-memchr = { version = "2.7.4", optional = true }
-unicode-width = "0.1.11"
-
-[dev-dependencies]
-annotate-snippets = { path = ".", features = ["testing-colors"] }
-anstream = "0.6.13"
-difference = "2.0.0"
-divan = "0.1.14"
-glob = "0.3.1"
-serde = { version = "1.0.199", features = ["derive"] }
-snapbox = { version = "0.6.0", features = ["diff", "term-svg", "cmd", "examples"] }
-toml = "0.5.11"
-tryfn = "0.2.1"
-
-[[bench]]
-name = "bench"
-harness = false
-
-[[test]]
-name = "fixtures"
-harness = false
-
-[features]
-default = []
-simd = ["memchr"]
-testing-colors = []
-
-[lints.rust]
+[workspace.lints.rust]
 rust_2018_idioms = { level = "warn", priority = -1 }
 unreachable_pub = "warn"
 unsafe_op_in_unsafe_fn = "warn"
@@ -60,7 +25,7 @@ unused_lifetimes = "warn"
 unused_macro_rules = "warn"
 unused_qualifications = "warn"
 
-[lints.clippy]
+[workspace.lints.clippy]
 bool_assert_comparison = "allow"
 branches_sharing_code = "allow"
 checked_conversions = "warn"
@@ -115,6 +80,68 @@ string_lit_as_bytes = "warn"
 string_to_string = "warn"
 todo = "warn"
 trait_duplication_in_bounds = "warn"
+uninlined_format_args = "warn"
 verbose_file_reads = "warn"
 wildcard_imports = "warn"
 zero_sized_map_values = "warn"
+
+[package]
+name = "annotate-snippets"
+version = "0.11.4"
+description = "Library for building code annotations"
+categories = []
+keywords = ["code", "analysis", "ascii", "errors", "debug"]
+repository.workspace = true
+license.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+include.workspace = true
+
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
+
+[package.metadata.release]
+tag-name = "{{version}}"
+pre-release-replacements = [
+  {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1},
+  {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1},
+  {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1},
+  {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## [Unreleased] - ReleaseDate\n", exactly=1},
+  {file="CHANGELOG.md", search="<!-- next-url -->", replace="<!-- next-url -->\n[Unreleased]: https://github.com/rust-lang/annotate-snippets-rs/compare/{{tag_name}}...HEAD", exactly=1},
+]
+
+[badges]
+maintenance = { status = "actively-developed" }
+
+[dependencies]
+anstyle = "1.0.4"
+memchr = { version = "2.7.4", optional = true }
+unicode-width = "0.1.11"
+
+[dev-dependencies]
+annotate-snippets = { path = ".", features = ["testing-colors"] }
+anstream = "0.6.13"
+difference = "2.0.0"
+divan = "0.1.14"
+glob = "0.3.1"
+serde = { version = "1.0.199", features = ["derive"] }
+snapbox = { version = "0.6.0", features = ["diff", "term-svg", "cmd", "examples"] }
+toml = "0.5.11"
+tryfn = "0.2.1"
+
+[[bench]]
+name = "bench"
+harness = false
+
+[[test]]
+name = "fixtures"
+harness = false
+
+[features]
+default = []
+simd = ["memchr"]
+testing-colors = []
+
+[lints]
+workspace = true
diff --git a/LICENSE-APACHE b/LICENSE-APACHE
index 261eeb9e..8f71f43f 100644
--- a/LICENSE-APACHE
+++ b/LICENSE-APACHE
@@ -178,7 +178,7 @@
    APPENDIX: How to apply the Apache License to your work.
 
       To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
+      boilerplate notice, with the fields enclosed by brackets "{}"
       replaced with your own identifying information. (Don't include
       the brackets!)  The text should be enclosed in the appropriate
       comment syntax for the file format. We also recommend that a
@@ -186,7 +186,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright [yyyy] [name of copyright owner]
+   Copyright {yyyy} {name of copyright owner}
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
@@ -199,3 +199,4 @@
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
+
diff --git a/LICENSE-MIT b/LICENSE-MIT
index 5655fa31..a2d01088 100644
--- a/LICENSE-MIT
+++ b/LICENSE-MIT
@@ -1,11 +1,11 @@
-Copyright 2017 Mozilla
+Copyright (c) Individual contributors
 
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
 
 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.
diff --git a/release.toml b/release.toml
index dac7b6ba..160b061b 100644
--- a/release.toml
+++ b/release.toml
@@ -1 +1,2 @@
-allow-branch = ["master"]
+dependent-version = "fix"
+allow-branch = ["main"]
diff --git a/src/renderer/display_list.rs b/src/renderer/display_list.rs
index fedf2689..1823e61a 100644
--- a/src/renderer/display_list.rs
+++ b/src/renderer/display_list.rs
@@ -198,7 +198,7 @@ impl<'a> DisplaySet<'a> {
             self.format_label(line_offset, &annotation.label, stylesheet, buffer)
         } else {
             let id = match &annotation.id {
-                Some(id) => format!("[{}]", id),
+                Some(id) => format!("[{id}]"),
                 None => String::new(),
             };
             buffer.append(
@@ -290,12 +290,12 @@ impl<'a> DisplaySet<'a> {
             } => {
                 let lineno_color = stylesheet.line_no();
                 if anonymized_line_numbers && lineno.is_some() {
-                    let num = format!("{:>width$} |", ANONYMIZED_LINE_NUM, width = lineno_width);
+                    let num = format!("{ANONYMIZED_LINE_NUM:>lineno_width$} |");
                     buffer.puts(line_offset, 0, &num, *lineno_color);
                 } else {
                     match lineno {
                         Some(n) => {
-                            let num = format!("{:>width$} |", n, width = lineno_width);
+                            let num = format!("{n:>lineno_width$} |");
                             buffer.puts(line_offset, 0, &num, *lineno_color);
                         }
                         None => {
@@ -645,7 +645,7 @@ impl<'a> DisplaySet<'a> {
                             } else if formatted_len != 0 {
                                 formatted_len += 2;
                                 let id = match &annotation.annotation.id {
-                                    Some(id) => format!("[{}]", id),
+                                    Some(id) => format!("[{id}]"),
                                     None => String::new(),
                                 };
                                 buffer.puts(
@@ -1292,10 +1292,7 @@ fn format_body(
             None
         }
     }) {
-        panic!(
-            "SourceAnnotation range `{:?}` is beyond the end of buffer `{}`",
-            bigger, source_len
-        )
+        panic!("SourceAnnotation range `{bigger:?}` is beyond the end of buffer `{source_len}`")
     }
 
     let mut body = vec![];
diff --git a/tests/fixtures/main.rs b/tests/fixtures/main.rs
index 07c69d81..bf37e73d 100644
--- a/tests/fixtures/main.rs
+++ b/tests/fixtures/main.rs
@@ -22,7 +22,7 @@ fn setup(input_path: std::path::PathBuf) -> tryfn::Case {
         .to_str()
         .unwrap();
     let file_name = input_path.file_name().unwrap().to_str().unwrap();
-    let name = format!("{}/{}", parent, file_name);
+    let name = format!("{parent}/{file_name}");
     let expected = Data::read_from(&input_path.with_extension("svg"), None);
     tryfn::Case {
         name,