diff --git a/CLAUDE.md b/CLAUDE.md
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -22,6 +22,7 @@ EffectCraft is a clean-room, open-source, pure-Rust motion graphics and visual e
 - **Everything is agent-drivable:** every interactive widget registers an automation id; UI state is serde.
 - **Community links** (Help menu, About, header Discord button): https://discord.gg/artcraft, https://getartcraft.com, https://getartcraft.com/apps/effectcraft, https://github.com/storytold/effectcraft.
 - **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, layers, assets, wasm).
+- **New workspace crates** must include `[lints] workspace = true` in `Cargo.toml` to inherit `[workspace.lints]`. Production crate roots must declare the required crate-level lint attributes before modules or other crate items; crate-level `//!` documentation may precede them.
 - **Commits:** one task id per commit (`M6.3: trim paths`). Only green states.
 
 ## Running and looking at the app
diff --git a/Cargo.toml b/Cargo.toml
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -135,9 +135,11 @@ sys-locale = "0.3"
 unsafe_code = "forbid"
 
 [workspace.lints.clippy]
+cognitive_complexity = "deny"
+too_many_arguments = "deny"
+wildcard_imports = "deny"
 dbg_macro = "warn"
 todo = "warn"
-too_many_arguments = "allow"
 needless_range_loop = "allow"
 type_complexity = "allow"
 
diff --git a/clippy.toml b/clippy.toml
--- a/clippy.toml
+++ b/clippy.toml
@@ -5,3 +5,4 @@ allow-indexing-slicing-in-tests = true
 disallowed-types = [
     { path = "egui::ComboBox", reason = "use craft_ui::ComboBox: egui's combo box menu can't grow back while open (egui#5225)" },
 ]
+cognitive-complexity-threshold = 30
diff --git a/crates/raster/src/inpaint.rs b/crates/raster/src/inpaint.rs
--- a/crates/raster/src/inpaint.rs
+++ b/crates/raster/src/inpaint.rs
@@ -607,38 +607,44 @@ pub struct FillInput {
     pub references: Vec<(usize, Image)>,
 }
 
-/// Trace pixel (x, y) of frame `t` through `flows` (`step` = +1 forward with `fwd`, −1 backward
-/// with `bwd`) to the nearest frame where it is visible: (sampled colour, frames travelled).
-fn trace(
-    t: usize,
+/// Shared frame and flow data for tracing pixels through a sequence.
+struct TraceContext<'a> {
+    fwd: &'a [Flow],
+    bwd: &'a [Flow],
+    known: &'a (dyn Fn(usize, i64, i64) -> bool + Sync),
+    frames: &'a [&'a Image],
+    max: usize,
+}
+
+/// Starting frame, point, and direction for a flow trace.
+struct TraceStart {
+    frame: usize,
     x: f64,
     y: f64,
     step: i64,
-    fwd: &[Flow],
-    bwd: &[Flow],
-    known: &dyn Fn(usize, i64, i64) -> bool,
-    frames: &[&Image],
-    max: usize,
-) -> Option<([f32; 4], usize)> {
-    let n = frames.len();
-    let (w, h) = (frames[0].width as f64, frames[0].height as f64);
-    let (mut px, mut py) = (x, y);
-    let mut k = t as i64;
-    for d in 1..=max {
-        let v = if step > 0 {
+}
+
+/// Trace a point to the nearest visible frame: (sampled colour, frames travelled).
+fn trace(context: &TraceContext<'_>, start: TraceStart) -> Option<([f32; 4], usize)> {
+    let n = context.frames.len();
+    let (w, h) = (context.frames[0].width as f64, context.frames[0].height as f64);
+    let (mut px, mut py) = (start.x, start.y);
+    let mut k = start.frame as i64;
+    for d in 1..=context.max {
+        let v = if start.step > 0 {
             if k as usize + 1 >= n {
                 return None;
             }
-            fwd[k as usize].at(px, py)
+            context.fwd[k as usize].at(px, py)
         } else {
             if k == 0 {
                 return None;
             }
-            bwd[k as usize - 1].at(px, py)
+            context.bwd[k as usize - 1].at(px, py)
         };
         px += v[0] as f64;
         py += v[1] as f64;
-        k += step;
+        k += start.step;
         if px < 0.0 || py < 0.0 || px >= w || py >= h {
             return None;
         }
@@ -646,9 +652,9 @@ fn trace(
         let (x0, y0) = ((px - 0.5).floor() as i64, (py - 0.5).floor() as i64);
         let ok = [(x0, y0), (x0 + 1, y0), (x0, y0 + 1), (x0 + 1, y0 + 1)]
             .iter()
-            .all(|&(xx, yy)| known(k as usize, xx.clamp(0, w as i64 - 1), yy.clamp(0, h as i64 - 1)));
+            .all(|&(xx, yy)| (context.known)(k as usize, xx.clamp(0, w as i64 - 1), yy.clamp(0, h as i64 - 1)));
         if ok {
-            return Some((frames[k as usize].sample_bilinear_clamped(px, py), d));
+            return Some((context.frames[k as usize].sample_bilinear_clamped(px, py), d));
         }
     }
     None
@@ -697,6 +703,7 @@ pub fn fill_sequence(input: &FillInput, o: &FillOpts, progress: &mut dyn FnMut(u
     // 2. Propagation (and the lighting-correction border ring).
     let refs: Vec<&Image> = frames.iter().collect();
     let known = |k: usize, x: i64, y: i64| !holes[k][y as usize * w + x as usize];
+    let trace_context = TraceContext { fwd: &fwd, bwd: &bwd, known: &known, frames: &refs, max: n };
     let ring_r = 2usize;
     let propagated: Vec<(Image, Vec<bool>)> = (0..n)
         .into_par_iter()
@@ -708,8 +715,8 @@ pub fn fill_sequence(input: &FillInput, o: &FillOpts, progress: &mut dyn FnMut(u
             }
             let sample = |i: usize| -> Option<[f32; 4]> {
                 let (x, y) = ((i % w) as f64 + 0.5, (i / w) as f64 + 0.5);
-                let f = trace(t, x, y, 1, &fwd, &bwd, &known, &refs, n);
-                let b = trace(t, x, y, -1, &fwd, &bwd, &known, &refs, n);
+                let f = trace(&trace_context, TraceStart { frame: t, x, y, step: 1 });
+                let b = trace(&trace_context, TraceStart { frame: t, x, y, step: -1 });
                 match (f, b) {
                     (Some((a, da)), Some((c, dc))) => {
                         let (wa, wc) = (1.0 / da as f32, 1.0 / dc as f32);
diff --git a/crates/script/src/prelude.js b/crates/script/src/prelude.js
--- a/crates/script/src/prelude.js
+++ b/crates/script/src/prelude.js
@@ -189,7 +189,12 @@ BridgeTalk.pump = function () { return false; };
 
 // ------------------------------------------------------------------ File / Folder
 
-function __join(a, b) { return a.replace(/[\/\\]+$/, "") + "/" + b; }
+function __trimPathSeparators(path) {
+  var end = path.length;
+  while (end > 0 && (path.charAt(end - 1) === "/" || path.charAt(end - 1) === "\\")) end--;
+  return path.slice(0, end);
+}
+function __join(a, b) { return __trimPathSeparators(a) + "/" + b; }
 
 function File(path) {
   if (!(this instanceof File)) return new File(path);
@@ -292,9 +297,9 @@ Folder.prototype = {
   get fsName() { return this.__path; },
   get fullName() { return this.__path; },
   get absoluteURI() { return this.__path; },
-  get name() { return this.__path.replace(/[\/\\]+$/, "").split(/[\/\\]/).pop(); },
+  get name() { return __trimPathSeparators(this.__path).split(/[\/\\]/).pop(); },
   get displayName() { return this.name; },
-  get path() { var p = this.__path.replace(/[\/\\]+$/, "").split(/[\/\\]/); p.pop(); return p.join("/"); },
+  get path() { var p = __trimPathSeparators(this.__path).split(/[\/\\]/); p.pop(); return p.join("/"); },
   get parent() { return new Folder(this.path); },
   get exists() { return JSON.parse(__file("isDir", this.__path)) === true; },
   toString: function () { return this.__path; },
diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs
--- a/crates/script/src/tests.rs
+++ b/crates/script/src/tests.rs
@@ -456,6 +456,38 @@ fn layers_items_and_markers() {
     );
 }
 
+#[test]
+fn folder_paths_trim_trailing_separators() {
+    let mut s = session();
+    let o = ok(
+        &mut s,
+        r#"
+        var paths = ['', '/', '///', 'a/b/', 'a\\b\\/', 'a/b', 'a/\n'];
+        var result = paths.map(function (path) {
+          var folder = new Folder(path);
+          return [folder.name, folder.path, __join(path, 'child')];
+        });
+        // A long run followed by a non-separator used to cause quadratic regex backtracking.
+        var longPath = Array(20001).join('/') + 'x';
+        result.push([new Folder(longPath).name, new Folder(longPath).path.length, __join(longPath, 'child').length]);
+        result
+        "#,
+    );
+    assert_eq!(
+        o.result,
+        json!([
+            ["", "", "/child"],
+            ["", "", "/child"],
+            ["", "", "/child"],
+            ["b", "a", "a/b/child"],
+            ["b", "a", "a\\b/child"],
+            ["b", "a", "a/b/child"],
+            ["\n", "a", "a/\n/child"],
+            ["x", 19999, 20007]
+        ])
+    );
+}
+
 #[test]
 fn security_gate_for_files() {
     let dir = std::env::temp_dir().join(format!("ec-script-gate-{}", std::process::id()));
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -5,6 +5,9 @@ edition.workspace = true
 license.workspace = true
 publish = false
 
+[lints]
+workspace = true
+
 [dependencies]
 serde_json = { workspace = true }
 
