diff --git a/crates/engine/src/graphic_templates.rs b/crates/engine/src/graphic_templates.rs
--- a/crates/engine/src/graphic_templates.rs
+++ b/crates/engine/src/graphic_templates.rs
@@ -19,7 +19,7 @@ use std::sync::{Arc, Mutex, OnceLock};
 
 use filmcraft_geom::{Affine, Vec2};
 use filmcraft_project::graphic::{self, layer_display_name, layer_indices};
-use filmcraft_project::graphic_design::{CharStyle, GraphicMeta, LayerExtra, Pin, PinTarget, RollMode, SourceGraphic, apply_char_style, clear_char_style};
+use filmcraft_project::graphic_design::{CharStyle, DesignEnd, GraphicMeta, LayerExtra, Pin, PinTarget, RollMode, SourceGraphic, apply_char_style, clear_char_style};
 use filmcraft_project::gtemplate::{
     self, ControlKind, FontResource, GraphicsTemplate, TEMPLATE_EXTENSION, TemplateControl, base64_decode, base64_encode, builtin_templates, ensure_uids,
     layer_uid,
@@ -642,8 +642,16 @@ fn set_roll(s: &mut Session, p: &Value) -> Result<Value> {
 fn set_responsive_time(s: &mut Session, p: &Value) -> Result<Value> {
     let clip = target_clip(s, p).ok_or_else(|| bad("graphics.setResponsiveTime", "no graphic clip"))?;
     let (intro, outro) = (dur_p(s, p, "intro"), dur_p(s, p, "outro"));
-    if intro.is_none() && outro.is_none() {
-        return Err(bad("graphics.setResponsiveTime", "need `introFrames` / `outroFrames` (or seconds / ticks)"));
+    let end = p.get("end").and_then(|v| v.as_str());
+    let parsed_end = end
+        .map(|x| match x.to_ascii_lowercase().as_str() {
+            "animate" => Ok(DesignEnd::Animate),
+            "cut" => Ok(DesignEnd::Cut),
+            _ => Err(bad("graphics.setResponsiveTime", "`end` is `animate` or `cut`")),
+        })
+        .transpose()?;
+    if intro.is_none() && outro.is_none() && parsed_end.is_none() {
+        return Err(bad("graphics.setResponsiveTime", "need `introFrames` / `outroFrames` (or seconds / ticks), or `end`"));
     }
     let m = edit_meta(s, clip, "Responsive Design - Time", |m, it| {
         if !m.has_responsive_time() {
@@ -656,6 +664,9 @@ fn set_responsive_time(s: &mut Session, p: &Value) -> Result<Value> {
         if let Some(x) = outro {
             m.outro = x.max(Tick::ZERO);
         }
+        if let Some(x) = parsed_end {
+            m.end = x;
+        }
         if m.intro + m.outro > m.design_duration {
             return Err(bad("graphics.setResponsiveTime", "the intro and outro are longer than the graphic"));
         }
@@ -665,7 +676,7 @@ fn set_responsive_time(s: &mut Session, p: &Value) -> Result<Value> {
         }
         Ok(())
     })?;
-    Ok(json!({"clip": clip.0, "intro": m.intro.0, "outro": m.outro.0, "designDuration": m.design_duration.0}))
+    Ok(json!({"clip": clip.0, "intro": m.intro.0, "outro": m.outro.0, "designDuration": m.design_duration.0, "end": if m.end == DesignEnd::Cut { "cut" } else { "animate" }}))
 }
 
 fn edges_p(p: &Value) -> [bool; 4] {
diff --git a/crates/project/src/graphic_design.rs b/crates/project/src/graphic_design.rs
--- a/crates/project/src/graphic_design.rs
+++ b/crates/project/src/graphic_design.rs
@@ -383,6 +383,20 @@ pub fn roll_offset(r: &Roll, u: Tick, d: Tick, canvas: (u32, u32), content: [f64
     }
 }
 
+/// What the clip end plays of the graphic's out-animation: `Animate` (the default) runs the
+/// protected outro against the design end; `Cut` holds the pose before the out-animation to the
+/// last frame, so a graphic can end on its current pose without editing keyframes.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+pub enum DesignEnd {
+    #[default]
+    Animate,
+    Cut,
+}
+
+fn is_animate_end(e: &DesignEnd) -> bool {
+    *e == DesignEnd::default()
+}
+
 /// Graphic-level settings of a graphic clip (`TrackItem::graphic`).
 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
 #[serde(default, rename_all = "camelCase")]
@@ -393,6 +407,9 @@ pub struct GraphicMeta {
     pub intro: Tick,
     /// Protected outro duration (up to the clip end).
     pub outro: Tick,
+    /// Whether the outro animates or the clip holds the pose before it and cuts.
+    #[serde(skip_serializing_if = "is_animate_end")]
+    pub end: DesignEnd,
     /// Media time of the clip start when the intro / outro were set; keyframes are read relative
     /// to it, so trimming the head keeps the intro playing from the new start.
     pub design_in: Tick,
@@ -419,17 +436,19 @@ impl GraphicMeta {
 /// Responsive Design – Time: the media time at which to read the graphic's keyframes at
 /// clip-relative time `u` of a clip `d` long. Inside the intro, time runs from `design_in`;
 /// inside the outro, it runs up to the designed end; the middle is stretched linearly between
-/// them. Without an intro or outro this is `design_in + u` (the plain clip time).
+/// them. Without an intro or outro this is `design_in + u` (the plain clip time). With `end`
+/// `Cut`, the outro holds the pose the out-animation starts from instead of playing it.
 pub fn remap_time(m: &GraphicMeta, u: Tick, d: Tick) -> Tick {
     if !m.has_responsive_time() {
         return m.design_in + u;
     }
     let (intro, outro, dd) = (m.intro.0.max(0), m.outro.0.max(0), m.design_duration.0);
+    let cut = m.end == DesignEnd::Cut;
     let (u, d) = (u.0, d.0);
     let local = if u < intro {
         u
     } else if d - u <= outro {
-        dd - (d - u)
+        if cut { dd - outro } else { dd - (d - u) }
     } else {
         let (a, b) = (intro as i128, (d - outro) as i128);
         let (aa, bb) = (intro as i128, (dd - outro) as i128);
@@ -590,4 +609,18 @@ mod tests {
         let plain = GraphicMeta { design_in: sec(1.0), ..Default::default() };
         assert_eq!(remap_time(&plain, sec(2.0), sec(3.0)), sec(3.0));
     }
+
+    #[test]
+    fn cut_end_holds_the_pose_before_the_out_animation() {
+        let mut m = GraphicMeta { intro: sec(0.5), outro: sec(0.5), design_in: Tick::ZERO, design_duration: sec(6.0), ..Default::default() };
+        // the default keeps the current timing: the outro plays against the design end
+        assert_eq!(m.end, DesignEnd::Animate);
+        assert_eq!(remap_time(&m, sec(2.9), sec(3.0)), sec(5.9));
+        m.end = DesignEnd::Cut;
+        // end Cut: the tail holds the pose the out-animation starts from, the middle still stretches
+        assert_eq!(remap_time(&m, sec(2.9), sec(3.0)), sec(5.5));
+        assert_eq!(remap_time(&m, sec(1.5), sec(3.0)), sec(3.0));
+        // an extended clip holds the same pose through its tail
+        assert_eq!(remap_time(&m, sec(8.7), sec(9.0)), sec(5.5));
+    }
 }
