diff --git a/ROADMAP.md b/ROADMAP.md
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -2,7 +2,7 @@
 
 **Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away
 
-> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-25 · **Change:** minor (AutoSum over a selected row puts its total to the right, and fills a selection's empty last row and column) · **Target:** Microsoft Excel (Microsoft 365)
+> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-26 · **Change:** minor (colour scales paint the values between their stops as Excel does) · **Target:** Microsoft Excel (Microsoft 365)
 
 GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX
 is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web.
@@ -135,6 +135,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
 
 | Date | What landed |
 |---|---|
+| 2026-10-26 | Conditional formatting: a value between two colour-scale stops takes the colour Excel paints. Each channel moves from the earlier stop's byte toward the later one and the fraction is dropped, so a channel that rises is floored and one that falls is ceiled: red → green over 0..20 reads 243/12 at value 1 and 128/127 at the halfway point, where rounding gave 242/13 and 128/128 (measured in Excel over a whole ramp) |
 | 2026-10-25 | AutoSum over a selection does what Excel does: a selected row gets one total to its right (in its last cell if that is empty), not a one-cell total under each cell; a block whose last column is empty gets each row's total there, one whose last row is empty each column's, and with both the corner gets the total of the block; otherwise totals go under each column as before |
 | 2026-10-24 | Go To lists the named ranges of every sheet (`Sheet2!Name` for a name scoped to another sheet; hidden names, constants and formulas left out) and the references entered before, a double-click goes to an entry, and the Reference box takes several names or references separated by commas (`Price, Type`, `B14:C22,F19:G30`) |
 | 2026-10-24 | Status bar: a right-click opens a menu that sets which selection statistics show (Average, Count and Sum by default; Numerical Count, Minimum and Maximum are new), kept with the preferences (`view.statusBar`) |
@@ -242,6 +243,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-26 | minor | Progress log: colour scales paint the values between their stops as Excel does |
 | 2026-10-25 | minor | Progress log: AutoSum over a selected row, and a selection's empty last row and column |
 | 2026-10-23 | minor | Progress log: accounting negatives in number text |
 | 2026-10-23 | minor | Progress log: INDEX with one index on a 2-D reference is #REF! |
diff --git a/crates/engine/src/cf.rs b/crates/engine/src/cf.rs
--- a/crates/engine/src/cf.rs
+++ b/crates/engine/src/cf.rs
@@ -125,13 +125,16 @@ fn threshold(wb: &Workbook, sheet: usize, at: CellRef, st: &RuleStats, k: &CfVal
     }
 }
 
+/// The colour `t` of the way from `a` to `b`, as Excel paints it.
+///
+/// Excel moves each channel from `a`'s byte toward `b`'s and drops the fraction instead of
+/// rounding it, so a channel that rises is floored while one that falls is ceiled: the halfway
+/// point of red → green is `[128, 127, 0]`, not `[128, 128, 0]`. Measured in Excel over a whole
+/// ramp (#1848).
 fn lerp(a: [u8; 3], b: [u8; 3], t: f64) -> [u8; 3] {
     let t = t.clamp(0.0, 1.0);
-    [
-        (a[0] as f64 + (b[0] as f64 - a[0] as f64) * t).round() as u8,
-        (a[1] as f64 + (b[1] as f64 - a[1] as f64) * t).round() as u8,
-        (a[2] as f64 + (b[2] as f64 - a[2] as f64) * t).round() as u8,
-    ]
+    let mix = |i: usize| (a[i] as f64 + ((b[i] as f64 - a[i] as f64) * t).trunc()) as u8;
+    [mix(0), mix(1), mix(2)]
 }
 
 pub use gridcraft_model::icon_count;
@@ -503,6 +506,34 @@ mod tests {
         assert_eq!(look("A4").and_then(|l| l.scale_fill), Some([0x63, 0xBE, 0x7B]));
     }
 
+    /// In-between values take Excel's colour: each channel moves from the earlier stop's byte
+    /// toward the later one and the fraction is dropped, so red → green over 0..20 reads 243/12
+    /// at 1 and 128/127 at the halfway point (measured in Excel, #1848).
+    #[test]
+    fn color_scale_interpolates_each_channel_toward_its_start_colour() {
+        let mut s = crate::Session::new();
+        s.new_workbook();
+        let values: Vec<serde_json::Value> = (0..=20).map(|n| json!([n])).collect();
+        s.execute("range.setValues", json!({"range": "A1", "values": values})).unwrap();
+        let sh = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().wb).sheet_mut(0).unwrap();
+        sh.cond_formats = vec![CondFormat {
+            ranges: vec![RangeRef::parse("A1:A21").unwrap()],
+            rule: CfRule::ColorScale { stops: vec![(CfValueKind::Number(0.0), Color::rgb(0xFF, 0, 0)), (CfValueKind::Max, Color::rgb(0, 0xFF, 0))] },
+            priority: 1,
+            stop_if_true: false,
+        }];
+        let fill = |a: &str| {
+            let d = s.doc().unwrap();
+            CfCache::new().look(&d.wb, 0, CellRef::parse(a).unwrap()).and_then(|l| l.scale_fill)
+        };
+        // The rising green channel is floored, the falling red one ceiled.
+        assert_eq!(fill("A1"), Some([0xFF, 0, 0]));
+        assert_eq!(fill("A2"), Some([243, 12, 0]));
+        assert_eq!(fill("A11"), Some([0x80, 0x7F, 0]));
+        assert_eq!(fill("A14"), Some([90, 165, 0]));
+        assert_eq!(fill("A21"), Some([0, 0xFF, 0]));
+    }
+
     /// Looking cells up does work in proportion to the cells asked for, not the rule's range: the
     /// statistics are only gathered for rules that read them, and once per rule.
     #[test]
diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md
--- a/docs/target-app-parity.md
+++ b/docs/target-app-parity.md
@@ -1,6 +1,6 @@
 # Target-app parity: GridCraft vs Microsoft Excel
 
-> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-22 · **Change:** minor (several windows: a native window per workbook window, New Window on the same workbook, Arrange All; no score change) · **Target:** Microsoft Excel (Microsoft 365)
+> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-26 · **Change:** minor (colour scales paint the values between their stops as Excel does; no score change) · **Target:** Microsoft Excel (Microsoft 365)
 
 This is the authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it,
 [`gaps.md`](gaps.md) lists every shortfall, and the area checklists hold the detail:
@@ -334,6 +334,7 @@ The inventory carried over from the 2026-10-07 ROADMAP.md, updated for what land
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-26 | minor | Conditional-formatting colour scales: a value between two stops takes the colour Excel paints, each channel moving from the earlier stop's byte and dropping the fraction (red → green halfway is 128,127,0 where rounding gave 128,128,0); no score change |
 | 2026-10-22 | minor | Several windows on the desktop: a native window per workbook window, New Window on the same workbook, Switch Windows and Arrange All; "one window" removed from UI/UX fidelity, Hardware and the interaction-fidelity discount; no score change |
 | 2026-10-21 | minor | Page Setup paper sizes: `B4`/`B5`/`B6` are the JIS sizes Japanese Excel uses (ISO B4/B5/B6 by name), and Japanese Postcard, Japanese Double Postcard, the chou/kaku envelopes and A6 are added; PDF MediaBox and `file.print` follow, and the Size menu lists them |
 | 2026-10-18 | trivial | Stale chart notes: all chart kinds survive XLSX round-trip (PRs #158, #161) and trendlines landed |
