diff --git a/Cargo.lock b/Cargo.lock
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4722,6 +4722,7 @@ dependencies = [
 name = "vectorcraft-engine"
 version = "0.16.0"
 dependencies = [
+ "chrono",
  "craft-docpath",
  "crc32fast",
  "flate2",
diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml
--- a/crates/engine/Cargo.toml
+++ b/crates/engine/Cargo.toml
@@ -37,6 +37,7 @@ vectorcraft-affinity = { workspace = true }
 vectorcraft-scriptcheck = { workspace = true }
 image = { workspace = true }
 flate2 = { workspace = true }
+chrono = { workspace = true }
 # The XML parts of Word documents placed as type (place/docx.rs).
 roxmltree = "0.21"
 kurbo = { workspace = true }
diff --git a/crates/engine/src/cmd/fileio/naming.rs b/crates/engine/src/cmd/fileio/naming.rs
--- a/crates/engine/src/cmd/fileio/naming.rs
+++ b/crates/engine/src/cmd/fileio/naming.rs
@@ -100,13 +100,19 @@ pub fn render(template: &str, p: &NameParts<'_>) -> String {
 
 /// `YYYY-MM-DD` (or `YYYYMMDD`) as `[year, month, day, 0, 0, 0]`; `None` when it isn't a date.
 pub fn parse_date(s: &str) -> Option<[i64; 6]> {
-    let digits: String = s.trim().chars().filter(|c| *c != '-').collect();
+    let s = s.trim();
+    let digits: String = match s.len() {
+        8 => s.to_string(),
+        10 if s.as_bytes()[4] == b'-' && s.as_bytes()[7] == b'-' => s.chars().filter(|c| *c != '-').collect(),
+        _ => return None,
+    };
     if digits.len() != 8 || !digits.bytes().all(|b| b.is_ascii_digit()) {
         return None;
     }
     let num = |a: usize, b: usize| digits.get(a..b).and_then(|t| t.parse::<i64>().ok());
     let (y, m, d) = (num(0, 4)?, num(4, 6)?, num(6, 8)?);
-    ((1..=12).contains(&m) && (1..=31).contains(&d)).then_some([y, m, d, 0, 0, 0])
+    chrono::NaiveDate::from_ymd_opt(i32::try_from(y).ok()?, u32::try_from(m).ok()?, u32::try_from(d).ok()?)?;
+    Some([y, m, d, 0, 0, 0])
 }
 
 #[cfg(test)]
@@ -149,6 +155,14 @@ mod tests {
         assert_eq!(render("#", &NameParts { counter: 12345, ..parts() }), "12345");
     }
 
+    #[test]
+    fn dates_are_strict_about_hyphens_and_days() {
+        for bad in ["2026-1-011", "2-0-2-6-1-0-1-6", "2026--10--16", "2026-02-31", "2026-10-16-", "202610-16"] {
+            assert_eq!(parse_date(bad), None, "{bad}");
+        }
+        assert_eq!(parse_date("2024-02-29"), Some([2024, 2, 29, 0, 0, 0]));
+    }
+
     #[test]
     fn dates_parse() {
         assert_eq!(parse_date("2026-10-16"), Some([2026, 10, 16, 0, 0, 0]));
