diff --git a/crates/effects/src/migrate.rs b/crates/effects/src/migrate.rs
--- a/crates/effects/src/migrate.rs
+++ b/crates/effects/src/migrate.rs
@@ -458,6 +458,41 @@ fn same_point(a: &Value, b: &Value) -> bool {
     a.len() == b.len() && a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-6)
 }
 
+/// The Cineon Converter Gamma that gave linear output before project schema 3.
+pub const CINEON_LINEAR_GAMMA_BEFORE_SCHEMA_3: f64 = 1.7;
+
+/// Bring Cineon Converter's Gamma, saved before project schema 3, onto today's scale.
+///
+/// Log to Linear raised the linear value to `1.7 / gamma`, and Linear to Log to `gamma / 1.7`;
+/// they now use `1 / gamma` and `gamma`, so Gamma 1 is linear and a larger value lightens the
+/// midtones. Dividing the value, its keyframes and their speeds by 1.7 keeps
+/// what the effect renders. Expressions are not rewritten. Instances of other effects are left
+/// as they are.
+pub fn cineon_gamma_to_unit_scale(effect: &str, g: &mut PropGroup) {
+    if effect == "ec.utility.cineon"
+        && let Some(pr) = g.get_mut("gamma")
+    {
+        scale_cineon_gamma(pr);
+    }
+}
+
+/// [`cineon_gamma_to_unit_scale`] for one Gamma property (an animation preset that saved the
+/// property without its effect).
+pub fn scale_cineon_gamma(pr: &mut Property) {
+    let scale = |v: &mut Value| {
+        if let Value::Scalar(x) = v {
+            *x /= CINEON_LINEAR_GAMMA_BEFORE_SCHEMA_3;
+        }
+    };
+    scale(&mut pr.value);
+    for k in &mut pr.keys {
+        scale(&mut k.value);
+        for e in k.in_ease.iter_mut().chain(k.out_ease.iter_mut()) {
+            e.speed /= CINEON_LINEAR_GAMMA_BEFORE_SCHEMA_3;
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/effects/src/utility.rs b/crates/effects/src/utility.rs
--- a/crates/effects/src/utility.rs
+++ b/crates/effects/src/utility.rs
@@ -100,12 +100,13 @@ impl Cineon {
     fn knee(&self) -> f32 {
         1.0 - (self.rolloff / 100.0).clamp(0.0, 0.99)
     }
-    /// Normalised code value (code / 1023) → linear.
+    /// Normalized code value (code / 1023) → linear, raised to `1 / gamma`: Gamma 1 gives linear
+    /// light, and a larger Gamma lightens the midtones.
     pub fn log_to_lin(&self, v: f32) -> f32 {
         let off = self.offset();
         let code = v * 1023.0;
         let mut lin = (10f32.powf((code - self.white) * 0.002 / 0.6) - off) / (1.0 - off).max(1e-6);
-        lin = lin.max(0.0).powf(1.7 / self.gamma.max(0.01));
+        lin = lin.max(0.0).powf(1.0 / self.gamma.max(0.01));
         let k = self.knee();
         if k < 1.0 && lin > k {
             let w = 1.0 - k;
@@ -123,7 +124,7 @@ impl Cineon {
             let w = 1.0 - k;
             lin = k - w * (1.0 - (lin - k) / w).max(1e-6).ln();
         }
-        lin = lin.max(0.0).powf(self.gamma.max(0.01) / 1.7);
+        lin = lin.max(0.0).powf(self.gamma.max(0.01));
         let code = self.white + (lin * (1.0 - off) + off).max(1e-9).log10() * 0.6 / 0.002;
         code / 1023.0
     }
@@ -138,7 +139,7 @@ fn cineon(ctx: &EffectCtx, mut b: Buf) -> Buf {
         gamma: ctx.params.f("gamma") as f32,
         rolloff: ctx.params.f("highlightRolloff") as f32,
     };
-    let std = Cineon { black: 95.0, white: 685.0, ib: 0.0, iw: 1.0, gamma: 1.7, rolloff: c.rolloff };
+    let std = Cineon { black: 95.0, white: 685.0, ib: 0.0, iw: 1.0, gamma: 1.0, rolloff: c.rolloff };
     let kind = ctx.params.e("conversionType");
     b.img.map_straight(|v| {
         v.map(|x| match kind {
@@ -380,7 +381,7 @@ pub fn specs() -> Vec<EffectSpec> {
                 p("internalBlackPoint", "Internal Black Point", num(0.0), slider(-1.0, 2.0, 0.0, 1.0, 3)),
                 p("tenBitWhitePoint", "10 Bit White Point", num(685.0), slider(0.0, 1023.0, 0.0, 1023.0, 0)),
                 p("internalWhitePoint", "Internal White Point", num(1.0), slider(-1.0, 2.0, 0.0, 1.0, 3)),
-                p("gamma", "Gamma", num(1.7), slider(0.1, 5.0, 0.1, 5.0, 2)),
+                p("gamma", "Gamma", num(1.0), slider(0.05, 5.0, 0.1, 5.0, 2)),
                 p("highlightRolloff", "Highlight Rolloff", num(20.0), slider(0.0, 100.0, 0.0, 100.0, 0)),
             ],
             cineon,
@@ -486,13 +487,34 @@ mod tests {
 
     #[test]
     fn cineon_round_trip() {
-        let c = Cineon { black: 95.0, white: 685.0, ib: 0.0, iw: 1.0, gamma: 1.7, rolloff: 20.0 };
-        for code in [100.0f32, 200.0, 350.0, 500.0, 600.0] {
-            let v = code / 1023.0;
-            let back = c.lin_to_log(c.log_to_lin(v));
-            assert!((back - v).abs() < 1e-3, "{code}: {back}");
+        for gamma in [1.0, 1.7, 0.6] {
+            let c = Cineon { black: 95.0, white: 685.0, ib: 0.0, iw: 1.0, gamma, rolloff: 20.0 };
+            for code in [100.0f32, 200.0, 350.0, 500.0, 600.0] {
+                let v = code / 1023.0;
+                let back = c.lin_to_log(c.log_to_lin(v));
+                assert!((back - v).abs() < 1e-3, "gamma {gamma}, {code}: {back}");
+            }
+            assert!(c.log_to_lin(95.0 / 1023.0).abs() < 1e-4);
         }
-        assert!(c.log_to_lin(95.0 / 1023.0).abs() < 1e-4);
+    }
+
+    /// Gamma 1 (the default) gives linear light: Kodak's printing-density curve with nothing
+    /// raised, so code 400 is (10^(-0.95) - 10^(-1.9667)) / (1 - 10^(-1.9667)) = 0.10251. A larger
+    /// Gamma lightens the midtones (`lin^(1/gamma)`), and Linear to Log undoes it.
+    #[test]
+    fn cineon_gamma_1_is_linear_and_larger_lightens() {
+        let c = |gamma| Cineon { black: 95.0, white: 685.0, ib: 0.0, iw: 1.0, gamma, rolloff: 0.0 };
+        let lin = c(1.0).log_to_lin(400.0 / 1023.0);
+        assert!((lin - 0.10251).abs() < 1e-4, "{lin}");
+        let lighter = c(2.0).log_to_lin(400.0 / 1023.0);
+        assert!((lighter - 0.10251f32.sqrt()).abs() < 1e-4, "{lighter}");
+        assert!((c(2.0).lin_to_log(lighter) - 400.0 / 1023.0).abs() < 1e-4);
+        let out = run("ec.utility.cineon", &[], {
+            let mut img = Image::new(1, 1);
+            img.set(0, 0, [400.0 / 1023.0, 400.0 / 1023.0, 400.0 / 1023.0, 1.0]);
+            img
+        });
+        assert!((out.img.data[0][0] - 0.10251).abs() < 1e-4, "the default Gamma is linear: {:?}", out.img.data[0]);
     }
 
     #[test]
diff --git a/crates/engine/src/cmd/animation.rs b/crates/engine/src/cmd/animation.rs
--- a/crates/engine/src/cmd/animation.rs
+++ b/crates/engine/src/cmd/animation.rs
@@ -16,6 +16,11 @@ use crate::{EngineError, KeyRef, Result, Session, cmd};
 /// Preset file format version tag (`*.ecpreset`, JSON).
 pub const PRESET_TAG: &str = "effectcraftAnimationPreset";
 
+/// The preset format version written under [`PRESET_TAG`]. 2: Cineon Converter's Gamma is on
+/// the scale of project schema 3 (1 is linear); Gamma values in version 1 presets are converted
+/// when applied ([`effectcraft_effects::migrate::cineon_gamma_to_unit_scale`]).
+pub const PRESET_VERSION: u64 = 2;
+
 fn has_selected_props(s: &Session) -> std::result::Result<(), String> {
     has_comp(s)?;
     if selected_leaf_props(s).is_empty() && selected_effects(s).is_empty() { Err("select properties or effects first".into()) } else { Ok(()) }
@@ -82,7 +87,7 @@ fn save_preset(s: &mut Session, p: &Value) -> Result<Value> {
     let name = str_p(p, "name")
         .map(str::to_string)
         .unwrap_or_else(|| std::path::Path::new(&path).file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_default());
-    let doc = json!({PRESET_TAG: 1, "name": name, "properties": props_json, "effects": fx_json});
+    let doc = json!({PRESET_TAG: PRESET_VERSION, "name": name, "properties": props_json, "effects": fx_json});
     let text = serde_json::to_string_pretty(&doc).unwrap_or_default();
     s.services.write_file(&path, text.as_bytes()).map_err(|e| EngineError::Other(format!("cannot write {path}: {e}")))?;
     Ok(json!({"path": path, "properties": props.len(), "effects": effects.len()}))
@@ -110,9 +115,18 @@ fn apply_preset(s: &mut Session, p: &Value) -> Result<Value> {
         s.save_prefs();
         s.prefs_revision += 1;
     }
-    let effects: Vec<PropGroup> =
+    let mut effects: Vec<PropGroup> =
         doc["effects"].as_array().map(|a| a.iter().filter_map(|v| serde_json::from_value(v.clone()).ok()).collect()).unwrap_or_default();
-    let props: Vec<(String, Property)> = doc["properties"]
+    let unit_gamma = doc[PRESET_TAG].as_u64().unwrap_or(1) < PRESET_VERSION;
+    if unit_gamma {
+        for g in &mut effects {
+            if let GroupKind::Effect { effect } = &g.kind {
+                let effect = effect.clone();
+                effectcraft_effects::migrate::cineon_gamma_to_unit_scale(&effect, g);
+            }
+        }
+    }
+    let mut props: Vec<(String, Property)> = doc["properties"]
         .as_array()
         .map(|a| {
             a.iter()
@@ -129,14 +143,26 @@ fn apply_preset(s: &mut Session, p: &Value) -> Result<Value> {
                 .collect()
         })
         .unwrap_or_default();
+    if unit_gamma {
+        for (path, pr) in &mut props {
+            if is_cineon_gamma_path(path) {
+                effectcraft_effects::migrate::scale_cineon_gamma(pr);
+            }
+        }
+    }
     let t = s.time();
     let mut applied = 0;
     let mut skipped = vec![];
+    let gamma_controls = unit_gamma.then(|| crate::session::cineon_gamma_controls(&s.project));
     s.edit("Apply Animation Preset", None, |proj, _| {
         let mut next = proj.next_id;
         for lid in &ids {
             let l = layer_mut(proj, cid, *lid)?;
             let lt = l.layer_time(t);
+            let exposed_gamma = match l.source {
+                LayerSource::Comp { item } => gamma_controls.as_ref().map(|controls| (item, controls)),
+                _ => None,
+            };
             if let Some(fx) = l.props.sub_mut("effects") {
                 for g in &effects {
                     let mut g = g.clone();
@@ -163,6 +189,13 @@ fn apply_preset(s: &mut Session, p: &Value) -> Result<Value> {
                         if src.expr.is_some() {
                             pr.expr = src.expr.clone();
                         }
+                        if resolved.starts_with("essential/")
+                            && exposed_gamma.is_some_and(|(item, controls)| {
+                                effectcraft_project::essential::control_of(&pr.match_id).is_some_and(|id| controls.contains(&(item, id)))
+                            })
+                        {
+                            effectcraft_effects::migrate::scale_cineon_gamma(pr);
+                        }
                         applied += 1;
                     }
                     _ => skipped.push(path.clone()),
@@ -175,6 +208,14 @@ fn apply_preset(s: &mut Session, p: &Value) -> Result<Value> {
     Ok(json!({"applied": applied, "skipped": skipped}))
 }
 
+/// Whether a preset property path (`effects/<match name>[#n]/<property>`, as
+/// [`match_path_of`] writes it) is a Cineon Converter's Gamma.
+fn is_cineon_gamma_path(path: &str) -> bool {
+    let Some((seg, tail)) = path.strip_prefix("effects/").and_then(|r| r.split_once('/')) else { return false };
+    let match_id = seg.rsplit_once('#').map_or(seg, |(m, _)| m);
+    match_id == "ec.utility.cineon" && tail == "gamma"
+}
+
 // ---------------------------------------------------------------- keyframes
 
 fn add_keyframe(s: &mut Session, _: &Value) -> Result<Value> {
diff --git a/crates/engine/src/session.rs b/crates/engine/src/session.rs
--- a/crates/engine/src/session.rs
+++ b/crates/engine/src/session.rs
@@ -8,4 +8,5 @@ mod dispatch;
 mod edit;
 mod render;
 
+pub(crate) use edit::cineon_gamma_controls;
 pub use edit::upgrade_effects;
diff --git a/crates/engine/src/session/edit.rs b/crates/engine/src/session/edit.rs
--- a/crates/engine/src/session/edit.rs
+++ b/crates/engine/src/session/edit.rs
@@ -260,7 +260,10 @@ impl Session {
 
 /// Bring effect instances saved by earlier versions up to date with the effect registry
 /// (renamed / regrouped / added parameters, reordered popups; see
-/// [`effectcraft_effects::migrate`]), and a project from before
+/// [`effectcraft_effects::migrate`]), Cineon Converter's Gamma and the Essential Properties
+/// values that expose it in a project from before
+/// [`effectcraft_project::SCHEMA_CINEON_GAMMA`] onto today's scale
+/// ([`effectcraft_effects::migrate::cineon_gamma_to_unit_scale`]), and a project from before
 /// [`effectcraft_project::SCHEMA_EFFECT_SPACE`] into effect space
 /// ([`effectcraft_effects::migrate::to_effect_space`]). Stamps the current schema.
 pub fn upgrade_effects(p: &mut Project) {
@@ -272,6 +275,10 @@ pub fn upgrade_effects(p: &mut Project) {
             bounds.insert((*cid, l.id), effectcraft_render::effect_bounds(p, c, l));
         }
     }
+    let unit_gamma = p.schema < effectcraft_project::SCHEMA_CINEON_GAMMA;
+    if unit_gamma {
+        cineon_gamma_instance_values_to_unit_scale(p);
+    }
     let mut next = p.next_id;
     for (cid, it) in p.items.iter_mut() {
         let effectcraft_project::ItemKind::Comp(c) = &mut it.kind else { continue };
@@ -290,6 +297,10 @@ pub fn upgrade_effects(p: &mut Project) {
                 let effectcraft_project::Node::Group(g) = n else { continue };
                 let effectcraft_project::GroupKind::Effect { effect } = &g.kind else { continue };
                 if let Some(spec) = effectcraft_effects::find(effect) {
+                    // Before `upgrade_instance`, so a Gamma it adds keeps today's default.
+                    if unit_gamma {
+                        effectcraft_effects::migrate::cineon_gamma_to_unit_scale(spec.id, g);
+                    }
                     effectcraft_effects::migrate::upgrade_instance(spec, g, &mut effectcraft_project::build::Ids(&mut next), size);
                     if to_effect_space {
                         effectcraft_effects::migrate::to_effect_space(spec, g, size, d, &clone_d);
@@ -301,3 +312,74 @@ pub fn upgrade_effects(p: &mut Project) {
     p.next_id = next;
     p.schema = effectcraft_project::SCHEMA_VERSION;
 }
+
+pub(crate) fn cineon_gamma_controls(p: &Project) -> std::collections::HashSet<(ItemId, u64)> {
+    use effectcraft_project::essential;
+    type Controls = std::collections::HashSet<(ItemId, u64)>;
+    // A Cineon Converter's Gamma on `l`, or an Essential Properties value of `l` for a Gamma
+    // control of the comp it nests.
+    let is_gamma = |l: &Layer, uid: Uid, controls: &Controls| {
+        let direct = l.props.sub("effects").is_some_and(|fx| {
+            fx.groups().any(|g| {
+                matches!(&g.kind, effectcraft_project::GroupKind::Effect { effect } if effect == "ec.utility.cineon")
+                    && g.get("gamma").is_some_and(|pr| pr.uid == uid)
+            })
+        });
+        let nested = || {
+            let effectcraft_project::LayerSource::Comp { item } = l.source else { return false };
+            let mut found = false;
+            if let Some(g) = essential::group(l) {
+                g.walk("", &mut |_, pr| {
+                    found |= pr.uid == uid && essential::control_of(&pr.match_id).is_some_and(|id| controls.contains(&(item, id)));
+                });
+            }
+            found
+        };
+        direct || nested()
+    };
+    let mut controls = Controls::new();
+    loop {
+        let mut added = vec![];
+        for (cid, c) in p.comps() {
+            let Some(eg) = &c.essential else { continue };
+            for ctl in eg.flat() {
+                if controls.contains(&(*cid, ctl.id)) {
+                    continue;
+                }
+                let targets = eg.resolve(ctl.id).map(|m| m.kind.targets()).unwrap_or_default();
+                if !targets.is_empty() && targets.iter().all(|(lid, uid)| c.layer(*lid).is_some_and(|l| is_gamma(l, *uid, &controls))) {
+                    added.push((*cid, ctl.id));
+                }
+            }
+        }
+        if added.is_empty() {
+            break;
+        }
+        controls.extend(added);
+    }
+    controls
+}
+
+fn cineon_gamma_instance_values_to_unit_scale(p: &mut Project) {
+    use effectcraft_project::essential;
+    let controls = cineon_gamma_controls(p);
+    if controls.is_empty() {
+        return;
+    }
+    for it in p.items.values_mut() {
+        let effectcraft_project::ItemKind::Comp(c) = &mut it.kind else { continue };
+        let nests = |l: &Layer| matches!(l.source, effectcraft_project::LayerSource::Comp { item } if controls.iter().any(|(cid, _)| *cid == item));
+        if !c.layers.iter().any(nests) {
+            continue;
+        }
+        for l in &mut Arc::make_mut(c).layers {
+            let effectcraft_project::LayerSource::Comp { item } = l.source else { continue };
+            let Some(g) = l.props.sub_mut(essential::GROUP) else { continue };
+            g.walk_mut(&mut |pr| {
+                if essential::control_of(&pr.match_id).is_some_and(|id| controls.contains(&(item, id))) {
+                    effectcraft_effects::migrate::scale_cineon_gamma(pr);
+                }
+            });
+        }
+    }
+}
diff --git a/crates/engine/src/tests/effects.rs b/crates/engine/src/tests/effects.rs
--- a/crates/engine/src/tests/effects.rs
+++ b/crates/engine/src/tests/effects.rs
@@ -625,7 +625,7 @@ fn opening_an_old_project_upgrades_effect_instances() {
 /// its origin. Opening converts them to effect space so points that were set keep their place
 /// in the comp; points at their default become the layer centre; layers with a source
 /// rectangle, adjustment layers (their effects see the comp below as it is) and expression
-/// controls keep their values. Saving writes schema 2, and opening that again changes nothing.
+/// controls keep their values. Saving writes the current schema, and opening that again changes nothing.
 #[test]
 fn opening_a_schema_1_project_moves_shape_layer_effect_points_into_effect_space() {
     let mut s = Session::default();
@@ -656,7 +656,7 @@ fn opening_a_schema_1_project_moves_shape_layer_effect_points_into_effect_space(
     set(&mut s, adjust, on_adjust, "center", [40.0, 30.0]);
     assert_eq!(point(&s, shape, bulge, "center"), KV::Vec2([100.0, 50.0]), "Bulge's default centre");
 
-    let old = s.project.to_file_json().unwrap().replacen("\"schema\": 2", "\"schema\": 1", 1);
+    let old = s.project.to_file_json().unwrap().replacen(&format!("\"schema\": {}", effectcraft_project::SCHEMA_VERSION), "\"schema\": 1", 1);
     assert!(old.contains("\"schema\": 1"));
     let dir = std::env::temp_dir().join(format!("ec-effect-space-{}", std::process::id()));
     std::fs::create_dir_all(&dir).unwrap();
@@ -673,7 +673,7 @@ fn opening_a_schema_1_project_moves_shape_layer_effect_points_into_effect_space(
         assert_eq!(point(s2, adjust, on_adjust, "center"), KV::Vec2([40.0, 30.0]), "an adjustment layer's effects see the comp");
     };
     check(&s2);
-    // Saved as schema 2 and opened again: nothing moves twice.
+    // Saved with the current schema and opened again: nothing moves twice.
     s2.execute("file.save", json!({"path": path})).unwrap();
     let mut s3 = Session::default();
     s3.execute("file.open", json!({"path": path})).unwrap();
@@ -681,6 +681,279 @@ fn opening_a_schema_1_project_moves_shape_layer_effect_points_into_effect_space(
     check(&s3);
 }
 
+/// Cineon Converter's Log to Linear before schema 3: the linear value raised to `1.7 / gamma`
+/// (no Highlight Rolloff).
+fn cineon_before_schema_3(v: f32, gamma: f32) -> f32 {
+    let off = 10f32.powf((95.0 - 685.0) * 0.002 / 0.6);
+    ((10f32.powf((v * 1023.0 - 685.0) * 0.002 / 0.6) - off) / (1.0 - off)).max(0.0).powf(1.7 / gamma)
+}
+
+/// A project saved before schema 3 holds Cineon Converter's Gamma on the old scale, where 1.7
+/// gave linear output. Opening it divides the Gamma, its keyframes and their speeds by 1.7, so
+/// the frames render as they did; saving and opening again converts nothing a second time.
+#[test]
+fn opening_a_schema_2_project_keeps_what_cineon_converter_renders() {
+    let mut s = Session::default();
+    // 32 bpc, so frames are not rounded to 8-bit steps.
+    s.execute("file.projectSettings", json!({"bitDepth": 32})).unwrap();
+    s.execute("comp.new", json!({"name": "Main", "width": 8, "height": 8, "frameRate": 30, "duration": 2})).unwrap();
+    let animated = s.execute("layer.newSolid", json!({"color": [0.4, 0.5, 0.6], "width": 8, "height": 8})).unwrap()["layer"].as_u64().unwrap();
+    let cid = s.active_comp_id().unwrap();
+    let input = s.render(cid, Tick::ZERO, Default::default()).get(4, 4);
+    let fx = apply(&mut s, animated, "Cineon Converter");
+    s.execute("prop.set", json!({"layer": animated, "path": "effects/#1/highlightRolloff", "value": 0.0})).unwrap();
+    // Old-scale values: 2.2 at 0 s and 3.4 at 1 s.
+    animate_effect(&mut s, animated, fx, "gamma", &[(0.0, 2.2), (1.0, 3.4)]);
+    let still = s.execute("layer.newSolid", json!({"color": [0.4, 0.5, 0.6], "width": 8, "height": 8})).unwrap()["layer"].as_u64().unwrap();
+    let still_fx = apply(&mut s, still, "Cineon Converter");
+    s.execute("prop.set", json!({"layer": still, "path": "effects/#1/gamma", "value": 2.2})).unwrap();
+    s.execute("layer.setSwitch", json!({"layers": [still], "switch": "video", "value": false})).unwrap();
+
+    let mut old: serde_json::Value = serde_json::from_str(&s.project.to_file_json().unwrap()).unwrap();
+    old["schema"] = json!(2);
+    let dir = std::env::temp_dir().join(format!("ec-cineon-gamma-{}", std::process::id()));
+    std::fs::create_dir_all(&dir).unwrap();
+    let path = dir.join("old.ecproj").to_string_lossy().to_string();
+    std::fs::write(&path, old.to_string()).unwrap();
+    let mut s2 = Session::default();
+    s2.execute("file.open", json!({"path": path})).unwrap();
+    assert_eq!(s2.project.schema, effectcraft_project::SCHEMA_VERSION);
+    let check = |s2: &Session| {
+        let gamma = |lid: u64, uid: u64| effect_group(s2, lid, uid).prop("gamma").unwrap().clone();
+        let g = gamma(animated, fx);
+        let keys: Vec<f64> = g.keys.iter().map(|k| k.value.as_f64()).collect();
+        assert!((keys[0] - 2.2 / 1.7).abs() < 1e-9 && (keys[1] - 3.4 / 1.7).abs() < 1e-9, "{keys:?}");
+        assert!((gamma(still, still_fx).value.as_f64() - 2.2 / 1.7).abs() < 1e-9, "a static Gamma is converted too");
+        for (t, old_gamma) in [(0.0, 2.2), (0.5, 2.8), (1.0, 3.4)] {
+            let got = s2.render(cid, Tick::from_seconds_f64(t), Default::default()).get(4, 4);
+            for c in 0..3 {
+                let want = cineon_before_schema_3(input[c], old_gamma);
+                assert!((got[c] - want).abs() < 1e-4, "{t} s, channel {c}: {} rendered, {want} before", got[c]);
+            }
+        }
+    };
+    check(&s2);
+    s2.execute("file.save", json!({"path": path})).unwrap();
+    let mut s3 = Session::default();
+    s3.execute("file.open", json!({"path": path})).unwrap();
+    let _ = std::fs::remove_dir_all(dir);
+    check(&s3);
+}
+
+/// A Cineon Converter saved before schema 3 without a Gamma property gets today's default
+/// (1, linear output) when opened, not that default divided by 1.7.
+#[test]
+fn opening_a_schema_2_cineon_converter_without_gamma_gives_linear_output() {
+    let mut s = Session::default();
+    s.execute("file.projectSettings", json!({"bitDepth": 32})).unwrap();
+    s.execute("comp.new", json!({"name": "Main", "width": 8, "height": 8, "frameRate": 30, "duration": 2})).unwrap();
+    let l = s.execute("layer.newSolid", json!({"color": [0.4, 0.5, 0.6], "width": 8, "height": 8})).unwrap()["layer"].as_u64().unwrap();
+    let cid = s.active_comp_id().unwrap();
+    let input = s.render(cid, Tick::ZERO, Default::default()).get(4, 4);
+    let fx = apply(&mut s, l, "Cineon Converter");
+    s.execute("prop.set", json!({"layer": l, "path": "effects/#1/highlightRolloff", "value": 0.0})).unwrap();
+
+    fn drop_gamma(v: &mut serde_json::Value) {
+        match v {
+            serde_json::Value::Object(o) => {
+                if let Some(serde_json::Value::Array(children)) = o.get_mut("children") {
+                    children.retain(|c| !(c["node"] == json!("Prop") && c["match"] == json!("gamma")));
+                }
+                o.values_mut().for_each(drop_gamma);
+            }
+            serde_json::Value::Array(a) => a.iter_mut().for_each(drop_gamma),
+            _ => {}
+        }
+    }
+    let mut old: serde_json::Value = serde_json::from_str(&s.project.to_file_json().unwrap()).unwrap();
+    old["schema"] = json!(2);
+    drop_gamma(&mut old);
+    assert!(!old.to_string().contains("\"match\":\"gamma\""));
+    let dir = std::env::temp_dir().join(format!("ec-cineon-no-gamma-{}", std::process::id()));
+    std::fs::create_dir_all(&dir).unwrap();
+    let path = dir.join("old.ecproj").to_string_lossy().to_string();
+    std::fs::write(&path, old.to_string()).unwrap();
+    let mut s2 = Session::default();
+    s2.execute("file.open", json!({"path": path})).unwrap();
+    let _ = std::fs::remove_dir_all(dir);
+    let gamma = effect_group(&s2, l, fx).prop("gamma").unwrap().value.as_f64();
+    assert!((gamma - 1.0).abs() < 1e-9, "the missing Gamma gets today's default: {gamma}");
+    let got = s2.render(cid, Tick::ZERO, Default::default()).get(4, 4);
+    for c in 0..3 {
+        let want = cineon_before_schema_3(input[c], 1.7);
+        assert!((got[c] - want).abs() < 1e-4, "channel {c}: {} rendered, {want} linear", got[c]);
+    }
+}
+
+/// A precomp instance that overrides an exposed Cineon Converter Gamma in a project saved
+/// before schema 3 has its Essential Properties value converted too, so it renders as before.
+#[test]
+fn opening_a_schema_2_project_converts_an_overridden_cineon_gamma() {
+    let mut s = Session::default();
+    s.execute("file.projectSettings", json!({"bitDepth": 32})).unwrap();
+    s.execute("comp.new", json!({"name": "Inner", "width": 8, "height": 8, "frameRate": 30, "duration": 2})).unwrap();
+    let inner = s.active_comp_id().unwrap();
+    let solid = s.execute("layer.newSolid", json!({"color": [0.4, 0.5, 0.6], "width": 8, "height": 8})).unwrap()["layer"].as_u64().unwrap();
+    let input = s.render(inner, Tick::ZERO, Default::default()).get(4, 4);
+    apply(&mut s, solid, "Cineon Converter");
+    s.execute("prop.set", json!({"layer": solid, "path": "effects/#1/highlightRolloff", "value": 0.0})).unwrap();
+    s.execute("prop.set", json!({"layer": solid, "path": "effects/#1/gamma", "value": 1.7})).unwrap();
+    let control = s.execute("essential.addProperty", json!({"layer": solid, "path": "effects/#1/gamma"})).unwrap()["controls"][0].as_u64().unwrap();
+    // A hidden solid whose Gamma control also drives its Opacity: one value cannot keep both
+    // renders, so its instance value stays as saved.
+    let mixed_layer = s.execute("layer.newSolid", json!({"color": [0.4, 0.5, 0.6], "width": 8, "height": 8})).unwrap()["layer"].as_u64().unwrap();
+    apply(&mut s, mixed_layer, "Cineon Converter");
+    let mixed = s.execute("essential.addProperty", json!({"layer": mixed_layer, "path": "effects/#1/gamma"})).unwrap()["controls"][0].as_u64().unwrap();
+    s.execute("essential.linkProperty", json!({"control": mixed, "layer": mixed_layer, "path": "transform/opacity"})).unwrap();
+    s.execute("layer.setSwitch", json!({"layers": [mixed_layer], "switch": "video", "value": false})).unwrap();
+    s.execute("comp.new", json!({"name": "Main", "width": 8, "height": 8, "frameRate": 30, "duration": 2})).unwrap();
+    let main = s.active_comp_id().unwrap();
+    let inst = s.execute("layer.addItem", json!({"item": inner.0})).unwrap()["layer"].as_u64().unwrap();
+    // Old-scale value: 2.2.
+    s.execute("essential.set", json!({"layer": inst, "control": control, "value": 2.2})).unwrap();
+    s.execute("essential.set", json!({"layer": inst, "control": mixed, "value": 2.2})).unwrap();
+
+    let mut old: serde_json::Value = serde_json::from_str(&s.project.to_file_json().unwrap()).unwrap();
+    old["schema"] = json!(2);
+    let dir = std::env::temp_dir().join(format!("ec-cineon-essential-{}", std::process::id()));
+    std::fs::create_dir_all(&dir).unwrap();
+    let path = dir.join("old.ecproj").to_string_lossy().to_string();
+    std::fs::write(&path, old.to_string()).unwrap();
+    let mut s2 = Session::default();
+    s2.execute("file.open", json!({"path": path})).unwrap();
+    let _ = std::fs::remove_dir_all(dir);
+    let controls = s2.execute("essential.instance", json!({"comp": main.0, "layer": inst})).unwrap()["controls"].clone();
+    let find = |id: u64| controls.as_array().unwrap().iter().find(|c| c["control"] == id).unwrap().clone();
+    let c = find(control);
+    assert_eq!(c["overridden"], json!(true), "{c}");
+    assert!((c["value"].as_f64().unwrap() - 2.2 / 1.7).abs() < 1e-9, "{c}");
+    let m = find(mixed);
+    assert!((m["value"].as_f64().unwrap() - 2.2).abs() < 1e-9, "a control that also drives Opacity keeps its value: {m}");
+    let got = s2.render(main, Tick::ZERO, Default::default()).get(4, 4);
+    for ch in 0..3 {
+        let want = cineon_before_schema_3(input[ch], 2.2);
+        assert!((got[ch] - want).abs() < 1e-4, "channel {ch}: {} rendered, {want} before", got[ch]);
+    }
+}
+
+/// A Gamma exposed again by a middle comp, from its precomp layer's Essential Properties, is
+/// converted in the outer instance that overrides it too.
+#[test]
+fn opening_a_schema_2_project_converts_a_cineon_gamma_exposed_through_a_nested_precomp() {
+    let mut s = Session::default();
+    s.execute("comp.new", json!({"name": "Inner", "width": 8, "height": 8, "frameRate": 30, "duration": 2})).unwrap();
+    let inner = s.active_comp_id().unwrap();
+    let solid = s.execute("layer.newSolid", json!({"color": [0.4, 0.5, 0.6], "width": 8, "height": 8})).unwrap()["layer"].as_u64().unwrap();
+    apply(&mut s, solid, "Cineon Converter");
+    s.execute("prop.set", json!({"layer": solid, "path": "effects/#1/gamma", "value": 1.7})).unwrap();
+    let inner_control = s.execute("essential.addProperty", json!({"layer": solid, "path": "effects/#1/gamma"})).unwrap()["controls"][0].as_u64().unwrap();
+    s.execute("comp.new", json!({"name": "Middle", "width": 8, "height": 8, "frameRate": 30, "duration": 2})).unwrap();
+    let middle = s.active_comp_id().unwrap();
+    let mid_layer = s.execute("layer.addItem", json!({"item": inner.0})).unwrap()["layer"].as_u64().unwrap();
+    let path = format!("essential/{}", effectcraft_project::essential::match_id(inner_control));
+    let control = s.execute("essential.addProperty", json!({"layer": mid_layer, "path": path})).unwrap()["controls"][0].as_u64().unwrap();
+    s.execute("comp.new", json!({"name": "Main", "width": 8, "height": 8, "frameRate": 30, "duration": 2})).unwrap();
+    let main = s.active_comp_id().unwrap();
+    let inst = s.execute("layer.addItem", json!({"item": middle.0})).unwrap()["layer"].as_u64().unwrap();
+    // Old-scale value: 2.2.
+    s.execute("essential.set", json!({"layer": inst, "control": control, "value": 2.2})).unwrap();
+
+    let mut old: serde_json::Value = serde_json::from_str(&s.project.to_file_json().unwrap()).unwrap();
+    old["schema"] = json!(2);
+    let dir = std::env::temp_dir().join(format!("ec-cineon-nested-{}", std::process::id()));
+    std::fs::create_dir_all(&dir).unwrap();
+    let path = dir.join("old.ecproj").to_string_lossy().to_string();
+    std::fs::write(&path, old.to_string()).unwrap();
+    let mut s2 = Session::default();
+    s2.execute("file.open", json!({"path": path})).unwrap();
+    let _ = std::fs::remove_dir_all(dir);
+    let c = s2.execute("essential.instance", json!({"comp": main.0, "layer": inst})).unwrap()["controls"][0].clone();
+    assert!((c["value"].as_f64().unwrap() - 2.2 / 1.7).abs() < 1e-9, "{c}");
+}
+
+/// An animation preset saved before Cineon Converter's Gamma scale changed (format version 1)
+/// is converted when applied, for a saved effect and for a saved Gamma property alone; a preset
+/// saved now is applied as it is.
+#[test]
+fn version_1_presets_convert_cineon_converter_gamma() {
+    let (mut s, a, _) = comp_with_two_solids();
+    let fx = apply(&mut s, a, "Cineon Converter");
+    s.execute("prop.set", json!({"layer": a, "path": "effects/#1/gamma", "value": 2.2})).unwrap();
+    let gamma = |s: &Session, n: usize| layer(s, a).effects().unwrap().groups().nth(n).unwrap().get("gamma").unwrap().value.as_f64();
+    let tag = "effectcraftAnimationPreset";
+    let dir = std::env::temp_dir().join(format!("ec-cineon-preset-{}", std::process::id()));
+    std::fs::create_dir_all(&dir).unwrap();
+    let effect_path = dir.join("effect.ecpreset").to_string_lossy().to_string();
+    select_effect(&mut s, a, fx);
+    s.execute("anim.savePreset", json!({"path": effect_path})).unwrap();
+    let prop_path = dir.join("gamma.ecpreset").to_string_lossy().to_string();
+    s.execute("prop.select", json!({"layer": a, "path": "effects/#1/gamma"})).unwrap();
+    s.execute("anim.savePreset", json!({"path": prop_path})).unwrap();
+    let read = |p: &str| serde_json::from_str::<serde_json::Value>(&std::fs::read_to_string(p).unwrap()).unwrap();
+    let (effect_preset, prop_preset) = (read(&effect_path), read(&prop_path));
+    let _ = std::fs::remove_dir_all(dir);
+    assert_eq!(effect_preset[tag], json!(2));
+
+    s.execute("anim.applyPreset", json!({"preset": effect_preset.clone(), "layers": [a]})).unwrap();
+    assert!((gamma(&s, 1) - 2.2).abs() < 1e-9, "a current preset is applied as saved");
+    let mut v1 = effect_preset;
+    v1[tag] = json!(1);
+    s.execute("anim.applyPreset", json!({"preset": v1, "layers": [a]})).unwrap();
+    assert!((gamma(&s, 2) - 2.2 / 1.7).abs() < 1e-9, "a version 1 effect is converted");
+
+    s.execute("prop.set", json!({"layer": a, "path": "effects/#1/gamma", "value": 1.0})).unwrap();
+    let mut v1 = prop_preset;
+    v1[tag] = json!(1);
+    let r = s.execute("anim.applyPreset", json!({"preset": v1, "layers": [a]})).unwrap();
+    assert_eq!(r["skipped"], json!([]), "{r}");
+    assert!((gamma(&s, 0) - 2.2 / 1.7).abs() < 1e-9, "a version 1 Gamma property is converted");
+}
+
+#[test]
+fn presets_convert_exposed_cineon_gamma_for_direct_and_nested_controls() {
+    for nested in [false, true] {
+        for mixed in [false, true] {
+            let mut s = Session::default();
+            s.execute("comp.new", json!({"name": "Inner", "width": 8, "height": 8, "frameRate": 30, "duration": 2})).unwrap();
+            let mut source = s.active_comp_id().unwrap();
+            let solid = s.execute("layer.newSolid", json!({"width": 8, "height": 8})).unwrap()["layer"].as_u64().unwrap();
+            apply(&mut s, solid, "Cineon Converter");
+            let mut control = s.execute("essential.addProperty", json!({"layer": solid, "path": "effects/#1/gamma"})).unwrap()["controls"][0].as_u64().unwrap();
+            if mixed {
+                s.execute("essential.linkProperty", json!({"control": control, "layer": solid, "path": "transform/opacity"})).unwrap();
+            }
+            if nested {
+                s.execute("comp.new", json!({"name": "Middle", "width": 8, "height": 8, "frameRate": 30, "duration": 2})).unwrap();
+                let middle = s.active_comp_id().unwrap();
+                let instance = s.execute("layer.addItem", json!({"item": source.0})).unwrap()["layer"].as_u64().unwrap();
+                let path = format!("essential/{}", effectcraft_project::essential::match_id(control));
+                control = s.execute("essential.addProperty", json!({"layer": instance, "path": path})).unwrap()["controls"][0].as_u64().unwrap();
+                source = middle;
+            }
+            s.execute("comp.new", json!({"name": "Main", "width": 8, "height": 8, "frameRate": 30, "duration": 2})).unwrap();
+            let instance = s.execute("layer.addItem", json!({"item": source.0})).unwrap()["layer"].as_u64().unwrap();
+            let path = format!("essential/{}", effectcraft_project::essential::match_id(control));
+            s.execute("essential.set", json!({"layer": instance, "control": control, "value": 2.2})).unwrap();
+            s.execute("prop.select", json!({"layer": instance, "path": path})).unwrap();
+            let file = std::env::temp_dir().join(format!("ec-cineon-exposed-preset-{}-{nested}-{mixed}.ecpreset", std::process::id()));
+            s.execute("anim.savePreset", json!({"path": file})).unwrap();
+            let preset: serde_json::Value = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
+            std::fs::remove_file(file).unwrap();
+            assert_eq!(preset["properties"][0]["path"], path);
+            for version in [2, 1] {
+                let mut preset = preset.clone();
+                preset["effectcraftAnimationPreset"] = json!(version);
+                s.execute("essential.set", json!({"layer": instance, "control": control, "value": 0.5})).unwrap();
+                let result = s.execute("anim.applyPreset", json!({"preset": preset, "layers": [instance]})).unwrap();
+                assert_eq!(result["skipped"], json!([]));
+                let value = s.execute("prop.get", json!({"layer": instance, "path": path})).unwrap()["value"].as_f64().unwrap();
+                let expected = if version == 1 && !mixed { 2.2 / 1.7 } else { 2.2 };
+                assert!((value - expected).abs() < 1e-9, "nested={nested}, mixed={mixed}, version={version}: {value} != {expected}");
+            }
+        }
+    }
+}
+
 /// #410: `effect.warning` reports what an OCIO effect can't render as asked (Effect Controls
 /// shows it under the effect's name): a transform of its custom config that passes colours
 /// through, a colour space the config lacks or a config it can't read; nothing for the
diff --git a/crates/gpu/src/fx_pixel2.rs b/crates/gpu/src/fx_pixel2.rs
--- a/crates/gpu/src/fx_pixel2.rs
+++ b/crates/gpu/src/fx_pixel2.rs
@@ -228,7 +228,7 @@ fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
             p.u[0] = [0, pr.e("conversionType"), 0, 0];
             p.f[0] = [offset(black, white), knee, white, pr.f("gamma") as f32];
             p.f[1] = [pr.f("internalBlackPoint") as f32, pr.f("internalWhitePoint") as f32, 0.0, 0.0];
-            p.f[2] = [offset(95.0, 685.0), knee, 685.0, 1.7];
+            p.f[2] = [offset(95.0, 685.0), knee, 685.0, 1.0];
         }
         "ec.utility.hdrcompander" => {
             p.u[0] = [1, (pr.e("mode") == 1) as u32, 0, 0];
diff --git a/crates/gpu/src/shaders/fx_pixel2.wgsl b/crates/gpu/src/shaders/fx_pixel2.wgsl
--- a/crates/gpu/src/shaders/fx_pixel2.wgsl
+++ b/crates/gpu/src/shaders/fx_pixel2.wgsl
@@ -261,7 +261,7 @@ fn fp2_vector(@builtin(global_invocation_id) gid: vec3<u32>) {
 fn fp2_log_to_lin(v: f32, c: vec4<f32>, r: vec2<f32>) -> f32 {
     let code = v * 1023.0;
     var lin = (pow(10.0, (code - c.z) * 0.002 / 0.6) - c.x) / max(1.0 - c.x, 1e-6);
-    lin = powz(max(lin, 0.0), 1.7 / max(c.w, 0.01));
+    lin = powz(max(lin, 0.0), 1.0 / max(c.w, 0.01));
     let k = c.y;
     if (k < 1.0 && lin > k) {
         let w = 1.0 - k;
@@ -279,7 +279,7 @@ fn fp2_lin_to_log(v: f32, c: vec4<f32>, r: vec2<f32>) -> f32 {
         let w = 1.0 - k;
         lin = k - w * log(max(1.0 - (lin - k) / w, 1e-6));
     }
-    lin = powz(max(lin, 0.0), max(c.w, 0.01) / 1.7);
+    lin = powz(max(lin, 0.0), max(c.w, 0.01));
     let code = c.z + log(max(lin * (1.0 - c.x) + c.x, 1e-9)) * 0.4342944819032518 * 0.6 / 0.002;
     return code / 1023.0;
 }
diff --git a/crates/project/src/lib.rs b/crates/project/src/lib.rs
--- a/crates/project/src/lib.rs
+++ b/crates/project/src/lib.rs
@@ -40,8 +40,8 @@ pub use footage::{AlphaMode, FieldOrder, Footage, FootageKind, Proxy, Solid, Sou
 pub use item::{Item, ItemId, ItemKind, LayerId};
 pub use layer::{AutoOrient, FrameBlend, Layer, LayerSource, LightKind, MatteKind, PrimitiveKind, Quality, Sampling, Switches, TrackMatte};
 pub use settings::{
-    APP_VERSION, BitDepth, ColorEngine, CuePoint, HdrMode, Marker, ProjectError, ProjectSettings, SCHEMA_EFFECT_SPACE, SCHEMA_VERSION, SavedView,
-    TimeDisplayStyle, is_newer_version, saved_by, saved_view,
+    APP_VERSION, BitDepth, ColorEngine, CuePoint, HdrMode, Marker, ProjectError, ProjectSettings, SCHEMA_CINEON_GAMMA, SCHEMA_EFFECT_SPACE, SCHEMA_VERSION,
+    SavedView, TimeDisplayStyle, is_newer_version, saved_by, saved_view,
 };
 
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
diff --git a/crates/project/src/settings.rs b/crates/project/src/settings.rs
--- a/crates/project/src/settings.rs
+++ b/crates/project/src/settings.rs
@@ -3,12 +3,17 @@ use super::*;
 
 /// The project file schema. 2: effects on layers without a source rectangle (shape, text)
 /// measure positions from the top-left of their comp-sized bounds (effect space), not from
-/// the layer's origin; projects from schema 1 are converted when opened (#227).
-pub const SCHEMA_VERSION: u32 = 2;
+/// the layer's origin; projects from schema 1 are converted when opened (#227). 3: Cineon
+/// Converter's Gamma is 1 for linear output, where it was 1.7; Gamma values in projects from
+/// earlier schemas are divided by 1.7 when opened.
+pub const SCHEMA_VERSION: u32 = 3;
 
 /// The first schema in which effect positions are in effect space (see [`SCHEMA_VERSION`]).
 pub const SCHEMA_EFFECT_SPACE: u32 = 2;
 
+/// The first schema in which Cineon Converter's Gamma 1 gives linear output (see [`SCHEMA_VERSION`]).
+pub const SCHEMA_CINEON_GAMMA: u32 = 3;
+
 /// The EffectCraft version, written into project files as `savedBy`.
 pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
 
diff --git a/docs/architecture.md b/docs/architecture.md
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -121,6 +121,14 @@ their bottom-right quarter. Tracking data of effects that analyse footage (Warp
 Camera Tracker, Mocha shape) is left as it is: footage has a source rectangle, whose effect space
 did not change.
 
+Schema 3 changed Cineon Converter's Gamma so that Gamma 1 gives linear output (it was 1.7): Log
+to Linear raises the linear value to `1 / gamma` (it was `1.7 / gamma`).
+Opening a project from an earlier schema divides each Cineon Converter's Gamma, its keyframes and
+their speeds by 1.7, so the effect renders as before; expressions are not rewritten. Animation
+presets carry the same change as format version 2 (`effectcraftAnimationPreset: 2`): applying a
+version 1 preset divides the Gamma of each Cineon Converter it holds, and a Gamma property it
+holds alone, by 1.7.
+
 ## 4. Rendering
 
 `Renderer::comp_frame(comp, t)` walks the layers bottom to top. For each visible layer it renders
