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 | Charts: 100% Stacked Line and 100% Stacked Line with Markers (`insert.chart` `{type: "line", subtype: "stacked100"}` and `"stacked100Markers"`), drawn as each value's share of its category's total; XLSX `percentStacked` line charts open as 100% stacked instead of stacked |
 | 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/chart/src/render.rs b/crates/chart/src/render.rs
--- a/crates/chart/src/render.rs
+++ b/crates/chart/src/render.rs
@@ -165,8 +165,10 @@ pub fn legend_entries(chart: &Chart, data: &ChartData) -> Vec<(String, Rgba, Swa
                 .clone()
                 .map(|s| {
                     let sw = match plot_kind(chart, s) {
-                        ChartKind::Line | ChartKind::LineStacked | ChartKind::Radar | ChartKind::ScatterLines => Swatch::Line,
-                        ChartKind::LineMarkers => Swatch::LineMarker,
+                        ChartKind::Line | ChartKind::LineStacked | ChartKind::LineStacked100 | ChartKind::Radar | ChartKind::ScatterLines => {
+                            Swatch::Line
+                        }
+                        ChartKind::LineMarkers | ChartKind::LineStacked100Markers => Swatch::LineMarker,
                         ChartKind::Scatter | ChartKind::Bubble => Swatch::Marker,
                         _ => Swatch::Box,
                     };
@@ -866,6 +868,7 @@ enum Fam {
     Col100,
     Line,
     LineStack,
+    Line100,
     Area,
     AreaStack,
     Area100,
@@ -878,6 +881,7 @@ fn family(k: ChartKind) -> Fam {
         ColumnStacked100 | BarStacked100 => Fam::Col100,
         Line | LineMarkers | Scatter | ScatterLines | Radar | Stock => Fam::Line,
         LineStacked => Fam::LineStack,
+        LineStacked100 | LineStacked100Markers => Fam::Line100,
         Area => Fam::Area,
         AreaStacked => Fam::AreaStack,
         AreaStacked100 => Fam::Area100,
@@ -887,14 +891,14 @@ fn family(k: ChartKind) -> Fam {
 
 /// Families stacked to 100%: each category's values are shares of its total.
 fn is_100(f: Fam) -> bool {
-    matches!(f, Fam::Col100 | Fam::Area100)
+    matches!(f, Fam::Col100 | Fam::Line100 | Fam::Area100)
 }
 
 fn is_col(f: Fam) -> bool {
     matches!(f, Fam::Col | Fam::ColStack | Fam::Col100)
 }
 fn is_line(f: Fam) -> bool {
-    matches!(f, Fam::Line | Fam::LineStack)
+    matches!(f, Fam::Line | Fam::LineStack | Fam::Line100)
 }
 
 fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx, horizontal: bool) {
@@ -907,7 +911,7 @@ fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx, horizonta
     let any_primary = data.series.iter().any(|s| !s.secondary);
     let sec: Vec<bool> = data.series.iter().map(|s| s.secondary && any_primary && !horizontal).collect();
 
-    // Totals for 100% stacking, per axis and family (100% columns and 100% areas stack apart).
+    // Totals for 100% stacking, per axis and family (100% columns, lines and areas stack apart).
     let mut totals: Vec<((bool, Fam), Vec<f64>)> = Vec::new();
     for (i, s) in data.series.iter().enumerate() {
         let fam = fams.get(i).copied().unwrap_or(Fam::Col);
@@ -938,7 +942,7 @@ fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx, horizonta
     for (i, s) in data.series.iter().enumerate() {
         let fam = fams.get(i).copied().unwrap_or(Fam::Col);
         let a = sec.get(i).copied().unwrap_or(false);
-        let stacked = matches!(fam, Fam::ColStack | Fam::Col100 | Fam::LineStack | Fam::AreaStack | Fam::Area100);
+        let stacked = matches!(fam, Fam::ColStack | Fam::Col100 | Fam::LineStack | Fam::Line100 | Fam::AreaStack | Fam::Area100);
         let key = (a, fam);
         let si = if stacked {
             match stacks.iter().position(|(k, _, _)| *k == key) {
@@ -964,7 +968,7 @@ fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx, horizonta
                 }),
                 (Some((_, pos, _)), _) => {
                     let v = v.unwrap_or(0.0);
-                    let v = if fam == Fam::Area100 { share(key, j, v) } else { v };
+                    let v = if is_100(fam) { share(key, j, v) } else { v };
                     pos.get_mut(j).map(|acc| {
                         let base = *acc;
                         *acc = clean(*acc + v);
@@ -1150,7 +1154,7 @@ fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx, horizonta
                     let pts: Vec<Option<[f32; 2]>> = row.iter().enumerate().map(|(j, sp)| sp.map(|(_, t)| [center(j), vpos(t, &sc)])).collect();
                     draw_runs(ctx, &pts, s.color, 2.25, s.smooth.then_some(plot));
                     let k = kinds.get(i).copied().unwrap_or(ChartKind::Line);
-                    if k == ChartKind::LineMarkers && cw >= 3.0 {
+                    if matches!(k, ChartKind::LineMarkers | ChartKind::LineStacked100Markers) && cw >= 3.0 {
                         draw_markers(ctx, pts.iter().flatten().copied(), 3.5, s.color);
                     }
                     if labels_on {
diff --git a/crates/chart/src/tests.rs b/crates/chart/src/tests.rs
--- a/crates/chart/src/tests.rs
+++ b/crates/chart/src/tests.rs
@@ -5,7 +5,7 @@ use gridcraft_model::{Anchor, Chart, ChartKind, Color, LegendPos, Series, Sparkl
 
 use crate::*;
 
-const ALL_KINDS: [ChartKind; 26] = [
+const ALL_KINDS: [ChartKind; 28] = [
     ChartKind::ColumnClustered,
     ChartKind::ColumnStacked,
     ChartKind::ColumnStacked100,
@@ -15,6 +15,8 @@ const ALL_KINDS: [ChartKind; 26] = [
     ChartKind::Line,
     ChartKind::LineMarkers,
     ChartKind::LineStacked,
+    ChartKind::LineStacked100,
+    ChartKind::LineStacked100Markers,
     ChartKind::Pie,
     ChartKind::Doughnut,
     ChartKind::Area,
@@ -1137,6 +1139,62 @@ fn hundred_percent_stacked_areas_fill_each_category_to_its_total() {
     assert!(frac(top(south, 2)).abs() < 1e-3, "Q3: South reaches {}", frac(top(south, 2)));
 }
 
+#[test]
+fn hundred_percent_stacked_lines_end_each_category_at_its_total() {
+    let (north_color, south_color) = ([20, 96, 130, 255], [25, 107, 36, 255]);
+    for kind in [ChartKind::LineStacked100, ChartKind::LineStacked100Markers] {
+        let d = ChartData {
+            category_levels: vec![],
+            palette: vec![],
+            categories: ["Q1", "Q2", "Q3", "Q4"].iter().map(|s| s.to_string()).collect(),
+            series: vec![series("North", &[10.0, 30.0, 0.0, 5.0], north_color, kind), series("South", &[30.0, 10.0, 0.0, 15.0], south_color, kind)],
+        };
+        let mut c = chart(kind);
+        c.data_labels = false;
+        let p = render(&c, &d, 640.0, 400.0, &ApproxMeasure);
+        let t = texts(&p);
+        assert!(t.contains(&"100%") && t.contains(&"0%"), "{kind:?}: {t:?}");
+        assert!(!t.contains(&"40"), "{kind:?}: {t:?}");
+        // The value axis labels sit on their gridlines.
+        let label_y = |s: &str| {
+            p.iter()
+                .find_map(|p| match p {
+                    Prim::Text { text, y, .. } if text == s => Some(*y),
+                    _ => None,
+                })
+                .unwrap_or(f32::NAN)
+        };
+        let (y0, y100) = (label_y("0%"), label_y("100%"));
+        assert!(y0 > y100, "{kind:?}: {y0} {y100}");
+        let frac = |y: f32| (y0 - y) / (y0 - y100);
+        let line = |color: Rgba| {
+            p.iter()
+                .find_map(|p| match p {
+                    // The series line is 2.25 wide; its legend swatch is 2.0.
+                    Prim::Line { pts, color: c, width, .. } if *c == color && *width > 2.1 => {
+                        Some(pts.iter().map(|q| frac(q[1])).collect::<Vec<f32>>())
+                    }
+                    _ => None,
+                })
+                .unwrap_or_default()
+        };
+        let (north, south) = (line(north_color), line(south_color));
+        assert_eq!((north.len(), south.len()), (4, 4), "{kind:?}: {north:?} {south:?}");
+        // A category whose total is zero stays on the baseline.
+        for (j, (n, s)) in [(0.25, 1.0), (0.75, 1.0), (0.0, 0.0), (0.25, 1.0)].into_iter().enumerate() {
+            assert!((north[j] - n).abs() < 1e-3 && (south[j] - s).abs() < 1e-3, "{kind:?} Q{}: {} {}", j + 1, north[j], south[j]);
+        }
+        // Point markers have a white outline; the legend's marker swatches have none.
+        let circles = |outlined: bool| {
+            p.iter()
+                .filter(|p| matches!(p, Prim::Circle { fill: Some(f), stroke, .. } if (*f == north_color || *f == south_color) && stroke.is_some() == outlined))
+                .count()
+        };
+        let with_markers = kind == ChartKind::LineStacked100Markers;
+        assert_eq!((circles(true), circles(false)), if with_markers { (8, 2) } else { (0, 0) }, "{kind:?}");
+    }
+}
+
 #[test]
 fn two_level_categories_resolve_and_draw() {
     let years = ["2023", "", "", "2024", "", ""];
diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs
--- a/crates/engine/src/cmd/insert.rs
+++ b/crates/engine/src/cmd/insert.rs
@@ -65,7 +65,7 @@ pub fn specs() -> Vec<CommandSpec> {
             "Insert Chart",
             ["Insert", "Charts"],
             None,
-            "{range?, type?: column|bar|line|pie|doughnut|area|scatter|bubble|radar|combo|histogram|waterfall|funnel|treemap|sunburst|boxWhisker|stock, subtype?: clustered|stacked|stacked100|markers, title?, at?: \"H2\", width?, height?}",
+            "{range?, type?: column|bar|line|pie|doughnut|area|scatter|bubble|radar|combo|histogram|waterfall|funnel|treemap|sunburst|boxWhisker|stock, subtype?: clustered|stacked|stacked100|markers|stacked100Markers, title?, at?: \"H2\", width?, height?}",
             has_doc,
             insert_chart
         ),
@@ -578,6 +578,8 @@ pub fn chart_kind(t: &str, sub: &str) -> ChartKind {
         ("bar", _) => ChartKind::BarClustered,
         ("line", "markers") => ChartKind::LineMarkers,
         ("line", "stacked") => ChartKind::LineStacked,
+        ("line", "stacked100") => ChartKind::LineStacked100,
+        ("line", "stacked100Markers") => ChartKind::LineStacked100Markers,
         ("line", _) => ChartKind::Line,
         ("pie", _) => ChartKind::Pie,
         ("doughnut", _) => ChartKind::Doughnut,
diff --git a/crates/engine/tests/it/chart_line_stacked100.rs b/crates/engine/tests/it/chart_line_stacked100.rs
new file mode 100644
--- /dev/null
+++ b/crates/engine/tests/it/chart_line_stacked100.rs
@@ -0,0 +1,38 @@
+//! The 100% stacked line charts: `insert.chart` and `chart.set` take type `line` with subtype
+//! `stacked100` or `stacked100Markers`.
+
+use gridcraft_engine::Session;
+use gridcraft_model::ChartKind;
+use serde_json::json;
+
+fn session() -> Session {
+    let mut s = Session::new();
+    s.new_workbook();
+    s.execute("range.setValues", json!({"range": "A1", "values": [["Quarter", "North", "South"], ["Q1", "10", "30"], ["Q2", "30", "10"]]})).unwrap();
+    s
+}
+
+fn kinds(s: &Session) -> Vec<ChartKind> {
+    s.doc().unwrap().wb.sheet(0).unwrap().charts.iter().map(|c| c.kind).collect()
+}
+
+#[test]
+fn insert_chart_makes_hundred_percent_stacked_lines() {
+    let mut s = session();
+    s.execute("insert.chart", json!({"range": "A1:C3", "type": "line", "subtype": "stacked100"})).unwrap();
+    s.execute("insert.chart", json!({"range": "A1:C3", "type": "line", "subtype": "stacked100Markers"})).unwrap();
+    assert_eq!(kinds(&s), [ChartKind::LineStacked100, ChartKind::LineStacked100Markers]);
+}
+
+#[test]
+fn chart_set_changes_a_chart_to_a_hundred_percent_stacked_line_and_back() {
+    let mut s = session();
+    s.execute("insert.chart", json!({"range": "A1:C3", "type": "line", "subtype": "stacked"})).unwrap();
+    assert_eq!(kinds(&s), [ChartKind::LineStacked]);
+    s.execute("chart.set", json!({"type": "line", "subtype": "stacked100Markers"})).unwrap();
+    assert_eq!(kinds(&s), [ChartKind::LineStacked100Markers]);
+    s.execute("chart.set", json!({"type": "line", "subtype": "stacked100"})).unwrap();
+    assert_eq!(kinds(&s), [ChartKind::LineStacked100]);
+    s.execute("edit.undo", json!({})).unwrap();
+    assert_eq!(kinds(&s), [ChartKind::LineStacked100Markers]);
+}
diff --git a/crates/engine/tests/it/main.rs b/crates/engine/tests/it/main.rs
--- a/crates/engine/tests/it/main.rs
+++ b/crates/engine/tests/it/main.rs
@@ -3,6 +3,7 @@
 mod autofit_filter_buttons;
 mod cf_formula_validate;
 mod chart_area_stacked100;
+mod chart_line_stacked100;
 mod chart_workbook_names;
 mod command_sequence_fuzz;
 mod cross_sheet_cut;
diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs
--- a/crates/mcp/src/tools.rs
+++ b/crates/mcp/src/tools.rs
@@ -260,7 +260,7 @@ pub fn tool_definitions() -> Vec<Value> {
                 json!({
                     "range": string("Source data range including headers, e.g. \"A1:C6\""),
                     "type": enumeration("Chart type", CHART_TYPES),
-                    "subtype": enumeration("Variant (optional)", &["clustered", "stacked", "stacked100", "markers"]),
+                    "subtype": enumeration("Variant (optional)", &["clustered", "stacked", "stacked100", "markers", "stacked100Markers"]),
                     "title": string("Chart title"),
                     "at": string("Top-left anchor cell (default: right of the data)"),
                 }),
diff --git a/crates/model/src/features.rs b/crates/model/src/features.rs
--- a/crates/model/src/features.rs
+++ b/crates/model/src/features.rs
@@ -406,6 +406,8 @@ pub enum ChartKind {
     Line,
     LineMarkers,
     LineStacked,
+    LineStacked100,
+    LineStacked100Markers,
     Pie,
     Doughnut,
     Area,
diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs
--- a/crates/ui-egui/src/dialogs.rs
+++ b/crates/ui-egui/src/dialogs.rs
@@ -2646,7 +2646,7 @@ fn paste_name(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &m
 }
 
 /// Every chart type the All Charts tab offers: `(type, subtype, label)` for `insert.chart`.
-const ALL_CHARTS: [(&str, &str, &str); 21] = [
+const ALL_CHARTS: [(&str, &str, &str); 23] = [
     ("column", "", "Clustered Column"),
     ("column", "stacked", "Stacked Column"),
     ("column", "stacked100", "100% Stacked Column"),
@@ -2656,6 +2656,8 @@ const ALL_CHARTS: [(&str, &str, &str); 21] = [
     ("line", "", "Line"),
     ("line", "markers", "Line with Markers"),
     ("line", "stacked", "Stacked Line"),
+    ("line", "stacked100", "100% Stacked Line"),
+    ("line", "stacked100Markers", "100% Stacked Line with Markers"),
     ("pie", "", "Pie"),
     ("doughnut", "", "Doughnut"),
     ("area", "", "Area"),
diff --git a/crates/ui-egui/src/i18n/el.rs b/crates/ui-egui/src/i18n/el.rs
--- a/crates/ui-egui/src/i18n/el.rs
+++ b/crates/ui-egui/src/i18n/el.rs
@@ -881,6 +881,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
         "Line" => "Γραμμή",
         "Line with Markers" => "Γραμμή με δείκτες",
         "Stacked Line" => "Σωρευμένες γραμμές",
+        "100% Stacked Line" => "Σωρευμένες γραμμές 100%",
+        "100% Stacked Line with Markers" => "Σωρευμένες γραμμές 100% με δείκτες",
         "Pie" => "Πίτα",
         "Doughnut" => "Δακτύλιος",
         "Area" => "Περιοχή",
diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs
--- a/crates/ui-egui/src/i18n/it.rs
+++ b/crates/ui-egui/src/i18n/it.rs
@@ -765,6 +765,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
         "Line" => "Linee",
         "Line with Markers" => "Linee con indicatori",
         "Stacked Line" => "Linee in pila",
+        "100% Stacked Line" => "Linee in pila 100%",
+        "100% Stacked Line with Markers" => "Linee in pila 100% con indicatori",
         "Pie" => "Torta",
         "Doughnut" => "Anello",
         "Area" => "Area",
diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs
--- a/crates/ui-egui/src/i18n/ja.rs
+++ b/crates/ui-egui/src/i18n/ja.rs
@@ -777,6 +777,8 @@ pub(super) const DIALOGS: &[(&str, &str)] = &[
     ("Line", "折れ線"),
     ("Line with Markers", "マーカー付き折れ線"),
     ("Stacked Line", "積み上げ折れ線"),
+    ("100% Stacked Line", "100% 積み上げ折れ線"),
+    ("100% Stacked Line with Markers", "マーカー付き 100% 積み上げ折れ線"),
     ("Pie", "円"),
     ("Doughnut", "ドーナツ"),
     ("Area", "面"),
diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs
--- a/crates/ui-egui/src/i18n/pl.rs
+++ b/crates/ui-egui/src/i18n/pl.rs
@@ -759,6 +759,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
         "Line" => "Liniowy",
         "Line with Markers" => "Liniowy ze znacznikami",
         "Stacked Line" => "Liniowy skumulowany",
+        "100% Stacked Line" => "Liniowy 100% skumulowany",
+        "100% Stacked Line with Markers" => "Liniowy 100% skumulowany ze znacznikami",
         "Pie" => "Kołowy",
         "Doughnut" => "Pierścieniowy",
         "Area" => "Warstwowy",
diff --git a/crates/ui-egui/src/i18n/pt.rs b/crates/ui-egui/src/i18n/pt.rs
--- a/crates/ui-egui/src/i18n/pt.rs
+++ b/crates/ui-egui/src/i18n/pt.rs
@@ -780,6 +780,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
         "Line" => "Linhas",
         "Line with Markers" => "Linhas com Marcadores",
         "Stacked Line" => "Linhas Empilhadas",
+        "100% Stacked Line" => "Linhas 100% Empilhadas",
+        "100% Stacked Line with Markers" => "Linhas 100% Empilhadas com Marcadores",
         "Pie" => "Pizza",
         "Doughnut" => "Rosca",
         "Area" => "Área",
diff --git a/crates/ui-egui/src/i18n/ro.rs b/crates/ui-egui/src/i18n/ro.rs
--- a/crates/ui-egui/src/i18n/ro.rs
+++ b/crates/ui-egui/src/i18n/ro.rs
@@ -765,6 +765,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
         "Line" => "Linie",
         "Line with Markers" => "Linii cu marcaje",
         "Stacked Line" => "Linii stivuite",
+        "100% Stacked Line" => "Linii 100% stivuite",
+        "100% Stacked Line with Markers" => "Linii 100% stivuite cu marcaje",
         "Pie" => "Radială",
         "Doughnut" => "Inel",
         "Area" => "Arie",
diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs
--- a/crates/ui-egui/src/i18n/uk.rs
+++ b/crates/ui-egui/src/i18n/uk.rs
@@ -762,6 +762,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
         "Line" => "Line",
         "Line with Markers" => "Line with Markers",
         "Stacked Line" => "Stacked Line",
+        "100% Stacked Line" => "100% Stacked Line",
+        "100% Stacked Line with Markers" => "100% Stacked Line with Markers",
         "Pie" => "Pie",
         "Doughnut" => "Doughnut",
         "Area" => "Area",
diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs
--- a/crates/ui-egui/src/ribbon.rs
+++ b/crates/ui-egui/src/ribbon.rs
@@ -1739,6 +1739,8 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) {
                         ("Line", "line", ""),
                         ("Line with Markers", "line", "markers"),
                         ("Stacked Line", "line", "stacked"),
+                        ("100% Stacked Line", "line", "stacked100"),
+                        ("100% Stacked Line with Markers", "line", "stacked100Markers"),
                         ("Area", "area", ""),
                         ("Stacked Area", "area", "stacked"),
                         ("100% Stacked Area", "area", "stacked100"),
diff --git a/crates/ui-egui/tests/it/interaction.rs b/crates/ui-egui/tests/it/interaction.rs
--- a/crates/ui-egui/tests/it/interaction.rs
+++ b/crates/ui-egui/tests/it/interaction.rs
@@ -407,6 +407,29 @@ fn insert_column_or_bar_menu_inserts_a_hundred_percent_stacked_bar() {
     assert_eq!(kinds, [gridcraft_engine::model::ChartKind::BarStacked100]);
 }
 
+#[test]
+fn insert_line_or_area_menu_inserts_hundred_percent_stacked_lines() {
+    // Insert › Line or Area (keytip N, N1) lists both 100% stacked line types.
+    let mut s = blank();
+    s.execute("range.setValues", json!({"range": "A1", "values": [["Quarter", "North", "South"], ["Q1", 10, 30], ["Q2", 30, 10]]})).unwrap();
+    s.execute("selection.set", json!({"range": "A1:C3"})).unwrap();
+    let mut h = harness(s);
+    for label in ["100% Stacked Line", "100% Stacked Line with Markers"] {
+        key(&mut h, Key::F10, Modifiers::NONE);
+        for (k, t) in [(Key::N, "n"), (Key::N, "n"), (Key::Num1, "1")] {
+            h.input_mut().events.push(Event::Key { key: k, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
+            h.input_mut().events.push(Event::Text(t.into()));
+            h.input_mut().events.push(Event::Key { key: k, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE });
+            h.run_steps(2);
+        }
+        h.run_steps(2);
+        h.get_by_role_and_label(egui::accesskit::Role::Button, label).click();
+        h.run_steps(2);
+    }
+    let kinds: Vec<_> = h.state().session.active().unwrap().wb.active().unwrap().charts.iter().map(|c| c.kind).collect();
+    assert_eq!(kinds, [gridcraft_engine::model::ChartKind::LineStacked100, gridcraft_engine::model::ChartKind::LineStacked100Markers]);
+}
+
 #[test]
 /// Every main ribbon tab and dark mode render with accessibility checks.
 fn renders_every_ribbon_tab_without_panicking() {
@@ -622,6 +645,24 @@ fn all_charts_inserts_a_hundred_percent_stacked_area() {
     assert_eq!(kinds, [gridcraft_engine::model::ChartKind::AreaStacked100]);
 }
 
+#[test]
+fn all_charts_inserts_a_hundred_percent_stacked_line_with_markers() {
+    let mut s = blank();
+    s.execute("range.setValues", json!({"range": "A1", "values": [["Quarter", "North", "South"], ["Q1", 10, 30], ["Q2", 30, 10]]})).unwrap();
+    s.execute("selection.set", json!({"range": "A1:C3"})).unwrap();
+    let mut h = harness(s);
+    h.state_mut().open_dialog("recommendedCharts", json!({"tab": "All Charts"}));
+    h.run_steps(4);
+    assert!(h.query_by_label("100% Stacked Line").is_some(), "the list has 100% Stacked Line");
+    h.get_by_label("100% Stacked Line with Markers").click();
+    h.run_steps(2);
+    key(&mut h, Key::Enter, Modifiers::NONE);
+    h.run_steps(2);
+    assert!(h.state().dialog.is_none());
+    let kinds: Vec<_> = h.state().session.active().unwrap().wb.active().unwrap().charts.iter().map(|c| c.kind).collect();
+    assert_eq!(kinds, [gridcraft_engine::model::ChartKind::LineStacked100Markers]);
+}
+
 #[test]
 fn recommended_charts_command_without_a_type_opens_the_dialog() {
     // #1399: the ribbon button and the NR keytip ran the command directly and inserted the first
diff --git a/crates/xlsx/src/chart.rs b/crates/xlsx/src/chart.rs
--- a/crates/xlsx/src/chart.rs
+++ b/crates/xlsx/src/chart.rs
@@ -29,12 +29,15 @@ fn group_kind(g: &El) -> Option<ChartKind> {
             }
         }
         "lineChart" | "line3DChart" => {
-            if stacked || pct {
-                ChartKind::LineStacked
-            } else {
-                let group_markers = g.child_val("marker").is_none_or(|v| v == "1" || v == "true");
-                let all_none = g.kids("ser").all(|s| s.path(&["marker", "symbol"]).and_then(|m| m.attr("val")) == Some("none"));
-                if !group_markers || (all_none && g.kids("ser").next().is_some()) { ChartKind::Line } else { ChartKind::LineMarkers }
+            let group_markers = g.child_val("marker").is_none_or(|v| v == "1" || v == "true");
+            let all_none = g.kids("ser").all(|s| s.path(&["marker", "symbol"]).and_then(|m| m.attr("val")) == Some("none"));
+            let markers = group_markers && !(all_none && g.kids("ser").next().is_some());
+            match (stacked, pct, markers) {
+                (true, _, _) => ChartKind::LineStacked,
+                (_, true, true) => ChartKind::LineStacked100Markers,
+                (_, true, false) => ChartKind::LineStacked100,
+                (_, _, true) => ChartKind::LineMarkers,
+                (_, _, false) => ChartKind::Line,
             }
         }
         "pieChart" | "pie3DChart" | "ofPieChart" => ChartKind::Pie,
@@ -284,6 +287,7 @@ pub fn read_chart(root: &El) -> Option<Chart> {
 enum G {
     Bar { bar: bool, grouping: &'static str },
     Line { stacked: bool, markers: bool },
+    Line100 { markers: bool },
     Pie,
     Doughnut,
     Area { grouping: &'static str },
@@ -303,6 +307,8 @@ fn group_for(k: ChartKind) -> G {
         ChartKind::Line => G::Line { stacked: false, markers: false },
         ChartKind::LineMarkers => G::Line { stacked: false, markers: true },
         ChartKind::LineStacked => G::Line { stacked: true, markers: false },
+        ChartKind::LineStacked100 => G::Line100 { markers: false },
+        ChartKind::LineStacked100Markers => G::Line100 { markers: true },
         ChartKind::Pie => G::Pie,
         ChartKind::Doughnut => G::Doughnut,
         ChartKind::Area => G::Area { grouping: "standard" },
@@ -533,7 +539,7 @@ fn series_xml(s: &Series, idx: usize, g: G, wb: &Workbook, si: usize) -> String
         }
     }
     let color = s.color.as_ref().and_then(|c: &Color| srgb(c, &wb.theme));
-    let line_like = matches!(g, G::Line { .. } | G::Radar | G::Scatter { lines: true });
+    let line_like = matches!(g, G::Line { .. } | G::Line100 { .. } | G::Radar | G::Scatter { lines: true });
     match (&color, line_like) {
         (Some(c), true) => {
             let _ = write!(x, "<c:spPr><a:ln w=\"28575\" cap=\"rnd\"><a:solidFill>{c}</a:solidFill><a:round/></a:ln></c:spPr>");
@@ -552,7 +558,9 @@ fn series_xml(s: &Series, idx: usize, g: G, wb: &Workbook, si: usize) -> String
     }
     match g {
         G::Bar { .. } => x.push_str("<c:invertIfNegative val=\"0\"/>"),
-        G::Line { markers: false, .. } | G::Scatter { lines: true } | G::Stock => x.push_str("<c:marker><c:symbol val=\"none\"/></c:marker>"),
+        G::Line { markers: false, .. } | G::Line100 { markers: false } | G::Scatter { lines: true } | G::Stock => {
+            x.push_str("<c:marker><c:symbol val=\"none\"/></c:marker>")
+        }
         G::Bubble => x.push_str("<c:invertIfNegative val=\"0\"/>"),
         _ => {}
     }
@@ -581,7 +589,7 @@ fn series_xml(s: &Series, idx: usize, g: G, wb: &Workbook, si: usize) -> String
                 x.push_str(&cat_data(c, &s.values, wb, si));
             }
             x.push_str(&num_data("val", Some(&s.values), wb, si));
-            if matches!(g, G::Line { .. }) {
+            if matches!(g, G::Line { .. } | G::Line100 { .. }) {
                 let _ = write!(x, "<c:smooth val=\"{}\"/>", s.smooth as u8);
             }
         }
@@ -670,6 +678,9 @@ fn group_xml(g: G, series: &[(usize, &Series)], ch: &Chart, axes: (u32, u32), wb
             "<c:lineChart><c:grouping val=\"{}\"/><c:varyColors val=\"0\"/>{sers}{dl}<c:marker val=\"1\"/>{ax}</c:lineChart>",
             if stacked { "stacked" } else { "standard" }
         ),
+        G::Line100 { .. } => {
+            format!("<c:lineChart><c:grouping val=\"percentStacked\"/><c:varyColors val=\"0\"/>{sers}{dl}<c:marker val=\"1\"/>{ax}</c:lineChart>")
+        }
         G::Pie => format!("<c:pieChart><c:varyColors val=\"1\"/>{sers}{dl}<c:firstSliceAng val=\"0\"/></c:pieChart>"),
         G::Doughnut => {
             format!("<c:doughnutChart><c:varyColors val=\"1\"/>{sers}{dl}<c:firstSliceAng val=\"0\"/><c:holeSize val=\"50\"/></c:doughnutChart>")
@@ -766,7 +777,9 @@ pub fn write_chart(wb: &Workbook, si: usize, ch: &Chart, book_name: &str) -> Str
     if ch.kind == ChartKind::Combo {
         // Series without a kind of their own: the first is a column, the others lines (as drawn).
         let kind = |i: usize, x: &Series| x.kind.filter(|k| *k != ChartKind::Combo).or((i > 0).then_some(ChartKind::Line));
-        let is_line = |k: Option<ChartKind>| matches!(k.map(group_for), Some(G::Line { .. } | G::Area { .. } | G::Radar | G::Scatter { .. }));
+        let is_line = |k: Option<ChartKind>| {
+            matches!(k.map(group_for), Some(G::Line { .. } | G::Line100 { .. } | G::Area { .. } | G::Radar | G::Scatter { .. }))
+        };
         let bars: Vec<(usize, &Series)> = all.iter().filter(|(i, x)| !x.secondary && !is_line(kind(*i, x))).copied().collect();
         let lines: Vec<(usize, &Series)> = all.iter().filter(|(i, x)| !x.secondary && is_line(kind(*i, x))).copied().collect();
         let sec: Vec<(usize, &Series)> = all.iter().filter(|(_, x)| x.secondary).copied().collect();
diff --git a/crates/xlsx/src/tests/chart_line_percent_stacked.rs b/crates/xlsx/src/tests/chart_line_percent_stacked.rs
new file mode 100644
--- /dev/null
+++ b/crates/xlsx/src/tests/chart_line_percent_stacked.rs
@@ -0,0 +1,57 @@
+//! Line chart groups by `grouping` and markers: `percentStacked` opens as a 100% stacked line.
+
+use gridcraft_model::{ChartKind, Workbook};
+
+use crate::chart::{read_chart, write_chart};
+use crate::xml::parse;
+
+/// Reads a line chart whose one group has `grouping` and whose one series holds `marker`.
+fn kind(grouping: &str, marker: &str) -> ChartKind {
+    let xml = format!(
+        "<c:chartSpace xmlns:c=\"http://schemas.openxmlformats.org/drawingml/2006/chart\"><c:chart><c:plotArea><c:lineChart>\
+         <c:grouping val=\"{grouping}\"/><c:ser><c:idx val=\"0\"/><c:order val=\"0\"/>{marker}\
+         <c:val><c:numRef><c:f>Data!$A$1:$A$5</c:f></c:numRef></c:val></c:ser><c:marker val=\"1\"/>\
+         <c:axId val=\"1\"/><c:axId val=\"2\"/></c:lineChart><c:catAx><c:axId val=\"1\"/></c:catAx><c:valAx><c:axId val=\"2\"/></c:valAx>\
+         </c:plotArea></c:chart></c:chartSpace>"
+    );
+    let root = parse(xml.as_bytes()).unwrap();
+    read_chart(&root).unwrap().kind
+}
+
+const NO_MARKER: &str = "<c:marker><c:symbol val=\"none\"/></c:marker>";
+
+#[test]
+fn percent_stacked_lines_open_as_hundred_percent_stacked() {
+    assert_eq!(kind("percentStacked", NO_MARKER), ChartKind::LineStacked100);
+    assert_eq!(kind("percentStacked", ""), ChartKind::LineStacked100Markers);
+}
+
+#[test]
+fn standard_and_stacked_lines_keep_their_kinds() {
+    assert_eq!(kind("standard", NO_MARKER), ChartKind::Line);
+    assert_eq!(kind("standard", ""), ChartKind::LineMarkers);
+    assert_eq!(kind("stacked", NO_MARKER), ChartKind::LineStacked);
+    assert_eq!(kind("stacked", ""), ChartKind::LineStacked);
+}
+
+#[test]
+fn a_hundred_percent_stacked_line_series_in_a_combo_chart_is_saved_as_a_line() {
+    let ser =
+        |i: usize| format!("<c:ser><c:idx val=\"{i}\"/><c:order val=\"{i}\"/><c:val><c:numRef><c:f>Data!$A$1:$A$5</c:f></c:numRef></c:val></c:ser>");
+    let xml = format!(
+        "<c:chartSpace xmlns:c=\"http://schemas.openxmlformats.org/drawingml/2006/chart\"><c:chart><c:plotArea>\
+         <c:barChart><c:barDir val=\"col\"/><c:grouping val=\"clustered\"/>{}<c:axId val=\"1\"/><c:axId val=\"2\"/></c:barChart>\
+         <c:lineChart><c:grouping val=\"percentStacked\"/>{}<c:axId val=\"1\"/><c:axId val=\"2\"/></c:lineChart>\
+         </c:plotArea></c:chart></c:chartSpace>",
+        ser(0),
+        ser(1)
+    );
+    let ch = read_chart(&parse(xml.as_bytes()).unwrap()).unwrap();
+    assert_eq!(ch.kind, ChartKind::Combo);
+    assert_eq!(ch.series.iter().map(|s| s.kind).collect::<Vec<_>>(), [Some(ChartKind::ColumnClustered), Some(ChartKind::LineStacked100Markers)]);
+    let saved = write_chart(&Workbook::default(), 0, &ch, "Book1");
+    let again = read_chart(&parse(saved.as_bytes()).unwrap()).unwrap();
+    let kinds: Vec<Option<ChartKind>> = again.series.iter().map(|s| s.kind).collect();
+    assert_eq!(kinds.first(), Some(&Some(ChartKind::ColumnClustered)), "{saved}");
+    assert!(kinds.get(1).is_some_and(|k| !matches!(k, Some(ChartKind::ColumnClustered))), "the line series stays out of the column group: {saved}");
+}
diff --git a/crates/xlsx/src/tests/mod.rs b/crates/xlsx/src/tests/mod.rs
--- a/crates/xlsx/src/tests/mod.rs
+++ b/crates/xlsx/src/tests/mod.rs
@@ -5,6 +5,7 @@ mod cellmetadata;
 mod chart_book_names;
 mod chart_bool_default;
 mod chart_combo_kinds;
+mod chart_line_percent_stacked;
 mod chart_ref_offsets;
 mod csv_tests;
 mod encrypted_tests;
diff --git a/crates/xlsx/src/tests/roundtrip.rs b/crates/xlsx/src/tests/roundtrip.rs
--- a/crates/xlsx/src/tests/roundtrip.rs
+++ b/crates/xlsx/src/tests/roundtrip.rs
@@ -425,6 +425,8 @@ fn sample() -> Workbook {
         ChartKind::Line,
         ChartKind::LineMarkers,
         ChartKind::LineStacked,
+        ChartKind::LineStacked100,
+        ChartKind::LineStacked100Markers,
         ChartKind::Pie,
         ChartKind::Doughnut,
         ChartKind::Area,
diff --git a/docs/chart-parity.md b/docs/chart-parity.md
--- a/docs/chart-parity.md
+++ b/docs/chart-parity.md
@@ -1,6 +1,6 @@
 # Chart parity
 
-> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-24 · **Change:** minor (100% stacked area charts) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
+> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-25 · **Change:** minor (100% stacked line charts) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
 
 Excel's chart types and chart elements against GridCraft. Part of
 [target-app-parity.md](target-app-parity.md). **Breadth ~50%, ready ~30%, 40–60 h** (estimated).
@@ -15,7 +15,7 @@ Sources: `crates/model/src/features.rs` (`ChartKind`, `Chart`, `Series`), `crate
 |---|---|---|---|
 | Column | clustered, stacked, 100% stacked, 3-D clustered/stacked/100%, 3-D column | 3 of 7 | no 3-D |
 | Bar | clustered, stacked, 100%, 3-D ×3 | 3 of 6 | no 3-D |
-| Line | line, stacked, 100% stacked, with markers ×3, 3-D line | 3 of 7 (line, with markers, stacked) | no 100% stacked; smoothed lines landed (#62) |
+| Line | line, stacked, 100% stacked, with markers ×3, 3-D line | 5 of 7 (line, with markers, stacked, 100% stacked, 100% stacked with markers) | no stacked with markers, no 3-D; 100% stacked drawn as shares of each category's total and kept as `percentStacked` in XLSX; smoothed lines landed (#62) |
 | Pie | pie, 3-D pie, pie of pie, bar of pie, doughnut | 2 of 5 (pie, doughnut) | no exploded, pie of pie, bar of pie |
 | Area | area, stacked, 100%, 3-D ×3 | 3 of 6 | no 3-D; 100% stacked drawn as shares of each category's total and kept as `percentStacked` in XLSX |
 | X Y (scatter) | markers, smooth lines (±markers), straight lines (±markers) | 2 of 5 | no smooth lines |
@@ -31,7 +31,7 @@ Sources: `crates/model/src/features.rs` (`ChartKind`, `Chart`, `Series`), `crate
 | Waterfall, Funnel | 2 | 2 of 2 | |
 | Filled map | 1 | 0 | needs geographic boundary data (licensing decision) |
 | Combo | custom combinations, secondary axis | yes (line on secondary axis) | |
-| **Total** | ~60 subtypes | **26 (~43%)** | usage-weighted breadth higher (~65%): column, bar, line, pie and scatter dominate |
+| **Total** | ~60 subtypes | **28 (~47%)** | usage-weighted breadth higher (~65%): column, bar, line, pie and scatter dominate |
 
 ## Chart elements and formatting
 
@@ -90,6 +90,7 @@ are (#1686). Such series get no `numCache`/`strCache` on save (#1690).
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-25 | minor | 100% Stacked Line and 100% Stacked Line with Markers: inserted from the ribbon and All Charts, drawn as shares of each category's total, read and written as `percentStacked` |
 | 2026-10-24 | minor | 100% Stacked Area: inserted from the ribbon and All Charts, drawn as shares of each category's total, read and written as `percentStacked` |
 | 2026-10-18 | trivial | Stale notes: stock/combo, treemap/sunburst, box and waterfall kinds survive XLSX round-trip (PRs #158, #161) |
 | 2026-10-16 | trivial | Series that refer to defined names (`[0]!Name`, `Sheet!Name`) resolve instead of showing `#NAME?`, and are saved qualified, as Excel needs (#1686) |
