diff --git a/crates/lottie/src/shapes.rs b/crates/lottie/src/shapes.rs
--- a/crates/lottie/src/shapes.rs
+++ b/crates/lottie/src/shapes.rs
@@ -3,8 +3,8 @@
 
 use effectcraft_color::BlendMode;
 use effectcraft_keyframe::{Gradient, ShapePath, Value};
-use effectcraft_project::build::{self, Ids};
-use effectcraft_project::{Node, PropGroup};
+use effectcraft_project::build::{self, EXTRA_DASHES, Ids};
+use effectcraft_project::{Node, ParamUi, PropGroup};
 use serde_json::{Map, Value as Json, json};
 
 use crate::anim::{self, Ex, Im, export_prop, import_prop};
@@ -356,7 +356,7 @@ fn import_blend(j: &Json, g: &mut PropGroup) {
     }
 }
 
-fn import_stroke(im: &Im, j: &Json, g: &mut PropGroup) {
+fn import_stroke(im: &Im, ids: &mut Ids, j: &Json, g: &mut PropGroup) {
     set(im, g, "width", j.get("w"), &anim::to_scalar);
     if let Some(lc) = int(j, "lc") {
         set_enum(g, "cap", lc.saturating_sub(1).clamp(0, 2) as u32);
@@ -374,17 +374,32 @@ fn import_stroke(im: &Im, j: &Json, g: &mut PropGroup) {
     if let Some(list) = j.get("d").and_then(Json::as_array)
         && let Some(d) = g.sub_mut("dashes")
     {
-        let mut seen_dash = false;
+        // The n-th `d` and `g` are the n-th Dash/Gap pair; the extra pairs are created as the Dashes "+" does.
+        let (mut dashes, mut gaps) = (0, 0);
         for e in list {
-            let m = match e.get("n").and_then(Json::as_str) {
-                Some("d") if !seen_dash => {
-                    seen_dash = true;
-                    "dash"
+            let (n, pair) = match e.get("n").and_then(Json::as_str) {
+                Some("d") => (&mut dashes, 0),
+                Some("g") => (&mut gaps, 1),
+                Some("o") => {
+                    set(im, d, "offset", e.get("v"), &anim::to_scalar);
+                    continue;
                 }
-                Some("g") => "gap",
-                Some("o") => "offset",
                 _ => continue,
             };
+            let k = *n;
+            *n += 1;
+            let m = match k {
+                0 => ["dash", "gap"][pair],
+                _ => {
+                    let Some(&(dm, gm)) = EXTRA_DASHES.get(k - 1) else { continue };
+                    if d.get(dm).is_none() {
+                        let at = d.children.iter().position(|c| c.match_id() == "offset").unwrap_or(d.children.len());
+                        d.children.insert(at, ids.prop(gm, &format!("Gap {}", k + 1), Value::Scalar(0.0)).with_ui(ParamUi::Pixels).into());
+                        d.children.insert(at, ids.prop(dm, &format!("Dash {}", k + 1), Value::Scalar(0.0)).with_ui(ParamUi::Pixels).into());
+                    }
+                    [dm, gm][pair]
+                }
+            };
             set(im, d, m, e.get("v"), &anim::to_scalar);
         }
     }
@@ -474,7 +489,7 @@ pub(crate) fn import_items(im: &mut Im, ids: &mut Ids, items: &[Json]) -> Vec<Pr
                 let mut g = build::shape_stroke(ids, [1.0, 1.0, 1.0, 1.0], 2.0);
                 set(im, &mut g, "color", j.get("c"), &anim::to_color);
                 set(im, &mut g, "opacity", j.get("o"), &anim::to_scalar);
-                import_stroke(im, j, &mut g);
+                import_stroke(im, ids, j, &mut g);
                 import_blend(j, &mut g);
                 g
             }
@@ -501,7 +516,7 @@ pub(crate) fn import_items(im: &mut Im, ids: &mut Ids, items: &[Json]) -> Vec<Pr
                         set_enum(&mut g, "rule", 1);
                     }
                 } else {
-                    import_stroke(im, j, &mut g);
+                    import_stroke(im, ids, j, &mut g);
                 }
                 import_blend(j, &mut g);
                 g
diff --git a/crates/lottie/src/tests.rs b/crates/lottie/src/tests.rs
--- a/crates/lottie/src/tests.rs
+++ b/crates/lottie/src/tests.rs
@@ -407,6 +407,30 @@ fn offset_paths_miter_uses_animatable_lottie_value() {
     assert_eq!(second.get("miter").unwrap().value, Value::Scalar(6.0));
 }
 
+#[test]
+fn extra_dash_gap_pairs_survive_a_roundtrip() {
+    let (mut p, cid) = setup();
+    let l = shape_layer(&mut p, cid, |ids| {
+        let mut stroke = build::shape_stroke(ids, [0.0, 0.0, 1.0, 1.0], 3.0);
+        let d = stroke.sub_mut("dashes").unwrap();
+        d.get_mut("dash").unwrap().value = Value::Scalar(10.0);
+        d.get_mut("gap").unwrap().value = Value::Scalar(5.0);
+        let at = d.children.iter().position(|n| n.match_id() == "offset").unwrap();
+        d.children.insert(at, ids.prop("gap2", "Gap 2", Value::Scalar(3.0)).into());
+        d.children.insert(at, ids.prop("dash2", "Dash 2", Value::Scalar(20.0)).into());
+        let rect = build::shape_rect(ids, [60.0, 40.0], [0.0, 0.0], 0.0);
+        vec![build::shape_group(ids, "G", vec![rect, stroke])]
+    });
+    push(&mut p, cid, l);
+    let (q, nc, _, _) = roundtrip(&p, cid, &ExportOptions::default());
+    let c = q.comp(nc).unwrap();
+    let g = c.layers[0].props.sub("contents").unwrap().groups().next().unwrap();
+    let stroke = g.sub("contents").unwrap().groups().find(|g| g.match_id == "stroke").unwrap();
+    let d = stroke.sub("dashes").unwrap();
+    let v = |m: &str| d.get(m).map(|p| p.value.as_f64());
+    assert_eq!((v("dash"), v("gap"), v("dash2"), v("gap2")), (Some(10.0), Some(5.0), Some(20.0), Some(3.0)));
+}
+
 // ---------------------------------------------------------------- layers
 
 #[test]
