diff --git a/app/api/template/test/T3.docx b/app/api/template/test/T3.docx
index 78dd677..7573a45 100644
Binary files a/app/api/template/test/T3.docx and b/app/api/template/test/T3.docx differ
diff --git a/app/api/template/test/make-protected.mjs b/app/api/template/test/make-protected.mjs
new file mode 100644
index 0000000..c35f2b2
--- /dev/null
+++ b/app/api/template/test/make-protected.mjs
@@ -0,0 +1,298 @@
+// ทดลอง: เปิด Restrict Edit (readOnly + enforcement) และฝัง custom properties
+// (token / primary-key / owner) ลงไฟล์ docx — ตามแนวทาง Token-Based ใน note.md
+//
+// ใช้: node app/api/template/test/make-protected.mjs [in.docx] [out.docx]
+// ไม่ส่ง argument = อ่านและเขียนทับ output-T3.docx (ผล render ล่าสุด)
+//
+// สิ่งที่แก้ในไฟล์ docx:
+// 1. word/settings.xml — w:documentProtection edit="readOnly" enforcement="1"
+// (เอกสารทั้งฉบับอ่านอย่างเดียว — ทดสอบแล้ว: readOnly ล็อค CC ด้วย
+// จึงต้องคู่กับข้อ 2 ถึงจะแก้ใน CC ได้)
+// 2. word/document.xml ฯลฯ — ครอบ w:sdt ทุกตัวที่มี w:tag ด้วย
+// w:permStart(w:edGrp="everyone") / w:permEnd — กลไก "Exceptions: Everyone"
+// ของแผง Restrict Editing ใน Word (บริเวณใน perm แก้ได้ นอกนั้นล็อค)
+// พร้อมเติม w:lock="sdtLocked" ให้ CC ที่ยังไม่มี กันลบช่องตอนอยู่ในโหมดนี้
+// 3. docProps/custom.xml — สร้างใหม่ เก็บค่า custom properties แบบ vt:lpwstr
+// 4. [Content_Types].xml — เพิ่ม Override สำหรับ custom.xml
+// 5. _rels/.rels — เพิ่ม Relationship ชี้ docProps/custom.xml
+import PizzaZip from "pizzip";
+import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
+import xpath from "xpath";
+import { readFileSync, writeFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { dirname, join } from "node:path";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const inPath = process.argv[2] ?? join(__dirname, "output-T3.docx");
+const outPath = process.argv[3] ?? inPath;
+
+// ตัวอย่างข้อมูลที่จะฝัง (ใน production จะมาจาก backend ตอน render เอกสาร)
+const CUSTOM_PROPS = {
+ token: "eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiJ1MTAwMSIsImZpZCI6IkRPTi0yMDI2LTAwMDEyMyIsImV4cCI6MTc5NjI0NzIwMH0.demo",
+ "primary-key": "DOC-2026-000123",
+ owner: "u1001",
+};
+
+const W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
+const CUSTOM_NS =
+ "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties";
+const VT_NS =
+ "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes";
+const REL_CUSTOM =
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
+const CT_CUSTOM =
+ "application/vnd.openxmlformats-officedocument.custom-properties+xml";
+const FMTID = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";
+
+const parser = new DOMParser();
+const serializer = new XMLSerializer();
+
+const parseXml = (text) => parser.parseFromString(text, "text/xml");
+
+// xmldom serializer อาจคง xml declaration ไว้แล้ว — เติมเฉพาะเมื่อยังไม่มี
+// (ถ้า prepend ซ้ำจะได้ declaration สองอัน ไฟล์ XML invalid)
+const serialize = (doc) => {
+ const text = serializer.serializeToString(doc);
+ return text.startsWith("\r\n' + text;
+};
+
+// ครอบ w:sdt ที่มี w:tag ทุกตัวด้วย w:permStart/w:permEnd (edGrp="everyone")
+// และเติม w:lock="sdtLocked" ให้ CC ที่ยังไม่มี — คืนจำนวน CC ที่ครอบ
+function unlockContentControls(zip) {
+ const select = xpath.useNamespaces({ w: W_NS });
+ const asNode = (n) => n;
+ const parts = Object.keys(zip.files).filter((n) =>
+ /^word\/(document|header\d*|footer\d*)\.xml$/.test(n),
+ );
+
+ // id ของ perm ต้องไม่ซ้ำในเอกสาร — เริ่มสูงกว่าค่าสูงสุดที่มีอยู่ (จากทุก part)
+ let nextId = 999;
+ for (const part of parts) {
+ const ids = select(
+ "//w:permStart/@w:id | //w:permEnd/@w:id",
+ asNode(parseXml(zip.file(part).asText())),
+ );
+ for (const id of ids) {
+ const n = Number.parseInt(id.value, 10);
+ if (Number.isFinite(n) && n >= nextId) nextId = n;
+ }
+ }
+
+ let wrapped = 0;
+ let locked = 0;
+ for (const part of parts) {
+ const doc = parseXml(zip.file(part).asText());
+
+ // เฉพาะ sdt ระดับบนสุด (ไม่อยู่ใน sdt อื่น) มี w:tag = ช่องข้อมูลของ template
+ // และยังไม่ถูกครอบด้วย permStart อยู่แล้ว (กัน double-wrap ตอนรันซ้ำ)
+ const sdts = select(
+ "//w:sdt[./w:sdtPr/w:tag][not(ancestor::w:sdt)][not(preceding-sibling::*[1][self::w:permStart])]",
+ asNode(doc),
+ );
+ if (sdts.length === 0) continue;
+
+ for (const sdt of sdts) {
+ // เติม lock ให้ CC ที่ยังไม่มี — กันลบช่องตอนเปิดช่องให้แก้ผ่าน permission
+ const sdtPr = select("./w:sdtPr", sdt)[0];
+ const hasLock = select("./w:lock", sdtPr).length > 0;
+ if (!hasLock) {
+ const lock = doc.createElementNS(W_NS, "w:lock");
+ lock.setAttribute("w:val", "sdtLocked");
+ // w:lock ต้องอยู่หลัง w:id ตามลำดับ schema ของ sdtPr
+ const idEl = select("./w:id", sdtPr)[0];
+ if (idEl && idEl.nextSibling) {
+ sdtPr.insertBefore(lock, idEl.nextSibling);
+ } else {
+ sdtPr.appendChild(lock);
+ }
+ locked++;
+ }
+
+ const start = doc.createElementNS(W_NS, "w:permStart");
+ start.setAttribute("w:id", String(nextId));
+ start.setAttribute("w:edGrp", "everyone");
+ const end = doc.createElementNS(W_NS, "w:permEnd");
+ end.setAttribute("w:id", String(nextId));
+ nextId++;
+
+ sdt.parentNode.insertBefore(start, sdt);
+ sdt.parentNode.insertBefore(end, sdt.nextSibling);
+ wrapped++;
+ }
+
+ zip.file(part, serialize(doc));
+ }
+ return { wrapped, locked };
+}
+
+function enableReadOnlyProtection(zip) {
+ const file = zip.file("word/settings.xml");
+ if (!file) throw new Error("word/settings.xml not found");
+
+ const doc = parseXml(file.asText());
+ const prots = doc.getElementsByTagNameNS(W_NS, "documentProtection");
+ const prot = prots.length > 0 ? prots[0] : null;
+
+ if (prot) {
+ prot.setAttribute("w:edit", "readOnly");
+ prot.setAttribute("w:enforcement", "1");
+ } else {
+ // T3 มี element นี้อยู่แล้ว แต่รองรับไฟล์ที่ไม่มี: แทรกเป็น child แรกของ w:settings
+ const el = doc.createElementNS(W_NS, "w:documentProtection");
+ el.setAttribute("w:edit", "readOnly");
+ el.setAttribute("w:enforcement", "1");
+ doc.documentElement.insertBefore(el, doc.documentElement.firstChild);
+ }
+
+ zip.file("word/settings.xml", serialize(doc));
+}
+
+function setCustomProperties(zip, props) {
+ // --- 1. docProps/custom.xml : สร้างใหม่ หรือ merge กับของเดิม ---
+ const existing = zip.file("docProps/custom.xml");
+ let doc;
+ if (existing) {
+ doc = parseXml(existing.asText());
+ // ลบ property ชื่อซ้ำออกก่อน (เขียนทับค่าใหม่)
+ const all = Array.from(doc.getElementsByTagNameNS(CUSTOM_NS, "property"));
+ for (const p of all) {
+ if (Object.prototype.hasOwnProperty.call(props, p.getAttribute("name"))) {
+ p.parentNode.removeChild(p);
+ }
+ }
+ } else {
+ doc = parseXml(
+ `` +
+ ``,
+ );
+ }
+
+ // pid ต้อง >= 2 และไม่ซ้ำ — เริ่มจากค่าสูงสุดที่มีอยู่ +1
+ const usedPids = Array.from(
+ doc.getElementsByTagNameNS(CUSTOM_NS, "property"),
+ ).map((p) => Number.parseInt(p.getAttribute("pid"), 10) || 1);
+ let nextPid = Math.max(1, ...usedPids) + 1;
+
+ for (const [name, value] of Object.entries(props)) {
+ const prop = doc.createElementNS(CUSTOM_NS, "property");
+ prop.setAttribute("fmtid", FMTID);
+ prop.setAttribute("pid", String(nextPid++));
+ prop.setAttribute("name", name);
+ const val = doc.createElementNS(VT_NS, "vt:lpwstr");
+ val.textContent = value;
+ prop.appendChild(val);
+ doc.documentElement.appendChild(prop);
+ }
+ zip.file("docProps/custom.xml", serialize(doc));
+
+ // --- 2. [Content_Types].xml : เพิ่ม Override (ครั้งเดียว) ---
+ const ct = parseXml(zip.file("[Content_Types].xml").asText());
+ const hasCt = Array.from(ct.getElementsByTagName("Override")).some(
+ (o) => o.getAttribute("PartName") === "/docProps/custom.xml",
+ );
+ if (!hasCt) {
+ const el = ct.createElement("Override");
+ el.setAttribute("PartName", "/docProps/custom.xml");
+ el.setAttribute("ContentType", CT_CUSTOM);
+ ct.documentElement.appendChild(el);
+ zip.file("[Content_Types].xml", serialize(ct));
+ }
+
+ // --- 3. _rels/.rels : เพิ่ม Relationship (ครั้งเดียว) ---
+ const rels = parseXml(zip.file("_rels/.rels").asText());
+ const relNodes = Array.from(rels.getElementsByTagName("Relationship"));
+ if (!relNodes.some((r) => r.getAttribute("Type") === REL_CUSTOM)) {
+ const usedIds = new Set(relNodes.map((r) => r.getAttribute("Id")));
+ let n = 1;
+ while (usedIds.has(`rId${n}`)) n++;
+ const el = rels.createElement("Relationship");
+ el.setAttribute("Id", `rId${n}`);
+ el.setAttribute("Type", REL_CUSTOM);
+ el.setAttribute("Target", "docProps/custom.xml");
+ rels.documentElement.appendChild(el);
+ zip.file("_rels/.rels", serialize(rels));
+ }
+}
+
+// อ่านกลับมาตรวจทุกอย่างหลังเขียนไฟล์
+function verify(path) {
+ const zip = new PizzaZip(readFileSync(path));
+
+ const settings = zip.file("word/settings.xml")?.asText() ?? "";
+ const prot = settings.match(/]*\/>/)?.[0];
+ const enforced =
+ prot?.includes('w:edit="readOnly"') && prot?.includes('w:enforcement="1"');
+
+ const customXml = zip.file("docProps/custom.xml")?.asText();
+ const propsBack = {};
+ if (customXml) {
+ const doc = parseXml(customXml);
+ for (const p of Array.from(
+ doc.getElementsByTagNameNS(CUSTOM_NS, "property"),
+ )) {
+ propsBack[p.getAttribute("name")] = p.textContent;
+ }
+ }
+
+ const rels = zip.file("_rels/.rels")?.asText() ?? "";
+ const ct = zip.file("[Content_Types].xml")?.asText() ?? "";
+
+ const docXml = zip.file("word/document.xml")?.asText() ?? "";
+ const sdtPrs = docXml.match(/[\s\S]*?<\/w:sdtPr>/g) ?? [];
+ const plain = sdtPrs.filter((p) => / / `${k}="${v.length > 40 ? v.slice(0, 40) + "…" : v}"`)
+ .join(", ") || "(ว่าง)",
+ );
+ console.log(
+ `relationship : ${rels.includes(REL_CUSTOM) ? "✓" : "✗ ไม่พบ"} | content-type override: ${ct.includes(CT_CUSTOM) ? "✓" : "✗ ไม่พบ"}`,
+ );
+ console.log(`content controls : ${sdtPrs.length} ตัว (plain=${plain}, rich=${rich}, มี tag=${tagged})`);
+ console.log(
+ `permission ranges : permStart=${permStarts} / permEnd=${permEnds} (ต้องเท่ากับมี tag=${tagged}) ` +
+ (permStarts === tagged && permEnds === tagged ? "✓" : "✗"),
+ );
+ console.log(
+ `CC ไม่มี lock : ${missingLock} ตัว (ต้องเป็น 0 — กันลบช่อง) ${missingLock === 0 ? "✓" : "✗"}`,
+ );
+
+ return (
+ enforced &&
+ !!customXml &&
+ permStarts === tagged &&
+ permEnds === tagged &&
+ missingLock === 0
+ );
+}
+
+const zip = new PizzaZip(readFileSync(inPath));
+enableReadOnlyProtection(zip);
+const { wrapped, locked } = unlockContentControls(zip);
+setCustomProperties(zip, CUSTOM_PROPS);
+
+if (locked > 0) {
+ console.log(`เติม w:lock="sdtLocked" ให้ CC อีก ${locked} ตัวที่ยังไม่มี`);
+}
+console.log(`ครอบ CC ด้วย permission range: ${wrapped} ตัว`);
+
+const out = zip.generate({ type: "nodebuffer", compression: "DEFLATE" });
+writeFileSync(outPath, out);
+console.log(`เขียนไฟล์แล้ว: ${outPath} (${out.length.toLocaleString()} bytes)`);
+
+const ok = verify(outPath);
+process.exit(ok ? 0 : 1);
diff --git a/app/api/template/test/note.md b/app/api/template/test/note.md
index 94c0c4d..919190b 100644
--- a/app/api/template/test/note.md
+++ b/app/api/template/test/note.md
@@ -55,3 +55,73 @@ data(json)น่าจะปรับเป็นแบบนี้เพื่
- render ปัจจุบันมีไว้ให้ทางศาลทดสอบ อันที่ใช้จริงหลังกดสร้าง Template เพื่อ Download จะสร้างเอกสารเวอร์ชั่นแรกใน object storage หรือถ้ามีอยู่แล้ว จะเป็นการอัปเดต
- Backend จัดการ version
- อัปเดตข้อมูลและเอกสารกลับระบบเดิม ส่วนนี้ยังไม่ชัดเจนอาจจะทำ mock api เตรียมไว้ก่อน
+
+# ทดลอง Restrict Edit + Custom Properties (2026-10-07)
+
+สคริปต์ `make-protected.mjs` — อ่าน docx → เปิด protection + ฝัง custom properties → เขียนไฟล์ → อ่านกลับ verify ทุกครั้ง
+
+```sh
+node app/api/template/test/make-protected.mjs [in.docx] [out.docx]
+# ไม่ส่ง argument = อ่านและเขียนทับ output-T3.docx (ค่าตัวอย่างฝังอยู่ในตัวสคริปต์ CUSTOM_PROPS)
+```
+
+## Restrict Edit (เปิดผ่านโค้ด)
+
+- อยู่ที่ `word/settings.xml` → ``
+- T3.docx มี element นี้อยู่เดิมแต่เป็น `w:enforcement="0"` (ตั้งค่าไว้ใน Word แต่ไม่ได้กดบังคับใช้) — สคริปต์แค่พลิกเป็น 1 ไม่ต้องแทรก element ใหม่
+- **บทเรียนจากทดสอบใน Word จริง (2026-10-07): `readOnly` + enforcement ล็อคทั้งเอกสารรวมถึง Content Control ด้วย** — CC ไม่ได้ถูกยกเว้นให้แก้อัตโนมัติ ต้องคู่กับข้อถัดไป
+- **กลไกที่ถูกต้อง: ครอบ `w:sdt` ทุกตัวที่มี `w:tag` ด้วย `` … ``** — คือกลไก "Exceptions (optional): Everyone" ที่แผง Restrict Editing ของ Word ใช้เวลาเราเลือกบริเวณแล้วติ๊ก Everyone (บริเวณใน permission range แก้ได้ นอกนั้นล็อค) — แทรกที่ระดับเดียวกับ sdt (inline ใน `w:p` หรือ block ใน `w:body` ก็ valid ตาม grammar)
+- สคริปต์เติม `w:lock="sdtLocked"` ให้ CC ที่ยังไม่มีด้วย (T3 ขาด acc1, nottitle) — พอช่องถูกเปิดให้แก้ผ่าน permission แล้ว ช่องที่ไม่มี lock จะถูก**ลบ**ได้ตามไปด้วย
+- ไม่ใส่ password → ผู้ใช้ปลดเองได้จาก Review → Restrict Editing → Stop Protection — ตั้งใจไว้แบบนี้เพราะ concept คือ token ใน custom property เป็นตัวกำกับสิทธิ์จริง ส่วน protection เป็นแค่เกราะกันแก้พลาด (ถ้าจะใส่ password ต้องคำนวณ legacy hash: SHA-1 ของ password+salt วนตาม `w:cryptSpinCount` ตาม ECMA-376 — ยังไม่ทำ)
+
+## ฝังข้อมูลใน Property เอกสาร (token / primary-key / owner)
+
+ทางเลือกที่ศึกษา:
+1. **`docProps/custom.xml` (custom properties) — ที่เลือกใช้** เห็นได้ใน Word: File → Info → Properties → Advanced Properties → แท็บ Custom; add-in ฝั่ง Word (Office.js) อ่าน/เขียนได้ผ่าน `Document.properties.customProperties` (WordApi 1.3+)
+ - ไฟล์ที่ไม่มี part นี้อยู่ (กรณีปกติของ docx) ต้องทำครบ 3 อย่างพร้อมกัน ไม่งั้น Word ถือว่าไฟล์เสีย:
+ 1. สร้าง `docProps/custom.xml` — property แต่ละตัว: `fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"` คงที่ + `pid` ≥ 2 ไม่ซ้ำ + `name` + ค่า string ใน `vt:lpwstr`
+ 2. `[Content_Types].xml` เพิ่ม ``
+ 3. `_rels/.rels` เพิ่ม Relationship ชนิด `.../relationships/custom-properties` Target `docProps/custom.xml`
+2. `docProps/core.xml` (core properties: title/subject/creator/description) — แก้ง่าย เห็นในหน้า Properties ปกติ เหมาะกับข้อมูลที่แสดงต่อผู้ใช้ ไม่ใช่ token
+3. `w:docVar` ใน `word/settings.xml` (document variables) — ไม่โผล่ใน UI เลย อ่านได้ผ่าน VBA/VSTO (`Document.Variables`) แต่ Word JS API ไม่มีทางตรง — ไม่เลือก
+
+gotcha ที่เจอ: `XMLSerializer` ของ @xmldom/xmldom คง `` declaration ไว้ในผลลัพธ์อยู่แล้ว — ห้าม prepend เพิ่มเอง จะได้ declaration ซ้อนเป็น XML invalid (Word/node อ่านไม่ได้)
+
+## extract บอก alignment รายช่อง — key `@align` (2026-10-07) — **ถอดออกแล้วตามคำขอ (ยังไม่เอา)**
+
+- เคย implement ใน `lib-tmpl.ts` + spec `lib/openapi.ts` แล้ว**ถอดกลับสถานะเดิม** — extract ปัจจุบันคืนแค่ `{ tag: text }` เหมือนเดิม
+- แนวทางที่พิสูจน์แล้วว่าใช้ได้ (เผื่อกลับมาทำภายหลัง): คืน key `@align` (ค่า w:jc เช่น `thaiDistribute`) เฉพาะช่องที่ต่างจาก default ของเอกสาร โดย resolve: `w:pPr/w:jc` ตรงของย่อหน้า → style ที่ใช้ (ไล่ `w:pStyle` → `w:basedOn`) → `docDefaults` ("left"/"start" นับเป็นไม่ตั้ง) — บริบทย่อหน้าของช่อง: CC block = `w:p` แรกใน `w:sdtContent` / CC inline = `w:p` ที่ครอบ sdt
+- ผลทดสอบตอน implement: direct pPr ชนะ style ✓ inline/block จับได้ทั้งคู่ ✓ และ T3 จัดช่อง 8 ตัวผ่าน style อยู่แล้ว (nottype/blackyear/redyear/acc1/acc2/groupname/detail1/pname — ชิดขวา/กลาง) — ช่องเหล่านี้จะโผล่ `@align` ตามจริง
+
+## จัด paragraph แบบ Thai Distributed — ตัดสินใจ: จัดเองใน Word ตอนทำ template (2026-10-07)
+
+- ค่า OOXML คือ `` ใน `w:pPr` (ใน Word UI: Paragraph → Alignment → Thai Distributed, ทางลัด Ctrl+Shift+J) — ต่างจาก `distribute` ธรรมดาตรงที่จัดกระจายช่องว่างระหว่างคำเป็นหลัก ไม่บีบยืดตัวอักษร/สระ-วรรณยุกต์
+- **ไม่ตั้งผ่านโค้ด** — ผู้ออกแบบ template จัดเองใน Word ตอนทำ template ทางเดียวกับที่ตั้งฟอนต์/ย่อหน้าอื่น ๆ (เคยทดลองตั้ง default ทั้งเอกสารผ่าน `styles.xml` → `w:docDefaults/w:pPrDefault/w:pPr` แล้วถอดออกตามคำขอ)
+- วิธีจัดเองใน Word:
+ - รายย่อหน้า/ราย CC: คลิกในย่อหน้า → `Ctrl+Shift+J` หรือ Home → Paragraph launcher → Alignment เลือก Thai Distributed
+ - ทั้งเอกสาร: คลิกขวา style Normal → Modify → Format → Paragraph → Thai Distributed
+ - จัด "เฉพาะช่อง CC" ได้เฉพาะ CC แบบ block (มี `w:p` ใน `w:sdtContent` ของตัวเอง เช่น `detail1`) — CC แบบ inline อยู่ในบรรทัดเดียวกับข้อความอื่น การจัดมีผลทั้งบรรทัด
+- จุดสำคัญต่อ render pipeline: `insertBlockContent` ของ lib-tmpl clone `w:pPr` ของย่อหน้าแรกให้ทุกบรรทัดที่แทนค่า — จัดไว้ใน CC ตั้งแต่ต้นค่าจะสืบทอดไปทุกบรรทัดที่ render ใส่ข้อมูลอัตโนมัติ ไม่ต้องทำอะไรฝั่ง render
+- (อ้างอิงเชิงเทคนิคกรณีต้องตั้งผ่านโค้ดภายหลัง: `w:jc` ใน `w:pPr` เป็น fixed sequence — ต้องอยู่หลัง `w:autoSpaceDN`/`w:spacing`/`w:ind` และก่อน `w:textDirection`/`w:rPr`/`w:sectPr`)
+
+## ผล verify (อัตโนมัติทุกครั้งที่รันสคริปต์)
+
+- `documentProtection` เปิดใช้ (readOnly + enforcement=1) ✓
+- `docProps/custom.xml` มี + relationship + content-type override ครบ ✓
+- Content Control 27 ตัวครบเดิม (plain 26 = มี `w:text`, rich 1 = `detail1`) ✓
+- permission ranges: permStart/permEnd ครบตามจำนวน CC ที่มี tag (27/27) ✓
+- ทุก CC มี `w:lock="sdtLocked"` (กันลบช่อง) ✓
+- `extractContentControls` (lib-tmpl) อ่านคืนได้ทุก tag ตรงกับ output.docx ก่อน protect 100% ✓
+
+## ผลทดสอบใน Word จริง (2026-10-07 รอบ 1)
+
+- ✅ แถบเหลือง protection ขึ้น / แก้นอก CC ไม่ได้ / ลบ CC ไม่ได้
+- ❌ B5: **CC แก้ไม่ได้** เพราะ readOnly ล็อคทั้งเอกสาร → แก้ด้วย permStart/permEnd ตามด้านบนแล้ว (รอบ 2)
+- ✅ custom properties ครบ / ปลด protection ได้แบบไม่มี password
+
+## เหลือตรวจใน Word จริง (รอบ 2)
+
+- เปิด `output-T3.docx` → พิมพ์/แก้ในช่อง CC ต้องได้แล้ว (B5)
+- ลบ CC ต้องไม่ได้ทุกตัว (รวม acc1, nottitle ที่เติม lock ให้)
+- ขั้น E เดิม: F12 เซฟสำเนา → เปิดใหม่ → properties ยังอยู่ครบ
+- (ผ่านแล้วทั้งหมด → ย้าย logic ไปเป็นฟังก์ชันใน lib-tmpl.ts แล้วต่อเข้า render pipeline)
diff --git a/app/api/template/test/output-T3.docx b/app/api/template/test/output-T3.docx
index f8dd71e..c7619be 100644
Binary files a/app/api/template/test/output-T3.docx and b/app/api/template/test/output-T3.docx differ