diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -155,7 +155,7 @@ tools, and Front, Top, Left and Custom views.
 
 A dedicated Render Queue, with Render Settings and Output Modules: **H.264** MP4 and
 **ProRes** MOV (Proxy to 4444 XQ with alpha) with audio, **HEVC** (Main / Main 10) and **AV1**
-MP4, **WebM** (VP9 with inter frames and alpha, or AV1; Opus audio), PNG, JPEG, TIFF and 32-bit EXR sequences, animated GIF and WAV/AIFF. Field
+MP4, **WebM** (VP9 with inter frames and alpha, or AV1; Opus audio), PNG, JPEG, TIFF, Targa and 32-bit EXR sequences, animated GIF and WAV/AIFF. Field
 rendering with 3:2 pulldown, effect/solo/guide/depth overrides, crop, region of interest and
 resize, Render Settings and Output Module templates with defaults, post-render actions, storage
 overflow and render logs. The same queue runs from the command line. Every encoder is pure
diff --git a/apps/effectcraft-cli/src/main.rs b/apps/effectcraft-cli/src/main.rs
--- a/apps/effectcraft-cli/src/main.rs
+++ b/apps/effectcraft-cli/src/main.rs
@@ -11,7 +11,7 @@
 //! effectcraft-cli get <comp> <layer> <path> [--time S]        read a property
 //! effectcraft-cli set <comp> <layer> <path> <value> [--time S] [--expression E]
 //! effectcraft-cli render-frame [--comp C] [--time S|--frame N] [--max-side PX|--scale K] [--out F.png] [--transparent]
-//! effectcraft-cli render [--comp C] --out FILE [--format h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff] [--start S] [--end S]
+//! effectcraft-cli render [--comp C] --out FILE [--format h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|tga|gif|wav|aiff] [--start S] [--end S]
 //!     [--work-area] [--fps N] [--resolution full|half|third|quarter|K] [--quality best|draft] [--channels rgb|rgba]
 //!     [--jpeg-quality N] [--bitrate KBPS] [--prores proxy|lt|standard|hq|4444|4444xq] [--audio auto|on|off]
 //!     [--profile main|main10] [--level auto|4.1] [--rate-control bitrate|quality] [--video-quality 1-100]
@@ -107,7 +107,7 @@ const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|se
          [--jpeg-quality N] [--bitrate KBPS] [--prores PROFILE] [--audio auto|on|off]
          [--profile main|main10] [--level auto|4.1] [--rate-control bitrate|quality] [--video-quality N]
          [--keyint FRAMES] [--webm-codec vp9|av1] [--audio-bitrate KBPS] [--opus-app audio|voice] | --queue
-                                           (formats h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff;
+                                           (formats h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|tga|gif|wav|aiff;
                                            --profile..--keyint: HEVC / AV1, --audio-bitrate/--opus-app: WebM Opus)
                                            a relative --out is relative to the working directory
                                            --progress: progress lines on stderr when it is not a terminal
diff --git a/crates/engine/src/cmd/render_queue.rs b/crates/engine/src/cmd/render_queue.rs
--- a/crates/engine/src/cmd/render_queue.rs
+++ b/crates/engine/src/cmd/render_queue.rs
@@ -203,7 +203,7 @@ fn apply_om_template(om: &mut OutputModule, t: &OutputModule) {
 }
 
 /// The `format` parameter's values.
-const FORMATS: &str = "format: h264|hevc|av1|prores|uncompressed|webm|png|jpeg|tiff|exr|gif|wav|aiff";
+const FORMATS: &str = "format: h264|hevc|av1|prores|uncompressed|webm|png|jpeg|tiff|exr|tga|gif|wav|aiff";
 
 /// Apply Output Module parameters (`template` first). `roi`: the viewer's region of interest
 /// (captured by `crop.useRoi`).
@@ -999,7 +999,7 @@ pub fn specs() -> Vec<CommandSpec> {
             "Add to Render Queue",
             ["Composition"],
             ["Cmd+M", "Cmd+Shift+/"],
-            "{comp?: id|name, template?: Render Settings template, format?: h264|hevc|av1|prores|uncompressed|webm|png|jpeg|tiff|exr|gif|wav|aiff, output?: path|template, log?: errorsOnly|plusSettings|plusPerFrameInfo, quality?: best|draft|1-100 (jpeg, webm), resolution?: full|half|third|quarter|scale, proxyUse?, effects?, solo?, guideLayers?, colorDepth?, frameBlending?, fieldRender?, pulldown?, motionBlur?, timeSpan?: workArea|comp|custom, start?: s, end?: s, duration?: s, frameRate?: fps|null, skipExisting?: bool, storageOverflow?: bool, channels?: rgb|rgba|alpha, color?, bitrate?: kbps, webmBitrate?, keyframeInterval?, proresProfile?, profile?, level?, rateControl?, webmCodec?, audioBitrate?, opusApplication?, crop?, resize?, includeProjectLink?, audio?: auto|on|off, sampleRate?, audioChannels?, audioFormat?, loop?: bool (values as in setRenderSettings / setOutputModule)}",
+            "{comp?: id|name, template?: Render Settings template, format?: h264|hevc|av1|prores|uncompressed|webm|png|jpeg|tiff|exr|tga|gif|wav|aiff, output?: path|template, log?: errorsOnly|plusSettings|plusPerFrameInfo, quality?: best|draft|1-100 (jpeg, webm), resolution?: full|half|third|quarter|scale, proxyUse?, effects?, solo?, guideLayers?, colorDepth?, frameBlending?, fieldRender?, pulldown?, motionBlur?, timeSpan?: workArea|comp|custom, start?: s, end?: s, duration?: s, frameRate?: fps|null, skipExisting?: bool, storageOverflow?: bool, channels?: rgb|rgba|alpha, color?, bitrate?: kbps, webmBitrate?, keyframeInterval?, proresProfile?, profile?, level?, rateControl?, webmCodec?, audioBitrate?, opusApplication?, crop?, resize?, includeProjectLink?, audio?: auto|on|off, sampleRate?, audioChannels?, audioFormat?, loop?: bool (values as in setRenderSettings / setOutputModule)}",
             can_add,
             add
         ),
@@ -1019,7 +1019,7 @@ pub fn specs() -> Vec<CommandSpec> {
             "Output Module Settings...",
             [],
             None,
-            "{item?|index?, module?: n (1 = first), template?: name, format?: h264|hevc|av1|prores|uncompressed|webm|png|jpeg|tiff|exr|gif|wav|aiff, channels?: rgb|rgba|alpha, color?: straight|premultiplied, quality?: 1-100, bitrate?: kbps, webmBitrate?: bool, keyframeInterval?: frames (0 = auto), proresProfile?: proxy|lt|standard|hq|4444|4444xq, profile?: main|main10 (HEVC/AV1), level?: auto|4.1, rateControl?: bitrate|quality, webmCodec?: vp9|av1, audioBitrate?: Opus kbps, opusApplication?: audio|voice, crop?: bool|{useRoi?, roi?, top?, left?, bottom?, right?}, cropTop?, cropLeft?, cropBottom?, cropRight?, resize?: bool|{preset?, width?, height?, lockAspect?, quality?: low|high}, resizeWidth?, resizeHeight?, postRenderAction?: none|import|importAndReplace|setProxy, includeProjectLink?: bool, audio?: auto|on|off, sampleRate?, audioChannels?: mono|stereo, audioFormat?: 16|24|32, loop?: bool, output?}",
+            "{item?|index?, module?: n (1 = first), template?: name, format?: h264|hevc|av1|prores|uncompressed|webm|png|jpeg|tiff|exr|tga|gif|wav|aiff, channels?: rgb|rgba|alpha, color?: straight|premultiplied, quality?: 1-100, bitrate?: kbps, webmBitrate?: bool, keyframeInterval?: frames (0 = auto), proresProfile?: proxy|lt|standard|hq|4444|4444xq, profile?: main|main10 (HEVC/AV1), level?: auto|4.1, rateControl?: bitrate|quality, webmCodec?: vp9|av1, audioBitrate?: Opus kbps, opusApplication?: audio|voice, crop?: bool|{useRoi?, roi?, top?, left?, bottom?, right?}, cropTop?, cropLeft?, cropBottom?, cropRight?, resize?: bool|{preset?, width?, height?, lockAspect?, quality?: low|high}, resizeWidth?, resizeHeight?, postRenderAction?: none|import|importAndReplace|setProxy, includeProjectLink?: bool, audio?: auto|on|off, sampleRate?, audioChannels?: mono|stereo, audioFormat?: 16|24|32, loop?: bool, output?}",
             has_items,
             set_output_module
         ),
diff --git a/crates/engine/src/render_queue.rs b/crates/engine/src/render_queue.rs
--- a/crates/engine/src/render_queue.rs
+++ b/crates/engine/src/render_queue.rs
@@ -208,6 +208,16 @@ pub fn bytes_per_frame(om: &OutputModule, w: u32, h: u32, fps: f64) -> f64 {
             let bpp = if om.channels == effectcraft_project::render_queue::Channels::Rgba { 4 } else { 3 };
             ((w as u64 * bpp).next_multiple_of(2) * h as u64) as f64
         }
+        // An 18-byte header and 24-bit BGR, 32-bit BGRA or 8-bit grey (Alpha) pixels, uncompressed.
+        OutputFormat::TargaSequence => {
+            use effectcraft_project::render_queue::Channels;
+            let bpp = match om.channels {
+                Channels::Rgb => 3,
+                Channels::Rgba => 4,
+                Channels::Alpha => 1,
+            };
+            (18 + w as u64 * h as u64 * bpp) as f64
+        }
         OutputFormat::WebM => mbps(8.0) * px / fps,
         OutputFormat::Gif => w as f64 * h as f64 * 0.5,
         OutputFormat::Wav | OutputFormat::Aiff => om.audio_sample_rate as f64 * 4.0 / fps,
diff --git a/crates/engine/src/rq_tests.rs b/crates/engine/src/rq_tests.rs
--- a/crates/engine/src/rq_tests.rs
+++ b/crates/engine/src/rq_tests.rs
@@ -116,6 +116,25 @@ fn persists_in_project_file() {
     assert!(effectcraft_project::Project::from_json(&old).unwrap().render_queue.is_empty());
 }
 
+#[test]
+fn targa_sequence_output_module() {
+    let (mut s, _) = rq_session();
+    let a = s.execute("renderQueue.add", json!({"format": "tga", "output": "/out/[compName]_[#####].tga", "channels": "rgba"})).unwrap();
+    let om = &s.project.render_queue[0].output;
+    assert_eq!((format!("{:?}", om.format), om.channels.label()), ("TargaSequence".to_string(), "RGB + Alpha"));
+    assert!(a["outputPath"].as_str().unwrap().ends_with("_[#####].tga"), "{a}");
+    // Uncompressed: an 18-byte header and 4 bytes a pixel with alpha, 3 without.
+    assert_eq!(crate::render_queue::bytes_per_frame(om, 7, 4, 10.0), 18.0 + 7.0 * 4.0 * 4.0);
+    s.execute("renderQueue.setOutputModule", json!({"index": 1, "channels": "rgb"})).unwrap();
+    assert_eq!(crate::render_queue::bytes_per_frame(&s.project.render_queue[0].output, 7, 4, 10.0), 18.0 + 7.0 * 4.0 * 3.0);
+    let f = s.execute("renderQueue.formats", json!({})).unwrap();
+    let tga = f["formats"].as_array().unwrap().iter().find(|f| f["id"] == "TargaSequence").cloned();
+    assert_eq!(
+        tga,
+        Some(json!({"id": "TargaSequence", "label": "Targa Sequence", "extension": "tga", "sequence": true, "alpha": true, "audio": false, "available": true}))
+    );
+}
+
 #[test]
 fn failure_toast_names_the_first_reason() {
     struct Failing;
@@ -186,7 +205,7 @@ fn without_exporter() {
     s.execute("renderQueue.add", json!({})).unwrap();
     assert!(!s.is_enabled("renderQueue.render"));
     let f = s.execute("renderQueue.formats", json!({})).unwrap();
-    assert_eq!(f["formats"].as_array().unwrap().len(), 13);
+    assert_eq!(f["formats"].as_array().unwrap().len(), 14);
     assert_eq!(f["formats"][0]["available"], false);
 }
 
diff --git a/crates/export/Cargo.toml b/crates/export/Cargo.toml
--- a/crates/export/Cargo.toml
+++ b/crates/export/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "effectcraft-export"
-description = "Render Queue jobs and output modules: H.264 MP4 (+AAC), ProRes and uncompressed MOV (+PCM) via FilmCraft, HEVC / AV1 MP4 (+AAC) and WebM (VP9 or AV1 + Opus) via EffectCraft encoders, PNG/JPEG/TIFF/EXR sequences, animated GIF, WAV/AIFF"
+description = "Render Queue jobs and output modules: H.264 MP4 (+AAC), ProRes and uncompressed MOV (+PCM) via FilmCraft, HEVC / AV1 MP4 (+AAC) and WebM (VP9 or AV1 + Opus) via EffectCraft encoders, PNG/JPEG/TIFF/EXR/Targa sequences, animated GIF, WAV/AIFF"
 version.workspace = true
 edition.workspace = true
 license.workspace = true
@@ -22,7 +22,7 @@ effectcraft-opusenc = { workspace = true }
 effectcraft-vp9enc = { workspace = true }
 effectcraft-hevcenc = { workspace = true }
 effectcraft-av1enc = { workspace = true }
-image = { workspace = true, features = ["png", "jpeg", "tiff", "exr"] }
+image = { workspace = true, features = ["png", "jpeg", "tiff", "exr", "tga"] }
 gif = { workspace = true }
 rayon = { workspace = true }
 web-time = { workspace = true }
diff --git a/crates/export/src/lib.rs b/crates/export/src/lib.rs
--- a/crates/export/src/lib.rs
+++ b/crates/export/src/lib.rs
@@ -7,6 +7,7 @@
 //! | Uncompressed | QuickTime MOV (`raw `) | 8-bit RGB (depth 24, rows padded to even bytes) or ARGB (depth 32) | RGB + Alpha (straight or premultiplied) | PCM 16/24-bit or 32-bit float, mono/stereo |
 //! | PNG / JPEG / TIFF sequence | one file per frame | 8-bit | PNG, TIFF | — |
 //! | OpenEXR sequence | one file per frame | 32-bit float, linear light, premultiplied | yes | — |
+//! | Targa sequence | one file per frame | uncompressed 8-bit: 24-bit RGB, 32-bit RGB + Alpha (8 alpha bits), 8-bit grey for Alpha; top-left origin | yes (straight or premultiplied) | — |
 //! | Animated GIF | GIF89a | 256-colour palette per frame (NeuQuant) | 1-bit | — |
 //! | WebM | WebM (Matroska) | VP9 profile 0 (`effectcraft-vp9enc`: key + inter frames, a key frame every 2 s, loop filter), 8-bit 4:2:0; or AV1 (`effectcraft-av1enc`) | VP9 alpha (BlockAdditional) | Opus (`effectcraft-opusenc`: SILK / hybrid / CELT by bitrate and application), 48 kHz |
 //! | HEVC | MP4 (`hvc1`) | `effectcraft-hevcenc` Main / Main 10 4:2:0, I + P slices, bitrate or constant quality, level, key-frame interval | no | AAC-LC (FilmCraft) |
@@ -338,19 +339,22 @@ fn write_still<W: std::io::Write + std::io::Seek>(
                 .write_image(&rgb, w, h, ExtendedColorType::Rgb8)
                 .map_err(|e| ExportError::Encode(e.to_string()))
         }
-        OutputFormat::PngSequence | OutputFormat::TiffSequence => {
+        OutputFormat::PngSequence | OutputFormat::TiffSequence | OutputFormat::TargaSequence => {
             let px = cx.pixels(img, comp, channels, w, h);
             let (data, ct) = match channels {
                 Channels::Rgba => (px, ExtendedColorType::Rgba8),
                 Channels::Alpha => (px.as_chunks::<4>().0.iter().map(|p| p[0]).collect(), ExtendedColorType::L8),
                 Channels::Rgb => (px.as_chunks::<4>().0.iter().flat_map(|p| [p[0], p[1], p[2]]).collect(), ExtendedColorType::Rgb8),
             };
-            if fmt == OutputFormat::PngSequence {
-                image::codecs::png::PngEncoder::new(file).write_image(&data, w, h, ct).map_err(|e| ExportError::Encode(e.to_string()))
-            } else {
+            match fmt {
+                OutputFormat::PngSequence => image::codecs::png::PngEncoder::new(file).write_image(&data, w, h, ct),
+                // Uncompressed (the encoder's run-length compression is off), top-left origin, the
+                // alpha bits in the image descriptor: 8 for RGB + Alpha, 0 otherwise.
+                OutputFormat::TargaSequence => image::codecs::tga::TgaEncoder::new(file).disable_rle().write_image(&data, w, h, ct),
                 // (the TIFF encoder needs Seek)
-                image::codecs::tiff::TiffEncoder::new(file).write_image(&data, w, h, ct).map_err(|e| ExportError::Encode(e.to_string()))
+                _ => image::codecs::tiff::TiffEncoder::new(file).write_image(&data, w, h, ct),
             }
+            .map_err(|e| ExportError::Encode(e.to_string()))
         }
         f => Err(ExportError::Unsupported(format!("{} is not a still format", f.label()))),
     }
diff --git a/crates/export/tests/it/main.rs b/crates/export/tests/it/main.rs
--- a/crates/export/tests/it/main.rs
+++ b/crates/export/tests/it/main.rs
@@ -10,5 +10,6 @@ mod native_audio_sample_positions;
 mod opus_roundtrip;
 mod render_settings;
 mod roundtrip;
+mod targa_sequence;
 mod uncompressed_mov;
 mod vp9_roundtrip;
diff --git a/crates/export/tests/it/roundtrip.rs b/crates/export/tests/it/roundtrip.rs
--- a/crates/export/tests/it/roundtrip.rs
+++ b/crates/export/tests/it/roundtrip.rs
@@ -260,6 +260,8 @@ fn image_sequences_roundtrip() {
         (OutputFormat::JpegSequence, "jpg", 0.06, Channels::Rgb),
         (OutputFormat::TiffSequence, "tif", 0.01, Channels::Rgb),
         (OutputFormat::TiffSequence, "tif", 0.01, Channels::Rgba),
+        (OutputFormat::TargaSequence, "tga", 0.01, Channels::Rgb),
+        (OutputFormat::TargaSequence, "tga", 0.01, Channels::Rgba),
     ] {
         let d = out_dir(&format!("seq-{ext}-{ch:?}"));
         let om = OutputModule { channels: ch, ..OutputModule::for_format(fmt) };
diff --git a/crates/export/tests/it/targa_sequence.rs b/crates/export/tests/it/targa_sequence.rs
new file mode 100644
--- /dev/null
+++ b/crates/export/tests/it/targa_sequence.rs
@@ -0,0 +1,218 @@
+//! Targa sequence output: every file is an uncompressed TGA (Truevision TGA 2.0 specification:
+//! an 18-byte header, BGR(A) pixels, top-left origin, the alpha bits in the image descriptor)
+//! holding exactly the pixels the lossless PNG sequence of the same job writes, and EffectCraft's
+//! own Targa import (#745) reads the frames back as the same picture.
+//!
+//! The comp is 7×4 @ 10 fps, 3 frames: frame 0 is the background alone, frames 1 and 2 add a
+//! 2×2 solid in the top two rows (so a flipped picture shows), on a half pixel horizontally, so
+//! its left and right columns carry partial alpha.
+
+use std::path::{Path, PathBuf};
+use std::sync::Arc;
+
+use effectcraft_color::Label;
+use effectcraft_export::render_queue::{AlphaMode, Channels, OutputFormat, OutputModule, RenderSettings, TimeSpan, sequence_path};
+use effectcraft_export::{Job, export};
+use effectcraft_keyframe::Value;
+use effectcraft_media::MediaPool;
+use effectcraft_project::{Comp, Footage, FootageKind, ItemId, ItemKind, LayerSource, Project, Solid, build};
+use effectcraft_render::NoFootage;
+use effectcraft_time::{FrameRate, Tick};
+
+const W: u32 = 7;
+const H: u32 = 4;
+const FRAMES: u64 = 3;
+
+fn out_dir(name: &str) -> PathBuf {
+    let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/test-out/export").join(name);
+    let _ = std::fs::remove_dir_all(&d);
+    std::fs::create_dir_all(&d).expect("mkdir");
+    d
+}
+
+fn project() -> (Project, ItemId) {
+    let mut p = Project::default();
+    let mut comp = Comp::new(W, H, FrameRate::new(10, 1), Tick::from_seconds_f64(FRAMES as f64 / 10.0));
+    comp.background = [0.1, 0.3, 0.6];
+    let sid = p.add_item("Red", Label::Red, None, ItemKind::Solid(Solid { color: [0.9, 0.2, 0.1], width: 2, height: 2, pixel_aspect: 1.0 }));
+    let mut l = build::layer(&mut p, &comp, "Red", LayerSource::Solid { item: sid }, (2, 2), None);
+    l.props.prop_mut("transform/position").expect("position").value = Value::Vec3([3.5, 1.0, 0.0]);
+    l.in_point = comp.frame_rate.tick_of(1);
+    comp.layers.push(l);
+    let cid = p.add_item("Comp 1", Label::Sandstone, None, ItemKind::Comp(comp.into()));
+    (p, cid)
+}
+
+/// Render the comp through `om` to `dir/f_[#####].<ext>`; the first frame's path.
+fn render(p: &Project, cid: ItemId, om: &OutputModule, dir: &Path) -> String {
+    let s = RenderSettings { time_span: TimeSpan::LengthOfComp, ..Default::default() };
+    let path = dir.join(format!("f_[#####].{}", om.format.extension())).to_string_lossy().to_string();
+    let job = Job {
+        project: p,
+        footage: &NoFootage,
+        expr: None,
+        accel: None,
+        comp: cid,
+        settings: &s,
+        output: om,
+        path: &path,
+        sink: None,
+        options: Default::default(),
+        nested_switches: true,
+    };
+    let r = export(&job, &mut |_| true).expect("export");
+    assert_eq!((r.frames, r.width, r.height), (FRAMES, W, H));
+    r.path
+}
+
+/// The numbered files of a render of `ext`: frames 0..FRAMES and nothing after.
+fn frame_paths(dir: &Path, ext: &str) -> Vec<String> {
+    let template = dir.join(format!("f_[#####].{ext}")).to_string_lossy().to_string();
+    assert!(!Path::new(&sequence_path(&template, FRAMES as i64)).exists(), "no frame after the last");
+    (0..FRAMES as i64).map(|k| sequence_path(&template, k)).collect()
+}
+
+/// A Targa file's header fields and its pixels as top-down RGBA bytes.
+struct Tga {
+    image_type: u8,
+    width: u16,
+    height: u16,
+    depth: u8,
+    descriptor: u8,
+    rgba: Vec<u8>,
+}
+
+fn read_tga(path: &str) -> Tga {
+    let b = std::fs::read(path).unwrap_or_else(|e| panic!("{path}: {e}"));
+    let u16_at = |i: usize| u16::from_le_bytes([b[i], b[i + 1]]);
+    assert_eq!((b[0], b[1]), (0, 0), "{path}: no image ID, no colour map");
+    assert_eq!((u16_at(8), u16_at(10)), (0, 0), "{path}: X / Y origin");
+    let (width, height, depth, descriptor) = (u16_at(12), u16_at(14), b[16], b[17]);
+    let bpp = usize::from(depth / 8);
+    // Uncompressed, without an image ID, colour map or 2.0 footer: header and pixels only.
+    assert_eq!(b.len(), 18 + usize::from(width) * usize::from(height) * bpp, "{path}: file size");
+    let rgba = b[18..]
+        .chunks(bpp)
+        .flat_map(|p| match p {
+            [b, g, r, a] => [*r, *g, *b, *a],
+            [b, g, r] => [*r, *g, *b, 255],
+            [v] => [*v, *v, *v, 255],
+            _ => panic!("{path}: {depth}-bit pixels"),
+        })
+        .collect();
+    Tga { image_type: b[2], width, height, depth, descriptor, rgba }
+}
+
+fn png_rgba(path: &str) -> Vec<u8> {
+    image::open(path).unwrap_or_else(|e| panic!("{path}: {e}")).to_rgba8().into_raw()
+}
+
+#[test]
+fn targa_sequence_writes_the_png_sequence_pixels() {
+    let (p, cid) = project();
+    for (channels, alpha_mode, image_type, depth, descriptor) in [
+        (Channels::Rgb, AlphaMode::Straight, 2, 24, 0x20),
+        (Channels::Rgba, AlphaMode::Straight, 2, 32, 0x28),
+        (Channels::Rgba, AlphaMode::Premultiplied, 2, 32, 0x28),
+        (Channels::Alpha, AlphaMode::Straight, 3, 8, 0x20),
+    ] {
+        let name = format!("tga-{channels:?}-{alpha_mode:?}");
+        let tga_dir = out_dir(&name);
+        let png_dir = out_dir(&format!("{name}-png"));
+        let om = OutputModule { channels, alpha_mode, ..OutputModule::for_format(OutputFormat::TargaSequence) };
+        let first = render(&p, cid, &om, &tga_dir);
+        assert!(first.ends_with("f_00000.tga"), "{first}");
+        render(&p, cid, &OutputModule { format: OutputFormat::PngSequence, ..om.clone() }, &png_dir);
+        let mut frames = Vec::new();
+        for (tga, png) in frame_paths(&tga_dir, "tga").iter().zip(frame_paths(&png_dir, "png")) {
+            let t = read_tga(tga);
+            assert_eq!((t.image_type, t.width, t.height, t.depth, t.descriptor), (image_type, W as u16, H as u16, depth, descriptor), "{tga}");
+            assert_eq!(t.rgba, png_rgba(&png), "{tga} matches {png}");
+            frames.push(t.rgba);
+        }
+        // Frame 0 is background only; frames 1 and 2 hold the solid, so the numbering follows
+        // the comp's frames.
+        let at = |f: &[u8], x: u32, y: u32| -> [u8; 4] {
+            let i = ((y * W + x) * 4) as usize;
+            [f[i], f[i + 1], f[i + 2], f[i + 3]]
+        };
+        assert_ne!(at(&frames[0], 3, 1), at(&frames[1], 3, 1), "{name}");
+        assert_eq!(frames[1], frames[2], "{name}");
+        // Top-down rows: the solid is in the top half, the bottom rows are still background.
+        assert_eq!(at(&frames[1], 3, 3), at(&frames[0], 3, 3), "{name}: bottom row");
+        match channels {
+            Channels::Rgb => {
+                assert_eq!(at(&frames[0], 0, 0)[3], 255);
+                assert_eq!(at(&frames[1], 3, 1), [230, 51, 26, 255], "{name}: the opaque solid");
+            }
+            Channels::Rgba => {
+                assert_eq!(at(&frames[0], 0, 0), [0, 0, 0, 0], "{name}: transparent background");
+                assert_eq!(at(&frames[1], 3, 1), [230, 51, 26, 255], "{name}: the opaque solid");
+                let edge = at(&frames[1], 2, 1);
+                assert!(edge[3] > 0 && edge[3] < 255, "{name}: partial alpha at the solid's edge {edge:?}");
+                if alpha_mode == AlphaMode::Straight {
+                    // Unmatted from the renderer's premultiplied pixel: the solid's colour, within rounding.
+                    assert!(edge[..3].iter().zip([230u8, 51, 26]).all(|(a, b)| a.abs_diff(b) <= 1), "{name}: straight colour {edge:?}");
+                } else {
+                    assert!(edge[0] < 230 && edge[0] <= edge[3], "{name}: premultiplied colour {edge:?}");
+                }
+            }
+            Channels::Alpha => {
+                assert_eq!(at(&frames[0], 0, 0), [0, 0, 0, 255], "{name}: no matte yet");
+                assert_eq!(at(&frames[1], 3, 1), [255, 255, 255, 255], "{name}: the solid's matte");
+            }
+        }
+    }
+}
+
+/// Numbered stills read back as an image sequence through the media layer (Targa: #745).
+fn import_sequence(paths: &[String]) -> (Footage, MediaPool) {
+    let pool = MediaPool::new();
+    pool.set_read_ahead(false);
+    let first = effectcraft_media::probe(&paths[0]).expect("probe our output");
+    assert_eq!((first.kind, first.width, first.height), (FootageKind::Still, W, H));
+    for p in paths {
+        let bytes: Arc<[u8]> = std::fs::read(p).expect("read").into();
+        pool.add_bytes(p, bytes);
+    }
+    let mut footage = Footage { kind: FootageKind::Sequence, sequence: paths.to_vec(), ..first };
+    footage.sync_sequence_duration();
+    (footage, pool)
+}
+
+#[test]
+fn targa_sequence_round_trips_through_targa_import() {
+    let (p, cid) = project();
+    for alpha_mode in [AlphaMode::Straight, AlphaMode::Premultiplied] {
+        let name = format!("tga-import-{alpha_mode:?}");
+        let tga_dir = out_dir(&name);
+        let png_dir = out_dir(&format!("{name}-png"));
+        let om = OutputModule { channels: Channels::Rgba, alpha_mode, ..OutputModule::for_format(OutputFormat::TargaSequence) };
+        render(&p, cid, &om, &tga_dir);
+        // The reference: the same job as a straight-alpha PNG sequence (PNG's alpha is labelled).
+        render(&p, cid, &OutputModule { format: OutputFormat::PngSequence, alpha_mode: AlphaMode::Straight, ..om.clone() }, &png_dir);
+        let (mut tga, tga_pool) = import_sequence(&frame_paths(&tga_dir, "tga"));
+        let (png, png_pool) = import_sequence(&frame_paths(&png_dir, "png"));
+        assert_eq!(tga.codec, "TGA");
+        assert_eq!(tga.sequence_frames(), FRAMES as i64);
+        // Targa doesn't label its alpha: import guesses straight, and Interpret Footage sets
+        // premultiplied for a premultiplied render.
+        assert_eq!(tga.alpha, effectcraft_project::AlphaMode::Straight);
+        if alpha_mode == AlphaMode::Premultiplied {
+            tga.alpha = effectcraft_project::AlphaMode::Premultiplied;
+        }
+        // Premultiplying to 8 bits rounds partially transparent colour.
+        let tol = if alpha_mode == AlphaMode::Straight { 0.0 } else { 1.0 / 255.0 };
+        for k in 0..FRAMES as i64 {
+            let a = tga_pool.frame_at(&tga, tga.frame_rate.tick_of(k)).expect("decode Targa");
+            let b = png_pool.frame_at(&png, png.frame_rate.tick_of(k)).expect("decode PNG");
+            assert_eq!((a.width, a.height), (W, H));
+            for (x, y) in a.data.iter().zip(&b.data) {
+                assert!(x.iter().zip(y).all(|(x, y)| (x - y).abs() <= tol), "{name} frame {k}: {x:?} != {y:?}");
+            }
+        }
+        let opaque = tga_pool.frame_at(&tga, tga.frame_rate.tick_of(1)).expect("decode");
+        assert_eq!(opaque.data[opaque.idx(0, 0)][3], 0.0, "{name}: the background stays transparent");
+        assert_eq!(opaque.data[opaque.idx(3, 1)][3], 1.0, "{name}: the solid is opaque");
+    }
+}
diff --git a/crates/project/src/render_queue.rs b/crates/project/src/render_queue.rs
--- a/crates/project/src/render_queue.rs
+++ b/crates/project/src/render_queue.rs
@@ -509,6 +509,9 @@ pub enum OutputFormat {
     TiffSequence,
     /// OpenEXR sequence (32-bit float, linear light).
     ExrSequence,
+    /// Targa sequence: uncompressed 24-bit RGB or 32-bit RGB + Alpha (8 alpha bits), top-left
+    /// origin.
+    TargaSequence,
     /// Animated GIF.
     Gif,
     /// WebM: VP9 video (alpha with RGB + Alpha) + Opus audio.
@@ -528,7 +531,7 @@ pub enum OutputFormat {
 }
 
 impl OutputFormat {
-    pub const ALL: [OutputFormat; 13] = [
+    pub const ALL: [OutputFormat; 14] = [
         OutputFormat::H264,
         OutputFormat::Hevc,
         OutputFormat::Av1,
@@ -539,6 +542,7 @@ impl OutputFormat {
         OutputFormat::JpegSequence,
         OutputFormat::TiffSequence,
         OutputFormat::ExrSequence,
+        OutputFormat::TargaSequence,
         OutputFormat::Gif,
         OutputFormat::Wav,
         OutputFormat::Aiff,
@@ -552,6 +556,7 @@ impl OutputFormat {
             OutputFormat::JpegSequence => "JPEG Sequence",
             OutputFormat::TiffSequence => "TIFF Sequence",
             OutputFormat::ExrSequence => "OpenEXR Sequence",
+            OutputFormat::TargaSequence => "Targa Sequence",
             OutputFormat::Gif => "Animated GIF",
             OutputFormat::WebM => "WebM (VP9 + Opus)",
             OutputFormat::Wav => "WAV",
@@ -569,6 +574,7 @@ impl OutputFormat {
             OutputFormat::JpegSequence => "jpg",
             OutputFormat::TiffSequence => "tif",
             OutputFormat::ExrSequence => "exr",
+            OutputFormat::TargaSequence => "tga",
             OutputFormat::Gif => "gif",
             OutputFormat::WebM => "webm",
             OutputFormat::Wav => "wav",
@@ -577,7 +583,10 @@ impl OutputFormat {
         }
     }
     pub fn is_sequence(self) -> bool {
-        matches!(self, OutputFormat::PngSequence | OutputFormat::JpegSequence | OutputFormat::TiffSequence | OutputFormat::ExrSequence)
+        matches!(
+            self,
+            OutputFormat::PngSequence | OutputFormat::JpegSequence | OutputFormat::TiffSequence | OutputFormat::ExrSequence | OutputFormat::TargaSequence
+        )
     }
     pub fn is_movie(self) -> bool {
         matches!(self, OutputFormat::H264 | OutputFormat::ProRes | OutputFormat::Uncompressed | OutputFormat::WebM | OutputFormat::Hevc | OutputFormat::Av1)
@@ -603,7 +612,7 @@ impl OutputFormat {
         if matches!(self, OutputFormat::H264 | OutputFormat::Hevc) { (even(w), even(h)) } else { (w, h) }
     }
     /// Parse a format name: `h264`, `mp4`, `prores`, `mov`, `uncompressed`, `png`, `jpeg`/`jpg`,
-    /// `tiff`/`tif`, `exr`, `gif`, or a label.
+    /// `tiff`/`tif`, `exr`, `tga`/`targa`, `gif`, or a label.
     pub fn from_name(s: &str) -> Option<OutputFormat> {
         let n = s.trim().to_ascii_lowercase().replace([' ', '.', '-', '_'], "");
         Some(match n.as_str() {
@@ -616,6 +625,7 @@ impl OutputFormat {
             "jpg" | "jpeg" | "jpegsequence" => OutputFormat::JpegSequence,
             "tif" | "tiff" | "tiffsequence" => OutputFormat::TiffSequence,
             "exr" | "openexr" | "openexrsequence" => OutputFormat::ExrSequence,
+            "tga" | "targa" | "targasequence" => OutputFormat::TargaSequence,
             "gif" | "animatedgif" => OutputFormat::Gif,
             "webm" | "vp9" | "webmvp9opus" => OutputFormat::WebM,
             "wav" | "wave" => OutputFormat::Wav,
@@ -1682,6 +1692,28 @@ mod tests {
         assert_eq!(serde_json::from_str::<OutputModule>(&json).unwrap().format, f);
     }
 
+    #[test]
+    fn targa_sequence_format() {
+        let f = OutputFormat::from_name("tga").unwrap();
+        assert_eq!(f, OutputFormat::TargaSequence);
+        assert_eq!(OutputFormat::from_name(&format!("{f:?}")), Some(f));
+        assert_eq!(OutputFormat::from_name("Targa"), Some(f));
+        assert_eq!(OutputFormat::from_name("Targa Sequence"), Some(f));
+        assert_eq!(OutputFormat::from_path("/out/shot_00001.TGA"), Some(f));
+        assert!(OutputFormat::ALL.contains(&f));
+        assert!(f.is_sequence() && f.supports_alpha() && !f.supports_audio() && !f.is_movie());
+        assert_eq!((f.label(), f.extension()), ("Targa Sequence", "tga"));
+        assert_eq!(f.coded_size(3, 5), (3, 5));
+        // A movie name becomes a numbered sequence; RGB + Alpha survives the switch.
+        let mut m = OutputModule::for_format(OutputFormat::ProRes);
+        m.output = "/out/Comp 1.mov".into();
+        m.channels = Channels::Rgba;
+        m.set_format(f);
+        assert_eq!((m.output.as_str(), m.channels), ("/out/Comp 1_[#####].tga", Channels::Rgba));
+        let json = serde_json::to_string(&m).unwrap();
+        assert_eq!(serde_json::from_str::<OutputModule>(&json).unwrap().format, f);
+    }
+
     #[test]
     fn hevc_av1_codec_options() {
         assert_eq!(OutputFormat::from_name("H.265"), Some(OutputFormat::Hevc));
diff --git a/docs/architecture.md b/docs/architecture.md
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -37,7 +37,7 @@ cpal or muda. Everything in L0 to L4, the egui UI and the web app also build for
 | L3 | `render` | Evaluation and compositing: sources, masks, effects, transforms, 3D, motion blur, mattes, blending, layer cache, audio mixdown |
 | L3 | `media` | Footage decoding (FilmCraft's pure-Rust codecs), image sequences, Photoshop and SVG stills, frame cache |
 | L3 | `expr` | The expression engine (JavaScript via boa) with the After Effects object model |
-| L3 | `export` | Render queue encoding: H.264, HEVC and AV1 MP4, ProRes, WebM (VP9 + alpha or AV1, Opus), PNG/JPEG/TIFF/EXR sequences, GIF, WAV/AIFF |
+| L3 | `export` | Render queue encoding: H.264, HEVC and AV1 MP4, ProRes, WebM (VP9 + alpha or AV1, Opus), PNG/JPEG/TIFF/EXR/Targa sequences, GIF, WAV/AIFF |
 | L3 | `gpu` | The GPU compositor and GPU effects on wgpu compute shaders (Metal, Vulkan, Direct3D 12, WebGPU), checked against the CPU renderer |
 | L3 | `interchange` | Premiere Pro interop through timeline interchange (FCP7 XML, FCPXML, OTIO, EDL, AAF, OMF via FilmCraft's `filmcraft-interchange`): sequences ↔ compositions (§6a) |
 | L3 | `lottie` | Lottie JSON / dotLottie import and export (layers, precomps, eased and spatial keyframes, shapes, masks, mattes) with a warnings list for what Lottie cannot express |
diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md
--- a/docs/file-format-parity.md
+++ b/docs/file-format-parity.md
@@ -48,7 +48,7 @@ trips and renders, checked by tests, not against After Effects), **partial** (na
 | GIF | R/W | yes | yes (animated) | good |
 | BMP | R | yes | — | good |
 | WebP | — | yes (still; animated WebP as footage with its frames, timing and alpha, #762) | — | beyond After Effects |
-| Targa `.tga` | R/W | yes (still and sequence; alpha interpreted on import) | none | good for import (#745); no Targa output |
+| Targa `.tga` | R/W | yes (still and sequence; alpha interpreted on import) | yes (sequence; uncompressed 24-bit RGB or 32-bit RGB + Alpha, straight or premultiplied) | good: import (#745); output reads back through it unchanged; the alpha is not labelled in the file (no TGA 2.0 extension area), so it is interpreted on import |
 | DPX / Cineon `.dpx` `.cin` | R/W | yes (still and sequence; 8/10/12/16-bit luma, RGB, RGBA, both byte orders; log code values shown as stored) | none | partial: import (#756); no Interpret Footage ▸ Cineon Settings (log to linear), no DPX output; common in film VFX |
 | Radiance `.hdr` | R/W | yes (still and sequence, 32-bit linear float like OpenEXR) | none | good for import (#745); no Radiance output |
 | SGI, PICT, ElectricImage, RLA/RPF images | R | none | none | Legacy; RPF camera data import exists (`keys.rpfCameraImport`) |
@@ -96,19 +96,19 @@ trips and renders, checked by tests, not against After Effects), **partial** (na
 | Mocha shapes | R (Mocha AE) | own documented JSON | — | Mocha's format is not public |
 | Vanishing Point `.vpe` | R | none | — | no public specification; the menu entry explains why |
 
-## Render queue outputs (`OutputFormat::ALL`, 13)
+## Render queue outputs (`OutputFormat::ALL`, 14)
 
 H.264 MP4, HEVC MP4, AV1 MP4, ProRes MOV, uncompressed 8-bit RGB / RGB + Alpha MOV (`raw `, + PCM;
-#372), WebM (VP9 or AV1 + Opus), PNG / JPEG / TIFF / OpenEXR sequences, animated GIF, WAV, AIFF.
+#372), WebM (VP9 or AV1 + Opus), PNG / JPEG / TIFF / OpenEXR / Targa sequences, animated GIF, WAV, AIFF.
 Missing against After Effects + Media Encoder: 16-bpc uncompressed, AVI and lossless-compressed
-single-file video (Animation / PNG codecs), DPX / Targa / Photoshop sequences, MXF, DNxHR, hardware
+single-file video (Animation / PNG codecs), DPX / Photoshop sequences, MXF, DNxHR, hardware
 encoders ([hardware-parity.md](hardware-parity.md)), and rendering safely over an existing file
 (#325).
 
 ## Estimate
 
 **≈ 130–230 h**: `.aep` / `.aepx` import 80–160 h (after the owner decision), wiring TGA / DPX /
-HDR / MXF / AVI / DNx import and DPX/TGA output 10–20 h, FBX animation and skinning, USD and Substance 20–40 h,
+HDR / MXF / AVI / DNx import and DPX output 10–20 h, FBX animation and skinning, USD and Substance 20–40 h,
 relinking 6–12 h, `.mogrt`-compatible templates 15–30 h (partly parallel with the rest).
 
 ## Revision history
