diff --git a/Cargo.toml b/Cargo.toml
index 8ab386045e0f8e12380479442f2efdf72fcb87cb..c256cf0d2ab12a3f8ba40447dc76bfc08edbcc65 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,7 +10,6 @@ edition = "2021"
 [dependencies]
 arrayvec = { version = "0.7.4", features = ["serde"] }
 clap = { version = "4.5.0", features = ["derive"] }
-colored = "2.1.0"
 itertools = "0.12.1"
 lexical-sort = "0.3.1"
 num-traits = "0.2.18"
@@ -28,6 +27,8 @@ unicode-width = "0.1.11"
 float_eq = { version = "1.0.1", features = ["derive"] }
 snafu = "0.8.2"
 serde_json = { version = "1.0.127", features = ["preserve_order"] }
+color-print = { git = "https://gitlab.com/iago-lito/color-print", branch = "dev", version = "0.3.6" }
+color-print-proc-macro = { git = "https://gitlab.com/iago-lito/color-print", branch = "dev", version = "0.3.6" }
 
 [dev-dependencies]
 rand = "0.8.5"
diff --git a/src/bin/aphid/main.rs b/src/bin/aphid/main.rs
index 415a42f1a9d4fe8ef4fa379d715b9fe47cc2b06c..bbeb0821e05049b81015f65a4ec4c7cb940c486f 100644
--- a/src/bin/aphid/main.rs
+++ b/src/bin/aphid/main.rs
@@ -15,7 +15,7 @@ use aphid::{
     Config, GeneTree, GeneTriplet, GenesForest, LocalGeneTriplet, VERSION,
 };
 use clap::Parser;
-use colored::Colorize;
+use color_print::{ceprintln, cformat, cprintln};
 use float_eq::float_eq;
 use serde_json::json;
 use snafu::{ensure, Snafu};
@@ -33,10 +33,10 @@ fn main() {
     println!("Running Aphid v{VERSION}.");
     match run() {
         Ok(()) => {
-            println!("Success. {}", "✓".bold().green());
+            cprintln!("Success. <bold,green>✓</>");
         }
         Err(e) => {
-            eprintln!("{} {e}", "🗙 Error:".bold().red());
+            ceprintln!("<bold,red>🗙 Error:</> {e}");
             process::exit(1);
         }
     }
@@ -100,13 +100,13 @@ fn run() -> Result<(), Error> {
 
 fn read_inputs(args: &args::Args, interner: &mut Interner) -> Result<(Config, GenesForest), Error> {
     let path = &args.config;
-    println!("Read config from {}.", format!("{path:?}").blue());
+    cprintln!("Read config from <b>{}</>.", path.display());
     let config = Config::from_file(path, interner)?;
 
     let path = &config.trees;
-    println!("Read gene trees from {}:", format!("{path:?}").blue());
+    cprintln!("Read gene trees from <b>{}</>:", path.display());
     let forest = GenesForest::from_file(path, interner)?;
-    println!("  Found {} gene trees.", forest.len());
+    cprintln!("  Found <g>{}</> gene trees.", forest.len());
 
     Ok((config, forest))
 }
@@ -151,19 +151,15 @@ fn prepare_output(args: &args::Args) -> Result<PathBuf, Error> {
     let output = path::absolute(&args.output)?;
     match (output.exists(), args.force) {
         (true, true) => {
-            println!(
-                "  {} existing folder: {}.",
-                "Removing".yellow(),
-                format!("{}", output.display()).blue()
+            cprintln!(
+                "  <y>Removing</> existing folder: <b>{}</>.",
+                output.display()
             );
             fs::remove_dir_all(&output)?;
         }
         (true, false) => OutputExistsErr { path: &output }.fail()?,
         (false, _) => {
-            println!(
-                "  Creating empty folder: {}.",
-                format!("{}", output.display()).blue()
-            );
+            cprintln!("  Creating empty folder: <b>{}</>.", output.display());
         }
     };
     fs::create_dir_all(&output)?;
@@ -173,10 +169,7 @@ fn prepare_output(args: &args::Args) -> Result<PathBuf, Error> {
 
 fn write_config(output: &Path, config: &Config, interner: &Interner) -> Result<(), Error> {
     let path = output.join(out::CONFIG);
-    println!(
-        "  Write full configuration to {}.",
-        format!("{}", path.display()).blue()
-    );
+    cprintln!("  Write full configuration to <b>{}</>.", path.display());
     let mut file = File::create(path)?;
     writeln!(file, "{:#}", config.resolve(interner).json())?;
     Ok(())
@@ -272,13 +265,13 @@ fn topology_filter<'i>(
     }
 
     // Report.
-    println!(
-        "  {n_excluded} tree{s_were} excluded from analysis based on their topology.",
+    cprintln!(
+        "  <g>{n_excluded}</> tree{s_were} excluded from analysis based on their topology.",
         s_were = if n_excluded > 1 { "s were" } else { " was" },
     );
     if let Some(min) = unrl {
-        println!(
-            "  {n_unresolved} included triplet{s_were} unresolved (internal length <= {min}).",
+        cprintln!(
+            "  <g>{n_unresolved}</> included triplet{s_were} unresolved (internal length ⩽ {min}).",
             s_were = if n_unresolved > 1 { "s were" } else { " was" },
         );
     }
@@ -307,12 +300,12 @@ fn geometry_filter(
         let (triplet_longer, imbalance) = imbalance(triplet_means_sum, rest_means_sum);
         let (t, r) = ("'triplet' section", "'outgroup' and 'other' section");
         let (small, large) = if triplet_longer { (t, r) } else { (r, t) };
-        println!(
-            "  Branch lengths in {large} are on average {imbalance} times longer \
+        cprintln!(
+            "  Branch lengths in {large} are on average <g>{imbalance}</> times longer \
                than in {small}."
         );
         let global_shape = triplet_means_sum / rest_means_sum;
-        println!("  Mean tree shape: {global_shape}");
+        cprintln!("  Mean tree shape: <g>{global_shape}</>");
 
         for i in topology_included {
             let tree = &pruned[i];
@@ -344,8 +337,8 @@ fn geometry_filter(
 
     // Report.
     if n_excluded > 0 {
-        println!(
-            "  --> {n_excluded} tree{s_were} excluded from analysis based on their geometry.",
+        cprintln!(
+            "  --> <g>{n_excluded}</> tree{s_were} excluded from analysis based on their geometry.",
             s_were = if n_excluded > 1 { "s were" } else { " was" },
         );
     } else {
@@ -366,7 +359,7 @@ fn final_tree_selection(
         .map(|&i| details[i].mean_lengths.total.unwrap())
         .summed_mean::<usize>();
 
-    println!("  mean branch length accross trees: l = {global_mean}");
+    cprintln!("  mean branch length accross trees: l = <g>{global_mean}</>");
     let mut triplets = Vec::new();
     for i in included {
         let local = local_triplets[i].take().unwrap(); // Exists since included.
@@ -382,9 +375,9 @@ fn final_tree_selection(
 
 fn write_detail(output: &Path, details: &[detail::Tree]) -> Result<(), Error> {
     let path = output.join(out::DETAIL);
-    println!(
-        "Write full trees analysis/filtering detail to {}.",
-        format!("{}", path.display()).blue()
+    cprintln!(
+        "Write full trees analysis/filtering detail to <b>{}</>.",
+        path.display()
     );
     let mut file = File::create(path)?;
     writeln!(file, "{:#}", json!(details))?;
@@ -396,8 +389,8 @@ fn display_summary(included: &[usize], details: &[detail::Tree], config: &Config
 
     let s = |n| if n > 1 { "s" } else { "" };
     let n = details.iter().map(|d| u64::from(!d.triplet.included)).sum();
-    println!(
-        "  - {n} triplet{} rejected (incomplete or non-monophyletic).",
+    cprintln!(
+        "  - <g>{n}</> triplet{} rejected (incomplete or non-monophyletic).",
         s(n),
     );
 
@@ -406,8 +399,8 @@ fn display_summary(included: &[usize], details: &[detail::Tree], config: &Config
             .iter()
             .filter_map(|d| d.triplet.analysis.as_ref().map(|a| u64::from(!a.resolved)))
             .sum();
-        println!(
-            "  - {n} triplet{} considered unresolved (internal branch length ⩽ {min}).",
+        cprintln!(
+            "  - <g>{n}</> triplet{} considered unresolved (internal branch length ⩽ {min}).",
             s(n),
         );
     }
@@ -416,8 +409,8 @@ fn display_summary(included: &[usize], details: &[detail::Tree], config: &Config
         .iter()
         .map(|d| u64::from(!d.outgroup.included))
         .sum();
-    println!(
-        "  - {n} outgroup{} rejected (empty or non-monophyletic).",
+    cprintln!(
+        "  - <g>{n}</> outgroup{} rejected (empty or non-monophyletic).",
         s(n)
     );
 
@@ -425,8 +418,8 @@ fn display_summary(included: &[usize], details: &[detail::Tree], config: &Config
         .iter()
         .map(|d| d.top.as_ref().map_or(1, |top| (!top.included).into()))
         .sum::<u64>();
-    println!(
-        "  - {n} tree top{} rejected (non-root LCA(triplet, outgroup){}).",
+    cprintln!(
+        "  - <g>{n}</> tree top{} rejected (non-root LCA(triplet, outgroup){}).",
         s(n),
         if config.filters.triplet_other_monophyly {
             " or non-monophyletic (triplet, other)"
@@ -440,15 +433,15 @@ fn display_summary(included: &[usize], details: &[detail::Tree], config: &Config
             .iter()
             .map(|d| u64::from(!d.included_geometry))
             .sum();
-        println!(
-            "  - {n} tree shape{} rejected \
+        cprintln!(
+            "  - <g>{n}</> tree shape{} rejected \
              (triplet/outgroup imbalance larger than {max} times the average).",
             s(n)
         );
     }
 
     let n = included.len() as u64;
-    println!("  => {n} tree{} kept for analysis.", s(n));
+    cprintln!(" ==> <s,g>{n}</> tree{} kept for analysis.", s(n));
 }
 
 fn learn(triplets: &[GeneTriplet], config: &Config) -> Result<(), Error> {
@@ -456,8 +449,8 @@ fn learn(triplets: &[GeneTriplet], config: &Config) -> Result<(), Error> {
 
     let parms = &config.search.init_parms;
     let (opt, opt_lnl) = optimize_likelihood(triplets, parms, &config.search)?;
-    println!("Optimized ln-likelihood: {opt_lnl}");
-    println!("Optimized parameters: {opt:#?}\n");
+    cprintln!("Optimized ln-likelihood: <g>{opt_lnl}</>");
+    println!("Optimized parameters: {}\n", opt.colored());
 
     // Smoke test:
     // TODO: turn into actual testing by including 'official' example data.
@@ -479,7 +472,8 @@ enum Error {
     Io { source: std::io::Error },
     #[snafu(transparent)]
     Check { source: aphid::config::check::Error },
-    #[snafu(display("Output folder already exists: {}.", format!("{}", path.display()).blue()))]
+    // TODO: have color_print feature this without the need to allocate an intermediate string?
+    #[snafu(display("Output folder already exists: {}.", cformat!("<b>{}</>", path.display())))]
     OutputExists { path: PathBuf },
     #[snafu(transparent)]
     ForestParse {
diff --git a/src/config/check.rs b/src/config/check.rs
index 16bb02207142c12663d24c3a2cb6e522306ae931..0d855f6f97695975435d715fd50a5ecd2137e474 100644
--- a/src/config/check.rs
+++ b/src/config/check.rs
@@ -8,7 +8,6 @@ use std::{
 };
 
 use arrayvec::ArrayVec;
-use colored::Colorize;
 use regex::Regex;
 use snafu::{ensure, Snafu};
 
@@ -212,14 +211,12 @@ impl BfgsConfig {
         let linsearch_trace_path = check_path(linsearch_trace)?;
         if let (None, Some(path)) = (&main_trace_path, &linsearch_trace) {
             return err!((
-                "A path is specified with {linopt} \
+                "A path is specified with <b>`search.bfgs.linsearch_trace`</> \
                  to log the detailed trace of BFGS linear search during each step, \
                  but none is specified to log the global trace of each step. \
-                 Consider setting {mainopt} option.\n\
-                 The path given was: {path}",
-                linopt = "`search.bfgs.linsearch_trace`".blue(),
-                mainopt = "`search.bfgs.main_trace`".blue(),
-                path = format!("{:?}", path.to_string_lossy()).black(),
+                 Consider setting <b>`search.bfgs.main_trace`</> option.\n\
+                 The path given was: <k>{}</>",
+                path.display(),
             ))
             .fail();
         }
diff --git a/src/model/parameters.rs b/src/model/parameters.rs
index bc59a80716189e603c0a68824a1eb564c98c03f3..b570a860c337e227886df3a96c6c65760e52212b 100644
--- a/src/model/parameters.rs
+++ b/src/model/parameters.rs
@@ -4,6 +4,7 @@
 use std::fmt::{self, Display};
 
 use arrayvec::ArrayVec;
+use color_print::cwriteln;
 use serde::Serialize;
 use tch::Tensor;
 
@@ -102,3 +103,41 @@ impl fmt::Debug for ValGrad {
         write!(f, "{self}")
     }
 }
+
+// Colored parameters for stdout.
+pub struct Colored<'p, F: Num + Display + Sized>(&'p Parameters<F>);
+
+impl<F: Num + Display + Sized + fmt::Debug> Display for Colored<'_, F> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        writeln!(f, "Parameters {{")?;
+        let Parameters {
+            theta,
+            tau_1,
+            tau_2,
+            p_ab,
+            p_ac,
+            p_bc,
+            p_ancient,
+            gf_times,
+        } = &self.0;
+        cwriteln!(f, "  theta: <b>{theta:?}</>,")?;
+        cwriteln!(f, "  tau_1: <b>{tau_1:?}</>,")?;
+        cwriteln!(f, "  tau_2: <b>{tau_2:?}</>,")?;
+        cwriteln!(f, "  p_ab: <b>{p_ab:?}</>,")?;
+        cwriteln!(f, "  p_ac: <b>{p_ac:?}</>,")?;
+        cwriteln!(f, "  p_bc: <b>{p_bc:?}</>,")?;
+        cwriteln!(f, "  p_ancient: <b>{p_ancient:?}</>,")?;
+        writeln!(f, "  gf_times: [")?;
+        for t in &gf_times.0 {
+            cwriteln!(f, "    <b>{t}</>,")?;
+        }
+        writeln!(f, "  ]")?;
+        writeln!(f, "}}")
+    }
+}
+
+impl<F: Num + Display + Sized> Parameters<F> {
+    pub fn colored(&self) -> Colored<F> {
+        Colored(self)
+    }
+}