diff --git a/ROADMAP.md b/ROADMAP.md
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -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-25 | Fill handle: from numbers, the right-drag menu offers Linear Trend and Growth Trend, which continue the best-fit line or exponential curve of the source (1, 2 gives 3, 4, 5 or 4, 8, 16) |
 | 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`) |
diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs
--- a/crates/engine/src/cmd/edit.rs
+++ b/crates/engine/src/cmd/edit.rs
@@ -79,7 +79,7 @@ pub fn specs() -> Vec<CommandSpec> {
             "AutoFill",
             [],
             None,
-            "{source: \"A1:A2\", target: \"A1:A10\", mode?: series|alternate|copy|formats|values|extend|days|weekdays|months|years}: the fill-handle drag (alternate = Ctrl+drag: a lone number counts up, any other series copies; values = Fill Without Formatting: the series, targets keep their formats; extend = Fill Series: the series, and a lone number counts up; days, weekdays, months, years = Fill Days, Fill Weekdays, Fill Months, Fill Years: dates step by one unit, or by the difference between the first two dates in that unit, and weekdays skip Saturday and Sunday)",
+            "{source: \"A1:A2\", target: \"A1:A10\", mode?: series|alternate|copy|formats|values|extend|days|weekdays|months|years|linearTrend|growthTrend}: the fill-handle drag (alternate = Ctrl+drag: a lone number counts up, any other series copies; values = Fill Without Formatting: the series, targets keep their formats; extend = Fill Series: the series, and a lone number counts up; days, weekdays, months, years = Fill Days, Fill Weekdays, Fill Months, Fill Years: dates step by one unit, or by the difference between the first two dates in that unit, and weekdays skip Saturday and Sunday; linearTrend, growthTrend = Linear Trend, Growth Trend: the targets continue the least-squares line or the exponential curve fitted to each row or column of source numbers, and the source keeps its values; an error when a source cell is not a number or holds a formula, or for growthTrend when a row or column mixes signs or holds zero)",
             has_doc,
             auto_fill
         ),
@@ -1589,6 +1589,8 @@ fn auto_fill(s: &mut Session, p: &Json) -> Result<Json> {
         "weekdays" => crate::fill::FillMode::Dates(crate::fill::DateUnit::Weekday),
         "months" => crate::fill::FillMode::Dates(crate::fill::DateUnit::Month),
         "years" => crate::fill::FillMode::Dates(crate::fill::DateUnit::Year),
+        "linearTrend" => crate::fill::FillMode::LinearTrend,
+        "growthTrend" => crate::fill::FillMode::GrowthTrend,
         _ => crate::fill::FillMode::Series,
     };
     if target.count() > crate::fill::MAX_FILL_CELLS || src.count() > crate::fill::MAX_FILL_CELLS {
diff --git a/crates/engine/src/fill.rs b/crates/engine/src/fill.rs
--- a/crates/engine/src/fill.rs
+++ b/crates/engine/src/fill.rs
@@ -28,6 +28,12 @@ pub enum FillMode {
     /// counted in that unit (one unit when they fall in the same one). A source that isn't dates
     /// extends like a plain drag.
     Dates(DateUnit),
+    /// Linear Trend: the targets continue the least-squares line (y = mx + b) through the source's
+    /// numbers. The source keeps its values.
+    LinearTrend,
+    /// Growth Trend: the targets continue the exponential curve (y = b·m^x) fitted to the source's
+    /// numbers. The source keeps its values.
+    GrowthTrend,
 }
 
 /// The step of a [`FillMode::Dates`] fill.
@@ -47,6 +53,59 @@ impl FillMode {
     }
 }
 
+/// Whether a fill from `src` to `target` runs down or up the sheet rather than across it.
+fn fills_vertically(src: RangeRef, target: RangeRef) -> bool {
+    target.height() != src.height() || target.width() == src.width()
+}
+
+/// The numbers in each lane of `src` that a fill to `target` continues, or `None` when a source
+/// cell is empty or holds text, a logical value, an error or a formula.
+fn numeric_lanes(wb: &Workbook, sheet: usize, src: RangeRef, target: RangeRef) -> Option<Vec<Vec<f64>>> {
+    let sh = wb.sheet(sheet)?;
+    let vertical = fills_vertically(src, target);
+    let lanes: Vec<u32> = if vertical { (src.start.col..=src.end.col).collect() } else { (src.start.row..=src.end.row).collect() };
+    let len = if vertical { src.height() } else { src.width() };
+    lanes
+        .into_iter()
+        .map(|lane| {
+            (0..len)
+                .map(|i| {
+                    let c = if vertical { CellRef::new(src.start.row + i, lane) } else { CellRef::new(lane, src.start.col + i) };
+                    let cell = sh.cell(c)?;
+                    if cell.formula.is_some() || !cell.value.is_number() {
+                        return None;
+                    }
+                    cell.value.as_f64()
+                })
+                .collect()
+        })
+        .collect()
+}
+
+/// Whether the fill menu offers Linear Trend (`growth` false) or Growth Trend (`growth` true) for a
+/// fill from `src` to `target`: every source cell holds a number, and for Growth Trend the numbers
+/// of each lane are all above zero or all below zero.
+pub fn trend_applies(wb: &Workbook, sheet: usize, src: RangeRef, target: RangeRef, growth: bool) -> bool {
+    let Some(sh) = wb.sheet(sheet) else { return false };
+    let vertical = fills_vertically(src, target);
+    let (lanes, len) = if vertical { (src.start.col..=src.end.col, src.height()) } else { (src.start.row..=src.end.row, src.width()) };
+    for lane in lanes {
+        let (mut above, mut below) = (false, false);
+        for i in 0..len {
+            let c = if vertical { CellRef::new(src.start.row + i, lane) } else { CellRef::new(lane, src.start.col + i) };
+            let Some(n) = sh.cell(c).filter(|cell| cell.formula.is_none() && cell.value.is_number()).and_then(|cell| cell.value.as_f64()) else {
+                return false;
+            };
+            above |= n > 0.0;
+            below |= n < 0.0;
+            if growth && (n == 0.0 || (above && below)) {
+                return false;
+            }
+        }
+    }
+    true
+}
+
 /// Whether a number format shows a calendar date (`YYYY-MM-DD` and `m/d/yyyy h:mm` do; `0 "days"`
 /// and `h:mm` don't).
 fn is_date_format(fmt: &str) -> bool {
@@ -90,7 +149,7 @@ pub fn fill(cx: &mut Ctx, sheet: usize, src: RangeRef, target: RangeRef, mode: F
     let custom = cx.wb.custom_lists.clone();
     let Some(sh) = cx.wb.sheet(sheet) else { return Ok(()) };
     // Direction: down/up if target extends rows, else right/left.
-    let vertical = target.height() != src.height() || target.width() == src.width();
+    let vertical = fills_vertically(src, target);
     let forward = if vertical {
         target.end.row > src.end.row || target.start.row == src.start.row
     } else {
@@ -108,11 +167,25 @@ pub fn fill(cx: &mut Ctx, sheet: usize, src: RangeRef, target: RangeRef, mode: F
     if u64::from(lane_count).saturating_mul(u64::from(steps)) > MAX_FILL_CELLS {
         return Err(crate::EngineError::Other("The fill is too large.".into()));
     }
+    // Linear Trend and Growth Trend fit each lane's numbers.
+    let trends: Vec<Pattern> = match mode {
+        FillMode::LinearTrend | FillMode::GrowthTrend => {
+            let Some(lanes) = numeric_lanes(&cx.wb, sheet, src, target) else {
+                return Err(crate::EngineError::Other("Linear Trend and Growth Trend need a number in every source cell.".into()));
+            };
+            let fit = |nums: &Vec<f64>| if mode == FillMode::GrowthTrend { Pattern::growth(nums) } else { Some(Pattern::linear(nums)) };
+            let Some(trends) = lanes.iter().map(fit).collect() else {
+                return Err(crate::EngineError::Other("Growth Trend needs numbers that are all above zero or all below zero.".into()));
+            };
+            trends
+        }
+        _ => Vec::new(),
+    };
     let lanes: Vec<u32> = if vertical { (src.start.col..=src.end.col).collect() } else { (src.start.row..=src.end.row).collect() };
     let src_len = if vertical { src.height() } else { src.width() };
     let mut writes: Vec<(CellRef, Option<Cell>)> = Vec::new();
     let mut pictures = Vec::new();
-    for lane in lanes {
+    for (lane_i, lane) in lanes.into_iter().enumerate() {
         let src_cells: Vec<(CellRef, Option<Cell>)> = (0..src_len)
             .map(|i| {
                 let c = if vertical { CellRef::new(src.start.row + i, lane) } else { CellRef::new(lane, src.start.col + i) };
@@ -129,7 +202,9 @@ pub fn fill(cx: &mut Ctx, sheet: usize, src: RangeRef, target: RangeRef, mode: F
             cols.into_iter().map(|c| CellRef::new(lane, c)).collect()
         };
         let fmt = |c: &(CellRef, Option<Cell>)| cx.wb.styles.get(c.1.as_ref().map(|c| c.style).unwrap_or_default()).num_fmt.as_str();
-        let pattern = if mode.extends_series() || mode == FillMode::Alternate {
+        let pattern = if let Some(trend) = trends.get(lane_i) {
+            trend.clone()
+        } else if mode.extends_series() || mode == FillMode::Alternate {
             let first_fmt = src_cells.first().map(fmt).unwrap_or_default();
             let dates = match mode {
                 FillMode::Dates(unit) => Pattern::dates(&src_cells, unit, cx.wb.date_system, first_fmt),
@@ -163,7 +238,14 @@ pub fn fill(cx: &mut Ctx, sheet: usize, src: RangeRef, target: RangeRef, mode: F
                     writes.push((*t, Some(existing)));
                     continue;
                 }
-                FillMode::ValuesOnly | FillMode::Series | FillMode::Alternate | FillMode::Copy | FillMode::Extend | FillMode::Dates(_) => {
+                FillMode::ValuesOnly
+                | FillMode::Series
+                | FillMode::Alternate
+                | FillMode::Copy
+                | FillMode::Extend
+                | FillMode::Dates(_)
+                | FillMode::LinearTrend
+                | FillMode::GrowthTrend => {
                     let style = if mode == FillMode::ValuesOnly { sh.cell(*t).map(|c| c.style).unwrap_or_default() } else { cell.style };
                     if let Some(f) = &cell.formula {
                         if let Some(e) = f.expr() {
@@ -244,9 +326,42 @@ enum Pattern {
         step: i64,
         sys: gridcraft_core::DateSystem,
     },
+    /// Growth Trend, kept as logarithms so that a value the curve reaches is not lost to an
+    /// overflow or underflow on the way: the logarithms of the fitted magnitudes at the source's
+    /// last and first cells, the logarithm of the factor between neighboring cells, and the sign
+    /// of every value (1 or -1).
+    Growth {
+        last: f64,
+        first: f64,
+        step: f64,
+        sign: f64,
+    },
 }
 
 impl Pattern {
+    /// Linear Trend: the least-squares line through `nums`. A lone number gives a step of zero.
+    fn linear(nums: &[f64]) -> Pattern {
+        let (first, last, step) = least_squares(nums);
+        Pattern::Linear { last, first, step }
+    }
+
+    /// The exponential curve y = b·m^x fitted to `nums` at positions 0, 1, 2… by least squares on
+    /// the logarithms, as the GROWTH function fits positive numbers. Numbers below zero are fitted
+    /// by their magnitudes and keep their sign. `None` when the numbers are not all above zero or
+    /// all below zero. A lone number gives a factor of one.
+    fn growth(nums: &[f64]) -> Option<Pattern> {
+        let sign = if nums.iter().all(|y| *y > 0.0) {
+            1.0
+        } else if nums.iter().all(|y| *y < 0.0) {
+            -1.0
+        } else {
+            return None;
+        };
+        let logs: Vec<f64> = nums.iter().map(|y| (y * sign).ln()).collect();
+        let (first, last, step) = least_squares(&logs);
+        Some(Pattern::Growth { last, first, step, sign })
+    }
+
     /// The pattern of Fill Days, Fill Weekdays, Fill Months or Fill Years, or `None` when the
     /// source isn't all dates (numbers, not formulas, under a date format in its first cell).
     fn dates(cells: &[(CellRef, Option<Cell>)], unit: DateUnit, sys: gridcraft_core::DateSystem, fmt: &str) -> Option<Pattern> {
@@ -328,19 +443,7 @@ impl Pattern {
                 }
             }
             // Least-squares step for 2+ values (Excel uses a linear trend).
-            let n = nums.len() as f64;
-            let mean_x = (n - 1.0) / 2.0;
-            let mean_y = nums.iter().sum::<f64>() / n;
-            let mut num = 0.0;
-            let mut den = 0.0;
-            for (i, y) in nums.iter().enumerate() {
-                num += (i as f64 - mean_x) * (y - mean_y);
-                den += (i as f64 - mean_x).powi(2);
-            }
-            let step = if den == 0.0 { 0.0 } else { num / den };
-            let fitted_last = mean_y + step * (n - 1.0 - mean_x);
-            let fitted_first = mean_y - step * mean_x;
-            return Pattern::Linear { last: fitted_last, first: fitted_first, step };
+            return Pattern::linear(&nums);
         }
         // Text.
         if !vals.is_empty() && vals.iter().all(|v| v.is_text()) {
@@ -434,10 +537,30 @@ impl Pattern {
                 let base = if forward { *last } else { *first };
                 Some(Value::number(add_weekdays(*sys, base, step.checked_mul(k)?.checked_mul(sign)?)?))
             }
+            Pattern::Growth { last, first, step, sign: growth_sign } => {
+                let base = if forward { *last } else { *first };
+                Some(Value::number(round15(growth_sign * (base + step * (k * sign) as f64).exp())))
+            }
         }
     }
 }
 
+/// The least-squares line through `nums` at positions 0, 1, 2…: its values at the first and last
+/// positions, and its step. A lone number gives a step of zero.
+fn least_squares(nums: &[f64]) -> (f64, f64, f64) {
+    let n = nums.len() as f64;
+    let mean_x = (n - 1.0) / 2.0;
+    let mean_y = nums.iter().sum::<f64>() / n;
+    let mut num = 0.0;
+    let mut den = 0.0;
+    for (i, y) in nums.iter().enumerate() {
+        num += (i as f64 - mean_x) * (y - mean_y);
+        den += (i as f64 - mean_x).powi(2);
+    }
+    let step = if den == 0.0 { 0.0 } else { num / den };
+    (mean_y - step * mean_x, mean_y + step * (n - 1.0 - mean_x), step)
+}
+
 /// Whether `serial` falls on Monday to Friday; `None` outside the supported date range.
 fn is_weekday(sys: gridcraft_core::DateSystem, serial: f64) -> Option<bool> {
     datetime_from_serial(sys, serial).map(|d| d.weekday != 0 && d.weekday != 6)
diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs
--- a/crates/engine/src/tests.rs
+++ b/crates/engine/src/tests.rs
@@ -1003,6 +1003,94 @@ fn fill_days_weekdays_months_and_years_step_a_date_by_that_unit() {
     assert_eq!(v(&s, "J2"), Value::Number(46081.5));
 }
 
+#[test]
+fn linear_and_growth_trend_continue_the_fitted_line_or_curve() {
+    let mut s = s();
+    let nums = |s: &Session, cells: &[&str]| cells.iter().map(|c| v(s, c)).collect::<Vec<_>>();
+    let n = |xs: &[f64]| xs.iter().map(|x| Value::Number(*x)).collect::<Vec<_>>();
+    let fill =
+        |s: &mut Session, src: &str, target: &str, mode: &str| s.execute("edit.autoFill", json!({"source": src, "target": target, "mode": mode}));
+    // The examples on Excel's "Project values in a series" page: 1, 2 and 1, 3 for both trends, 2, 3
+    // for Growth Trend and 100, 95 for Linear Trend. 2, 3 gives 4, 5, 6 by Linear Trend.
+    for (col, start, linear, growth) in [
+        ("A", [1.0, 2.0], [3.0, 4.0, 5.0], [4.0, 8.0, 16.0]),
+        ("B", [1.0, 3.0], [5.0, 7.0, 9.0], [9.0, 27.0, 81.0]),
+        ("C", [2.0, 3.0], [4.0, 5.0, 6.0], [4.5, 6.75, 10.125]),
+    ] {
+        s.execute("range.setValues", json!({"range": format!("{col}1"), "values": [[start[0]], [start[1]]]})).unwrap();
+        let (src, target) = (format!("{col}1:{col}2"), format!("{col}1:{col}5"));
+        let cells: Vec<String> = (3..=5).map(|r| format!("{col}{r}")).collect();
+        let cells: Vec<&str> = cells.iter().map(String::as_str).collect();
+        fill(&mut s, &src, &target, "linearTrend").unwrap();
+        assert_eq!(nums(&s, &cells), n(&linear), "Linear Trend from {start:?}");
+        fill(&mut s, &src, &target, "growthTrend").unwrap();
+        assert_eq!(nums(&s, &cells), n(&growth), "Growth Trend from {start:?}");
+    }
+    s.execute("range.setValues", json!({"range": "F1", "values": [[100], [95]]})).unwrap();
+    fill(&mut s, "F1:F2", "F1:F4", "linearTrend").unwrap();
+    assert_eq!(nums(&s, &["F3", "F4"]), n(&[90.0, 85.0]));
+    // Three numbers: the targets follow the best fit and the source keeps its values. 1, 3, 4 fit
+    // y = 1.5x + 7/6; 1, 2, 8 fit y = 0.8909·2.8284^x.
+    s.execute("range.setValues", json!({"range": "D1", "values": [[1, 1], [3, 2], [4, 8]]})).unwrap();
+    fill(&mut s, "D1:D3", "D1:D5", "linearTrend").unwrap();
+    assert_eq!(nums(&s, &["D1", "D2", "D3", "D4", "D5"]), n(&[1.0, 3.0, 4.0, 5.66666666666667, 7.16666666666667]));
+    fill(&mut s, "E1:E3", "E1:E4", "growthTrend").unwrap();
+    assert_eq!(nums(&s, &["E1", "E2", "E3", "E4"]), n(&[1.0, 2.0, 8.0, 20.158736798318]));
+    // Upward and leftward the trend continues backward: 1, 2 gives 0 and -1, or 0.5 and 0.25.
+    s.execute("range.setValues", json!({"range": "G3", "values": [[1], [2]]})).unwrap();
+    fill(&mut s, "G3:G4", "G1:G4", "linearTrend").unwrap();
+    assert_eq!(nums(&s, &["G2", "G1"]), n(&[0.0, -1.0]));
+    s.execute("range.setValues", json!({"range": "J1", "values": [[1, 2]]})).unwrap();
+    fill(&mut s, "J1:K1", "H1:K1", "growthTrend").unwrap();
+    assert_eq!(nums(&s, &["I1", "H1"]), n(&[0.5, 0.25]));
+    // Negative numbers grow away from zero: -2, -4 gives -8.
+    s.execute("range.setValues", json!({"range": "L1", "values": [[-2], [-4]]})).unwrap();
+    fill(&mut s, "L1:L2", "L1:L3", "growthTrend").unwrap();
+    assert_eq!(v(&s, "L3"), Value::Number(-8.0));
+    // A tiny start with a huge factor still reaches 1e300 without overflowing on the way.
+    s.execute("range.setValues", json!({"range": "S1", "values": [[1e-200], [1e-100]]})).unwrap();
+    fill(&mut s, "S1:S2", "S1:S6", "growthTrend").unwrap();
+    let Value::Number(s6) = v(&s, "S6") else { panic!("S6 is {:?}", v(&s, "S6")) };
+    assert!((s6 / 1e300 - 1.0).abs() < 1e-9, "S6 = {s6}");
+    // A lone number repeats, and dates a month apart step by their days, not by months.
+    s.execute("cell.set", json!({"cell": "M1", "input": "7"})).unwrap();
+    fill(&mut s, "M1", "M1:M2", "linearTrend").unwrap();
+    assert_eq!(v(&s, "M2"), Value::Number(7.0));
+    s.execute("cell.set", json!({"cell": "N1", "input": "1/15/2026"})).unwrap();
+    s.execute("cell.set", json!({"cell": "N2", "input": "2/15/2026"})).unwrap();
+    fill(&mut s, "N1:N2", "N1:N3", "linearTrend").unwrap();
+    assert_eq!(v(&s, "N3"), Value::Number(46099.0), "2/15/2026 plus 31 days");
+    // Text or an empty cell in the source, and zero or mixed signs for Growth Trend, are refused
+    // and fill nothing.
+    s.execute("range.setValues", json!({"range": "O1", "values": [[1, 1, 0], ["x", -2, 2]]})).unwrap();
+    assert!(fill(&mut s, "O1:O2", "O1:O3", "linearTrend").is_err());
+    assert!(fill(&mut s, "P1:P2", "P1:P3", "growthTrend").is_err());
+    assert!(fill(&mut s, "Q1:Q2", "Q1:Q3", "growthTrend").is_err());
+    assert!(fill(&mut s, "R1:R2", "R1:R3", "linearTrend").is_err());
+    assert_eq!(nums(&s, &["O3", "P3", "Q3", "R3"]), [Value::Empty, Value::Empty, Value::Empty, Value::Empty]);
+    fill(&mut s, "P1:P2", "P1:P3", "linearTrend").unwrap();
+    assert_eq!(v(&s, "P3"), Value::Number(-5.0));
+}
+
+#[test]
+fn trend_fills_are_offered_for_numbers_only() {
+    let mut s = s();
+    let offered = |s: &Session, src: &str, target: &str, growth: bool| {
+        let r = |a: &str| gridcraft_core::RangeRef::parse(a).unwrap();
+        crate::fill::trend_applies(&s.doc().unwrap().wb, 0, r(src), r(target), growth)
+    };
+    s.execute("range.setValues", json!({"range": "A1", "values": [[1, -1, 1], [2, -3, "x"]]})).unwrap();
+    s.execute("cell.set", json!({"cell": "D1", "input": "=A1"})).unwrap();
+    s.execute("cell.set", json!({"cell": "D2", "input": "5"})).unwrap();
+    assert!(offered(&s, "A1:A2", "A1:A5", false) && offered(&s, "A1:A2", "A1:A5", true));
+    // Down A1:B2, each column keeps one sign; across it, row 1 holds 1 and -1.
+    assert!(offered(&s, "A1:B2", "A1:B5", true));
+    assert!(offered(&s, "A1:B2", "A1:E2", false) && !offered(&s, "A1:B2", "A1:E2", true));
+    assert!(!offered(&s, "C1:C2", "C1:C4", false), "text");
+    assert!(!offered(&s, "D1:D2", "D1:D4", false), "a formula");
+    assert!(!offered(&s, "E1:E2", "E1:E4", false), "empty cells");
+}
+
 #[test]
 fn only_a_number_in_a_date_format_offers_the_date_fills() {
     let mut s = s();
diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs
--- a/crates/ui-egui/src/grid.rs
+++ b/crates/ui-egui/src/grid.rs
@@ -157,6 +157,9 @@ pub struct GridState {
     pub context_menu_frame: u64,
     /// After a right-drag of the fill handle: the source, the dragged range and where to open the menu.
     pub fill_menu: Option<(RangeRef, RangeRef, Pos2)>,
+    /// The trends the fill menu lists, cached per (doc uid, revision, sheet, source, dragged range):
+    /// finding them reads every source cell.
+    pub fill_menu_trends: Option<((u64, u64, usize, RangeRef, RangeRef), Vec<(&'static str, &'static str)>)>,
     /// After a left-drag of the fill handle: the Auto Fill Options button beside the filled cells.
     pub fill_tag: Option<FillTag>,
     /// The right-clicked chart, picture, shape or slicer: where to open its menu, its kind and id.
@@ -3553,6 +3556,25 @@ fn fill_modes(app: &SheetApp, src: RangeRef) -> Vec<(&'static str, &'static str)
     FILL_MODES.into_iter().chain(DATE_FILL_MODES.into_iter().filter(|_| dates)).collect()
 }
 
+/// Linear Trend and Growth Trend, which the right-drag fill menu adds after the other fill modes
+/// when they apply to a fill from `src` to `target` on the active sheet.
+fn trend_fill_modes(app: &mut SheetApp, src: RangeRef, target: RangeRef) -> Vec<(&'static str, &'static str)> {
+    let Some(d) = app.session.active() else { return Vec::new() };
+    let key = (d.uid, d.revision, d.wb.active_sheet(), src, target);
+    if let Some((k, modes)) = &app.grid.fill_menu_trends
+        && *k == key
+    {
+        return modes.clone();
+    }
+    let modes: Vec<(&'static str, &'static str)> = [("Linear Trend", "linearTrend", false), ("Growth Trend", "growthTrend", true)]
+        .into_iter()
+        .filter(|(_, _, growth)| gridcraft_engine::fill::trend_applies(&d.wb, d.wb.active_sheet(), src, target, *growth))
+        .map(|(label, mode, _)| (label, mode))
+        .collect();
+    app.grid.fill_menu_trends = Some((key, modes.clone()));
+    modes
+}
+
 /// Remembers the fill just made so the Auto Fill Options button can offer the other ways.
 fn set_fill_tag(app: &mut SheetApp, src: RangeRef, target: RangeRef) {
     app.grid.fill_tag =
@@ -3636,6 +3658,7 @@ fn fill_menu(app: &mut SheetApp, ui: &mut egui::Ui, was_open: bool) {
     let Some((src, target, at)) = app.grid.fill_menu else { return };
     let mut close = false;
     let mut items = fill_modes(app, src);
+    items.extend(trend_fill_modes(app, src, target));
     items.extend([("-", ""), ("Series…", "ui:series")]);
     let response = egui::Area::new(egui::Id::unique("fill_menu")).fixed_pos(at).order(egui::Order::Foreground).show(ui.ctx(), |ui| {
         egui::Frame::popup(ui.style()).show(ui, |ui| {
diff --git a/crates/ui-egui/tests/it/fill_handle_right_drag.rs b/crates/ui-egui/tests/it/fill_handle_right_drag.rs
--- a/crates/ui-egui/tests/it/fill_handle_right_drag.rs
+++ b/crates/ui-egui/tests/it/fill_handle_right_drag.rs
@@ -1,6 +1,7 @@
 //! Dragging the fill handle with the right button fills nothing on release: it opens Excel's fill
-//! menu (Copy Cells, Fill Series, Fill Formatting Only, Fill Without Formatting, Series…, and from
-//! a date Fill Days, Fill Weekdays, Fill Months and Fill Years).
+//! menu (Copy Cells, Fill Series, Fill Formatting Only, Fill Without Formatting, Series…, from a
+//! date Fill Days, Fill Weekdays, Fill Months and Fill Years, and from numbers Linear Trend and
+//! Growth Trend).
 
 use egui::{Event, Key, Modifiers, PointerButton, Pos2, vec2};
 use egui_kittest::kittest::Queryable;
@@ -224,6 +225,42 @@ fn a_number_gets_no_date_fills() {
     }
 }
 
+#[test]
+fn growth_trend_multiplies_and_linear_trend_adds() {
+    let mut h = harness(one_two);
+    drag_handle(&mut h, "B2", "B5", PointerButton::Secondary);
+    h.get_by_label("Growth Trend").click();
+    h.run_steps(3);
+    assert_eq!(values(&h, &["B1", "B2", "B3", "B4", "B5"]), [n(1.0), n(2.0), n(4.0), n(8.0), n(16.0)]);
+    assert!(h.state().grid.fill_menu.is_none());
+    assert_eq!(selection(&h), "B1:B5");
+
+    let mut h = harness(one_two);
+    drag_handle(&mut h, "B2", "B5", PointerButton::Secondary);
+    h.get_by_label("Linear Trend").click();
+    h.run_steps(3);
+    assert_eq!(values(&h, &["B3", "B4", "B5"]), [n(3.0), n(4.0), n(5.0)]);
+}
+
+#[test]
+fn text_and_mixed_signs_leave_out_the_trends() {
+    let mut h = harness(|s| {
+        s.execute("range.setValues", json!({"range": "B1", "values": [["Item 1"], ["Item 2"]]})).unwrap();
+        s.execute("selection.set", json!({"range": "B1:B2"})).unwrap();
+    });
+    drag_handle(&mut h, "B2", "B4", PointerButton::Secondary);
+    assert!(h.query_by_label("Fill Series").is_some());
+    assert!(h.query_by_label("Linear Trend").is_none() && h.query_by_label("Growth Trend").is_none());
+
+    let mut h = harness(|s| {
+        s.execute("range.setValues", json!({"range": "B1", "values": [[-1], [2]]})).unwrap();
+        s.execute("selection.set", json!({"range": "B1:B2"})).unwrap();
+    });
+    drag_handle(&mut h, "B2", "B4", PointerButton::Secondary);
+    assert!(h.query_by_label("Linear Trend").is_some());
+    assert!(h.query_by_label("Growth Trend").is_none());
+}
+
 #[test]
 fn escape_mid_right_drag_cancels_it() {
     let mut h = harness(|s| {
diff --git a/docs/ui-parity.md b/docs/ui-parity.md
--- a/docs/ui-parity.md
+++ b/docs/ui-parity.md
@@ -1,6 +1,6 @@
 # UI parity
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (AutoSum over a selected row puts its total to the right) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (Linear Trend and Growth Trend in the right-drag fill menu; AutoSum over a selected row puts its total to the right) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
 
 Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
 **UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
@@ -52,7 +52,7 @@ this logic, with one undo step.
 | Click/drag/Shift/Ctrl selection, multi-area selection | rectangular drag works in one direction only in some cases (#28) |
 | Cursor while dragging cells, headers, freeze handles and objects | stable for the active drag (#936); Draw tools keep their crosshair; header arrows and a custom cell cursor remain unimplemented |
 | Selection outline glides to a new selection (Excel: "Provide feedback with animation") | done (10-11): 0.12 s, decelerating, with the fill and fill handle; View ▸ Animations (`view.animations`) turns it off. The timing is an estimate, not yet measured against Excel; the system's reduced-motion setting isn't followed yet |
-| Fill handle: drag to fill series, double-click to fill down, right-drag menu | fill done (Ctrl+drag swaps copy and series; month patterns use the workbook's 1900 or 1904 date system); right-drag menu partial: Copy Cells, Fill Series, Fill Formatting Only, Fill Without Formatting, and from a date Fill Days, Fill Weekdays, Fill Months and Fill Years, then Series…; a left-drag leaves an Auto Fill Options button with the same fill choices (gone once the workbook changes or the selection moves); Linear/Growth Trend and Flash Fill missing |
+| Fill handle: drag to fill series, double-click to fill down, right-drag menu | fill done (Ctrl+drag swaps copy and series; month patterns use the workbook's 1900 or 1904 date system); right-drag menu partial: Copy Cells, Fill Series, Fill Formatting Only, Fill Without Formatting, from a date Fill Days, Fill Weekdays, Fill Months and Fill Years, and from numbers Linear Trend and Growth Trend, then Series…; a left-drag leaves an Auto Fill Options button with the same fill choices except the trends (gone once the workbook changes or the selection moves); Flash Fill missing |
 | Drag-move and drag-copy cells (border drag, Ctrl/Option, Shift to insert) | done (M3) |
 | In-cell editing, Enter/Tab movement, Alt/Option+Enter line breaks | done |
 | Point mode (click/drag/arrow references), reference colouring, F4 | done; Command/Ctrl-click adds separate arguments with the edit's locale separator, and Shift-click extends the current reference from a fixed anchor on the same sheet. Pointer-event regressions cover repeated/reversed selections, manual separators, sheet switches and keyboard continuation. |
@@ -187,6 +187,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-25 | minor | The right-drag fill menu offers Linear Trend and Growth Trend when every source cell holds a number (`edit.autoFill` modes `linearTrend` and `growthTrend`). The targets continue the least-squares line (y = mx + b) or the exponential curve (y = b·m^x) fitted to each row or column of the source, so 1, 2 continues 3, 4, 5 or 4, 8, 16, and dragging up or left continues the trend backward. The source keeps its values, and a lone number repeats. Growth Trend is offered only when each row or column is all above zero or all below zero; numbers below zero grow away from zero |
 | 2026-10-25 | minor | AutoSum (Alt+=, Home › AutoSum) over a selected row puts its total to the right of the row, or in its last cell when that is empty, as in Excel; it wrote a one-cell total under each cell. A selection's empty last column takes each row's total and its empty last row each column's, with the block's total in the corner when both are empty |
 | 2026-10-25 | minor | Paste Name lays out only the names in view, so a workbook with thousands of names opens it at once; the arrow keys still scroll the chosen name into view. Go To matches a `Sheet2!Name` reference and hides a workbook name behind the active sheet's own name in any case and script (`Größe` = `GRÖSSE`), as formulas do. The two Page Layout view rows are one; the 0.7 in side margin used when Align with Page Margins is off is marked unverified |
 | 2026-10-24 | minor | Go To lists the named ranges on every sheet, as Excel's does ("The Go to popup window shows named ranges on every worksheet in your workbook"): a name scoped to another sheet appears as `Sheet2!Name`, and hidden names, constants and formulas are left out. It keeps the references entered (newest first, ten at most), and a double-click on an entry goes there. The Reference box and `edit.goTo` take several names or references separated by commas (`Price, Type`, `B14:C22,F19:G30,H21:H29`) and select them all; `edit.goTo {list: true}` returns the list |
