/* ============================================================================
   AllBox99 — โทเคนกลางของงานออกแบบ (design tokens)
   ============================================================================
   ไฟล์นี้คือ "แหล่งความจริงเดียว" ของฟอนต์ สี และสไตล์ร่วมของทุกหน้า
   ก่อนมีไฟล์นี้ CSS ชุดเดียวกันถูกก็อปไว้ทั้งใน staff_dashboard.html และ admin.html
   แก้ทีต้องแก้สองที่ และเพี้ยนกันไปแล้วจริง (ปุ่มกรองหน้าพนักงานเป็นเขียว หน้าแอดมินเป็นม่วง)

   ⚠️ ไฟล์นี้เป็น CSS ล้วน ไม่มี logic — ห้ามใส่อะไรที่เปลี่ยนพฤติกรรมของระบบ
   ⚠️ โหลดเป็น <link> ธรรมดา จึงเป็น CSS "นอก @layer" → ชนะทุกอย่างที่ Tailwind สร้าง
      (Tailwind v4 วางของตัวเองไว้ใน @layer theme/base/utilities ซึ่งแพ้ CSS นอกเลเยอร์เสมอ)
      ผลคือ: ทับตัวแปรของ Tailwind ได้ตรง ๆ แต่ก็ต้องระวังไม่เขียนกฎกว้าง ๆ
      ที่ไปทับ utility ของหน้าโดยไม่ตั้งใจ
   ============================================================================ */


/* ----------------------------------------------------------------------------
   1) สีสถานะ — เป็น "ความหมาย" ไม่ใช่การตกแต่ง
   ----------------------------------------------------------------------------
   ⚠️ ห้ามผูกสีชุดนี้กับสีแบรนด์ และห้ามให้ธีมใด ๆ มาเปลี่ยน
      เหลือง = แถวนี้มีความเคลื่อนไหววันนี้ / เขียว = ตรวจแล้ว / แดง = บันทึกไม่สำเร็จ
      ถ้าสีพวกนี้ขยับตามธีม พนักงานจะอ่านสถานะผิด
   ---------------------------------------------------------------------------- */
:root {
    /* ทั้งสองสถานะต้อง "เห็นชัด" เพราะเป็นสิ่งที่คนคีย์ใช้ไล่งาน
       ─────────────────────────────────────────────────────────────────
       เหลือง = แถวนี้ถูกคีย์/แก้ไขแล้ววันนี้    เขียว = ติ๊กตรวจแล้ว
       ทั้งคู่ตั้งความเข้มไว้ใกล้กัน (~19-20% จากสีขาว) เพื่อให้กวาดตาเจอทั้งสองแบบเท่า ๆ กัน
       แยกกันด้วย "สี" ไม่ใช่ด้วย "ความเข้ม"

       ⚠️ ห้ามทำให้เข้มไปกว่านี้มาก — สีแดง "บันทึกไม่สำเร็จ" (#fecaca) อยู่ที่ 32% จากขาว
          ถ้าเหลืองไล่ขึ้นไปใกล้ ๆ นั้น ช่องที่บันทึกไม่ติดจะกลืนหายไปในแถวเหลือง
          ซึ่งคือกรณีที่ผู้ใช้ต้องเห็นมากที่สุด */
    --status-moved:          #fae5b4;
    --status-moved-hover:    #f4d492;   /* ต้องเข้มกว่าพื้นชัดเจน ไม่งั้นไม่รู้ว่าเมาส์อยู่แถวไหน */
    --status-moved-rail:     #d99a2b;
    --status-checked:        #d5efe1;
    --status-checked-hover:  #bde3ce;
    --status-checked-rail:   #2f8f68;

    /* สองตัวนี้เป็นสถานะ "รายเซลล์" ไม่ใช่รายแถว จึงยังเข้มเต็มที่ ต้องสะดุดตาทันที */
    --status-saving:         #fef3c7;   /* amber-100 */
    --status-failed:         #fecaca;   /* red-200   */

    /* "บันทึกแล้ว" — สถานะรายเซลล์แบบชั่วคราว โผล่ ~0.9 วิ แล้วหายไปเอง
       ⚠️ เขียวตัวนี้คนละหน้าที่กับ --status-checked (พื้นแถว "ตรวจแล้ว")
          แยกออกจากกันด้วยรูปแบบและอายุ: ตัวนี้เป็น "ขีดใต้เซลล์" ที่หายเอง
          ส่วนตรวจแล้วเป็น "พื้นทั้งแถว" ที่ค้างอยู่จนกว่าจะกดออก จึงไม่ปนกัน
       ⚠️ ห้ามใช้สีแบรนด์ทำสัญญาณนี้ เพราะธีม cny/rose มีสีเน้นเป็นโทนแดง
          ขีดแดงหลังบันทึก "สำเร็จ" จะถูกอ่านเป็นความผิดพลาดทันที */
    --status-saved:          #16794f;

    --rail-width:            3px;
}


/* ----------------------------------------------------------------------------
   2) ฟอนต์
   ----------------------------------------------------------------------------
   ⚠️ ของเดิมสั่ง 'Inter' ซึ่ง "ไม่มีตัวอักษรไทย" ข้อความไทยทั้งระบบจึงตกไปใช้
      ฟอนต์ default ของเครื่อง (Mac = Thonburi, Windows = Leelawadee/Tahoma)
      → เครื่องที่ร้านกับเครื่องที่พัฒนาเห็นคนละหน้าตา และไทยกับตัวเลขเป็นคนละฟอนต์
      IBM Plex Sans Thai มีครบทั้งไทยและละติน จึงคุมหน้าตาได้ทุกเครื่อง
      (มีน้ำหนักถึง 700 — คลาส font-extrabold ที่ขอ 800 จะถูกปัดลงมาที่ 700)

   --font-sans คือตัวแปรของ Tailwind เอง การทับตรงนี้ทำให้คลาส .font-sans
   ในหน้า admin ใช้ฟอนต์เดียวกันโดยไม่ต้องแก้ HTML
   ---------------------------------------------------------------------------- */
:root {
    --font-app: 'IBM Plex Sans Thai', 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
    --font-sans: var(--font-app);
}

body {
    font-family: var(--font-app);
}


/* ----------------------------------------------------------------------------
   3) สีเน้นเดียวทั้งระบบ
   ----------------------------------------------------------------------------
   ก่อนหน้านี้ใช้คนละสีสามหน้า: login = indigo/violet, staff = emerald, admin = indigo
   รวมเหลือสีเดียว (indigo) เพื่อให้สีที่เหลือบนจอ "แปลว่าอะไรบางอย่าง" เสมอ
   ค่าตรงนี้ตรงกับ indigo ของ Tailwind เพื่อให้คลาสเดิมในหน้าเว็บกับ CSS ที่นี่เป็นสีเดียวกันเป๊ะ
   ---------------------------------------------------------------------------- */
:root {
    --brand-50:   #eef2ff;
    --brand-100:  #e0e7ff;
    --brand-300:  #a5b4fc;
    --brand-400:  #818cf8;   /* ค่าตั้งต้นของเฉดนี้ — ทุกธีมใน theme.css ทับให้เอง
                                มีไว้กันกรณีที่ธีมใหม่ลืมกำหนด แล้วแสงเรืองของเส้น
                                "บันทึกแล้ว" จะกลายเป็นค่าว่างโดยไม่มี error ให้เห็น */
    --brand-600:  #4f46e5;
    --brand-700:  #4338ca;

    --ink-muted:  #64748b;   /* slate-500 */
    --ink-faint:  #94a3b8;   /* slate-400 */
    --line:       #e2e8f0;   /* slate-200 */

    /* เส้นและพื้นของตารางข้อมูล */
    --grid-line:        #eef0f4;   /* เส้นคั่นแถว — บางมากโดยตั้งใจ */
    --grid-line-strong: #dcdfe6;   /* เส้นใต้หัวตาราง / เหนือแถวยอดรวม */
    --grid-head-bg:     #fbfbfc;
    --grid-foot-bg:     #f5f6f8;
    --grid-hover:       #e7ecf4;   /* แถวปกติตอนเอาเมาส์วาง */
}

/* ความหนาแน่นของแถว — สลับด้วย data-density ที่ <html> (ดู static/js/app.js)
   ค่าตั้งต้นเป็น compact เพราะหน้านี้คืองานคีย์ ไม่ใช่หน้าอ่าน */
:root                          { --row-pad-y: 0.4rem; --row-font: 0.875rem; }
:root[data-density="compact"]  { --row-pad-y: 0.15rem; --row-font: 0.8125rem; }


/* ----------------------------------------------------------------------------
   4) ตัวเลขในตารางต้องเรียงตรงคอลัมน์
   ----------------------------------------------------------------------------
   tabular figures = เลขทุกตัวกว้างเท่ากัน ("1" กินที่เท่า "8")
   ตัวเลขจึงไม่ขยับซ้าย-ขวาตอนพิมพ์ และเทียบยอดระหว่างแถวได้ด้วยสายตา
   ---------------------------------------------------------------------------- */
table {
    font-variant-numeric: tabular-nums;
}


/* ----------------------------------------------------------------------------
   5) ช่องที่เคอร์เซอร์อยู่ ต้องเห็นชัด
   ----------------------------------------------------------------------------
   ⚠️ เดิม .qty-wrap > .editable ตั้ง outline:none ไว้ และช่องอื่นก็ไม่มีสัญญาณใด ๆ
      ในตาราง 83 แถว × 10 คอลัมน์ที่เลื่อนช่องแบบ Excel จึงไม่รู้ว่าอยู่ตรงไหน

   ใช้ box-shadow ไม่ใช่ outline เพราะ outline:none เดิมยังต้องอยู่
   (กันเส้นประของเบราว์เซอร์ที่ทับ layout ของ .qty-wrap)

   ⚠️ เจาะจงเฉพาะช่องที่อยู่ใน <td> เท่านั้น เพื่อ "ไม่" ให้โดน #productSearch
      ซึ่งเป็น contenteditable นอกตารางและมีวงแหวนโฟกัสของตัวเองอยู่แล้ว
   ---------------------------------------------------------------------------- */
/* แถวที่กำลังแก้ไขอยู่ — ตีกรอบ "ทั้งแถว" ไม่ใช่แค่ช่อง
   ตารางกว้าง 10 คอลัมน์ การเห็นแค่ช่องไม่พอ ต้องรู้ด้วยว่ากำลังคีย์ของสินค้าตัวไหน
   ซึ่งชื่อสินค้าอยู่ไกลไปทางซ้ายสุด

   ⚠️ ต้องใช้ outline ที่ <tr> ห้ามใช้ border หรือ box-shadow ที่ <td>
      เพราะ td:first-child ใช้ box-shadow ทำ "แถบสีสถานะ" อยู่แล้ว (ดูข้อ 6)
      ถ้าไปเขียนทับตรงนั้น แถบเหลือง/เขียวจะหายทันทีที่คลิกเข้าไปแก้แถวนั้น
      outline อยู่คนละ property จึงซ้อนกันได้โดยไม่แย่งกัน

   :focus-within ครอบคลุมทั้งช่อง contenteditable ของตารางสต็อก
   และ <input>/<select> ของตารางขนส่ง โดยไม่ต้องเขียนแยก */
table.data-table tbody tr:focus-within {
    outline: 2px solid var(--brand-600);
    outline-offset: -2px;
}

/* ⚠️⚠️ ถ่างแถบสีสถานะให้กว้างขึ้นเฉพาะแถวที่กำลังแก้ไข — ห้ามลบ
   outline "ไม่ได้" แย่ง property กับ box-shadow ก็จริง แต่ตามลำดับการวาดของ CSS
   outline ถูกวาดเป็นลำดับสุดท้ายของสแต็ก จึงทับ inset shadow ของ <td> ที่เป็นตัวลูก
   กรอบกว้าง 2px ที่ offset -2px กินพื้นที่ x∈[0,2) ส่วนแถบสีกิน x∈[0,3)
   → ถ้าไม่ถ่าง แถบเหลือง/เขียวจะเหลือให้เห็นแค่ 1px บนแถวที่ผู้ใช้กำลังคีย์อยู่พอดี
     ซึ่งเป็นแถวที่ต้องอ่านสถานะออกมากที่สุด (พื้นสีจางมากโดยตั้งใจ ดูข้อ 6)

   ตั้งค่าผ่านตัวแปร ไม่ใช่เขียน box-shadow ใหม่ เพราะกฎแถบสีทั้งสามตัว (เหลือง/เขียว/ขนส่ง)
   อ่าน var(--rail-width) อยู่แล้ว จึงไม่ต้องเขียนสีซ้ำและไม่ไปยุ่งกับลำดับ !important ในข้อ 6 */
table.data-table tbody tr:focus-within td:first-child {
    --rail-width: 5px;
}

/* ตารางหลัก — ใช้ "กรอบทั้งแถว" ด้านบนเป็นสัญญาณโฟกัสอยู่แล้ว
   จึงไม่ตีวงแหวนรอบช่องซ้ำอีก (กรอบซ้อนกรอบอ่านยากกว่าเดิม)
   แต่ยังต้องปิด outline เริ่มต้นของเบราว์เซอร์ ซึ่ง Chrome วาดเป็นเส้นส้มนอกกรอบเซลล์ */
table.data-table td[contenteditable="true"]:focus,
table.data-table td [contenteditable="true"]:focus {
    outline: none;
}

/* ⚠️ ตารางที่ยังไม่ได้แปลงเป็น .data-table (ตอนนี้คือแท็บต้นทุนสินค้า) ไม่มีกรอบทั้งแถว
   จึงต้องเหลือวงแหวนรอบช่องไว้ ไม่งั้นจะไม่เหลือสัญญาณโฟกัสเลยสักอย่าง
   ถ้าวันหลังแปลงตารางนั้นเป็น .data-table ด้วย กฎนี้จะเลิกมีผลไปเอง */
table:not(.data-table) td[contenteditable="true"]:focus,
table:not(.data-table) td [contenteditable="true"]:focus {
    outline: none;
    box-shadow: inset 0 0 0 2px var(--brand-600);
    border-radius: 3px;
}


/* ----------------------------------------------------------------------------
   6) สถานะของแถวในตารางสต็อก
   ----------------------------------------------------------------------------
   ⚠️⚠️ ลำดับของบล็อกในข้อนี้ "ห้ามสลับ" — ทุกกฎใช้ !important เท่ากันหมด
        ตัวที่ประกาศทีหลังจึงเป็นตัวที่ชนะ:
          เหลือง (มีความเคลื่อนไหว) → เขียว (ตรวจแล้ว) → แดง (บันทึกไม่สำเร็จ)
        ถ้าสลับลำดับ แถวที่กำลังคีย์อยู่ (ซึ่งเป็นแถวเหลืองพอดี) จะไม่แสดงสีแดง
        ผู้ใช้จะไม่รู้ว่าบันทึกไม่ติด → ดู CLAUDE.md ข้อ 6

   กฎชุด ship-* ใช้เฉพาะตารางขนส่งในหน้า admin (หน้าพนักงานไม่มี element พวกนี้)
   ---------------------------------------------------------------------------- */

/* แถวที่มีความเคลื่อนไหวในวันนั้น */
.row-highlighted td {
    background-color: var(--status-moved) !important;
}
tr.row-highlighted:hover td {
    background-color: var(--status-moved-hover) !important;
}
/* แถบสีซ้ายแถว — วาดด้วย box-shadow ไม่ใช่ border จึงไม่ดันความกว้างคอลัมน์
   เซลล์แรกของทุกแถวคือช่องติ๊ก "ตรวจ" ซึ่งไม่ใช่ช่องคีย์ จึงไม่ชนกับวงแหวนโฟกัส */
tr.row-highlighted td:first-child {
    box-shadow: inset var(--rail-width) 0 0 var(--status-moved-rail);
}

/* แถวที่ติ๊ก "ตรวจแล้ว" — เก็บในหน่วยความจำเท่านั้น ไม่ลง DB
   ต้องประกาศหลังสีเหลือง เพื่อให้ชนะไฮไลต์ของแถวที่มีความเคลื่อนไหว */
tr.row-checked td {
    background-color: var(--status-checked) !important;
}
tr.row-checked:hover td {
    background-color: var(--status-checked-hover) !important;
}
tr.row-checked td:first-child {
    box-shadow: inset var(--rail-width) 0 0 var(--status-checked-rail);
}

/* แถวขนส่งที่ admin ติ๊กว่าตรวจแล้ว (สถานะนี้เก็บลง DB จริง) */
tr.ship-row-checked td {
    background-color: var(--status-checked) !important;
}
tr.ship-row-checked:hover td {
    background-color: var(--status-checked-hover) !important;
}
tr.ship-row-checked td:first-child {
    box-shadow: inset var(--rail-width) 0 0 var(--status-checked-rail);
}

/* กำลังบันทึก / บันทึกไม่สำเร็จ ของช่องติ๊กในตารางขนส่ง ต้องชนะสีเขียวด้านบน */
tr td.ship-check-saving,
tr.ship-row-checked td.ship-check-saving {
    background-color: var(--status-saving) !important;
}
tr td.ship-check-failed,
tr.ship-row-checked td.ship-check-failed {
    background-color: var(--status-failed) !important;
}

/* สีแดง "บันทึกไม่สำเร็จ" ต้องชนะไฮไลต์แถว (เหลือง/เขียว) ที่ประกาศ !important ไว้ด้านบน
   ⚠️ ตัว .save-failed เปล่า ๆ ท้ายลิสต์ห้ามลบ — ตารางขนส่งใส่คลาสนี้ที่ <input>/<select>
      ซึ่งไม่ใช่ td จึงไม่แมตช์ selector สามตัวแรกเลย
      (specificity คิดแยกรายตัวในลิสต์ ช่องสต็อกจึงยังใช้กฎ td ตามเดิม) */
tr td.save-failed,
tr.row-highlighted td.save-failed,
tr.row-checked td.save-failed,
.save-failed {
    background-color: var(--status-failed) !important;
}


/* ----------------------------------------------------------------------------
   6.1) สัญญาณ "บันทึกแล้ว" — ใช้ที่หน้าพนักงานเท่านั้นในตอนนี้
   ----------------------------------------------------------------------------
   ทำไมต้องมี: เดิมพอบันทึกสำเร็จ โค้ดแค่ "ถอดสีเหลืองออก" แล้วจบ ไม่มีอะไรมาแทน
   คนคีย์จึงแยกไม่ออกระหว่าง "ลงเรียบร้อยแล้ว" กับ "ยังไม่ได้ยิงเลย"
   สัญญาณลบ (แดง) มีอยู่แล้ว แต่สัญญาณบวกไม่มี — ความไม่มั่นใจจึงค้างอยู่ทั้งวัน

   ⚠️ วาดด้วย ::after ไม่ใช่ element ลูก เพราะ applyServerRecord() / autoSave()
      เขียนค่าลงเซลล์ด้วย innerText ซึ่งลบลูกทิ้งทุกครั้งที่ sync
      (บทเรียนเดียวกับสัญลักษณ์ › บนเซลล์ "ผู้อัปเดต" — CLAUDE.md ข้อ 15)
   ⚠️ ห้ามเปลี่ยนไปทาพื้นหลัง — พื้นถูกจองด้วยเหลือง/เขียว/แดงจนหมดแล้ว
      "ขีดใต้" เป็นชั้นที่ยังว่างจริง จึงไม่ต้องไปแย่งลำดับ !important กับใคร
   ⚠️ pointer-events: none จำเป็น เพราะปุ่ม ⋯ ในเซลล์นำเข้า/นำออกลอยทับพื้นที่นี้
      ด้วย position: absolute ถ้าไม่ปิดการรับคลิกไว้ ขีดจะไปกินคลิกของปุ่ม */
/* ชั้นที่ 3 — ตัวเลขในช่องเรืองเขียวแวบหนึ่ง
   ใช้ text-shadow ไม่ใช่ color เพราะสีตัวอักษรของแต่ละเซลล์ไม่เท่ากัน
   (ช่องที่ล็อกใช้ --brand-700, ช่องอ่านอย่างเดียวใช้เทา) การไปแตะ color
   ต้องรู้ค่าปลายทางของทุกกรณี ส่วน text-shadow ทับได้โดยไม่ต้องรู้อะไรเลย
   และไม่กินพื้นที่ layout จึงไม่ดันความกว้างคอลัมน์ */
.save-ok {
    position: relative;
    animation: ab99-saved-ink 1200ms ease-out;
}

@keyframes ab99-saved-ink {
    0%   { text-shadow: 0 0 0 rgba(22, 121, 79, 0); }
    22%  { text-shadow: 0 0 9px rgba(22, 121, 79, 0.75); }
    62%  { text-shadow: 0 0 9px rgba(22, 121, 79, 0.55); }
    100% { text-shadow: 0 0 0 rgba(22, 121, 79, 0); }
}

/* ชั้นที่ 1 — "คลื่น" เขียวจางกวาดขึ้นจากขอบล่างแล้วยุบหายไป
   ⚠️ เป็นแผ่นทับ (overlay) ไม่ใช่ background ของเซลล์ จึงไม่ไปแย่ง !important
      กับพื้นเหลือง/เขียว/แดง และยังอ่านออกบนทุกสีแถว
   ⚠️ ความเข้มสูงสุดตั้งไว้ต่ำโดยตั้งใจ (ราว 22%) เพราะมันทับ "ตัวเลข" ที่คนกำลังอ่าน
      ถ้าเข้มกว่านี้ ตัวเลขจะจมหายไปช่วงหนึ่งทุกครั้งที่บันทึก */
.save-ok::before {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(to top,
                rgba(22, 121, 79, 0.22) 0%,
                rgba(22, 121, 79, 0.10) 45%,
                rgba(22, 121, 79, 0) 100%);
    transform-origin: bottom;
    pointer-events: none;
    animation: ab99-saved-wash 1000ms cubic-bezier(0.16, 0.84, 0.44, 1) forwards;
}

/* ชั้นที่ 2 — เส้นยืนยันที่ขอบล่าง กวาดจาก "ซ้ายไปขวา" แล้วค้างไว้ก่อนจางหาย
   เป็นตัวที่ "อ่านออก" จริง ๆ ส่วนคลื่นด้านบนเป็นแค่จังหวะให้รู้สึกว่ามีอะไรเกิดขึ้น

   ⚠️ ทิศทางมาจาก transform-origin ไม่ใช่จาก keyframes — ถ้าลบบรรทัดนี้ทิ้ง
      ค่า default คือ center เส้นจะกลับไปกวาดออกจากกลางเซลล์ทันที
   สีของเส้นตามธีมที่ใช้อยู่ (--brand-600) ตามที่เจ้าของระบบสั่ง 2026-08-26
   ⚠️ ธีม cny / rose มีสีเน้นเป็นโทนแดง เส้นหลังบันทึก "สำเร็จ" จะออกแดงในสองธีมนั้น
      ตัวที่ยังบอกความหมาย "สำเร็จ" ไว้คือคลื่นและแสงเรืองของตัวเลข ซึ่งยังเป็นเขียว
      (--status-saved) โดยตั้งใจ — ห้ามเปลี่ยนสองชั้นนั้นตามธีมไปด้วย ไม่งั้นบนธีมแดง
      สัญญาณ "บันทึกสำเร็จ" กับ "บันทึกไม่สำเร็จ" จะกลายเป็นสีเดียวกันทั้งเซลล์
   ⚠️ แสงเรืองใช้ --brand-400 (เฉดอ่อนของธีม) ไม่ใช่สีเดียวกับเส้นแบบใส่ alpha
      เพราะ theme.css ให้มาเป็นค่าสีสำเร็จรูป ไม่ได้แยกเป็นช่อง R/G/B
      จึงเติมความโปร่งใสเข้าไปเองไม่ได้ถ้าไม่พึ่ง color-mix() ซึ่งเบราว์เซอร์เก่าไม่รองรับ */
.save-ok::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 3px;
    background: var(--brand-600);
    border-radius: 3px 3px 0 0;
    box-shadow: 0 -1px 6px var(--brand-400);
    transform-origin: left center;
    pointer-events: none;
    /* ⚠️ เส้นโค้งจังหวะต้องไม่เป็นแบบ "พุ่งตอนต้น" (เช่น 0.22, 0.61, 0.36, 1)
       ไม่งั้นถึงจะตั้งเวลายาวแค่ไหน ตาก็ยังเห็นเป็นการกระตุกครั้งเดียวอยู่ดี
       เพราะกวาดไปเกิน 70% ตั้งแต่ 150ms แรก — ใช้แบบเร่ง-ผ่อนสมมาตรแทน */
    animation: ab99-saved 1600ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

@keyframes ab99-saved-wash {
    0%   { opacity: 0;    transform: scaleY(0.08); }
    26%  { opacity: 1;    transform: scaleY(1); }
    60%  { opacity: 0.7;  transform: scaleY(1); }
    100% { opacity: 0;    transform: scaleY(0.55); }
}

/* จังหวะของเส้น (รวม 1600ms) — ทำให้ช้าลงตามที่เจ้าของระบบสั่ง 2026-08-26
   ของเดิมกวาดจบใน ~200ms ซึ่งเร็วเกินกว่าจะทันเห็นตอนกวาดตาไปแถวถัดไปแล้ว
     0–30%   (480ms) กวาดจากซ้ายไปขวา
     30–72%  (670ms) ค้างไว้ให้ทันอ่าน
     72–100% (450ms) จางหาย */
@keyframes ab99-saved {
    0%   { opacity: 0; transform: scaleX(0.05); }
    30%  { opacity: 1; transform: scaleX(1); }
    72%  { opacity: 1; transform: scaleX(1); }
    100% { opacity: 0; transform: scaleX(1); }
}

@keyframes ab99-saved-fade {
    0%, 72% { opacity: 1; }
    100%    { opacity: 0; }
}

@keyframes ab99-spin {
    to { transform: rotate(360deg); }
}

/* ชิปสถานะการบันทึกบนแถบหัวจอ — ตัวตอบคำถาม "ของลงครบหรือยัง" ก่อนปิดจอกลับบ้าน
   ใช้ทั้งสีและ "ข้อความ" เสมอ ไม่พึ่งสีอย่างเดียว คนตาบอดสีจึงอ่านออกเหมือนกัน */
.save-chip { transition: background 0.15s, border-color 0.15s; }
.save-chip[data-state="saving"] { background: #fffaf0; border-color: #f3d9a4; }
.save-chip[data-state="saving"] b { color: #92610a; }
.save-chip[data-state="saving"] > .ic { color: #b57614; animation: ab99-spin 1.1s linear infinite; }
.save-chip[data-state="saved"]  { background: #f0faf5; border-color: #bfe6d3; }
.save-chip[data-state="saved"] b { color: var(--status-saved); }
.save-chip[data-state="saved"] > .ic { color: var(--status-saved); }
.save-chip[data-state="failed"] { background: #fef2f2; border-color: #fecaca; }
.save-chip[data-state="failed"] b { color: #b91c1c; }
.save-chip[data-state="failed"] > .ic { color: #b91c1c; }

/* ผู้ใช้ที่ตั้งค่าระบบว่า "ลดการเคลื่อนไหว" ต้องยังเห็นสัญญาณ แค่ไม่ต้องขยับ */
@media (prefers-reduced-motion: reduce) {
    .save-ok::before { animation: ab99-saved-fade 1000ms ease-out forwards; transform: none; }
    /* เรืองแสงไม่ใช่ "การเคลื่อนไหว" จึงเก็บไว้ได้ — ตัดเฉพาะตัวที่ขยับ/ยืดหด */
    .save-ok::after  { animation: ab99-saved-fade 1600ms ease-out forwards; transform: none; }
    .save-chip[data-state="saving"] > .ic { animation: none; }
}


/* ----------------------------------------------------------------------------
   6.2) ตัวช่วยเติมหมายเหตุในหน้าต่างรายการนำเข้า-นำออก
   ----------------------------------------------------------------------------
   พิมพ์ตัวอักษรตัวแรกแล้วขึ้นชื่อที่ใช้บ่อยให้เลือก — จะเลือกหรือพิมพ์เองต่อก็ได้
   มีไว้เพื่อให้ "ชื่อสะกดตรงกันทั้งวัน" ซึ่งเป็นหัวใจของรายงานตรวจสอบการนำเข้า-นำออก
   (ของจริงเคยมี "ไอยรา" 3 แบบ และ "พี่กอล์ฟ" 4 แบบ — ดู CLAUDE.md ข้อ 18)

   ⚠️ ต้องแปะไว้ที่ <body> ไม่ใช่ในเซลล์ เพราะหน้าต่างรายการมีกรอบเลื่อนของตัวเอง
      ถ้าวางไว้ข้างในจะถูกตัดหายเมื่อรายการยาวเกินกรอบ */
.note-suggest {
    position: absolute;
    z-index: 60;
    min-width: 11rem;
    background: #fff;
    border: 1px solid var(--line);
    border-radius: 0.6rem;
    box-shadow: 0 10px 25px rgba(15, 23, 42, 0.12);
    padding: 0.25rem;
    font-size: 0.875rem;
}
.note-suggest button {
    display: block;
    width: 100%;
    text-align: left;
    padding: 0.4rem 0.6rem;
    border-radius: 0.4rem;
    cursor: pointer;
    background: transparent;
    border: 0;
    font: inherit;
    color: #1f2230;
}
.note-suggest button:hover,
.note-suggest button[aria-selected="true"] { background: var(--brand-100); color: var(--brand-700); }
.note-suggest .hint {
    padding: 0.3rem 0.6rem 0.15rem;
    font-size: 0.7rem;
    color: var(--ink-faint);
}
/* กลุ่มที่ 2 เป็นต้นไปมีเส้นคั่น — หัวข้อตัวเล็กสีจางอย่างเดียวแยกไม่ออกเวลากวาดตาเร็ว ๆ
   (กลุ่มแรกไม่ต้องมี จะกลายเป็นเส้นลอยใต้ขอบกล่อง) */
.note-suggest .hint:not(:first-child) {
    border-top: 1px solid var(--line);
    margin-top: 0.25rem;
    padding-top: 0.4rem;
}


/* ----------------------------------------------------------------------------
   7) คอลัมน์ "ขายกล่อง (ขนส่ง)" — ซ่อนชั่วคราว
   ----------------------------------------------------------------------------
   ลบสองบรรทัดล่างนี้เพื่อนำกลับมาแสดง ค่าหลังบ้านยังทำงานและถูกหักออกตลอด

   ⚠️ ต้องซ่อนด้วย display:none เท่านั้น ห้ามเปลี่ยนไปใช้วิธีที่ทำให้ "ข้อความหาย"
      เพราะ calculateTotals() / recalcRow() อ่านยอดจาก innerText ของ DOM
      และ innerText ของ element ที่ไม่ถูก render ยังคืนข้อความเดิมอยู่
   ---------------------------------------------------------------------------- */
.box-sales-col,
.box-sales-qty {
    display: none;
}


/* ----------------------------------------------------------------------------
   8) ช่องนำเข้า/นำออก — ตัวเลขที่พิมพ์ได้ + ปุ่มเปิดรายการรายครั้ง อยู่ใน td เดียวกัน
   ----------------------------------------------------------------------------
   ตัวที่ contenteditable คือ <span> ด้านใน ไม่ใช่ td
   ปุ่มจึงไม่กลายเป็นข้อความที่แก้ไขได้ และลำดับช่องของคีย์บอร์ดแบบ Excel ไม่เปลี่ยน
   ---------------------------------------------------------------------------- */
/* ⚠️⚠️ ปุ่ม ⋯ ต้องอยู่นอก flow (position:absolute) ห้ามเอากลับเข้าสาย flex
   ถ้าปุ่มอยู่ในสาย flex ตัวเลขจะถูกจัดกึ่งกลาง "พื้นที่ที่เหลือจากปุ่ม" ไม่ใช่กึ่งกลางเซลล์
   ซึ่งพังสองต่อ:
     1. ไม่ตรงกับแถวยอดรวม ซึ่งไม่มีปุ่มจึงกึ่งกลางเซลล์เต็ม ๆ
     2. ไม่ตรงกันเองระหว่างแถว เพราะ qtyBadgeText() คืน '⋯' บ้าง '⋯2' '⋯3' บ้าง
        ปุ่มจึงกว้างไม่เท่ากัน ตัวเลขแต่ละแถวเลยเยื้องคนละระยะ
   คอลัมน์นี้เป็นตัวเลขที่ต้องกวาดตาเทียบกันในแนวตั้ง การเยื้องจึงอ่านผิดได้ */
.qty-wrap {
    position: relative;
    display: flex;
    align-items: center;
}
.qty-wrap > .editable {
    flex: 1;
    min-width: 2.2rem;
    padding: 0.5rem 0.25rem;
    outline: none;
    text-align: center;
}
.qty-wrap > .editable.qty-locked {
    cursor: pointer;
    color: var(--brand-700);
    font-weight: 600;
}

.split-btn {
    position: absolute;
    right: 2px;
    top: 50%;
    transform: translateY(-50%);
    border-radius: 9999px;
    padding: 0 0.4rem;
    line-height: 1.35rem;
    font-size: 0.7rem;
    color: var(--ink-faint);
    background: rgba(148, 163, 184, 0.12);
    cursor: pointer;
    transition: all 0.15s;
}
.split-btn:hover {
    color: var(--brand-700);
    background: var(--brand-100);
}
.split-btn.split-btn-active {
    color: var(--brand-700);
    background: var(--brand-100);
    font-weight: 700;
}


/* ----------------------------------------------------------------------------
   9) ปุ่มกรองสินค้า
   ----------------------------------------------------------------------------
   เดิมหน้าพนักงานเป็นสีเขียว หน้าแอดมินเป็นสีม่วง ทั้งที่เป็นปุ่มชุดเดียวกัน
   รวมเป็นสีแบรนด์เดียวที่นี่
   ---------------------------------------------------------------------------- */
.filter-btn {
    border: 1px solid var(--line);
    background: #fff;
    color: var(--ink-muted);
    border-radius: 0.5rem;
    padding: 0.55rem 0.85rem;
    font-size: 0.8rem;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    transition: all 0.15s;
}
.filter-btn:hover {
    border-color: var(--brand-300);
    color: var(--brand-700);
}
.filter-btn.filter-btn-on {
    background: var(--brand-600);
    border-color: var(--brand-600);
    color: #fff;
}

/* ----------------------------------------------------------------------------
   แถบหัวหน้าจอ (app bar)
   ----------------------------------------------------------------------------
   ของเดิมเป็นข้อความยาวบรรทัดเดียวคั่นด้วย "|" ("ยินดีต้อนรับ: สาขา | วันที่ | เวลาทำงาน")
   อ่านยากและไม่มีลำดับชั้น ทั้งที่สามอย่างนี้คือ "บริบทที่กำลังคีย์อยู่"
   ซึ่งถ้าผิด = คีย์ผิดสาขา/ผิดวัน แยกเป็นชิปคนละใบจึงกวาดตาเห็นได้ในครั้งเดียว
   ---------------------------------------------------------------------------- */
.app-bar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.75rem 1.15rem;
    background: #fff;
    border: 1px solid var(--line);
    border-radius: 1rem;
    padding: 0.8rem 1.15rem;
    margin-bottom: 1rem;
}

.app-bar-brand { display: flex; align-items: center; gap: 0.7rem; }
.app-bar-logo  { height: 2.1rem; width: auto; object-fit: contain; border-radius: 0.4rem; }
.app-bar-title { margin: 0; font-size: 1rem; font-weight: 700; color: #1f2230; line-height: 1.3; }
.app-bar-sub   {
    margin: 0;
    font-size: 0.65rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.app-bar-divider { width: 1px; align-self: stretch; background: var(--line); }

.app-bar-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; flex: 1; }

/* ชิปบอกบริบท: สาขา / วันที่ / เวลาทำงาน */
.meta-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    background: #f7f8fa;
    border: 1px solid var(--line);
    border-radius: 0.6rem;
    padding: 0.3rem 0.65rem;
    font-size: 0.8125rem;
    color: var(--ink-muted);
    white-space: nowrap;
}
.meta-chip > .ic { color: var(--ink-faint); }
.meta-chip b { color: #1f2230; font-weight: 600; }

/* ชื่อสาขาถูกวาดใหม่โดย JS พร้อมคลาสของมันเอง (text-base) — บังคับให้พอดีกับชิป */
.meta-chip #branchNameLabel { font-size: 0.8125rem; }

/* dropdown เลือกสาขาในหน้า admin ให้ดูเป็นส่วนหนึ่งของชิป ไม่ใช่กล่องซ้อนกล่อง */
.chip-select {
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--brand-700);
    background: transparent;
    border: 0;
    padding: 0 0.15rem;
    cursor: pointer;
    outline: none;
    max-width: 14rem;
}
.chip-select:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; border-radius: 3px; }

.app-bar-action {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--ink-muted);
    border: 1px solid var(--line);
    border-radius: 0.6rem;
    padding: 0.4rem 0.75rem;
    text-decoration: none;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.app-bar-action:hover { color: #b91c1c; border-color: #fca5a5; background: #fef2f2; }
.app-bar-action:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; }


/* ----------------------------------------------------------------------------
   เซลล์ "ผู้อัปเดต" — กดเพื่อดูประวัติการแก้ไขของแถวนั้น
   ----------------------------------------------------------------------------
   ⚠️ สัญลักษณ์ต้องทำด้วย ::after ห้ามใส่ element ลูกเข้าไปในเซลล์
      เพราะ JS เขียนชื่อผู้อัปเดตด้วย innerText ซึ่งจะลบลูกทิ้งทุกครั้งที่ sync
      (pseudo-element ไม่ถูก innerText แตะ จึงรอด)
   ---------------------------------------------------------------------------- */
.updated-by-cell {
    cursor: pointer;
    transition: color 0.15s;
}
.updated-by-cell::after {
    content: "›";
    margin-left: 0.35rem;
    opacity: 0.4;
    font-weight: 700;
}
.updated-by-cell:hover {
    color: var(--brand-700);
}
.updated-by-cell:hover::after { opacity: 1; }
.updated-by-cell:focus-visible {
    outline: 2px solid var(--brand-600);
    outline-offset: -2px;
}

/* รายการในหน้าต่างประวัติ */
.audit-row {
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--grid-line);
    font-size: 0.8125rem;
}
.audit-row:last-child { border-bottom: 0; }
.audit-time {
    font-size: 0.6875rem;
    color: var(--ink-faint);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
/* แถวที่ "แก้คนละวันกับวันของข้อมูล" — ต้องสะดุดตาโดยไม่ไปยืมสีสถานะของตาราง
   ⚠️ ห้ามใช้ เหลือง/เขียว/แดง ตามข้อ 13 สามสีนั้นถูกจองเป็นความหมายทั้งระบบแล้ว
      ที่นี่จึงใช้ "น้ำหนักกับความเข้ม" แทนสี — อ่านออกทั้งคนตาปกติและคนตาบอดสี */
.audit-time-cross {
    color: var(--ink-strong, #1f2230);
    font-weight: 700;
}
.audit-field { color: var(--ink-muted); min-width: 7.5rem; }
.audit-value { font-weight: 700; color: #1f2230; }
.audit-actor { margin-left: auto; color: var(--ink-muted); white-space: nowrap; }

/* ----------------------------------------------------------------------------
   การ์ดเลือกธีม (แท็บ ตั้งค่า → ธีมหน้าจอ)
   ----------------------------------------------------------------------------
   ⚠️ สีในการ์ดเป็นค่าคงที่ที่เขียนไว้ใน HTML โดยตั้งใจ ไม่ได้ผูกกับ --brand-*
      เพราะต้องแสดง "สีของธีมนั้น ๆ" พร้อมกันทั้ง 8 ใบ ไม่ใช่สีของธีมที่ใช้อยู่
   ---------------------------------------------------------------------------- */
/* แถบสวิตช์โหมดธีม (เลือกเอง / สุ่มทุกวัน) */
.theme-mode {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    background: #f7f8fa;
    border: 1px solid var(--line);
    border-radius: 0.75rem;
    padding: 0.75rem 0.9rem;
    margin-bottom: 0.9rem;
}
.theme-mode-text { display: flex; flex-direction: column; gap: 0.15rem; min-width: 14rem; flex: 1; }
.theme-mode-title {
    display: flex; align-items: center; gap: 0.4rem;
    font-size: 0.875rem; font-weight: 600; color: #1f2230;
}
.theme-mode-hint { font-size: 0.75rem; color: var(--ink-muted); line-height: 1.45; }

/* จุดเล็ก ๆ บอกว่าธีมนี้อยู่ในกองสุ่ม — ธีมเทศกาลจะไม่มี */
.theme-card-pooled .theme-dot::after {
    content: "";
    position: absolute;
    inset: auto auto 0.55rem 1.85rem;
    width: 5px; height: 5px;
    border-radius: 50%;
    background: var(--brand-600);
}
.theme-card-pooled .theme-dot { position: relative; }

.theme-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
    gap: 0.75rem;
}
.theme-card {
    position: relative;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    text-align: left;
    font: inherit;
    background: #fff;
    border: 1px solid var(--line);
    border-radius: 0.75rem;
    padding: 0.9rem 0.9rem 0.8rem;
    cursor: pointer;
    transition: border-color 0.15s, box-shadow 0.15s;
}
.theme-card:hover { border-color: var(--ink-faint); }
.theme-card:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; }
.theme-card[aria-pressed="true"] {
    border-color: var(--brand-600);
    box-shadow: 0 0 0 2px var(--brand-100);
}
/* แถบเทศกาล — ตัวอย่างของแถบที่จะไปโผล่บนแถบหัวจอจริง */
.theme-band { position: absolute; inset: 0 0 auto 0; height: 3px; }
.theme-dot {
    width: 1.4rem; height: 1.4rem; border-radius: 0.4rem; margin-bottom: 0.35rem;
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.theme-name {
    display: flex; align-items: center; gap: 0.4rem;
    font-size: 0.875rem; font-weight: 600; color: #1f2230;
}
.theme-tag {
    font-size: 0.625rem; font-weight: 600; letter-spacing: 0.04em;
    color: var(--ink-muted); background: #f1f3f7;
    border-radius: 999px; padding: 0.05rem 0.4rem;
}
.theme-desc { font-size: 0.75rem; color: var(--ink-muted); line-height: 1.4; }
/* ป้าย "วันนี้" บนการ์ดธีมสีประจำวัน — ชี้ว่าโหมด "ตามวัน" จะหยิบใบไหนให้ */
.theme-tag-on { color: #ffffff; background: var(--brand-600); }


/* ----------------------------------------------------------------------------
   ไอคอน (แทนอิโมจิ)
   ----------------------------------------------------------------------------
   ชุดสัญลักษณ์อยู่ที่ templates/_icons.html ซึ่ง include ไว้ต้น <body> ของทุกหน้า
   ใช้:  <svg class="ic"><use href="#ic-print"></use></svg>

   ขนาดผูกกับ font-size ของตัวแม่ (1em) และสีสืบทอดจาก currentColor
   → ไอคอนจึงโตและเปลี่ยนสีตามข้อความที่มันอยู่ข้าง ๆ เสมอ ไม่ต้องตั้งค่าซ้ำทุกจุด
   ---------------------------------------------------------------------------- */
.ic {
    width: 1em;
    height: 1em;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.75;
    stroke-linecap: round;
    stroke-linejoin: round;
    vertical-align: -0.14em;
}
/* ไอคอนที่อยู่ในข้อความยาว ๆ ควรจางกว่าตัวอักษรเล็กน้อย จะได้ไม่แย่งสายตา */
.ic-muted { color: var(--ink-faint); }

/* ⚠️ ตัวที่มีไอคอนนำหน้าข้อความต้องเป็น flex ไม่งั้นไอคอนกับข้อความจะ "ตกคนละบรรทัด"
      เมื่อกล่องแคบลง (เจอจริงที่ปุ่มแท็บ, ปุ่มจัดการสินค้า และการ์ดสรุปยอดเงิน)
      :not(.flex) เพื่อไม่ไปทับตัวที่เทมเพลตจัด flex ไว้เองอยู่แล้ว
      และไม่แตะ th/td เด็ดขาด เพราะจะพัง table layout
   ⚠️⚠️ ทุก selector ที่ตั้ง display ต้องมี :not(.hidden) (test_HD1)
      ไฟล์นี้อยู่นอก @layer ของ Tailwind จึงชนะ .hidden เสมอ → ตัวที่มีไอคอนจะ "ซ่อนไม่ได้เลย"
      เจอจริง 2026-09-24: ป้าย "โหมดดูอย่างเดียว" (#mvReadonly) ในหน้าต่าง ⋯ ของหน้าพนักงาน
      ขึ้นตลอดเวลา แม้อยู่ในเวลาทำงานและช่องแก้ได้ปกติ */
button:not(.flex):not(.inline-flex):not(.hidden):has(> svg.ic),
a:not(.flex):not(.inline-flex):not(.hidden):has(> svg.ic),
h1:not(.flex):not(.hidden):has(> svg.ic),
h2:not(.flex):not(.hidden):has(> svg.ic),
h3:not(.flex):not(.hidden):has(> svg.ic),
p:not(.flex):not(.hidden):has(> svg.ic),
label:not(.flex):not(.hidden):has(> svg.ic) {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
}
/* ปุ่มที่ตั้งใจให้เต็มความกว้างต้องยังเต็มอยู่ (inline-flex จะหดตามเนื้อหา) */
button.w-full:not(.flex):not(.hidden):has(> svg.ic) {
    display: flex;
}
/* ⚠️ จัดกึ่งกลางได้เฉพาะปุ่มที่ไม่ได้สั่ง text-left ไว้
      รายการในเมนู dropdown เป็น "w-full text-left" — ถ้าไปจัดกึ่งกลาง
      แต่ละบรรทัดจะเยื้องไม่เท่ากันตามความยาวข้อความ ดูเหมือนเมนูเบี้ยวทั้งแผง */
button.w-full:not(.text-left):not(.flex):has(> svg.ic) {
    justify-content: center;
}

/* ปุ่มสลับความหนาแน่นของแถว (สบาย / กระชับ) — อยู่ท้ายแถบกรองสินค้า */
.density-seg {
    display: inline-flex;
    background: #eff0f3;
    border-radius: 0.5rem;
    padding: 2px;
    gap: 2px;
}
.density-seg button {
    font: inherit;
    font-size: 0.75rem;
    font-weight: 600;
    border: 0;
    background: transparent;
    color: var(--ink-muted);
    padding: 0.3rem 0.6rem;
    border-radius: 0.375rem;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.15s, color 0.15s;
}
.density-seg button[aria-pressed="true"] {
    background: #fff;
    color: var(--brand-700);
    box-shadow: 0 1px 2px rgba(16, 18, 24, 0.1);
}
.density-seg button:focus-visible {
    outline: 2px solid var(--brand-600);
    outline-offset: 1px;
}

/* แถวที่ถูกกรองออก — ใช้ display:none ซึ่ง innerText ยังคืนค่าเดิม
   ยอดรวมท้ายตารางและ recalcRow() ที่อ่านจาก DOM จึงไม่เพี้ยน (เหตุผลเดียวกับข้อ 7) */
tr.row-filtered-out {
    display: none;
}


/* ----------------------------------------------------------------------------
   10) ช่องค้นหาสินค้า
   ----------------------------------------------------------------------------
   ⚠️⚠️ ต้องเป็น <div contenteditable="plaintext-only"> ห้ามเปลี่ยนเป็น <input> เด็ดขาด
        password manager ของ Chrome ทำงานกับ <input> เท่านั้น พอเป็น input จะเจอสองอาการ
        บน production: เติมชื่อผู้ใช้จากหน้า login ลงช่องเอง และเด้ง dropdown รหัสผ่านทุกครั้งที่คลิก
        (ลอง autocomplete="off" / type="search" / เปลี่ยน name / readonly มาแล้ว เอาไม่อยู่ทั้งหมด
         — ดู CLAUDE.md ข้อ 7)

   ผลจากการเป็น contenteditable คือ placeholder ต้องทำเอง และต้องบังคับความสูงให้เท่า input เดิม
   ---------------------------------------------------------------------------- */
/* ⚠️ ใช้ร่วมกันทุกช่องค้นหาที่เป็น contenteditable — ตอนนี้มี #productSearch (ตารางสต็อกรายวัน)
   และ #svSearch (แท็บมูลค่าสต็อก) · เพิ่มช่องใหม่ให้เติม id ที่นี่ด้วย ไม่งั้นจะไม่มี placeholder
   และช่องจะเตี้ยกว่าที่ควรเป็น โดยไม่มี error ให้เห็น */
#productSearch,
#svSearch {
    min-height: 1.25rem;
    white-space: nowrap;
    overflow-x: auto;
}
#productSearch:empty::before,
#svSearch:empty::before {
    content: attr(data-placeholder);
    color: var(--ink-faint);
    pointer-events: none;
}


/* ----------------------------------------------------------------------------
   11) ตารางข้อมูลหลัก (`.data-table`)
   ----------------------------------------------------------------------------
   ใช้กับตารางที่คีย์ทุกวันเท่านั้น: ตารางสต็อก + ตารางขนส่ง ของทั้งหน้าพนักงานและแอดมิน
   แท็บรายงาน/ตั้งค่าในหน้า admin ยังเป็นแบบเดิม (ตั้งใจ — จะทยอยตามทีหลัง)

   ⚠️ เซลล์ในเทมเพลตยังมีคลาส `border border-slate-200` ติดอยู่ และมีทั้งโปรเจกต์ 215 จุด
      การไล่ลบด้วยมือเสี่ยงเกินความจำเป็น จึงทับด้วยกฎที่นี่แทน
      (app.css อยู่นอก @layer + specificity สูงกว่า จึงชนะ utility ของ Tailwind)
   ---------------------------------------------------------------------------- */

/* ⚠️ ต้องเป็น separate ไม่ใช่ collapse — เพราะ border ของ thead ที่ position:sticky
      จะ "ไม่ถูกวาด" เมื่อ border-collapse:collapse (ตัวตารางเป็นคนวาดเส้น ไม่ใช่เซลล์)
      หัวตารางจะกลายเป็นลอยไม่มีเส้นใต้ตอนเลื่อน */
table.data-table {
    border-collapse: separate;
    border-spacing: 0;
}

table.data-table th,
table.data-table td {
    border: 0;
}

/* หัวตาราง — ตรึงไว้เสมอ เลื่อนถึงแถว 70 ก็ยังรู้ว่าคอลัมน์ไหนคือนำเข้า ไหนคือนำออก */
table.data-table thead th {
    position: sticky;
    top: 0;
    z-index: 3;
    background: var(--grid-head-bg);
    border-bottom: 1px solid var(--grid-line-strong);
    padding: 0.5rem 0.6rem;
    font-size: 0.6875rem;
    letter-spacing: 0.03em;
    color: var(--ink-muted);
    white-space: nowrap;
}

/* แถวยอดรวม — ตรึงไว้ท้ายกรอบ ไม่ต้องเลื่อนลงไปดู
   พื้นต้องทึบ ไม่งั้นแถวที่เลื่อนอยู่ข้างล่างจะทะลุขึ้นมา */
table.data-table tfoot {
    background: none;
}
table.data-table tfoot td {
    position: sticky;
    bottom: 0;
    z-index: 3;
    background: var(--grid-foot-bg);
    border-top: 1px solid var(--grid-line-strong);
    padding: 0.5rem 0.6rem;
}

/* คอลัมน์หมายเหตุกว้างคงที่ — ข้อความยาวตัดขึ้นบรรทัดใหม่ในช่อง แทนที่จะดันคอลัมน์ (test_MV3)
   ของเดิมกว้างตามเนื้อหา: หมายเหตุยาวหนึ่งแถวทำให้คอลัมน์หมายเหตุโต 69 → 165px
   แล้วคอลัมน์ตัวเลขทั้งตารางเลื่อนไปทางซ้าย (เจอจริงในเบราว์เซอร์ 2026-09-24: คลิกพลาดช่อง
   เพราะตารางขยับใต้เมาส์หลังพิมพ์หมายเหตุเสร็จ)
   ⚠️ ต้องอยู่ใน min-width เท่านั้น — บนมือถือเซลล์เป็นแถวในการ์ด (ข้อ 13.6) ตรึงความกว้างแล้วการ์ดแคบตาม
   ⚠️ overflow-wrap: anywhere จำเป็น — คำที่ไม่มีช่องว่าง (เลขพัสดุ · ลิงก์) จะดันความกว้างขั้นต่ำของคอลัมน์ */
@media (min-width: 768px) {
    table.data-table th.remark-col,
    table.data-table td.remark-text {
        width: 9rem;
        min-width: 9rem;
        max-width: 9rem;
    }
    table.data-table td.remark-text {
        overflow-wrap: anywhere;
    }
}

/* เอาเมาส์วางแถวไหน แถวนั้นต้องเข้มขึ้นจนเห็นชัด
   ⚠️ ต้องทาที่ <td> ไม่ใช่ <tr> — เซลล์หลายช่องมีพื้นของตัวเอง (bg-slate-100/50 ของคอลัมน์
      ที่ระบบคำนวณให้ และ bg-white ของช่องคีย์) ถ้าทาที่ tr จะถูกพื้นของ td บังจนไม่เห็นอะไรเลย */
table.data-table tbody tr:hover td {
    background-color: var(--grid-hover);
}

/* แถวข้อมูล — เหลือเส้นแนวนอนเส้นเดียว ไม่ตีกรอบครบสี่ด้านทุกเซลล์
   ตาจึงไม่ต้องข้ามเส้น 9 เส้นต่อแถว */
table.data-table tbody td {
    border-bottom: 1px solid var(--grid-line);
    padding-top: var(--row-pad-y);
    padding-bottom: var(--row-pad-y);
    font-size: var(--row-font);
}

/* ช่องนำเข้า/นำออกคุมความสูงด้วย padding ของ span ข้างใน ต้องหดตามความหนาแน่นด้วย */
.qty-wrap > .editable {
    padding: var(--row-pad-y) 0.25rem;
}

/* ⚠️ เลื่อนช่องด้วยคีย์บอร์ดแล้วเบราว์เซอร์จะ scroll ให้เอง โดยกันระยะตาม scroll-margin
      ถ้าค่านั้นน้อยกว่าความสูงหัวตารางที่ตรึงอยู่ เซลล์ที่เพิ่งเลื่อนไปถึงจะไปโผล่ "ใต้" หัวตาราง
      มองไม่เห็นว่ากำลังพิมพ์อะไรอยู่

      ⚠️ ห้ามเขียนเป็นค่าคงที่ — หัวตารางสูงไม่เท่ากันในแต่ละหน้า (มีปุ่ม Clear ซ้อนใต้คำว่า "ตรวจ"
         ทำให้สูงราว 55px ไม่ใช่ 36px แบบหัวตารางบรรทัดเดียว) static/js/app.js เป็นคนวัดจริง
         แล้วเซ็ต --grid-head-h / --grid-foot-h ให้ */
table.data-table tbody td {
    /* +4px = ระยะหายใจ ไม่ให้เซลล์แปะติดขอบหัวตารางพอดีเป๊ะ (และเผื่อขอบ 1px ของกรอบเลื่อน) */
    scroll-margin-top: calc(var(--grid-head-h) + 4px);
    scroll-margin-bottom: calc(var(--grid-foot-h) + 4px);
}

/* กรอบเลื่อนของตารางสต็อก — จำเป็นต่อ position:sticky
   sticky อ้างอิงกับ "กล่องที่เลื่อนได้ที่ใกล้ที่สุด" ซึ่งก็คือ div นี้
   ถ้าไม่จำกัดความสูง กล่องนี้จะไม่มีวันเลื่อน หัวตารางจึงไม่มีวันติด */
.table-scroll {
    max-height: calc(100vh - 17rem);
    min-height: 18rem;
    overflow: auto;

    /* ค่าสำรองของความสูงหัวตาราง/แถวยอดรวม — ค่าจริงถูกวัดและเซ็ตโดย static/js/app.js */
    --grid-head-h: 3.5rem;
    --grid-foot-h: 2.5rem;
}

/* แท็บมูลค่าสต็อก — หัวตารางค้างตอนเลื่อน (เจ้าของสั่ง 2026-09-25 · test_SV10 / test_SV11)
   84 แถว เลื่อนลงแล้วหัวคอลัมน์หาย ต้องย้อนขึ้นไปดูว่าช่องไหนเป็นสาขาไหน
   ⚠️⚠️ sticky ยึดกับกล่องเลื่อนที่ใกล้ที่สุด — .sv-scroll มี overflow-x-auto อยู่แล้วแต่ไม่จำกัดความสูง
      จึงไม่เคยเลื่อนแนวตั้ง หัวตารางไม่มีวันค้าง → จำกัดความสูงให้กล่องเลื่อนเอง (แพตเทิร์นเดียวกับ .table-scroll)
   ⚠️ พื้นหัวตารางเดิมเป็นสีโปร่ง ค้างแล้วตัวเลขของแถวที่เลื่อนผ่านจะทะลุขึ้นมา → สีทึบที่ผสมไว้ให้หน้าตาเท่าเดิม
   ⚠️⚠️ ต้องเป็น border-collapse: separate (กฎเดียวกับ .data-table ข้อ 13.1)
      แบบ collapse ตัวตารางเป็นคนวาดเส้น ไม่ใช่เซลล์ → เส้นใต้หัวตารางไม่ขยับตาม และวัดจริงในเบราว์เซอร์
      เห็นตัวอักษรของแถวที่เลื่อนผ่านโผล่เป็นเศษ ๆ เหนือหัวตาราง (นอกกรอบเลื่อน)
      แต่ separate ทำให้เส้นระหว่างเซลล์ซ้อนเป็น 2px → ทุกเซลล์วาดแค่ขอบล่าง/ขวา
      ขอบนอกสุดใช้กรอบของ .sv-scroll แทน
   ⚠️ อยู่ใน min-width เท่านั้น — บนมือถือตารางนี้เป็นการ์ด หัวตารางถูกซ่อน (ข้อ 9) */
@media (min-width: 768px) {
    .sv-scroll {
        max-height: calc(100vh - 8rem);
        overflow: auto;
    }
    .sv-table {
        border-collapse: separate;
        border-spacing: 0;
    }
    .sv-table th,
    .sv-table td {
        border-top-width: 0;
        border-left-width: 0;
    }
    .sv-table tr > :last-child { border-right-width: 0; }
    .sv-table tfoot tr:last-child td { border-bottom-width: 0; }
    .sv-table thead th {
        position: sticky;
        top: 0;
        z-index: 2;
        background-color: #f9fbfd;              /* slate-50/80 บนพื้นขาว */
    }
    .sv-table thead th.sv-branch-col { background-color: #f6fafe; }   /* sky-50/40 */
    .sv-table thead th#svHeadQty { background-color: #f5f8fb; }       /* slate-100/50 */
    .sv-table thead th.sv-head-cost { background-color: #fcfcf2; }    /* yellow-50/50 */
    .sv-table thead th.sv-head-value { background-color: #f5fcfb; }   /* teal-50/50 */
}

/* ==========================================================================
   9. แท็บมูลค่าสต็อก บนมือถือ — พับตารางเป็น "การ์ดรายสินค้า"
   ==========================================================================
   ตารางนี้กว้าง 8 คอลัมน์ (ชื่อ + สาขาละคอลัมน์ + คงเหลือรวม + ต้นทุน + มูลค่า)
   บนจอ 375px จึงต้องเลื่อนแนวนอนตลอดจนอ่านไม่ออก (เจ้าของแจ้ง 2026-09-21)

   ⚠️⚠️ ทำด้วย CSS ล้วน ใช้ DOM และเส้นทางการวาดชุดเดิมทั้งหมด
        ห้ามแตกเป็นโค้ดวาดสองก๊อปเด็ดขาด — บทเรียนข้อ 16 ของ CLAUDE.md
        (แก้ก๊อปเดียวแล้วอีกก๊อปเป็นบั๊กค้างไว้ โดยไม่มีอะไรเตือน)

   ⚠️⚠️ ทุกกฎที่เปลี่ยน display ของ tr ต้องมี :not(.hidden) เสมอ
        เพราะ applyStockValueFilter() ซ่อนแถวด้วย tr.classList.toggle('hidden')
        และ `.sv-table tbody tr` (0,1,2) มีความจำเพาะสูงกว่า `.hidden` (0,1,0)
        ถ้าลืม → ช่องค้นหาใช้ไม่ได้เลยบนมือถือ ซึ่งเป็นจอที่ต้องใช้มันที่สุด
        (สินค้า 84 รายการ เลื่อนหาบนจอเล็กยากกว่าบนเดสก์ท็อปมาก) · test_SV8

   ⚠️ เดสก์ท็อปต้องเหมือนเดิมเป๊ะ — ทุกกฎจึงอยู่ใน @media max-width เท่านั้น */

@media (max-width: 767px) {

    /* หัวตารางถูกซ่อน → ชื่อคอลัมน์ย้ายไปเป็นป้ายบนแต่ละบรรทัดของการ์ดแทน (data-label) */
    .sv-table thead {
        display: none;
    }

    .sv-table,
    .sv-table tbody,
    .sv-table tfoot {
        display: block;
        width: 100%;
    }

    /* การ์ดหนึ่งใบ = หนึ่งแถวสินค้า */
    .sv-table tbody tr:not(.hidden),
    .sv-table tfoot tr:not(.hidden) {
        display: flex;
        flex-wrap: wrap;
        align-items: stretch;

        margin-bottom: 0.625rem;
        border: 1px solid #e2e8f0;
        border-radius: 0.75rem;
        overflow: hidden;
        background: #fff;
    }

    /* เส้นตารางเดิมไม่มีความหมายแล้วเมื่อเป็นการ์ด — กรอบการ์ดทำหน้าที่แทน */
    .sv-table td {
        border: 0;
    }

    /* ชื่อสินค้า = หัวการ์ด กินเต็มความกว้าง */
    .sv-table tbody td:first-child,
    .sv-table tfoot td:first-child {
        flex: 0 0 100%;
        padding: 0.5rem 0.875rem;
        font-weight: 700;
        background: #f8fafc;
        border-bottom: 1px solid #e2e8f0;
        text-align: left;
    }

    /* ช่องตัวเลขทุกช่อง: ป้ายซ้าย–ค่าขวา อ่านได้โดยไม่ต้องมีหัวตาราง */
    /* ⚠️ ใช้ flex-end + margin-right: auto ที่ป้าย ไม่ใช่ space-between
       ให้เป็นแพตเทิร์นเดียวกับตารางคีย์ข้อมูล (ข้อ 10) ซึ่ง "ต้อง" ใช้แบบนี้
       เพราะป้ายที่นั่นหายไปชั่วคราวตอน .save-ok ทำงาน — ผลบนจอเหมือนกันเป๊ะ
       แต่กันไม่ให้ใครก๊อปแบบ space-between ไปใช้กับตารางที่คีย์ได้ */
    .sv-table td[data-label] {
        display: flex;
        align-items: baseline;
        justify-content: flex-end;
        gap: 0.5rem;
        padding: 0.4375rem 0.875rem;
        text-align: right;
    }

    .sv-table td[data-label]::before {
        content: attr(data-label);
        margin-right: auto;
        font-size: 0.6875rem;
        font-weight: 600;
        color: #64748b;
        text-align: left;
        white-space: nowrap;
    }

    /* ทุกช่องตัวเลขวางบรรทัดละช่อง กินเต็มความกว้าง (เจ้าของสั่ง 2026-09-21)
       ⚠️ เคยลองแบบ 2 ช่องต่อบรรทัดแล้ว — ชื่อสาขาไทยยาว ("สาขาตลาดนานาเจริญ")
          พอบีบเหลือครึ่งจอ ป้ายกับตัวเลขเบียดกันจนกวาดตาไล่ทีละสาขาไม่สะดวก
       ⚠️ ต้องแสดงครบทุกสาขาแม้ยอดเป็น 0 เหมือนบนเดสก์ท็อป (CLAUDE.md ข้อ 8)
          ไม่งั้นจำนวนบรรทัดจะขยับตามวันที่ แล้วกวาดตาเทียบข้ามวันไม่ได้ */
    .sv-table tbody td[data-label],
    .sv-table tfoot td[data-label] {
        flex: 0 0 100%;
        max-width: 100%;
    }

    .sv-table td.sv-branch-col {
        border-bottom: 1px solid #f1f5f9;
    }

    /* แถวข้อความ (กำลังโหลด / ไม่มีสินค้า / โหลดไม่สำเร็จ) ไม่ใช่การ์ด */
    .sv-table td[colspan] {
        flex: 0 0 100%;
        text-align: center;
        background: transparent;
        border-bottom: 0;
        font-weight: 400;
    }

    /* แถวรวมท้ายตาราง — ⚠️ เป็นยอดของ "ทุกสินค้า" เสมอ ไม่ขยับตามตัวกรอง (ข้อ 8)
       จึงต้องเด่นและแยกออกจากการ์ดสินค้าให้ชัด ไม่งั้นจะถูกอ่านเป็นสินค้าอีกตัวหนึ่ง */
    .sv-table tfoot tr:not(.hidden) {
        border-color: #99f6e4;
        margin-top: 0.25rem;
    }

    .sv-table tfoot td:first-child {
        background: #f0fdfa;
        border-bottom-color: #99f6e4;
    }

    /* กรอบเลื่อนแนวนอนไม่ต้องทำงานแล้วเมื่อเป็นการ์ด
       ⚠️ ต้องถอดกรอบด้วย ไม่งั้นจะเห็นกรอบซ้อนกรอบการ์ดอีกชั้น */
    .sv-scroll {
        overflow-x: visible;
        border: 0;
        border-radius: 0;
    }
}

/* ==========================================================================
   10. ตารางคีย์ข้อมูล บนมือถือ — พับเป็น "การ์ดรายแถว"
   ==========================================================================
   ครอบ 4 ตาราง: สต็อกรายวัน + ขนส่ง ของทั้งหน้าพนักงานและหน้าแอดมิน
   (เจ้าของสั่ง 2026-09-21) ตารางสต็อกกว้าง 10 คอลัมน์ ขนส่ง 5-6 คอลัมน์
   บนจอ 375px ต้องเลื่อนแนวนอนตลอดจนคีย์ผิดช่องได้ง่าย

   ⚠️⚠️ CSS ล้วน ใช้ DOM และเส้นทางการวาดชุดเดิม ห้ามแตกเป็นโค้ดวาดเฉพาะมือถือ
        JS ของสองหน้าเป็นคนละก๊อปอยู่แล้ว (CLAUDE.md ข้อ 16) เพิ่มทางวาดอีก
        = กลายเป็นสี่ทางที่ต้องแก้พร้อมกันทุกครั้ง

   ⚠️ เดสก์ท็อปห้ามขยับแม้แต่นิดเดียว — ตารางนี้คืองานหลักของทั้งระบบ
      ทุกกฎจึงอยู่ใน @media max-width เท่านั้น (test_M8) */

@media (max-width: 767px) {

    /* หัวตารางถูกซ่อน → ชื่อคอลัมน์ย้ายไปเป็นป้ายบนแต่ละบรรทัดผ่าน data-label */
    .data-table thead {
        display: none;
    }

    .data-table,
    .data-table tbody,
    .data-table tfoot {
        display: block;
        width: 100%;
    }

    /* ⚠️⚠️ ต้องมี :not(.row-filtered-out) เสมอ — ตัวกรองสินค้าซ่อนแถวด้วยคลาสนั้น
       และ `.data-table tbody tr` (0,1,2) มีความจำเพาะสูงกว่า `tr.row-filtered-out` (0,1,1)
       ลืมแล้ว = ปุ่มกรอง 5 ตัวกับช่องค้นหาตายทั้งหมดบนมือถือ โดยยอดรวมยังถูกอยู่
       จึงไม่มีอะไรฟ้องว่าผิด (test_M5) */
    .data-table tbody tr:not(.row-filtered-out),
    .data-table tfoot tr:not(.row-filtered-out) {
        display: flex;
        flex-wrap: wrap;
        align-items: center;

        margin-bottom: 0.625rem;
        border: 1px solid #e2e8f0;
        border-radius: 0.75rem;
        overflow: hidden;
        background: #fff;
    }

    /* เส้นตารางเดิมหมดความหมายเมื่อเป็นการ์ด — กรอบการ์ดทำหน้าที่แทน
       ⚠️ ต้องเขียนเป็น `table.data-table tbody td` ให้ความจำเพาะเท่ากฎเดิม (0,1,3)
          ของเดิมเขียนสั้นเป็น `.data-table td` (0,1,1) ซึ่ง **แพ้** กฎ
          `table.data-table tbody td { border-bottom }` และ `tfoot td { border-top }`
          จึงเป็นกฎตายที่ไม่ได้ล้างอะไรเลย แถวยอดรวมเลยได้เส้นบนซ้อนกับเส้นล่างของเรา
       กฎที่เติมเส้นให้แต่ละกลุ่มด้านล่างจำเพาะกว่า (0,2,1 ขึ้นไป) จึงยังชนะตามเดิม */
    table.data-table tbody td,
    table.data-table tfoot td {
        border: 0;
    }

    /* ⚠️⚠️ ปิด sticky ของแถวยอดรวม — ห้ามลบ
       `table.data-table tfoot td { position: sticky; bottom: 0 }` (ข้อ 7) ยังมีผลบนมือถือ
       ที่การ์ดยอดรวมไม่พังคือ **บังเอิญ**: overflow:hidden ของการ์ดสร้าง scroll container
       ใหม่ขึ้นมาบัง ช่องจึงไม่มีที่ให้เลื่อน
       วัดจริงตอนเลื่อนให้การ์ดคร่อมขอบล่างจอ (375x812):
         มี overflow:hidden   → 693, 731, 771, 811, 851, 891   เรียงปกติ
         ถอด overflow:hidden  → 693, 731, 771, 772, 772, 772   นำเข้า/นำออก/คงเหลือ ซ้อนกัน
       → ใครถอด overflow:hidden วันหลังจะพังทันทีโดยไม่มีอะไรเตือน (test_M11) */
    table.data-table tfoot td {
        position: static;
    }

    /* ⚠️⚠️ แถบสีสถานะต้องย้ายมาอยู่ที่ "ทั้งใบการ์ด"
       ของเดิมวาดที่ td:first-child ซึ่งบนการ์ดคือช่องติ๊ก "ตรวจ" เล็ก ๆ บนหัวการ์ด
       จะเหลือขีดสั้น ๆ แค่มุมบนซ้าย ซึ่งอ่านไม่ออกว่าแถวนี้มีความเคลื่อนไหวหรือตรวจแล้ว
       ⚠️ ลำดับ เหลือง → เขียว → ขนส่ง ห้ามสลับ ด้วยเหตุผลเดียวกับข้อ 6
          (ความจำเพาะเท่ากัน ตัวที่ประกาศทีหลังชนะ)
       กฎเดิมที่ td:first-child ยังอยู่ ไม่ได้ถูกถอด — วาดสีเดียวกันตำแหน่งเดียวกัน จึงทับสนิท */
    .data-table tbody tr.row-highlighted:not(.row-filtered-out) {
        box-shadow: inset var(--rail-width) 0 0 var(--status-moved-rail);
    }
    .data-table tbody tr.row-checked:not(.row-filtered-out) {
        box-shadow: inset var(--rail-width) 0 0 var(--status-checked-rail);
    }
    .data-table tbody tr.ship-row-checked:not(.row-filtered-out) {
        box-shadow: inset var(--rail-width) 0 0 var(--status-checked-rail);
    }
    /* แถวที่กำลังคีย์อยู่ถ่างแถบให้กว้างขึ้น เหมือนกฎเดิมของเดสก์ท็อป (ข้อ 5)
       ตั้งผ่านตัวแปร ไม่เขียน box-shadow ใหม่ จึงไม่ต้องรู้ว่าแถวนี้เป็นสีอะไร */
    .data-table tbody tr:focus-within {
        --rail-width: 5px;
    }

    /* หัวการ์ด = เซลล์ที่ "ไม่มี" data-label (ช่องติ๊กตรวจ + ชื่อสินค้า + ปุ่มลบของตารางขนส่ง)
       รวมกันเป็นแถบเดียวด้านบน */
    /* ⚠️ ทุกกฎของ "เซลล์หัวการ์ด" ต้องยกเว้น [colspan] ให้หมด
       แถว colspan คือแบนเนอร์เตือนและแถว placeholder ซึ่งไม่ใช่หัวการ์ด
       ยกเว้นทีละกฎแบบนี้อ่านง่ายกว่าการไปแข่งความจำเพาะกับมัน (test_M12) */
    .data-table tbody td:not([data-label]):not([colspan]),
    .data-table tfoot td:not([data-label]):not([colspan]) {
        padding: 0.5rem 0.75rem;
        background: #f8fafc;
        text-align: left;
    }
    .data-table tbody tr:not(.row-filtered-out) > td:not([data-label]):not([colspan]):first-child,
    .data-table tfoot tr > td:not([data-label]):not([colspan]):first-child {
        border-bottom: 1px solid #e2e8f0;
    }
    /* ชื่อสินค้ากินพื้นที่ที่เหลือของแถบหัว ส่วนช่องติ๊กและปุ่มลบกว้างเท่าที่ต้องใช้ */
    .data-table td.product-name {
        flex: 1;
        font-weight: 700;
        border-bottom: 1px solid #e2e8f0;
    }
    .data-table tbody td:not([data-label]):not(.product-name):not([colspan]),
    .data-table tfoot td:not([data-label]):not(.product-name):not([colspan]) {
        flex: 0 0 auto;
        border-bottom: 1px solid #e2e8f0;
    }

    /* ป้าย "ยอดรวมประจำวัน" ของแถวยอดรวมต้องกินเต็มแถบหัวการ์ด
       (แถวนี้ไม่มีชื่อสินค้า ช่องแรกเป็น "-" ที่ถูกซ่อนด้วย card-omit) */
    .data-table tfoot td:not([data-label]):not(.card-omit) {
        flex: 1;
    }

    /* ⚠️ เซลล์ท้ายแถวที่ไม่มีป้าย = ปุ่มจัดการของตารางขนส่ง
       ต้องเป็นแถบท้ายการ์ดเต็มความกว้าง ไม่ใช่กล่องเล็ก ๆ ลอยอยู่มุมซ้าย
       (ตารางสต็อกไม่โดนกฎนี้ เพราะเซลล์สุดท้ายคือ "ผู้อัปเดต" ซึ่งมี data-label) */
    .data-table tbody td:not([data-label]):not([colspan]):last-child {
        flex: 1 1 100%;
        text-align: right;
        border-bottom: 0;
        border-top: 1px solid #e2e8f0;
    }

    /* บรรทัดข้อมูล: ป้ายซ้าย–ค่าขวา บรรทัดละช่อง */
    .data-table td[data-label] {
        display: flex;
        align-items: center;
        /* ⚠️⚠️ ห้ามใช้ space-between — ดูเหตุผลที่กฎป้ายด้านล่าง */
        justify-content: flex-end;
        gap: 0.5rem;
        flex: 0 0 100%;
        max-width: 100%;
        padding: 0.25rem 0.75rem;
        text-align: right;
        border-bottom: 1px solid #f1f5f9;
        min-height: 2.5rem;
    }

    /* ⚠️⚠️ ป้ายต้องหลบให้ .save-ok — pseudo-element ของเซลล์ถูกจองไปแล้วทั้งคู่
       markSaved() ใส่ .save-ok ที่ <td> แล้วข้อ 6.1 ใช้ ::before ทำคลื่นเขียว
       และ ::after ทำเส้นยืนยัน ถ้าป้ายไม่หลบ จะได้อย่างใดอย่างหนึ่ง:
         - ป้ายชนะ → คลื่นยืนยันตายบนมือถือ (สัญญาณไม่ครบ ขัดข้อ 16)
         - ป้ายถูกดันเป็น absolute ไปทับกลางเซลล์ 1.6 วินาทีทุกครั้งที่บันทึก
       ⚠️⚠️ และเพราะป้ายหายไปชั่วคราว การจัดแนวจึงต้องเป็น flex-end + margin-right: auto
          ห้ามใช้ space-between เด็ดขาด ไม่งั้นตอนป้ายหลบ ตัวเลขจะกระโดดไปซ้าย
          แล้วกระโดดกลับ ทุกครั้งที่บันทึก (test_M6) */
    .data-table td[data-label]:not(.save-ok)::before {
        content: attr(data-label);
        margin-right: auto;
        font-size: 0.6875rem;
        font-weight: 600;
        color: #64748b;
        text-align: left;
        white-space: nowrap;
    }

    /* เซลล์ที่ไม่มีความหมายบนการ์ด (ช่อง "-" ของแถวยอดรวม) */
    .data-table td.card-omit {
        display: none;
    }

    /* ⚠️ แถวเต็มความกว้าง = แบนเนอร์ "โหลดข้อมูลไม่สำเร็จ" (test_F2) และแถว placeholder
       ถ้าไม่มีกฎนี้จะตกไปโดนกฎของ "เซลล์หัวการ์ด" คือพื้นเทาและชิดขวา
       (วัดจริงก่อนแก้: text-align = right · background = rgb(248,250,252))
       ข้อความยังอ่านออกก็จริง แต่เป็นแบนเนอร์ที่สำคัญที่สุดในตาราง ไม่ควรดูเหมือนหัวคอลัมน์
       ⚠️ ต้องชนะกฎ :last-child ด้านบนด้วย จึงเขียนเป็น tbody/tfoot ให้จำเพาะกว่า */
    .data-table tbody td[colspan],
    .data-table tfoot td[colspan] {
        flex: 1 1 100%;
        max-width: 100%;
        text-align: center;
        background: transparent;
        border: 0;
        font-weight: 400;
    }

    /* ⚠️⚠️ ย้ำการซ่อนคอลัมน์ "ขายกล่อง (ขนส่ง)" — ห้ามลบ
       กฎเดิม `.box-sales-col, .box-sales-qty { display: none }` (ข้อ 7) จำเพาะแค่ (0,1,0)
       ส่วนกฎการ์ด `.data-table td[data-label]` เป็น (0,2,1) จึงทับแล้วคอลัมน์โผล่กลับมา
       เจอจริงตอนทำข้อ 10 นี้: บนจอ 375px ขึ้นบรรทัด "ขายกล่อง (ขนส่ง)" ทั้งในการ์ดสินค้า
       และการ์ดยอดรวม ทั้งที่เลิกใช้ช่องนี้ไปแล้ว → พนักงานจะคีย์ลงไปแล้วยอดถูกหัก
       ออกจากคงเหลือเหมือนยอดขาย (ข้อ 4) โดยไม่มีใครตั้งใจ
       ⚠️ คีย์บอร์ดแบบ Excel ก็พึ่ง display:none ตัวนี้ (visibleEditableCells กรองด้วย
          offsetParent) ถ้าคอลัมน์โผล่ ลำดับการเลื่อนช่องบนมือถือจะไม่ตรงกับเดสก์ท็อป
       เขียนเป็น tbody/tfoot เพื่อให้จำเพาะ (0,2,2) ชนะแน่นอน ไม่ต้องพึ่งลำดับการประกาศ */
    .data-table tbody td.box-sales-qty,
    .data-table tbody td.box-sales-col,
    .data-table tfoot td.box-sales-qty,
    .data-table tfoot td.box-sales-col {
        display: none;
    }

    /* ช่องนำเข้า/นำออก — ปุ่ม ⋯ ยังลอยด้วย position:absolute ตามกฎข้อ 8
       จึงต้องเว้นที่ทางขวาให้ ไม่งั้นตัวเลขจะมุดอยู่ใต้ปุ่ม */
    .data-table td[data-label] > .qty-wrap {
        flex: 1;
        min-width: 0;
    }
    .data-table td[data-label] > .qty-wrap > .editable {
        text-align: right;
        padding-right: 2.75rem;
    }

    /* ช่องคีย์ข้อความ/ตัวเลขต้องกว้างพอให้นิ้วจิ้มโดน และไม่ยุบจนมองไม่เห็นตอนว่าง */
    .data-table td[data-label].editable {
        min-width: 0;
    }
    .data-table td[data-label] > input,
    .data-table td[data-label] > select {
        flex: 1;
        min-width: 0;
        max-width: 62%;
    }

    /* ⚠️⚠️ แถบค้นหา/กรองสินค้าต้องตรึงไว้บนสุด
       พอถอดกรอบเลื่อนออก (กฎถัดไป) หน้าคีย์สต็อกยาว **28,256px ≈ 33 จอ**
       ที่สินค้า 84 รายการ (วัดจริงบนจอ 375px · การ์ดละ 318px)
       ตัวกรองคือทางเดียวที่รับมือกับความยาวขนาดนั้นได้ ถ้าไม่ตรึงไว้ ผู้ใช้ต้องลากกลับ
       ขึ้นไปบนสุดทุกครั้งที่จะเปลี่ยนคำค้น = ตัวช่วยที่เอื้อมไม่ถึงตอนที่ต้องใช้จริง
       ⚠️ พื้นต้องทึบ ไม่งั้นการ์ดที่เลื่อนอยู่ข้างล่างจะทะลุขึ้นมาซ้อน (เหตุผลเดียวกับ
          หัวตารางที่ตรึงไว้ในข้อ 13.1)
       ⚠️ z-index ต้องมากกว่า 3 ซึ่งเป็นค่าของหัวตาราง/แถวยอดรวมที่ตรึงไว้ */
    .grid-toolbar {
        position: sticky;
        top: 0;
        z-index: 5;
        background: #fff;
        padding-top: 0.5rem;
        padding-bottom: 0.5rem;
        box-shadow: 0 6px 8px -8px rgba(15, 23, 42, 0.35);
    }

    /* ⚠️ ปุ่มกรอง 5 ตัวห้ามตกบรรทัดบนมือถือ — ให้เลื่อนแนวนอนในแถวตัวเองแทน
       แถบนี้ถูกตรึงไว้บนสุด ทุกบรรทัดที่เพิ่มคือพื้นที่จอที่หายไปถาวร
       วัดจริงก่อนแก้: ปุ่มตกเป็น 2 บรรทัด ทำให้แถบสูง 197px = 24% ของจอ 812px */
    .filter-btn-row {
        flex-wrap: nowrap;
        overflow-x: auto;
        scrollbar-width: none;
        width: 100%;
    }
    .filter-btn-row::-webkit-scrollbar {
        display: none;
    }
    .filter-btn-row > * {
        flex: 0 0 auto;
    }

    /* ⚠️ ปุ่มสลับความหนาแน่นไม่มีความหมายบนการ์ด จึงซ่อนไว้เพื่อคืนพื้นที่ให้แถบที่ตรึงอยู่
       ความสูงของบรรทัดบนการ์ดถูกคุมด้วย min-height ของเซลล์ ไม่ใช่ --row-pad-y
       (ดูกฎถัดไปที่ปรับช่องนำเข้า/นำออกให้สูงเท่าบรรทัดอื่น) */
    .grid-toolbar .density-seg {
        display: none;
    }

    /* ช่องนำเข้า/นำออกใช้ padding ของ span ข้างในคุมความสูงตามความหนาแน่น (ข้อ 13.1)
       บนการ์ดต้องปิดทิ้ง ไม่งั้นสองบรรทัดนี้จะสูงไม่เท่าบรรทัดอื่นในใบเดียวกัน */
    .data-table td[data-label] > .qty-wrap > .editable {
        padding-top: 0;
        padding-bottom: 0;
    }

    /* กรอบเลื่อนของตารางสต็อกไม่ต้องทำงานบนมือถือ — ให้เลื่อนทั้งหน้าแทน
       ⚠️ การ์ดสูงกว่าแถวมาก กรอบเลื่อนซ้อนในจอเล็กจะกลายเป็นสองชั้นที่เลื่อนยาก
       หัวตารางถูกซ่อนแล้วจึงไม่มี sticky ให้ค้ำอยู่ดี */
    .table-scroll {
        max-height: none;
        min-height: 0;
        overflow-x: visible;
        overflow-y: visible;
        border: 0;
        border-radius: 0;
    }
}
