/* ==========================================================================
   YourAccount — Design System v2
   A financial ledger, not a marketing site: numbers lead, chrome recedes.

   Identity notes
   - Ground:  warm-neutral paper in light, deep pine in dark. Never pure grey —
              the neutrals carry a trace of the brand hue so the palette reads
              as one decision rather than a default.
   - Accent:  a single deep pine green. Everything else earns attention through
              weight and space, not colour.
   - Numbers: tabular figures, tighter tracking, larger optical size. In an
              accounting app the money column is the interface.
   ========================================================================== */

:root {
  /* ---- neutral ramp (warm paper → ink) ---- */
  --n-0:  #FFFFFF;
  --n-25: #FBFBF9;
  --n-50: #F5F6F3;
  --n-100:#EDEFEA;
  --n-150:#E6EAE4;
  --n-200:#D6DCD5;
  --n-300:#B9C3B8;
  --n-400:#8E9B8D;
  --n-500:#6B7A6B;
  /* The lightest neutral that still clears 4.5:1 on this palette's papers —
     anything above it is a decoration, never text. */
  --n-550:#5F6D61;
  --n-600:#4F5C50;
  --n-700:#3A453B;
  --n-800:#26302A;
  --n-900:#1C2622;
  --n-950:#0C1310;

  /* ---- brand ---- */
  --g-50:  #E9F4EE;
  --g-100: #CFE7DA;
  --g-200: #A3D2B9;
  --g-300: #6DB995;
  --g-400: #3C9C74;
  --g-500: #17805A;
  --g-600: #0E6549;
  --g-700: #0B503B;
  --g-800: #093F2F;
  --g-900: #072F24;

  /* ---- semantic (independent of the accent) ---- */
  --ok-500:     #15794F;
  /* เข้มขึ้นจาก #A9720B เพราะคู่ --warn บน --warn-bg ได้แค่ 3.73:1 (ต้อง 4.5)
     ป้าย "ค้างครึ่งทาง" เป็นป้ายเดียวที่ยังใช้สีเหลืองหลังจัดความหมายสีใหม่
     ถ้าอ่านไม่ชัด ก็ไม่เหลืออะไรที่สีเหลืองทำได้เลย · ตอนนี้ 4.95:1 */
  --warn-500:   #8F6007;
  --danger-500: #B3261E;
  --info-500:   #17607F;

  /* ---- assigned roles: LIGHT ---- */
  --bg:           var(--n-50);
  --bg-accent:    #F2F6F2;
  --surface:      var(--n-0);
  --surface-2:    var(--n-50);
  --surface-3:    var(--n-100);
  --surface-sunk: var(--n-100);
  --sidebar-bg:   var(--n-0);
  --topbar-bg:    rgba(255,255,255,.95);
  /* V4: ตัวควบคุมของเบราว์เซอร์ (ช่องติ๊ก · select · แถบเลื่อน · ปฏิทิน) ต้องรู้ว่าอยู่ธีมไหน — เดิมไม่มีเลยทั้งไฟล์
     ธีมมืดจึงได้ช่องติ๊กสีขาว · ต้องมีครบ 3 บล็อก (เกต qa_design_system ข้อ 16) */
  color-scheme: light;

  --ink:       var(--n-900);
  --ink-2:     var(--n-700);
  --muted:     var(--n-600);
  /* V4: รวมเป็นสีรองสีเดียว — --faint กับ --muted ต่างกันจนตาแยกไม่ออก (มืด 6.02 กับ 6.58)
     สองสีรองที่แยกไม่ออกคือเสียงรบกวน ไม่ใช่ลำดับชั้น · ชื่อ --faint คงไว้ให้กฎเดิมใช้ต่อได้ */
  --faint:     var(--muted);
  --line:      var(--n-150);
  --line-strong: var(--n-200);
  /* V4: ขอบของ "พื้นผิว" (การ์ด · การ์ดสรุป · กรอบตาราง) รับหน้าที่แทนเงาที่ถอดออก — ต้องเห็นจริง ≥1.3:1 กับพื้นหน้า
     (เดิม --line บนพื้น = 1.12:1 ที่ตาเห็นขอบได้เพราะเงาล้วน ๆ) · --line ยังเป็นเส้นแบ่งแถวที่บางกว่า
     ไม่งั้นทุกแถวของตารางเข้มขึ้นเป็นเสียงรบกวน */
  --card-line:   #CFD6CE;
  /* ขอบช่องกรอก — WCAG 1.4.11: ขอบที่บอกว่า "ตรงนี้พิมพ์ได้" ต้อง ≥3:1 กับพื้นข้าง ๆ ทุกสถานะ (ไม่มีข้อยกเว้น 2:1)
     ช่องกรอกพื้นเดียวกับการ์ด (--surface) — ขอบจึงเป็นสิ่งเดียวที่บอกขอบเขตช่อง
     (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · C32) เดิม #A8B3A7 = 2.17 บน --surface · 2.00 บน --bg
     พัก  #808D7F: 3.48 บน --surface #FFF · 3.21 บน --surface-2/--bg #F5F6F3 · 3.01 บน --surface-3 #EDEFEA
     ชี้  --n-500 #6B7A6B: 4.54 · 4.19 · 3.92 (เข้มกว่าตอนพักให้เห็นว่าชี้อยู่) */
  --ctl-line:    #808D7F;
  --ctl-line-hi: var(--n-500);

  --brand:      var(--g-600);
  /* Bootstrap ใช้ค่า rgb แยกสำหรับยูทิลิตี้ที่ต้องมีความโปร่ง (--bs-primary-rgb)
     CSS แปลง hex เป็น rgb เองไม่ได้ จึงต้องเขียนคู่ไว้ ตรงกับ --g-600 #0E6549 */
  --brand-rgb:  14, 101, 73;
  --brand-ink:  var(--g-700);
  --brand-hi:   var(--g-500);
  --brand-soft: var(--g-50);
  --brand-softer: #F3F9F5;
  --on-brand:   #FFFFFF;
  /* ⚠️ ตัวอักษรบนพื้นสีอันตราย — โหมดมืดต้องเป็นสีเข้ม ไม่ใช่ขาว
     เพราะ --danger ของโหมดมืดเป็นสีอ่อน (#E8837A) เพื่อให้อ่านออกบนพื้นเข้ม
     ขาวบนสีนั้นเหลือคอนทราสต์ราว 2.6:1 ซึ่งต่ำกว่าเกณฑ์ชัดเจน */
  --on-danger:  #FFFFFF;

  --ok: var(--ok-500);         --ok-bg: #E6F4EC;      --ok-line: #BFE3CE;
  --warn: var(--warn-500);     --warn-bg: #FCF3E0;    --warn-line: #EFDCB2;
  --danger: var(--danger-500); --danger-bg: #FCEBE9;  --danger-line: #F2CFCB;
  --info: var(--info-500);     --info-bg: #E7F1F6;    --info-line: #C3DCE8;

  /* ---- สีหมวด (ดีไซน์ V3 "เขียวมีชีวิต") — ภาษาที่สาม: "ที่อยู่ในระบบ" ----
     สีสถานะ = "มีข่าว" · สีกราฟ --c-* = "ก้อนข้อมูล" · สีหมวด --m-* = "อยู่โมดูลไหน"
     ห้ามสามภาษาปนกัน: --m-* อยู่ได้เฉพาะ tile (สี่เหลี่ยมมน มีแต่ไอคอน)
     พื้นจาง และเส้นขอบ — ห้ามลงตัวหนังสือ ห้ามลงตัวเลขเงิน ห้ามทำปุ่ม
     รูปทรงแบกความหมายคู่กับ hue: tile ≠ chip (แคปซูลตัวหนังสือ) เสมอ
     จึงไม่มีวันอ่าน mtile-sell เขียว เป็น chip-ok เขียว (คำตัดสินกรรมการ 31 ส.ค. 2569)
     ที่มาของหมวด: คีย์ 'mod' ใน MenuService::sections() — เกต qa_icons คุมให้ครบ
     ทุกคู่วัดแล้ว ≥5.4:1 ทั้งสองธีม · ประกาศครบสามบล็อกเสมอ (บทเรียน 31) */
  --m-sell:   var(--g-600); --m-sell-bg:   var(--g-50); --m-sell-line:   var(--g-100);
  --m-buy:    #8F3F13;      --m-buy-bg:    #FBEDE4;     --m-buy-line:    #F0D5C4;
  --m-master: #0F6B6D;      --m-master-bg: #E4F2F2;     --m-master-line: #BFE0E0;
  --m-money:  #1F5A99;      --m-money-bg:  #E1EFFB;     --m-money-line:  #BFDCF4;
  --m-books:  #3D4BA0;      --m-books-bg:  #EDECFB;     --m-books-line:  #D6D4F2;
  --m-hub:    #74338A;      --m-hub-bg:    #F5EAF8;     --m-hub-line:    #E2CDEB;
  --m-sys:    #4A5550;      --m-sys-bg:    #EEF1EF;     --m-sys-line:    #DCE2DE;
  /* แถบไล่สีของ meter/onboarding — เขียว→เทียล (identity เรา ไม่ใช่ม่วงฟ้า HR) */
  --grad-brand: linear-gradient(90deg, var(--g-600), #158584);

  /* ---- elevation: two-part shadows read as real light ---- */
  --sh-xs: 0 1px 1px rgba(16,32,24,.04), 0 1px 2px rgba(16,32,24,.05);
  /* เงาบอกว่าตารางยังเลื่อนต่อได้ทางไหน — ดู .table-wrap */
  --scroll-shade: rgba(16,32,24,.16);
  --sh-sm: 0 1px 2px rgba(16,32,24,.05), 0 2px 6px -1px rgba(16,32,24,.06);
  --sh-md: 0 2px 4px rgba(16,32,24,.05), 0 8px 20px -6px rgba(16,32,24,.10);
  --sh-lg: 0 4px 8px rgba(16,32,24,.06), 0 20px 40px -12px rgba(16,32,24,.18);
  --ring:  0 0 0 3px color-mix(in srgb, var(--brand) 22%, transparent);

  /* ---- shape ---- */
  /* สี่ขั้น + ทรงเม็ดยา — ยุบจากเจ็ดขั้นเมื่อ 28 ส.ค. 2569
     --r-xl (22px) ใช้จุดเดียวทั้งไฟล์ (.modal) และ --r-2xs (3px) ใช้สามจุด
     ขั้นที่มีผู้ใช้คนเดียวไม่ใช่ระบบ เป็นข้อยกเว้นที่ทำให้จอเดียวมีมุมหลายแบบ */
  --r-xs: 6px; --r-sm: 9px; --r: 12px; --r-lg: 16px; --r-pill: 999px;

  /* ขนาดของไอคอนแบบฟอนต์ (Bootstrap Icons)
     ⚠️ เป็นสเกลของ "รูป" ไม่ใช่ของ "ตัวอักษร" จึงไม่ใช่ --t-* และไม่นับเป็นขั้นที่ 9
     ต้องเป็น token เพราะทุกค่าที่ตาเห็นต้องมาจาก token (เกต qa_design_system) */
  /* สีเชิงหมวด — ใช้แยกก้อนข้อมูลในกราฟวงกลม/แท่งซ้อน
     ⚠️ ห้ามใช้สีสถานะ (ok/warn/danger) มาแยกหมวด
     สีสถานะ "แปลว่าอะไรบางอย่าง" — หมวดที่บังเอิญได้สีแดง
     จะดูเหมือนมีปัญหาทั้งที่มันแค่เป็นหมวดที่แปด
     ⚠️ เรียงให้สีที่อยู่ติดกันในลำดับ ต่างกันมากพอ เพราะกราฟวงกลม
     วางก้อนติดกันตามลำดับนี้เป๊ะ ๆ */
  /* ⚠️ ชุดนี้คือชุดของ "โหมดสว่าง" — เข้มกว่าที่ตาคาดไว้โดยตั้งใจ
     สีสดใสบนพื้นขาวได้คอนทราสต์แค่ 2.2–2.7 : 1 ซึ่งต่ำกว่าเกณฑ์ 3.0
     ของวัตถุที่ไม่ใช่ตัวหนังสือ ชิ้นโดนัทจะจางจนแทบมองไม่เห็น
     (เกต qa_contrast จับได้ทันทีที่เติมคู่สีกราฟเข้าไป) */
  --c-1: #1E8B62; --c-2: #2C6FB5; --c-3: #966B0C; --c-4: #8B3FA0;
  --c-5: #1F8A8C; --c-6: #B0521F; --c-7: #4F5FB8; --c-8: #5E7030;

  --ic-14: 14px; --ic-16: 16px; --ic-18: 18px;
  --ic-20: 20px; --ic-24: 24px; --ic-32: 32px;

  /* ---- rhythm (4px base) ---- */
  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px; --s-5: 20px;
  --s-6: 24px; --s-7: 32px; --s-8: 40px; --s-9: 56px;

  /* ---- structure ---- */
  --sidebar-w: 264px;
  --topbar-h: 64px;
  /* ความสูงของแถบเมนูล่างบนมือถือ — แถบอื่นที่เกาะขอบล่างต้องหลบให้ตัวนี้
     ประกาศเป็น token เพราะมีสองที่ต้องใช้ค่าเดียวกัน ถ้าแยกกันจะเพี้ยนเงียบ ๆ */
  --bottom-nav-h: 56px;
  /* Accounting screens are tables: the wider the viewport, the more columns and
     rows a person can compare at once. Content fills the width; only prose-like
     pages opt into a narrower measure with .content-narrow.

     ⚠️ เหตุผลข้างบนเป็นจริงกับ "ตาราง" ไม่ใช่กับทั้งหน้า
     ของเดิมไม่มีเพดานเลย บนจอ 2560px การ์ดสถิติใบหนึ่งจึงกว้างเกิน 600px
     แล้วตัวเลขลอยอยู่กลางที่ว่าง ซึ่งเป็นสาเหตุอันดับหนึ่งที่ซอฟต์แวร์ธุรกิจ
     ดูถูกบนจอใหญ่

     1720px กว้างพอให้ตารางสิบคอลัมน์ยังกางได้เต็ม แต่ไม่ปล่อยจนไร้ขอบเขต
     ⚠️ จอ 1440-1600px ซึ่งเป็นจอส่วนใหญ่ จะไม่มีอะไรเปลี่ยนเลยแม้แต่พิกเซลเดียว
     เพราะเนื้อหายังแคบกว่าเพดานอยู่แล้ว */
  /* ⚠️ 1720px เล็กเกินไปสำหรับจอที่คนใช้จริงวันนี้ — วัดแล้ว
     จอ 2560×1440 ทิ้งพื้นที่ขวา 294px · จอ 3440×1440 ทิ้ง 734px
     เนื้อหาชนเพดานแล้วหยุดโต ทั้งที่ยังมีที่ว่างให้ใช้อีกมาก

     2600px ทำให้จอถึง ~2900px กางเต็มโดยไม่เหลือขอบ (จอ 2560 ใช้ได้ 2264px
     หลังหักแถบเมนู 264 และระยะขอบ 32) และยังมีเพดานอยู่บนจออัลตราไวด์
     ซึ่งการปล่อยเต็มจะทำให้สายตาต้องกวาดจากชื่อลูกค้าไปยอดเงินไกลเกินไป

     ⚠️ ข้อความยาวไม่ได้พึ่งเพดานนี้ มันมีเพดานของตัวเองเป็นหน่วย ch อยู่แล้ว
     (.page-head .sub = 78ch · .page-explain = 76ch · .empty p = 44ch)
     การขยายเพดานนี้จึงไม่ทำให้บรรทัดข้อความยาวขึ้นแม้แต่ตัวเดียว */
  --content-max: 2400px;

  /* ---- motion ---- */
  --ease: cubic-bezier(.32,.72,.28,1);
  --fast: 140ms; --mid: 200ms;

  /* ---- ระดับจาง: ไม่มีโทเคน opacity โดยตั้งใจ (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · C17)
     เดิม --o-dim .55 ทำตัวอักษรเหลือ 3.62:1 — ของที่ "ตายแล้ว" แต่ยังต้องอ่านได้ ให้หรี่ด้วยสี --muted ---- */

  /* ---- type ---- */
  --font-display: "Anuphan", "IBM Plex Sans Thai", "Noto Sans Thai", system-ui, sans-serif;
  --font-body: "IBM Plex Sans Thai", "Anuphan", "Noto Sans Thai", "Leelawadee UI", system-ui, -apple-system, sans-serif;
  --font-num: "IBM Plex Sans Thai", ui-monospace, "SF Mono", Consolas, monospace;

  /* ---- ขั้นบันไดตัวอักษร: 8 ขั้น ไม่มีขั้นที่ 9 ----
     ก่อนหน้านี้ไฟล์นี้มี font-size ที่ต่างกัน 57 ค่า และหน้าเดียวเรนเดอร์ออกมา
     27 ขนาด (10.56 / 10.752 / 10.88 / 11.008 px ...) เศษทศนิยมพวกนั้นเกิดจาก
     em ซ้อน em ไม่มีใครตั้งใจเขียน
     ค่าอย่าง .845rem กับ .855rem ต่างกัน 0.16px — ตาแยกไม่ออก แต่รวมกันแล้ว
     ทำให้ทั้งระบบไม่นิ่ง เพราะไม่มีจังหวะให้ตาจับ
     ⚠️ ห้ามเพิ่มขั้นใหม่ และห้ามใช้ em กับ font-size — เกต qa_design_system จับ */
  /* ---- ดีไซน์ V4 (24 ก.ย. 2569): ทั้งสเกลขยับขึ้น — วัดจากหน้าจริงแล้วตัวหนังสือ 12px เป็นหนึ่งในห้าของทุกตัวอักษรบนจอ
     ลูกค้าหลักอายุ 40+ อ่านทั้งวัน · ขั้น "หัวข้อ" (17 · 19 · 22 · 28 · 32) ห่างกันอย่างน้อย 2px เพื่อให้ตาแยกได้
     ⚠️ 13/14/15 คือ "ขนาดตามบทบาท" (ชิป/หัวตาราง · ป้าย/คำอธิบาย · เนื้อความ) ไม่ใช่ขั้นลำดับชั้น —
        ห้ามใช้สามขนาดนี้สร้างลำดับชั้น ใช้น้ำหนักและสีแทน (ต่างกัน 1px ตาแยกไม่ออก)
     ⚠️ กระดาษตรึงสเกลเดิมไว้ใน @media print — ใบที่สั่งพิมพ์จากจอต้องไม่เปลี่ยนขนาด */
  --t-hero:    2rem;        /* 32px — พาดหัวหน้าเข้าสู่ระบบเท่านั้น */
  --t-display: 1.75rem;     /* 28px — ยอดเงินใหญ่บนการ์ด KPI */
  --t-title:   1.375rem;    /* 22px — ชื่อหน้า */
  --t-heading: 1.1875rem;   /* 19px — หัวข้อระดับหน้า · ตัวเลขที่เป็นข้อความ */
  --t-section: 1.0625rem;   /* 17px — หัวการ์ด */
  --t-body:    .9375rem;    /* 15px — เนื้อความ ช่องกรอก ตัวเลขในตาราง */
  --t-label:   .875rem;     /* 14px — ป้ายกำกับช่อง · คำอธิบายใต้ช่อง · บรรทัดรองของการ์ด */
  --t-micro:   .8125rem;    /* 13px — พื้นล่าง: หัวตาราง · ตรา · คีย์ลัด — ห้ามมีอะไรเล็กกว่านี้ (ชิปขึ้นไป --t-label แล้ว · V4 C1) */
  /* 16px — ใช้เฉพาะช่องกรอกบนอุปกรณ์สัมผัส ไม่ใช่ขั้นของสเกลข้อความ
     Safari บน iOS ซูมทั้งหน้าทันทีที่แตะช่องที่เล็กกว่า 16px แล้วไม่ซูมกลับให้ */
  --t-field:   1rem;

  /* ความสูงตัวควบคุมในแถวเดียวกัน (ดีไซน์ V4 W7) — แถบตัวกรองเคยมีช่อง 40 · ปุ่ม 36 · ปุ่มเล็ก 32 ในแถวเดียว
     ใช้ใน .filters และ .page-head-actions เท่านั้น (ปุ่มในแถวตาราง/การ์ดคงขนาดเดิม) · จอสัมผัสยกเป็น 44 */
  --ctl-h:     40px;

  /*
   * ขอบปลอดภัยของจอบาก — เดิมมีแต่ด้านล่าง (4 จุด) ไม่มีซ้าย/ขวาเลย
   * ไอโฟนจอบากตะแคงแนวนอน (ท่าที่คนใช้ดูตารางกว้างของโปรแกรมบัญชี)
   * บากย้ายไปอยู่ด้านข้าง แล้วปุ่มเปิดเมนูกับปุ่มบนแถบบนตกไปอยู่ใต้บาก
   * ⚠️ ทำเป็นตัวแปรเดียวให้ทุกชิ้นอ้าง ไม่ใช่ไล่เติม env() ทีละที่ —
   *    ชิ้นที่หกที่ใครเพิ่มพรุ่งนี้จะได้ขอบปลอดภัยฟรี (บทเรียนข้อ 85)
   */
  --edge-l: env(safe-area-inset-left, 0px);
  --edge-r: env(safe-area-inset-right, 0px);
  --edge-b: env(safe-area-inset-bottom, 0px);
  --t-print:   11.5pt;      /* กระดาษใช้หน่วยของกระดาษ */

  /* ---- รัศมีขั้นเล็กสุด สำหรับขีดและแถบบาง ---- */
}
/* ⚠️ :root ของเรามีพร็อพเพอร์ตี้จริงแล้ว (color-scheme · V4) จึง "ชน" กับ :root ของ Bootstrap
   ซึ่งตั้ง scroll-behavior: smooth ใน media ของมัน — ประกาศเป็นของเราเองด้วยค่าเดียวกับที่ใช้อยู่จริง
   (ลิงก์ในหน้า #id เลื่อนนุ่ม · คนที่ขอลดการเคลื่อนไหวได้การกระโดดตามเดิม) ไม่ใช่ปล่อยให้รั่วมาเงียบ ๆ */
@media (prefers-reduced-motion: no-preference) {
  :root { scroll-behavior: smooth; }
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg:           #0B120F;
    --bg-accent:    #0E1712;
    --surface:      #131C17;
    --surface-2:    #18231D;
    --surface-3:    #1E2A23;
    --surface-sunk: #0F1713;
    --sidebar-bg:   #101914;
    --topbar-bg:    rgba(16,25,20,.95);

    --ink:   #E8EFEA;
    --ink-2: #C4D0C8;
    --muted: #93A398;
    --faint: #93A398;
    --line:  #232F28;
    --line-strong: #33443A;
    --card-line: #2E3B33;
    /* C32 (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569) เดิม #46594D = 2.32 บน --surface · 2.16 บน --surface-2
       พัก #62786A: 3.66 บน --surface #131C17 · 3.40 บน --surface-2 #18231D · 3.99 บน --bg #0B120F · 3.13 บน --surface-3
       ชี้ #728879: 4.57 · 4.25 · 4.98 · 3.91 (เดิม #556B5C = 2.81 บน --surface-2 ต่ำกว่าที่คอมเมนต์อ้าง)
       ⚠️ ค่าต้องตรงกับบล็อก [data-theme="dark"] ข้างล่างเสมอ */
    --ctl-line: #62786A;
    --ctl-line-hi: #728879;

    --brand:      #45B588;
    --brand-rgb:  69, 181, 136;
    --brand-ink:  #63CB9F;
    --brand-hi:   #7BD8B0;
    --brand-soft: #14291F;
    --brand-softer: #101E18;
    --on-brand:   #04231A;
    --on-danger:  #2C1917;

    --ok: #58C48D;      --ok-bg: #12291D;      --ok-line: #1E4630;
    /* สีหมวดของกราฟ ชุดโหมดมืด — สว่างกว่าชุดสว่าง เพราะพื้นเป็นสีเข้ม
       ⚠️ ต้องมีทั้งบล็อกนี้และบล็อก [data-theme="dark"] ข้างล่าง
       ถ้ามีแค่บล็อกนี้ คนที่กดเลือกโหมดมืดเองจะได้สีชุดสว่างบนพื้นดำ */
    --c-1: #45B588; --c-2: #6BA9E6; --c-3: #D9AC4A; --c-4: #C57ED4;
    --c-5: #5FCFD0; --c-6: #E28C62; --c-7: #93A2EA; --c-8: #A8B877;
    --warn: #D8A64A;    --warn-bg: #2A2213;    --warn-line: #4A3A1B;
    --danger: #E8837A;  --danger-bg: #2C1917;  --danger-line: #4C2723;
    --info: #6FB6D6;    --info-bg: #12242C;    --info-line: #1E3D4A;

    /* สีหมวดชุดมืด — ink กลับขั้วเป็น tint สว่าง (ระดับเดียวกับ --c-* มืด)
       ⚠️ ต้องมีทั้งบล็อกนี้และบล็อก [data-theme="dark"] ข้างล่าง เหมือน --c-* */
    /* ⚠️ ink มืดจงใจ "ไม่เท่ากับ" hex ของ --c-* มืดสักตัว — สีหมวดกับสีกราฟ
       เป็นคนละภาษา โดนัท c-2 ต้องไม่สีเดียวกับ tile money บนจอเดียวกัน */
    --m-sell:   #63CB9F; --m-sell-bg:   #12291D; --m-sell-line:   #1E4630;
    --m-buy:    #E9A17C; --m-buy-bg:    #2E1B12; --m-buy-line:    #4A2E1D;
    --m-master: #67D4C3; --m-master-bg: #0F2A26; --m-master-line: #1C4640;
    --m-money:  #7FC4EC; --m-money-bg:  #102433; --m-money-line:  #1D3D54;
    --m-books:  #A2A8F0; --m-books-bg:  #191D36; --m-books-line:  #2C3258;
    --m-hub:    #D093DE; --m-hub-bg:    #2A1830; --m-hub-line:    #452A4E;
    --m-sys:    #9FAFA4; --m-sys-bg:    #1C2620; --m-sys-line:    #2E3B33;
    --grad-brand: linear-gradient(90deg, #63CB9F, #4FBFB0);

    --sh-xs: 0 1px 2px rgba(0,0,0,.45);
  --scroll-shade: rgba(0,0,0,.42);
    --sh-sm: 0 1px 3px rgba(0,0,0,.5), 0 2px 8px -2px rgba(0,0,0,.4);
    --sh-md: 0 3px 10px rgba(0,0,0,.5), 0 10px 26px -8px rgba(0,0,0,.55);
    --sh-lg: 0 8px 24px rgba(0,0,0,.55), 0 24px 56px -16px rgba(0,0,0,.7);
  }
}

:root[data-theme="dark"] {
  --bg: #0B120F; --bg-accent: #0E1712; --surface: #131C17; --surface-2: #18231D;
  --surface-3: #1E2A23; --surface-sunk: #0F1713; --sidebar-bg: #101914; --topbar-bg: rgba(16,25,20,.95);
  --ink: #E8EFEA; --ink-2: #C4D0C8; --muted: #93A398; --faint: #93A398;
  --line: #232F28; --line-strong: #33443A;
  --card-line: #2E3B33; --ctl-line: #62786A; --ctl-line-hi: #728879; /* C32 — อัตราส่วนอยู่ในบล็อก prefers ข้างบน */
  color-scheme: dark;
  --brand: #45B588; --brand-ink: #63CB9F; --brand-hi: #7BD8B0;
  --brand-rgb:  69, 181, 136;
  --brand-soft: #14291F; --brand-softer: #101E18; --on-brand: #04231A; --on-danger: #2C1917;
  --ok: #58C48D; --ok-bg: #12291D; --ok-line: #1E4630;
  /* สีหมวดของกราฟ ชุดโหมดมืด — บล็อกนี้คือกรณี "ผู้ใช้กดเลือกมืดเอง"
     ต้องมีค่าเดียวกับบล็อก prefers-color-scheme ข้างบนเสมอ */
  --c-1: #45B588; --c-2: #6BA9E6; --c-3: #D9AC4A; --c-4: #C57ED4;
  --c-5: #5FCFD0; --c-6: #E28C62; --c-7: #93A2EA; --c-8: #A8B877;
  --warn: #D8A64A; --warn-bg: #2A2213; --warn-line: #4A3A1B;
  --danger: #E8837A; --danger-bg: #2C1917; --danger-line: #4C2723;
  --info: #6FB6D6; --info-bg: #12242C; --info-line: #1E3D4A;
  /* สีหมวดชุดมืด — กรณี "ผู้ใช้กดเลือกมืดเอง" ค่าเดียวกับบล็อก prefers ข้างบนเสมอ */
  --m-sell:   #63CB9F; --m-sell-bg:   #12291D; --m-sell-line:   #1E4630;
  --m-buy:    #E9A17C; --m-buy-bg:    #2E1B12; --m-buy-line:    #4A2E1D;
  --m-master: #67D4C3; --m-master-bg: #0F2A26; --m-master-line: #1C4640;
  --m-money:  #7FC4EC; --m-money-bg:  #102433; --m-money-line:  #1D3D54;
  --m-books:  #A2A8F0; --m-books-bg:  #191D36; --m-books-line:  #2C3258;
  --m-hub:    #D093DE; --m-hub-bg:    #2A1830; --m-hub-line:    #452A4E;
  --m-sys:    #9FAFA4; --m-sys-bg:    #1C2620; --m-sys-line:    #2E3B33;
  --grad-brand: linear-gradient(90deg, #63CB9F, #4FBFB0);
  --sh-xs: 0 1px 2px rgba(0,0,0,.45);
  --scroll-shade: rgba(0,0,0,.42);
  --sh-sm: 0 1px 3px rgba(0,0,0,.5), 0 2px 8px -2px rgba(0,0,0,.4);
  --sh-md: 0 3px 10px rgba(0,0,0,.5), 0 10px 26px -8px rgba(0,0,0,.55);
  --sh-lg: 0 8px 24px rgba(0,0,0,.55), 0 24px 56px -16px rgba(0,0,0,.7);
}

/* ==========================================================================
   Base
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }

body {
  margin: 0;
  font-family: var(--font-body);
  /* V4: เนื้อความตั้งต้น = --t-body (15) · เดิมใช้ --t-section ซึ่งตอนนี้คือหัวการ์ด 17px */
  font-size: var(--t-body);
  line-height: 1.6;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  -webkit-text-size-adjust: 100%;
  font-feature-settings: "kern" 1;
}

a { color: var(--brand-ink); text-decoration: none; transition: color var(--fast) var(--ease); }
@media (hover: hover) { a:hover { color: var(--brand-hi); text-decoration: none; } }

/* ==========================================================================
   โล่กันชน Bootstrap — ตรึงทุกพร็อพเพอร์ตี้ที่เคยรั่วมาจาก bootstrap.min.css
   ==========================================================================

   ⚠️ ที่มา (บั๊กจริง 31 ส.ค. 2569): เจ้าของกด "+ ลูกค้าใหม่" แล้วไม่มีอะไร
   ขึ้นเลย — Bootstrap มี .modal { display:none } ของตัวเอง และการชนะของ
   CSS เป็น "รายพร็อพเพอร์ตี้": พร็อพเพอร์ตี้ที่ app.css ไม่เคยประกาศ
   จะรั่วจาก Bootstrap มาเงียบ ๆ ทั้งที่ไม่มีบรรทัดไหนของเราผิด

   บล็อกนี้ประกาศ "ทุกพร็อพเพอร์ตี้ที่ Bootstrap ตั้งบน selector ที่ชื่อชน
   กับของเรา" ด้วยค่าที่ระบบเรนเดอร์อยู่จริงวันนี้ — ไม่ใช่การออกแบบใหม่
   แค่ย้ายการตัดสินใจจาก "ค่าปริยายที่ถูก Bootstrap ขโมย" มาเป็นของเราเอง

   ⚠️ ต้องอยู่ "ต้นไฟล์" ก่อนกฎคอมโพเนนต์ทั้งหมด — กฎจริงข้างล่างจะชนะ
   โล่เสมอเมื่อประกาศพร็อพเพอร์ตี้เดียวกัน (น้ำหนักเท่ากัน ตัวหลังชนะ)
   · เกต qa:bootstrap-collision เทียบสองไฟล์ทุกครั้งที่รัน qa:all
   · เพิ่มคลาสใหม่ที่ชื่อไปชนกับ Bootstrap เมื่อไร เกตจะบอกเองว่าขาดตัวไหน */

body { font-weight: 400; text-align: start; }
h4 { font-size: var(--t-heading); }
button { margin: 0; line-height: inherit; text-transform: none; -webkit-appearance: button; border-radius: 0; }
input, select { margin: 0; line-height: inherit; }
select { text-transform: none; }
textarea { margin: 0; line-height: inherit; resize: vertical; }
kbd { background-color: var(--surface-3); }

.nav-link { text-decoration: none; background: none; border: 0; }
.card { position: relative; display: flex; flex-direction: column; min-width: 0; height: auto; color: var(--ink); }
.card-body { flex: 1 1 auto; color: inherit; }

/* ปุ่ม: ค่าของ Bootstrap เป็น var(--bs-btn-*) ที่ bridge ไม่ได้ตั้ง
   = โมฆะ = unset อยู่แล้ววันนี้ — ประกาศค่า unset ให้เป็นลายลักษณ์อักษร */
.btn { color: inherit; text-align: center; vertical-align: middle; background-color: transparent;
       -webkit-user-select: none; -moz-user-select: none; user-select: none; }
@media (hover: hover) { .btn:hover { color: inherit; background-color: transparent; border-color: currentColor; } }
.btn:disabled, .btn.disabled { color: inherit; background-color: transparent; border-color: currentColor; pointer-events: none; }

.input-group { position: relative; flex-wrap: wrap; align-items: stretch; width: 100%; }
.alert { position: relative; color: inherit; }

/* ⚠️ รอบสอง (31 ส.ค. 2569 เย็น): การรั่วไม่ได้มาทาง "ชื่อชนตรง ๆ" เสมอไป
   Bootstrap ยังยิงกฎ "เข้าไปในเขต" ของคลาสเราผ่านตัวเลือกลูกได้ด้วย
   ซึ่งเกตรอบแรกมองไม่เห็นเพราะเทียบชื่อ selector แบบตรงตัว

   สองตัวนี้เป็นบั๊กที่วัดได้บนจอจริง ไม่ใช่ทฤษฎี:
   1) .row>* { width:100% } — .totals ของเราใช้ .row เป็นแถวป้าย/ตัวเลข
      ลูกทุกตัวจึงกว้างเต็มแถว "ตกบรรทัด" ทุกแถวใน 12 ไฟล์ (ฟอร์มเอกสาร
      ทุกใบ, การ์ดภาษี, นำเข้าข้อมูล, หน้าเอกสารสาธารณะ) — เจ้าของเห็น
      หน้าสร้างใบกำกับภาษีแล้วบอกว่า "ยังไม่สวยเลย" คืออาการของข้อนี้
      · .row ยังได้ margin ติดลบซ้ายขวาอีก 24px ทำให้กล่องล้นกรอบการ์ด
   2) .card-link+.card-link { margin-left:1.25rem } — การ์ดใบที่ 2 เป็นต้นไป
      ในกริดเดียวกันแคบกว่าใบแรก 16px (วัดได้ 343 vs 327) ระยะห่างเพี้ยน
      ทั้งหน้าตั้งค่า แต่ "ดูเกือบถูก" จนไม่มีใครสังเกตมาตลอด */
.row { --bs-gutter-x: 0; --bs-gutter-y: 0; display: flex; flex-wrap: nowrap; margin-top: 0; margin-right: 0; margin-left: 0; }
.row > * { flex-shrink: initial; width: auto; max-width: none; padding-right: 0; padding-left: 0; margin-top: 0; }
.card-link + .card-link { margin-left: 0; }

/* ── โล่ชั้นที่สาม: selector แบบ "คลาสซ้อนคลาส" ─────────────────────────────
 *
 * ⚠️ สองชั้นแรกจับไม่ถึงแบบนี้
 *   ชั้นที่ 1 = ชื่อคลาสตรงกัน (.btn .card .modal)
 *   ชั้นที่ 2 = กฎที่ยิงเข้าลูกเรา (.row > * · .card-link + .card-link)
 *   ชั้นที่ 3 = **.a.b** — เราประกาศ .a ไว้ และใช้ .b เป็นสถานะ
 *              แต่ไม่เคยประกาศ ".a.b" คู่กัน น้ำหนัก selector ของ Bootstrap
 *              (สองคลาส) จึงสูงกว่าของเรา (คลาสเดียว) และชนะทุกครั้ง
 *
 * วัดจริง 1 ก.ย. 2569: รายการภาษาที่เลือกอยู่ในเมนูผู้ใช้ มีพื้นหลัง
 * rgb(13, 110, 253) — สีน้ำเงินของ Bootstrap ที่ไม่มีอยู่ในจานสีของระบบเลย
 * เพราะ `.dropdown-item.active` ของ Bootstrap ตั้ง background-color ไว้
 * ส่วนของเราประกาศแค่ `.dropdown-item` กับ `.dropdown-item:hover`
 *
 * ⚠️ ต้องประกาศ "สีของเรา" ทับ ไม่ใช่แค่ตั้ง background: none
 * รายการที่เลือกอยู่ต้องดูออกว่าเลือกอยู่ — ไม่งั้นแก้สีเพี้ยนแล้วได้ของที่แย่กว่า
 */
.dropdown-item.active {
  background: var(--brand-soft);
  color: var(--brand-ink);
  text-decoration: none;
}

.dropdown-item.disabled {
  color: var(--muted);
  background: none;
  pointer-events: none;
}

.nav-link.disabled { color: var(--muted); pointer-events: none; cursor: default; }

/* .show ของ Bootstrap ใช้กับเมนู/โมดัลที่ระบบเราคุมเอง — ห้ามให้มันตั้งค่าอะไรให้ */
.btn.active,
.btn.show { background: inherit; color: inherit; border-color: inherit; }

.nav-link.show { background: none; color: inherit; }
/* ปุ่มในช่องกรอกแบบมีของต่อท้าย — BS ยกมันขึ้นชั้น z-index:2 ซึ่งเคยพอ
   ทำให้ปุ่มลอยเหนือแผงที่ควรอยู่บนกว่า ตรึงค่าที่ระบบเราใช้จริงไว้ */
.input-group .btn { position: relative; z-index: auto; }
.input-group .btn:focus { z-index: 2; }

.modal-backdrop { width: auto; height: auto; }
.modal { overflow-x: hidden; overflow-y: hidden; }

/* ⚠️ Bootstrap ตั้ง `.modal.show .modal-dialog { transform: none }`
   ระบบเราไม่ได้ใช้ .modal-dialog และคุมการเปิด/ปิดเองด้วย .modal-backdrop.show
   แต่ต้องประกาศคู่นี้ไว้ให้เห็น ไม่งั้นวันที่มีคนเผลอใช้โครงของ Bootstrap
   transform ของมันจะโผล่มาแล้วโมดัลจะขยับผิดที่โดยไม่มีใครรู้ว่าใครสั่ง
   (โมดัลเคยหายทั้งแอปมาแล้วครั้งหนึ่งจากการชนกันแบบนี้) */
.modal.show { display: block; }

/* ── โล่ชั้นที่สี่: `.คลาส:pseudo` ที่กินสไตล์สถานะของเรา ────────────────────
 *
 * ⚠️ อันนี้ร้ายที่สุดในสี่ชั้น เพราะมันทำให้ **คนใช้คีย์บอร์ดมองไม่เห็นว่าตัวเองอยู่ตรงไหน**
 *
 * app.css ประกาศ `:focus-visible { outline: 2px solid var(--brand) }` ซึ่งเป็น
 * pseudo-class เปล่า ๆ (น้ำหนัก 0,1,0) ส่วน Bootstrap ประกาศ
 * `.btn:focus-visible { outline: 0 }` (น้ำหนัก 0,2,0) ซึ่งชนะทุกครั้ง
 *
 * วัดจริง 1 ก.ย. 2569: โฟกัสปุ่มด้วยคีย์บอร์ดแล้วได้ `outline: 0px none`
 * ทั้งที่ element ตรงกับ :focus-visible จริง — ปุ่มทุกตัวในระบบไม่มีวงแหวนโฟกัสเลย
 * (WCAG 2.4.7) และนี่คือส่วนหนึ่งของคำว่า "ใช้งานยุ่งยาก" ที่เจ้าของบอกไว้แต่แรก
 *
 * ⚠️ ต้องประกาศ "วงแหวนของเรา" ทับ ไม่ใช่แค่ปลด outline:0 ของเขา
 * และต้องครอบทุกคลาสที่ Bootstrap เขียนกฎ :focus/:focus-visible ให้
 */
.btn:focus-visible,
.nav-link:focus-visible,
.dropdown-item:focus-visible,
.form-control:focus-visible,
.form-select:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  box-shadow: none;
}

/* สีตอนโฟกัสต้องไม่กระโดดไปเป็นสีของ Bootstrap — ให้เหมือนตอน hover ของเราเอง */
.btn:focus-visible { color: inherit; background-color: inherit; border-color: inherit; }

.dropdown-item:focus,
.dropdown-item:active {
  background: var(--surface-2);
  color: var(--ink);
  text-decoration: none;
}

.dropdown-item:disabled { color: var(--muted); background: none; pointer-events: none; }

.nav-link:focus { color: inherit; }
.nav-link:disabled { color: var(--muted); pointer-events: none; cursor: default; }

.is-invalid:focus {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 22%, transparent);
}

/* ⚠️ Bootstrap ใส่ "ไอคอนพื้นหลัง + padding ขวา" ให้ .form-control.is-invalid
   ซึ่งจะทำให้ช่องกรอกที่ผิดพลาดมีรูปตกใจของ Bootstrap โผล่มาทับดีไซน์ของเรา
   และ padding ที่เพิ่มจะดันข้อความในช่องจนตัวเลขไม่ตรงคอลัมน์
   ระบบเราสื่อความผิดพลาดด้วยกรอบสีแดง + ข้อความใต้ช่อง (.invalid-msg) อยู่แล้ว */
/* สถานะปิดใช้งานและ hover ของช่องกรอก — ต้องเป็นของเรา ไม่ใช่ของ Bootstrap
   ช่องที่ปิดใช้งานต้อง "ดูปิด" ด้วยพื้นหลังจม ไม่ใช่แค่สีจาง
   เพราะคนอายุมากแยกสีจางกับสีปกติบนจอสว่างไม่ออก */
.form-control:disabled,
.form-select:disabled {
  background-color: var(--surface-sunk);
  color: var(--muted);
  border-color: var(--line);
  opacity: 1;
}

@media (hover: hover) { .form-control:hover { border-color: var(--line-strong); } }

/* .is-invalid ใช้กับช่องติ๊กไม่ได้ในระบบนี้ — ประกาศไว้กันค่าของ Bootstrap โผล่ */
.is-invalid:checked { background-color: var(--danger); border-color: var(--danger); }

.form-control.is-invalid,
.form-select.is-invalid {
  border-color: var(--danger);
  background-image: none;
  background-repeat: no-repeat;
  background-position: right center;
  background-size: 0 0;
  padding-right: var(--s-3);
}
.modal-body { position: relative; flex: 1 1 auto; }

.dropdown-menu { margin: 0; font-size: var(--t-body); color: var(--ink); text-align: start; list-style: none; }
.dropdown-item { clear: both; font-weight: 400; text-decoration: none; white-space: nowrap; }
@media (hover: hover) { .dropdown-item:hover { background-color: transparent; } }
.dropdown-divider { overflow: hidden; border-top: 1px solid var(--line); opacity: 1; }

/* ฟอร์มออกจากระบบในเมนูดรอปดาวน์
   ⚠️ display: contents เพื่อให้ปุ่มข้างในอยู่ในลำดับ/การจัดวางเดิมของเมนู
   ถ้าปล่อยให้ฟอร์มเป็นกล่องของตัวเอง รายการสุดท้ายจะหลุดแถวและกินระยะเพิ่ม
   (ออกจากระบบต้องเป็น POST + csrf — ดูเหตุผลที่ app/Config/Routes.php) */
.dropdown-form { display: contents; }
.dropdown-form .dropdown-item { width: 100%; text-align: start; background: none; border: 0; cursor: pointer; font: inherit; }

/* ⚠️ ชั้นที่ห้าของการรั่ว: `.คลาสเรา:not(.คลาสเขา)` (1 ก.ย. 2569)

   `.toast:not(.show) { display: none }` ของ Bootstrap มีน้ำหนัก 0,2,0
   ส่วน `.toast` ของเรามี 0,1,0 — Bootstrap จึงชนะเสมอไม่ว่าไฟล์ไหนโหลดทีหลัง
   ผลคือ **toast ทุกใบในแอปนี้ display: none มาตลอด** วัดบนจอจริงได้ 0×0

   ที่ทำให้ไม่มีใครสังเกต: ข้อความยังอยู่ใน DOM ครบทุกตัวอักษร
   การทดสอบที่อ่าน textContent จึงผ่านหมด — รวมถึงของผมเองรอบก่อนหน้านี้
   สิ่งที่หายคือ "คนเห็น" ไม่ใช่ "ข้อมูล"

   และมันกลบข้อความสำคัญที่สุดของแอป: forms.js ถอด .alert ออกจากหน้า
   แล้วส่งต่อให้ toast — เมื่อ toast มองไม่เห็น การกดบันทึกทุกครั้งจึงเงียบสนิท
   ทั้ง "บันทึกแล้ว" และ "ผิดพลาด"

   โล่ชั้น 1–4 มองไม่เห็น เพราะเทียบเฉพาะ selector ที่เราประกาศไว้ตรง ๆ
   ตัวนี้ต้องตอบด้วย selector รูปเดียวกันเป๊ะ ๆ ถึงจะมีน้ำหนักเท่ากันแล้วชนะด้วยลำดับ */
.toast:not(.show) { display: flex; }

/* ตรงข้ามกับ .toast ข้างบน: `.collapse` เรา **ยืม**พฤติกรรมของ Bootstrap มาใช้จริง
   (แผงตัวกรองในหน้ารายการเปิด/ปิดด้วย collapse ของ Bootstrap)
   ประกาศซ้ำไว้ที่นี่เพื่อให้เป็น "การเลือก" ไม่ใช่ "การลืม" — และเพื่อให้เกต
   ชั้นที่ห้าไม่ต้องมีรายการยกเว้น ซึ่งเป็นที่ที่บั๊กชอบไปนอน */
.collapse:not(.show) { display: none; }
/* [UAT2 F2-2] toast ไม่รับการคลิก/โฮเวอร์ทั้งใบ — คลิกทะลุไปของข้างใต้ ยกเว้นลิงก์/ปุ่มของมันเอง
   เดิมโผล่ใต้เมาส์ตรงปุ่มบันทึกมุมขวาล่าง (1440 · หน้าโปรไฟล์/ฟอร์มใบแจ้งหนี้) แล้ว: กดบันทึกซ้ำโดน toast ไม่โดนปุ่ม ·
   เมาส์ที่วางอยู่ทำให้มันหยุดนับถอยหลังและไม่หายตลอดกาล (app.js หยุดเฉพาะตอนเล็งลิงก์แล้ว) */
.toast { width: auto; pointer-events: none; }
.toast a, .toast button { pointer-events: auto; }
/* [UAT3 ผู้รีวิว] กล่องแม่ต้องไม่รับคลิกด้วย — ลูกที่ pointer-events: none ส่งจุดคลิกต่อให้ "ของที่อยู่ใต้" ซึ่งก็คือ .toast-host
   (fixed · z-index 100 · กรอบครอบ toast พอดี · บนจอ ≤480 กว้างเต็มแถบ) ⇒ คลิกปุ่มบันทึกใต้ toast ไปโดนกล่องแม่แทนตลอดอายุ toast
   ฉบับแรกใส่ที่ .toast อย่างเดียว แล้วโพรบนับ "ไม่ใช่ตัว toast" ว่าทะลุ ⇒ เขียวทั้งที่ยังบัง (บทเรียนข้อ 30) */
.toast-host { pointer-events: none; }
.pagination { list-style: none; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  margin: 0 0 var(--s-2);
  font-weight: 600;
  line-height: 1.28;
  /* ภาษาไทย: ติดลบเกิน -.01em วรรณยุกต์ชนกัน (เกต qa_design_system ข้อ 14) */
  letter-spacing: -.01em;
  color: var(--ink);
  text-wrap: balance;
}
/* V4: ชื่อหน้า = --t-title (22) · --t-display (28) สงวนให้ตัวเลขใหญ่ของการ์ดสรุป */
h1 { font-size: var(--t-title); }
h2 { font-size: var(--t-heading); }
h3 { font-size: var(--t-heading); }
p { margin: 0 0 var(--s-3); }

::selection { background: color-mix(in srgb, var(--brand) 26%, transparent); }

:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

.ic { flex: 0 0 auto; vertical-align: -.15em; }

/* Money and any aligned digits. Tabular + tightened so columns lock up. */
.num, .money, td.num, .tabnum {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
  letter-spacing: -.01em;
}

.small { font-size: var(--t-body); }
/* V4: <small> ของ Bootstrap = .875em ของตัวแม่ ⇒ ในข้อความ 14px ได้ 12.25px ต่ำกว่าพื้นล่าง — ผูกกับโทเคนตรง ๆ */
small { font-size: var(--t-label); }
.text-muted-2 { color: var(--muted); }
.eyebrow {
  font-size: var(--t-micro); font-weight: 600;
  text-transform: uppercase; color: var(--faint);
}

/* ==========================================================================
   Shell
   ========================================================================== */
.app { display: flex; min-height: 100vh; }

.sidebar {
  width: var(--sidebar-w); flex: 0 0 var(--sidebar-w);
  background: var(--sidebar-bg);
  border-right: 1px solid var(--line);
  display: flex; flex-direction: column;
  position: fixed; inset: 0 auto 0 0; z-index: 40;
  transition: transform .3s var(--ease);
}

.sidebar-brand {
  height: var(--topbar-h); flex: 0 0 var(--topbar-h);
  display: flex; align-items: center; gap: var(--s-3);
  padding: 0 var(--s-5);
  border-bottom: 1px solid var(--line);
}
.brand-mark {
  width: 36px; height: 36px; flex: 0 0 36px;
  border-radius: var(--r);
  background: linear-gradient(145deg, var(--g-400) 0%, var(--g-700) 100%);
  color: #fff; display: grid; place-items: center;
  font-family: var(--font-display);
  font-weight: 700; font-size: var(--t-section); letter-spacing: -.03em;
}
.brand-text {
  font-family: var(--font-display);
  font-weight: 600; font-size: var(--t-heading); letter-spacing: -.02em; white-space: nowrap; line-height: 1.2;
}
.brand-text small {
  display: block; font-family: var(--font-body);
  font-size: var(--t-micro); font-weight: 500; color: var(--faint);
  margin-top: 1px;
  /* ข้อความไทย ("บัญชีออนไลน์") ห้ามสืบทอด -.02em ของชื่อแบรนด์ภาษาอังกฤษ — วรรณยุกต์ชนกัน (แผน V4 W2 · ผู้ตรวจ V4 รอบสอง)
     เกตข้อ 14 ยกเว้น .brand-* ในฐานะโลโก้ภาษาอังกฤษ จึงมองไม่เห็นการสืบทอดนี้เอง */
  letter-spacing: 0;
}

.sidebar-nav { flex: 1 1 auto; overflow-y: auto; padding: var(--s-4) var(--s-3) var(--s-7); }
.sidebar-nav::-webkit-scrollbar { width: 5px; }
.sidebar-nav::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: var(--r-xs); }
.sidebar-nav { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }

.nav-section {
  margin: var(--s-5) 0 var(--s-2); padding: 0 var(--s-3);
  font-size: var(--t-micro); font-weight: 600;
  text-transform: uppercase; color: var(--faint);
}
.nav-section:first-child { margin-top: 0; }

.nav-link {
  position: relative;
  display: flex; align-items: center; gap: var(--s-3);
  padding: 9px var(--s-3); margin-bottom: 1px;
  border-radius: var(--r-sm);
  color: var(--ink-2); font-weight: 500; font-size: var(--t-section);
  white-space: nowrap; cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.nav-link .ic { color: var(--faint); transition: color var(--fast) var(--ease); }
@media (hover: hover) { .nav-link:hover { background: var(--surface-2); color: var(--ink); } }
@media (hover: hover) { .nav-link:hover .ic { color: var(--ink-2); } }
.nav-link.active {
  background: var(--brand-soft); color: var(--brand-ink); font-weight: 600;
}
.nav-link.active .ic { color: var(--brand); }
/* A quiet rail marks position without shouting */
.nav-link.active::before {
  /* ⚠️ ห้ามเขียน `left: -var(--s-3)` — ใส่เครื่องหมายลบหน้า var() ไม่ได้
     ของเดิมเขียนแบบนั้นแล้วมีบรรทัดสำรอง left: -12px ทับพอดีเลยไม่มีใครเห็นว่าพัง
     ถ้าวันหนึ่งมีคนลบบรรทัดสำรองออก ขีดบอกตำแหน่งจะเพี้ยนทันที */
  content: ""; position: absolute; left: calc(var(--s-3) * -1); top: 50%;
  transform: translateY(-50%);
  width: 3px; height: 18px; border-radius: 0 var(--r-xs) var(--r-xs) 0; background: var(--brand);
}
/*
 * ⚠️ ตราเป็นลิงก์ของตัวเอง จึงต้องมีกล่องครอบ (ซ้อน <a> ใน <a> ไม่ได้)
 * กล่องนี้รับหน้าที่ position: relative แทน .nav-link ซึ่งโหมดย่อ (rail) ใช้อ้างอิง
 */
.nav-row { position: relative; display: block; }
.nav-row.hidden-by-search { display: none; }

.nav-badge {
  position: absolute; right: var(--s-3); top: 50%; transform: translateY(-50%);
  background: var(--danger-bg); color: var(--danger);
  font-size: var(--t-micro); font-weight: 600; padding: 1px 7px; border-radius: var(--r-pill);
  text-decoration: none;
}
@media (hover: hover) { .nav-badge:hover { background: var(--danger); color: var(--on-danger); } }

/* ชื่อเมนูต้องไม่วิ่งไปอยู่ใต้ตรา */
.nav-row:has(.nav-badge) .nav-link { padding-right: 42px; }

/* ⚠️ ต้องเคลื่อนพร้อมแถบข้างด้วยจังหวะและเส้นโค้งเดียวกันเป๊ะ
   แถบข้างมี transition: width .22s var(--ease) อยู่แล้ว (กฎ body.rail .sidebar)
   ถ้าเนื้อหาไม่มี มันจะกระโดด 188px ในเฟรมเดียวขณะที่แถบข้างยังค่อย ๆ แคบลง
   ตาเห็นสองสิ่งที่ควรเคลื่อนพร้อมกัน เคลื่อนคนละจังหวะ = คำว่า "ขัด"
   ⚠️ ต่างกันแม้ 20ms ก็เห็น เพราะขอบของสองชิ้นจะไม่ตรงกันระหว่างทาง */
.main {
  flex: 1 1 auto; min-width: 0; margin-left: var(--sidebar-w);
  display: flex; flex-direction: column;
  transition: margin-left .22s var(--ease);
}

.topbar {
  height: var(--topbar-h); flex: 0 0 var(--topbar-h);
  background: var(--topbar-bg);
  backdrop-filter: saturate(180%) blur(12px);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--line);
  display: flex; align-items: center; gap: var(--s-3);
  padding: 0 var(--s-6); position: sticky; top: 0; z-index: 30;
}
.topbar-title {
  font-family: var(--font-display);
  font-weight: 600; font-size: var(--t-heading); letter-spacing: -.01em; margin: 0; color: var(--ink);
  line-height: 1.28; /* เดิมได้จากกฎ h1 — ป้ายนี้ไม่ใช่ h1 แล้ว (layouts/main.php) หน้าตาต้องเหมือนเดิม */
  /* A flex item will not shrink below its content unless min-width is cleared.
     Without this a long page title widens the bar past the viewport and the
     whole page scrolls sideways on a phone. */
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* V4 (W6): ชื่อหน้าบนแถบบนจางหายขณะที่ h1 ในเนื้อหน้ายังอยู่ในจอ (ux.js ติด body.h1-in-view)
   ⚠️ opacity ไม่ใช่ display — กล่องต้องคงที่ ไม่งั้นปุ่มบนแถบบนกระโดดทุกครั้งที่เลื่อนผ่าน h1
   โปรแกรมอ่านหน้าจอยังอ่านได้ (ไม่ใช่ข้อมูลลับ แค่ไม่พูดซ้ำด้วยตา) */
.topbar-title { transition: opacity var(--fast) var(--ease); }
body.h1-in-view .topbar-title { opacity: 0; }
.topbar-spacer { flex: 1 1 auto; }

.icon-btn {
  width: 40px; height: 40px; border-radius: var(--r-sm);
  display: inline-grid; place-items: center;
  background: transparent; border: 1px solid transparent; color: var(--ink-2);
  cursor: pointer;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease), color var(--fast) var(--ease);
}
@media (hover: hover) { .icon-btn:hover { background: var(--surface-2); border-color: var(--line); color: var(--ink); } }

.content {
  padding: var(--s-6) var(--s-7) var(--s-9);
  flex: 1 1 auto;
  /* ⚠️ ต้องจัดกึ่งกลาง ไม่งั้นพอมีเพดานแล้วเนื้อหาจะชิดซ้ายบนจอใหญ่
     ซึ่งดูแย่กว่าการยืดเต็มจอเสียอีก */
  width: 100%; max-width: var(--content-max); margin: 0 auto;
}
/* Reading-length pages (profile, single forms) still cap their measure. */
.content-narrow { max-width: 980px; margin: 0 auto; }

.page-head {
  display: flex; align-items: flex-start; gap: var(--s-4);
  flex-wrap: wrap; margin-bottom: var(--s-6);
}
.page-head h1 { margin: 0; }
.page-head .sub { color: var(--muted); font-size: var(--t-body); margin: var(--s-1) 0 0; max-width: 78ch; }
.page-head-actions { margin-left: auto; display: flex; gap: var(--s-2); flex-wrap: wrap; }
/* V4 (P5): ปุ่มทำลายบนหัวหน้า (ยกเลิกเอกสาร ฯลฯ) เป็นเส้นขอบสีแดง ไม่ทึบ และอยู่ท้ายแถว —
   เดิมเป็นจุดเด่นที่สุดของหน้าใบขาย ซึ่งกลับหัวกับลำดับชั้น · กล่องยืนยันยังทำงานเหมือนเดิม */
.page-head-actions .btn-danger { background: transparent; color: var(--danger); border-color: var(--danger-line); box-shadow: none; }
@media (hover: hover) { .page-head-actions .btn-danger:hover { background: var(--danger-bg); border-color: var(--danger); } }
.page-head-actions > .btn-danger, .page-head-actions > form:has(> .btn-danger) { order: 99; }
/* V4 (W8): มือถือ — ปุ่มหลักขึ้นก่อนเต็มแถว ปุ่มรองแบ่งแถวถัดไปกัน (ลำดับชั้นเห็นด้วยตา ไม่ใช่แค่สี) */
@media screen and (max-width: 560px) {
  /* (0,3,0) — ต้องชนะ .page-head-actions .btn { flex: 1 1 auto } ของบล็อก ≤860 ที่อยู่ล่างกว่า */
  .page-head .page-head-actions > .btn-primary { order: -1; flex: 1 1 100%; }
  /* ปุ่มหลักที่อยู่ในฟอร์ม (อนุมัติ · ดึงข้อมูลจาก HR) — ตัวฟอร์มคือลูกของแถบ ต้องขึ้นก่อนและเต็มแถวเหมือนกัน (ผู้ตรวจ V4) */
  .page-head .page-head-actions > form:has(> .btn-primary) { order: -1; flex: 1 1 100%; }
  .page-head .page-head-actions > form:has(> .btn-primary) > .btn-primary { width: 100%; }
}
/* ปุ่มลัดช่วงวันที่ที่ตรงกับช่วงปัจจุบัน (ux.js ใส่ aria-current ให้) — "ถูกเลือก" ต้องหน้าตาต่างจาก "กดเพื่อทำ" */
.btn[aria-current="true"] { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); font-weight: 600; }

.company-pill {
  display: inline-flex; align-items: center; gap: var(--s-2);
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r-pill); padding: 6px 14px 6px 10px;
  font-size: var(--t-body); font-weight: 500; color: var(--ink-2);
  cursor: pointer; max-width: 268px; min-width: 0;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
/* ป้ายกิจการของผู้ไม่มีสิทธิ์ settings.view เป็น <span> ที่กดไม่ได้ (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · R11)
   ⚠️ เคอร์เซอร์รูปมือ + สีตอนชี้ บนของที่กดไม่ได้ = บอกผู้ใช้ว่ากดได้ แล้วคลิกไม่เกิดอะไร (กติกาข้อ 4) */
span.company-pill { cursor: default; }
@media (hover: hover) { a.company-pill:hover, button.company-pill:hover { border-color: var(--line-strong); background: var(--surface-3); color: var(--ink); } }
.company-pill span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* V4 (W6): อักษรย่อของกิจการ — แสดงแทนชื่อที่ถูกตัดบนมือถือ/แท็บเล็ตสัมผัส (ช่วงเดียวกับแถวกระชับ)
   ชื่อเต็มอยู่ใน aria-label/title ของปุ่ม และแถบชื่อกิจการในเนื้อหน้าสำหรับผู้ใช้หลายกิจการ */
.company-initials { display: none; }
@media screen and (max-width: 860px), screen and (max-width: 1024px) and (any-pointer: coarse) {
  .company-pill > .ic:first-child,
  .company-pill > span { display: none; }
  .company-pill { padding: 4px 8px 4px 4px; gap: 4px; }
  /* ป้ายแบบลิงก์ (กิจการเดียว) ไม่มีลูกศร — ไม่ต้องเว้นขวาให้ลูกศร */
  .company-pill:not(:has(> .ic:last-child)) { padding-right: 4px; }
  .company-initials {
    display: inline-grid; place-items: center;
    width: 34px; height: 34px; border-radius: 50%;
    background: var(--brand-soft); color: var(--brand-ink);
    font-size: var(--t-label); font-weight: 600; line-height: 1;
    text-decoration: none;
  }
}
/* ⚠️ แท็บเล็ตที่จอกว้างกว่าจุดตัดมือถือก็ใช้นิ้ว (24 ก.ย. 2569) — device:sweep วัดได้ 38px ที่ 1024
   กฎ 44px เดิมอยู่ในบล็อก max-width จึงไม่ถึงแท็บเล็ต · คำถามที่ถูกคือ "ใช้นิ้วไหม" ไม่ใช่ "จอแคบไหม" */
@media (any-pointer: coarse) { .company-pill { min-height: 44px; } }

/* แผ่นรองโลโก้กิจการ — ขาวแท้ทุกธีมโดยตั้งใจ
   โลโก้ธุรกิจถูกออกแบบบนกระดาษขาว แผ่นรองที่เปลี่ยนสีตามธีมจะทำให้
   โลโก้โปร่งใส/สีเข้มหายไปทั้งตัวในโหมดมืด — นี่คือ "กระดาษ" ไม่ใช่พื้นผิว UI */
.logo-plate {
  background: var(--n-0);
  border-radius: var(--r-sm);
  padding: var(--s-1);
  object-fit: contain;
}
/* ⚠️ กฎสามบรรทัดนี้คือสิ่งเดียวที่ทำให้ชื่อกิจการยอมหด

   white-space: nowrap ทำให้ min-content ของ <span> เท่ากับความกว้างข้อความ
   ทั้งบรรทัด และ overflow: hidden ไม่ได้ลดค่านั้นลงเลย (คนละเรื่องกัน)
   <span> เป็นลูก flex ของป้าย จึงได้ min-width: auto ติดมาเป็นค่าปริยาย
   = ปฏิเสธการหดต่ำกว่าข้อความเต็ม แล้วส่งต่อความกว้างนั้นขึ้นไปให้
   .dropdown ซึ่งเป็นลูก flex ของแถบบน และได้ min-width: auto เหมือนกัน

   ผลคือที่ 390px ป้ายยืน 154px ไม่ยอมขยับ แล้วดันเมนูผู้ใช้ตกออกนอกจอ
   28px — ทุกหน้าที่มีแถบบน คือ 96 จาก 104 หน้า
   หน้าจอดูปกติทุกอย่างเพราะของที่ตกไปอยู่ "นอกจอ" ไม่ใช่ "ทับกัน"

   ⚠️ ต้องปลดทั้งสองชั้น ปลดชั้นเดียวไม่พอ — ชั้นที่ยังเหลือจะรับหน้าที่
   ปฏิเสธการหดต่อทันที (พิสูจน์ด้วยการวัดที่ 390px ไม่ใช่การอ่านโค้ด) */
.company-pill span { min-width: 0; }

/* ⚠️ กล่อง .dropdown อื่นในแถบบนบรรจุปุ่มขนาดคงที่ 44px (เกณฑ์นิ้วสัมผัส)
   ซึ่งหดไม่ได้ตามนิยาม การปล่อยให้กล่องหดจึงไม่ได้ประหยัดที่แม้แต่พิกเซลเดียว
   แต่ทำให้ปุ่มข้างในล้นกล่องออกไปทับของที่อยู่ถัดไป
   ให้กล่องเดียวที่มีอะไรให้หดจริงเป็นตัวที่หด */
.topbar .dropdown { flex-shrink: 0; }
.topbar .dd-company { min-width: 0; flex-shrink: 1; }

/* ⚠️ ป้ายต้องผูกความกว้างไว้กับกล่องของมัน ไม่ใช่ตัวเลขคงที่
   กล่องเป็น display: block ป้ายที่กว้างเกินจึงล้นออกมาเงียบ ๆ
   โดยกล่องยังรายงานความกว้างที่หดแล้วตามปกติ — วัดที่กล่องจะไม่มีวันเห็น */
.dd-company .company-pill { max-width: 100%; }

/* ==========================================================================
   Cards
   ========================================================================== */
.card {
  background: var(--surface);
  /* V4: ขอบรับหน้าที่แทนเงา (ถอดแล้ว) — --card-line ≥1.3:1 กับพื้นหน้า */
  border: 1px solid var(--card-line);
  border-radius: var(--r-lg);
  margin-bottom: var(--s-5);
  overflow: hidden;
}
.card-head {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--line);
  /* V4: พื้นเรียบ — ไล่สีหัวการ์ดคือชั้นที่สองในกล่องเดียว */
  background: var(--surface);
}
.card-head h2, .card-head h3 { margin: 0; font-size: var(--t-section); font-weight: 600; }
.card-head .spacer { flex: 1 1 auto; }
.card-body { padding: var(--s-5); }
.card-body.tight { padding: var(--s-3) var(--s-5); }
.card-foot {
  padding: var(--s-4) var(--s-5); border-top: 1px solid var(--line);
  /* V4: เส้นคั่น ไม่ใช่แถบเทาซ้อนในการ์ด */
  background: transparent;
  display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
}
.card-foot.end { justify-content: flex-end; }
/* ฟอร์มใน .card-foot เป็นลูกของกล่อง flex — min-width: auto = ความกว้างของช่องเลือกชื่อบัญชียาวที่ห้ามตัดบรรทัด
   ⇒ การ์ดรายการหักอื่น 2015 บนมือถือ: ช่องเลือกกว้าง 447px ในการ์ด 358px (UAT งานคุมเงิน · 29 ก.ย. 2569)
   ฟอร์มที่เป็นกองช่องกรอก (.field) กินเต็มแถว · ฟอร์มปุ่มเดี่ยวยังวางเรียงกับปุ่มอื่นได้ตามเดิม */
.card-foot > form { min-width: 0; max-width: 100%; }
.card-foot > form:has(> .field) { flex: 1 1 100%; }
/* ชิปสถานะบนหัวการ์ดไม่ถูกบีบจนตัดสองบรรทัด ("ยังค้าง" · UAT 29 ก.ย.) — หัวข้อยาวเป็นฝ่ายตัดบรรทัดแทน
   ชิปที่เป็นประโยคยาวเกิน 60% ของหัวการ์ดตัดบรรทัดในตัวเอง (white-space: normal) ไม่ใช่ล้นกรอบชิป */
.card-head > .chip { flex: 0 0 auto; max-width: 60%; white-space: normal; }

/* การ์ดที่เป็น "หัวข้อกลุ่ม" ของการ์ดลูก (ดีไซน์ V4 · P2) — คงหัวข้อและตำแหน่ง แต่ไม่วาดกล่อง
   ลูกเป็นการ์ดของตัวเองอยู่แล้ว กล่องนอกคือกล่องที่สองที่ไม่บอกอะไรเพิ่ม (G9)
   ⚠️ (0,2,0) ชนะ .card ของชั้นเกลาที่ใส่เงา/ขอบ (0,1,0) โดยไม่พึ่งลำดับบรรทัด */
.card.card-bare { background: none; border: 0; box-shadow: none; overflow: visible; }
.card.card-bare > .card-head { padding: 0 0 var(--s-3); border-bottom: 0; background: none; }
.card.card-bare > .card-body { padding: 0; }

/* ชื่อกิจการในกล่องยืนยัน (ดีไซน์ V4 W6) — บรรทัดเงียบใต้คำถาม แต่ต้องอ่านออกทุกธีม */
.confirm-company {
  display: flex; align-items: center; gap: var(--s-2);
  margin: var(--s-3) 0 0; padding-top: var(--s-3); border-top: 1px solid var(--line);
  font-size: var(--t-label); color: var(--ink-2);
}
.confirm-company .ic { color: var(--muted); flex: 0 0 auto; }

/* ⚠️ ห้ามใส่ margin-right: auto ให้ช่องติ๊กนี้
   วัดบนจอจริง 1 ก.ย. 2569: แถบบันทึกแบบติดขอบมี .docbar-total ที่ใช้ auto
   อยู่ก่อนแล้ว พอมีสอง auto ในแถบเดียว เบราว์เซอร์ "แบ่งครึ่ง" ที่ว่างให้ทั้งคู่
   ช่องติ๊กจึงไปลอยกลางแถบ (วัดได้ 474px จากขอบซ้าย) แทนที่จะชิดซ้าย
   ที่ถูกคือปล่อยให้ยอดรวมยึดซ้าย แล้วช่องติ๊กเกาะกลุ่มกับปุ่มที่มันควบคุม
   โดยมีเส้นคั่นบางกันกดพลาด */
.then-new {
  display: inline-flex; align-items: center; gap: var(--s-2);
  /* stretch แทนความสูงตายตัว — เส้นคั่นจะสูงเท่าปุ่มเสมอ
     แม้ปุ่มจะเปลี่ยนขนาดตามชุด token หรือตามความกว้างจอ */
  align-self: stretch; min-height: 38px;
  margin-right: var(--s-2); padding: var(--s-1) var(--s-3);
  border-right: 1px solid var(--line);
  font-size: var(--t-label); color: var(--ink-2); cursor: pointer;
  border-radius: var(--r-sm) 0 0 var(--r-sm);
}
@media (hover: hover) { .then-new:hover { color: var(--ink); background: var(--surface-sunk); } }
.then-new input { cursor: pointer; margin: 0; }
.then-new:focus-within { outline: 2px solid var(--brand); outline-offset: 2px; }

/* บนจอแคบ ปุ่มยืดเต็มแถว (.card-foot.is-sticky .btn { flex: 1 1 auto })
   ช่องติ๊กจึงต้องขึ้นบรรทัดของตัวเองทั้งแถบ ไม่ใช่แย่งที่กับปุ่ม */
@media (max-width: 560px) {
  .then-new {
    margin-right: 0; flex: 1 0 100%; justify-content: center;
    border-right: 0; border-bottom: 1px solid var(--line); border-radius: 0;
  }
}

/* A whole card that is one big link (settings hub, report catalogue). */
.card-link, a.card, a.stat {
  display: block; color: inherit; text-decoration: none;
  transition: box-shadow var(--mid) var(--ease), border-color var(--mid) var(--ease), transform var(--mid) var(--ease);
}
@media (hover: hover) { .card-link:hover, a.card:hover, a.stat:hover {
  text-decoration: none; color: inherit;
  border-color: var(--brand);
} }
.card-link .title, a.card .title {
  font-family: var(--font-display); font-size: var(--t-heading); font-weight: 600;
  margin: var(--s-1) 0 5px; color: var(--ink);
}
.card-link .ic, a.card > .card-body > .ic { color: var(--brand); }

/* A grid block carries the same bottom rhythm as a card, so a stat row and a
   card stack read as one column. Without this every page hand-rolls the gap
   and the rhythm drifts page to page. */
.grid { display: grid; gap: var(--s-4); margin-bottom: var(--s-5); }
.grid:last-child { margin-bottom: 0; }
.grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0,1fr)); }

/* ⚠️ การ์ดสถิติต้องไม่โตตามจอไปเรื่อย ๆ

   ไฟล์นี้เขียนเตือนตัวเองไว้ที่บรรทัด ~137 ว่า "การ์ดสถิติใบหนึ่งกว้างเกิน
   600px แล้วตัวเลขลอยอยู่กลางที่ว่าง ซึ่งเป็นสาเหตุอันดับหนึ่งที่ซอฟต์แวร์
   ธุรกิจดูถูกบนจอใหญ่" — แล้วแก้ด้วยการตั้งเพดานให้ "ทั้งหน้า" ที่ 1720px

   ⚠️ การคุมทั้งหน้าเพื่อคุมการ์ด คือการจ่ายด้วยของที่ไม่เกี่ยวข้อง
   ตารางซึ่งได้ประโยชน์จากความกว้างจริง ๆ ถูกบีบไปด้วยโดยไม่มีเหตุผล
   วัดแล้ว: จอ 2560 ทิ้งพื้นที่ขวา 294px · จอ 3440 ทิ้ง 734px

   คุมที่การ์ดโดยตรงแทน แล้วปล่อยให้ตารางกางเต็ม
   วัดจริงที่จอ 2560 ก่อนคุม: การ์ดในแถวสามใบกว้าง 723px — เกินเส้นที่ไฟล์
   เขียนห้ามไว้เอง 123px โดยที่เพดานทั้งหน้าไม่ได้ช่วยอะไรเลย

   ไม่จัดกึ่งกลาง — แถวสรุปที่ชิดซ้ายเหนือตารางเต็มความกว้าง เป็นแบบแผน
   ปกติของแดชบอร์ด ส่วนการจัดกึ่งกลางจะทำให้มันดูลอยไม่เกาะอะไร */
   ⚠️ ลองคุมที่การ์ดแล้ว แต่ไม่เอา — บันทึกไว้กันคนถัดไปลองซ้ำ

   ตั้ง .grid-3 { max-width: 1832px } ได้ผลตามตัวเลขทุกอย่าง (การ์ดหยุดที่
   600px พอดี 53 ใบไม่มีเกินสักใบ) แต่ผลบนจอจริงคือแถวสรุปจบที่ x=1670
   ขณะที่แถบกรอง ตาราง และทุกอย่างที่เหลือจบที่ x=1950 — ขอบขวารุ่งริ่ง
   ซึ่งคือ "ขยายไม่เต็ม" ในรูปแบบใหม่ ตรงกับสิ่งที่เจ้าของบอกว่าไม่สวยพอดี

   คำเตือน 600px ข้างบนเขียนขึ้นตอนที่ยังไม่มีเพดานเลย ซึ่งการ์ดจะโตไม่จำกัด
   ตอนนี้มีเพดาน 2400px คุมอยู่ การ์ดจึงกว้างสุด 723px ที่จอ 2560
   และ 763px ที่จอ 3440 — สูงกว่าเส้นเดิมจริง แต่มีขอบเขต
   เลือกให้ทุกส่วนกางเสมอกัน แลกกับการ์ดที่โปร่งขึ้นเล็กน้อย
   เพราะขอบที่ไม่เสมอกันสะดุดตากว่าการ์ดที่โปร่ง

/* A grid nested inside a card body is layout, not rhythm */
.card-body > .grid:only-child, .modal-body > .grid { margin-bottom: 0; }

/* ---- KPI tile: the number is the point ---- */
.stat {
  background: var(--surface);
  border: 1px solid var(--card-line);
  border-radius: var(--r-lg);
  /* V4: ระยะในผ่านตัวแปร — ขนาดแน่น (แถบสรุปเหนือรายการ · มือถือ) เปลี่ยนตัวแปร แล้วกฎ "เว้นที่ให้ tile"
     กับกฎ "ซ่อน tile" ใช้ค่าเดียวกันเสมอ ไม่ต้องมีกฎคู่ที่แข่งน้ำหนักกันทีละขนาด */
  padding: var(--stat-py, var(--s-5)) var(--stat-px, var(--s-5));
  position: relative;
  /* V4 (G10): ตัวเลขปรับขนาดตามความกว้างการ์ดเอง (cqi) — การ์ดสี่ใบต่อแถวที่ 1440 เหลือที่ให้ตัวเลข ~160px
     ยอด 1,821,514.50 ที่ 28px กว้าง ~166px จึงล้นกรอบ · ทุกกริดของการ์ดสรุปอยู่ใน .grid (ความกว้างมาจากกริด
     ไม่ใช่จากเนื้อหา) inline-size containment จึงไม่ทำให้การ์ดหดผิดขนาด */
  container-type: inline-size;
  transition: box-shadow var(--mid) var(--ease), transform var(--mid) var(--ease), border-color var(--mid) var(--ease);
}
/* V4: เฉพาะการ์ดที่เป็นลิงก์ — การ์ดที่กดไม่ได้แล้วขยับตอนชี้ คือสัญญาณหลอกว่ากดได้ */
@media (hover: hover) { a.stat:hover { border-color: var(--line-strong); } }
.stat .label {
  /* V4 (C1): ป้ายของตัวเลขสำคัญ ("ยอดค้างรับ") อ่านก่อนตัวเลข — ใช้ขนาดป้ายกำกับ (14) ไม่ใช่ขนาดพื้นล่าง */
  font-size: var(--t-label); color: var(--muted); font-weight: 500;
  margin-bottom: var(--s-2); display: flex; align-items: center; gap: 6px;
}
.stat .value {
  font-family: var(--font-display);
  font-size: var(--t-display); font-weight: 600; line-height: 1.15;
  white-space: nowrap;
  letter-spacing: -.03em;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--ink);
}
/* A tile whose value is words, not money — money size would shout. */
.stat .value.text {
  font-size: var(--t-heading); line-height: 1.45; letter-spacing: -.008em; font-weight: 600;
  word-break: break-word;
  white-space: normal;
}
/* ค่าที่เป็นประโยคแต่เป็นพาดหัวของหน้า (การ์ดพยากรณ์เงินสดเต็มความกว้าง) — ขนาดตัวเลขเดิม แต่ตัดบรรทัดได้
   ⚠️ .stat .value ห้ามตัดบรรทัดตั้งแต่ V4 (G10 · ตัวเลข) — ประโยคที่ห้ามตัดล้นกรอบบนมือถือแล้วหน้าเลื่อนแนวนอน (ผู้ตรวจ V4)
   :not(.text) ให้น้ำหนักชนะกฎ nowrap ของบล็อก ≤560 ที่อยู่ล่างกว่า */
.stat .value.value-sentence:not(.text) { white-space: normal; overflow-wrap: break-word; }
/* 14cqi = ยอด 14 ตัวอักษร (123,456,789.00) พอดีความกว้างเนื้อการ์ด · ไม่เกิน --t-display ไม่ต่ำกว่า --t-heading
   การ์ดในกริดเดียวกันกว้างเท่ากัน ตัวเลขจึงขนาดเท่ากันทั้งแถวเสมอ */
.stat .value:not(.text) { font-size: clamp(var(--t-heading), 14cqi, var(--t-display)); }
/* ค่าที่เป็นรหัส/เลขที่เอกสาร (JV202609-10648) — อักษรละตินกับขีดกว้างกว่าตัวเลข 14cqi จึงล้นการ์ดของหน้าใบสำคัญที่ 1440
   (G10 · ตรวจทานรอบสุดท้าย 25 ก.ย. 2569) · ยังขยายตามการ์ดแบบเดียวกันแต่เล็กลง และไม่ตัดบรรทัด — เลขที่ต้องอ่านเป็นก้อนเดียว
   ⚠️ น้ำหนัก 0,4,0 ชนะกฎ ≤560 ของ .stat .value (0,3,0) โดยตั้งใจ — ค่าต่ำสุด --t-body คือเพดานล่างของมือถือแทน */
.stat .value.value-id:not(.text) { font-size: clamp(var(--t-body), 10.5cqi, var(--t-display)); }
.stat .meta { font-size: var(--t-label); color: var(--muted); margin-top: var(--s-2); }
.stat .meta.up { color: var(--ok); font-weight: 500; }
.stat .meta.down { color: var(--danger); font-weight: 500; }
/*
 * ⚠️ ลิงก์ในการ์ดต้องไม่แตกกลางคำ
 *
 * ภาษาไทยไม่เว้นวรรคระหว่างคำ เบราว์เซอร์จึงตัดบรรทัดที่ตำแหน่งไหนก็ได้ในวลี
 * ที่วัดได้จริงบนแดชบอร์ด: "ดูอายุลูกหนี้ →" แตกเป็นสองบรรทัด
 * ลูกศรตกไปอยู่บรรทัดล่างลำพัง — อ่านแล้วเหมือนหน้าจอพัง ไม่ใช่เหมือนลิงก์
 *
 * ลูกศรคือคำสัญญาว่า "กดแล้วไปที่อื่น" มันต้องอยู่ติดกับคำที่มันขยายเสมอ
 */
.stat .meta a,
.stat .meta .cta {
  white-space: nowrap;
}


/* The category shows as a dot beside its label, which is how this system
   already says "state" everywhere else — see .chip::before.

   It used to be a coloured bar ruled across the top of every tile. Two of them
   side by side, one green and one orange at full strength, read as a warning
   about money that was merely being reported: an expense figure is not an
   alarm. The bar also had to be drawn inside a rounded, bordered box, so its
   ends never quite met the corners. A dot sits where the eye already is — on
   the words telling you what the number is — and costs the tile nothing. */
.stat .label::before {
  content: ""; flex: 0 0 auto;
  width: 6px; height: 6px; border-radius: 50%;
  /* A tile with no category still gets a dot, so labels line up across a row.
     It has to be dark enough to look placed rather than like a speck of dust —
     the border grey was not. */
  background: var(--n-400);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--n-400) 20%, transparent);
  margin-right: 1px;
}
.stat.tone-ok .label::before {
  background: var(--ok);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 18%, transparent);
}
.stat.tone-warn .label::before {
  background: var(--warn);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn) 18%, transparent);
}
.stat.tone-danger .label::before {
  background: var(--danger);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 20%, transparent);
}
.stat.tone-info .label::before {
  background: var(--info);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--info) 18%, transparent);
}

/* Only the tile that wants something done gets more than a dot. */
.stat.tone-danger { border-color: var(--danger-line); }
.stat.tone-danger .value { color: var(--danger); }

/* ==========================================================================
   Tables — the money surface
   ========================================================================== */
/* ตารางที่กว้างกว่าจอต้องบอกเองว่ายังมีต่อทางไหน ไม่ใช่ตัดจบกลางคำเฉย ๆ
   ชั้นที่ทาด้วย local จะเลื่อนไปพร้อมเนื้อหา ส่วนชั้นเงาทาด้วย scroll จึงอยู่กับที่
   เลื่อนจนสุดข้างไหน เงาข้างนั้นก็ถูกกลบหายไปเอง — ไม่ต้องใช้ JavaScript */
.table-wrap {
  overflow-x: auto;
  /* ⚠️ จองที่ให้แถบเลื่อนของตารางด้วยเหตุผลเดียวกับหน้าเว็บ
     ตารางที่กว้างเกินจอทำให้แถบเลื่อนโผล่ แล้วคอลัมน์ขยับตามทั้งตาราง
     ซึ่งบนตารางตัวเลขเงินคือการทำให้ตาที่กำลังไล่อ่านหลุดแถว */
  scrollbar-gutter: stable;
  background-color: var(--surface);
  background-image:
    linear-gradient(to right, var(--surface), transparent),
    linear-gradient(to left, var(--surface), transparent),
    linear-gradient(to right, var(--scroll-shade), transparent),
    linear-gradient(to left, var(--scroll-shade), transparent);
  background-position: left center, right center, left center, right center;
  background-repeat: no-repeat;
  background-size: 28px 100%, 28px 100%, 12px 100%, 12px 100%;
  background-attachment: local, local, scroll, scroll;
}
.card > .table-wrap { border-radius: 0 0 var(--r-lg) var(--r-lg); }
:not(.card) > .table-wrap { border: 1px solid var(--card-line); border-radius: var(--r-lg); }

table.tbl { width: 100%; border-collapse: collapse; font-size: var(--t-body); }
table.tbl th {
  text-align: left; font-size: var(--t-micro); font-weight: 600;
  text-transform: uppercase; color: var(--muted);
  padding: 10px 14px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
  position: sticky; top: 0; z-index: 1;
}
table.tbl td {
  /* V4: ตัวหนังสือโตขึ้น 1px ⇒ ลดระยะในเซลล์ลงให้แถวยังสูง ~44px (เดิม ~47) ไม่ใช่ยาวขึ้นทั้งตาราง */
  padding: 10px 14px;
  line-height: 1.45;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
  /* V4 (W5 · A3): ข้อมูลในแถวคือเนื้อหาหลักของหน้า ไม่ใช่ข้อความรอง — เดิม --ink-2 (9.8:1 เทียบ --ink 15:1)
     ทำให้ทุกหน้ารายการเป็น "ตัวหนังสือจาง" เกือบครึ่งจอ · ลำดับชั้นในแถวมาจากน้ำหนัก (ลิงก์ 600 · ยอด 500)
     และข้อความรองจริง (.small · .text-muted-2) ยังจางตามเดิม */
  color: var(--ink);
}
table.tbl td strong, table.tbl td b { color: var(--ink); font-weight: 600; }
table.tbl tbody tr { transition: background var(--fast) var(--ease); }
@media (hover: hover) { table.tbl tbody tr:hover { background: var(--brand-softer); } }
table.tbl tbody tr:last-child td { border-bottom: 0; }
/* แถวที่เป็นศูนย์ในแบบภาษี — หรี่ ไม่ใช่ซ่อน
   ⚠️ แบบ ภ.พ.30 ต้องกรอก e-Filing ไล่ตามลำดับข้อ ถ้าซ่อนข้อที่เป็นศูนย์
   คนกรอกจะนับข้อเลื่อนแล้วกรอกผิดช่อง — กติกาเดียวกับ .todo-card.is-zero
   ⚠️ หรี่ด้วยสี --muted ไม่ใช่ opacity — opacity .55 เหลือ 3.62:1 ต่ำกว่า AA (ตรวจความครบหลังดีไซน์ V4 · C17)
   แถวศูนย์ยังเป็นเนื้อหาที่ต้องอ่านได้ WCAG 1.4.3 ยกเว้นเฉพาะตัวควบคุมที่ปิดใช้งาน */
table.tbl tbody tr.is-zero td { color: var(--muted); }
table.tbl th.right, table.tbl td.right { text-align: right; }
table.tbl th.center, table.tbl td.center { text-align: center; }
table.tbl td.num { white-space: nowrap; color: var(--ink); font-weight: 500; }
/* [UAT2 B1] หมายเหตุใต้ยอดในช่องตัวเลข (หน้าสร้างไฟล์ธนาคาร) เป็นประโยค — ตัดบรรทัดได้ ไม่งั้นช่องยอดกว้างเท่าประโยค (441px)
   แล้วดันช่องเลขบัญชีเหลือ 71px เห็นแค่ "01212" และยอดโอนหลุดขอบจอบนเดสก์ท็อป 1440 · ตัวเลขยอดเองยังห้ามตัดบรรทัด */
table.tbl td.num .amount-note { white-space: normal; }
/* [UAT3 A-3] บรรทัดรองของแถวใบตัดชำระ (ชิปชนิดใบ + ยอดที่รับ) สืบทอด nowrap จาก td.num เหมือนกัน — ถอด u-nowrap ออกจากมุมมองอย่างเดียวไม่พอ
   (device:sweep วัดค่าที่เบราว์เซอร์คิดจริง: ยังเป็น nowrap) · ยอดเงินข้างในยังห้ามตัด (u-nowrap ของตัวเลข) */
table.tbl td.num .settle-sub { white-space: normal; }
/* [UAT3 R8-X1] บรรทัดรอง "หลังหักใบลดหนี้ X" ในช่องค้างชำระก็สืบทอด nowrap แบบเดียวกัน — วัดจริง 1440 · กิจการ 7834 หน้า 2:
   "หลังหักใบลดหนี้ 19,527.50 THB" กว้าง 200px ⇒ ช่องค้างชำระ 228px · ตารางกว้างเกินกรอบ 29px (เลื่อนแนวนอน)
   · คอลัมน์ลูกค้าเหลือ 91px ชื่อตัด 4 บรรทัดกลางคำ · ตัวเลขกับรหัสสกุลยังห้ามแยกกัน (.u-nowrap ในมุมมอง) */
table.tbl td.num [data-after-notes] { white-space: normal; }
/* [UAT 5 ต.ค. 2569 · V8-1] รายงานภาษีขาย/ซื้อ: ชื่อคู่ค้ายาวที่ไม่มีช่องว่าง (ชื่อที่มาจาก POS/การนำเข้า) ยึดความกว้างขั้นต่ำของคอลัมน์ชื่อ
   วัดจริง 1440 กิจการ 38153: คอลัมน์ชื่อ 290px ⇒ ตารางเลื่อนข้าง 50px คอลัมน์ "จำนวนเงินภาษี" หลุดขอบ · คอลัมน์เลขที่เหลือ 104px
   เลข POS + บรรทัดรอง "เลขที่ในระบบ …" ตัด 6–8 บรรทัด · ⇒ ชื่อตัดบรรทัดได้ทุกตำแหน่ง · เลขเอกสารห้ามตัด (.u-nowrap ในมุมมอง · บทเรียนข้อ 409) */
table.tbl td.vat-party { overflow-wrap: anywhere; }
/* หัวคอลัมน์ของรายงานภาษี ("เลขประจำตัวผู้เสียภาษี" · "มูลค่าสินค้าหรือบริการ" · "ชื่อผู้ซื้อสินค้า / ผู้รับบริการ") ยาวกว่าเนื้อ
   — หัวที่ห้ามตัดบรรทัดเป็นความกว้างขั้นต่ำของคอลัมน์ (วัดจริง: ห้าหัวรวม 638px) แล้วตารางล้นเมื่อคอลัมน์เลขที่ต้องกว้างพอให้ "เลขที่ในระบบ X" อยู่บรรทัดเดียว
   ⇒ ตารางนี้ให้หัวตัดบรรทัดได้ (หัวสองบรรทัดอ่านง่ายกว่าเลขเอกสารห้าบรรทัด) */
table.tbl.vat-report > thead > tr > th { white-space: normal; vertical-align: bottom; }
/* [UAT3 · design:audit C4 · 30 ก.ย. 2569] รายการขายบนจอกว้าง: ช่องยอดรวม/ค้างชำระต้องกว้างพอให้บรรทัดรอง (ชิปชนิดใบ + ยอดที่รับ ·
   "หลังลดหนี้ X") อยู่บรรทัดเดียว — บรรทัดรองตัดบรรทัดได้ (สองกฎข้างบน) ⇒ ตารางแบบอัตโนมัติยกที่ว่างให้คอลัมน์ลูกค้าก่อนเสมอ
   แล้วบรรทัดรองตกเป็นบรรทัดที่สาม · ความกว้างของหัวคอลัมน์ = ขั้นต่ำของคอลัมน์ (เนื้อยาวผิดปกติยังตัดบรรทัดได้ ไม่ดันตารางล้น)
   วัดจริง 1440 กิจการ 1: บรรทัดรอง 151px ⇒ ช่องยอดรวม 180 · ค้างชำระ 156 · คอลัมน์ลูกค้าเหลือ 164 ชื่อยาวยังสองบรรทัด
   ⚠️ เฉพาะ ≥1200px — ตารางเต็มที่แคบกว่านั้น (861–1199 ที่ใช้เมาส์) ถ้ายกความกว้างนี้ไปด้วยจะบีบคอลัมน์ลูกค้าจนแย่กว่าเดิม
   ⚠️ เฉพาะหน้าภาษาไทย — ป้ายอังกฤษกว้างกว่า (สถานะ "Awaiting payment" · วันที่ "30 Sep 2026") ความกว้างชุดนี้บีบคอลัมน์ลูกค้าหน้าอังกฤษ
      เหลือ ~95px ชื่อยาวสามบรรทัดทุกแถว (ภาพคู่มือ en-sales 30 ก.ย. 2569) · หน้าอังกฤษใช้ตารางอัตโนมัติตามเดิม (แถวใบตัดชำระที่มีไม่กี่แถวยอมสามบรรทัดแทน) */
@media screen and (min-width: 1200px) {
  html[lang="th"] table.tbl[data-doc-list="sales"] > thead > tr > th[data-card-role="amount"] { width: 11.25rem; }
  html[lang="th"] table.tbl[data-doc-list="sales"] > thead > tr > th[data-card-role="meta-l"] { width: 9.75rem; }
}
table.tbl tfoot td {
  padding: var(--s-3) var(--s-4); font-weight: 600; color: var(--ink);
  background: var(--surface-2); border-top: 2px solid var(--line-strong); border-bottom: 0;
}
table.tbl.compact th, table.tbl.compact td { padding: 9px var(--s-3); }
/* Child rows in a financial statement sit one step in from their heading. */
table.tbl td.indent { padding-left: var(--s-6); }
table.tbl td.indent-2 { padding-left: calc(var(--s-6) + var(--s-4)); }
table.tbl tr.section td {
  background: var(--brand-soft); color: var(--brand-ink);
  font-weight: 600; font-size: var(--t-label);
}
table.tbl tr.total td { font-weight: 600; color: var(--ink); background: var(--surface-2); }

/* ---- chips: state as shape + word, never colour alone ---- */
.chip {
  display: inline-flex; align-items: center; gap: 5px;
  /* V4 (C1): ชิปคือตัวหนังสือที่พบมากที่สุดในหน้ารายการ (สถานะทุกแถว) — วัดจริงเป็นแหล่งอันดับหนึ่ง
     ของตัวหนังสือ <14px บนหน้าหลักทุกหน้า · ขึ้นเป็น --t-label (14) น้ำหนัก 500 และบีบระยะบรรทัด
     ชิปจึงเตี้ยลง (29 → ~25px) ทั้งที่อ่านง่ายขึ้น (ลูกค้าหลักอายุ 40+) · ⚠️ แผน V4 W1 เดิมวางชิปไว้ที่ --t-micro */
  padding: 2px 10px; border-radius: var(--r-pill);
  font-size: var(--t-label); font-weight: 500; line-height: 1.5;
  white-space: nowrap; border: 1px solid transparent;
}
.chip::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.chip-ok     { background: var(--ok-bg);     color: var(--ok);     border-color: var(--ok-line); }
.chip-warn   { background: var(--warn-bg);   color: var(--warn);   border-color: var(--warn-line); }
.chip-danger { background: var(--danger-bg); color: var(--danger); border-color: var(--danger-line); }
.chip-info   { background: var(--info-bg);   color: var(--info);   border-color: var(--info-line); }
.chip-muted  { background: var(--surface-3); color: var(--muted);  border-color: var(--line); }
/* ชิป "ร่าง" = ขอบประ (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · C12/C24 · คำตัดสิน §14.2)
   ร่างกับใบที่ยกเลิก/ปิดไปแล้วเป็นชิปเทาเดียวกันโดยตั้งใจ (เทา = ยังไม่ต้องสนใจ) — แยกด้วย "รูปทรง" ไม่ใช่สี
   (หัวข้อ chips ข้างบน: state as shape + word, never colour alone) · ขอบประบอกว่า "ยังไม่ใช่ของจริง"
   ⚠️ สีขอบต้องเป็น --ctl-line (≥3:1 ทั้งสองธีม · C32) — --line ของ chip-muted จางจนเส้นประมองไม่เห็นบนพื้น surface-3 */
.chip.chip-draft { border-style: dashed; border-color: var(--ctl-line); }
/* ป้ายสั้นของชิป (V4 · คิวทวงหนี้) แสดงเฉพาะในแถวกระชับ — กฎที่สลับอยู่ในบล็อกแถวกระชับ */
.chip-short { display: none; }
/* [UAT2] ชิปชนิดใบบนบรรทัดสองของแถวใบตัดชำระ — มีเฉพาะแถวกระชับ (จอกว้างแสดงชิปชนิดใบในช่องยอดตามเดิม)
   กฎที่แสดงอยู่ในบล็อกแถวกระชับ ต่อจากกฎ chip-normal */
.settle-kind-line2 { display: none; }

/* ---- module tiles (ดีไซน์ V3): สีหมวด = "ที่อยู่ในระบบ" -------------------
   กติการูปทรง (คำตัดสินกรรมการ 31 ส.ค. 2569): tile เป็นสี่เหลี่ยมมนที่มี
   "แต่ไอคอน" — chip เป็นแคปซูลที่มี "แต่ตัวหนังสือ" ห้ามไขว้เด็ดขาด
   รูปทรงจึงแบกความหมายคู่กับสี: mtile-sell เขียวไม่มีวันถูกอ่านเป็น
   chip-ok เขียว แม้ hue จะเครือเดียวกัน (จุดที่ qa_contrast ตรวจไม่ได้โดยนิยาม)
   คู่ color+background อยู่ในกฎเดียวกันเสมอ → เกต qa_contrast กวาดวัดเองครบ */
.mtile {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: var(--r-sm); flex: 0 0 auto;
}
.mtile-sm { width: 26px; height: 26px; border-radius: var(--r-xs); }
.mtile-sell   { background: var(--m-sell-bg);   color: var(--m-sell); }
.mtile-buy    { background: var(--m-buy-bg);    color: var(--m-buy); }
.mtile-master { background: var(--m-master-bg); color: var(--m-master); }
.mtile-money  { background: var(--m-money-bg);  color: var(--m-money); }
.mtile-books  { background: var(--m-books-bg);  color: var(--m-books); }
.mtile-hub    { background: var(--m-hub-bg);    color: var(--m-hub); }
.mtile-sys    { background: var(--m-sys-bg);    color: var(--m-sys); }

/* tile บนการ์ด KPI นั่งซ้ายชิดตัวเลขแบบ YourHR — รอบแรกเคยเกาะมุมขวาบน
   แล้วการ์ดยังอ่านเป็น "ขาวโล่ง" เพราะสีหลบสายตาไปสุดขอบ (เจ้าของตัดสิน
   31 ส.ค.: ยังไม่ดีพอ) — ชิดตัวเลขแล้วสีกับเลขถูกอ่านเป็นก้อนเดียวกัน
   ทำด้วย padding ไม่ใช่ grid เพื่อไม่แตะโครงลูก (การ์ดแต่ละใบมี meta
   กี่บรรทัดก็ได้) · ตัวเลขเงินยังเป็น --ink เสมอ */
.stat:has(> .mtile) { padding-left: calc(var(--stat-px, var(--s-5)) + 44px + var(--s-4)); }
.stat > .mtile { position: absolute; top: var(--s-5); left: var(--s-5); width: 44px; height: 44px; }

/* V4 (W7): การ์ดสรุป "เหนือรายการ" = แถบสรุป ไม่ใช่แดชบอร์ด — สูงไม่เกิน ~88px
   ที่ 1440×900 การ์ดสูง 130px ดันแถวข้อมูลแรกของหน้าขายลงไป y=678 (เกณฑ์ C2 480)
   รู้จำจากโครงหน้าเอง: กริดการ์ดสรุปที่ตามด้วยแท็บ/ตัวกรอง/ปุ่มลัดช่วงวันที่ = หน้ารายการ
   แดชบอร์ด/หน้าการเงินไม่มีสิ่งนั้นตามหลัง จึงคงการ์ดใหญ่ที่ "ตัวเลขคือเรื่องหลัก"
   ⚠️ screen เท่านั้น — รายการที่สั่งพิมพ์จากจอต้องได้การ์ดสรุปแบบก่อน V4 (แผน V4: กระดาษต้องไม่เปลี่ยน · วัดด้วย --print) */
@media screen {
.grid:has(> .stat):is(:has(+ .tabs), :has(+ .filters), :has(+ .filters-toggle), :has(+ .date-preset), :has(+ .summary-line)) > .stat { --stat-py: var(--s-3); --stat-px: var(--s-4); }
.grid:has(> .stat):is(:has(+ .tabs), :has(+ .filters), :has(+ .filters-toggle), :has(+ .date-preset), :has(+ .summary-line)) > .stat:has(> .mtile) { padding-left: var(--stat-px); }
.grid:has(> .stat):is(:has(+ .tabs), :has(+ .filters), :has(+ .filters-toggle), :has(+ .date-preset), :has(+ .summary-line)) > .stat > .mtile { display: none; }
.grid:has(> .stat):is(:has(+ .tabs), :has(+ .filters), :has(+ .filters-toggle), :has(+ .date-preset), :has(+ .summary-line)) > .stat .label { margin-bottom: 2px; }
.grid:has(> .stat):is(:has(+ .tabs), :has(+ .filters), :has(+ .filters-toggle), :has(+ .date-preset), :has(+ .summary-line)) > .stat .meta { margin-top: 2px; }
.grid:has(> .stat):is(:has(+ .tabs), :has(+ .filters), :has(+ .filters-toggle), :has(+ .date-preset), :has(+ .summary-line)) > .stat .value:not(.text) { font-size: clamp(var(--t-section), 12cqi, var(--t-title)); }
}
/* จอกว้าง: บรรทัดรองขึ้นข้างตัวเลข (ป้าย / ตัวเลข · รายละเอียด) แทนบรรทัดของตัวเอง — แถบสรุปเหลือสองบรรทัด
   ถ้าที่ข้างตัวเลขไม่พอ (ยาวกว่า 10em) มันตัดลงบรรทัดใหม่เองทั้งก้อน ไม่บีบจนแตกทีละคำ */
@media screen and (min-width: 1025px) {
  .grid:has(> .stat):is(:has(+ .tabs), :has(+ .filters), :has(+ .filters-toggle), :has(+ .date-preset), :has(+ .summary-line)) > .stat {
    display: flex; flex-wrap: wrap; align-items: baseline; column-gap: var(--s-3);
  }
  .grid:has(> .stat):is(:has(+ .tabs), :has(+ .filters), :has(+ .filters-toggle), :has(+ .date-preset), :has(+ .summary-line)) > .stat > :not(.value):not(.meta):not(.mtile) { flex: 1 0 100%; }
  .grid:has(> .stat):is(:has(+ .tabs), :has(+ .filters), :has(+ .filters-toggle), :has(+ .date-preset), :has(+ .summary-line)) > .stat > .meta { flex: 1 1 10em; min-width: 0; }
}

/* V4 (W8): มือถือ — การ์ดสรุปสองคอลัมน์ (กริดอยู่ในบล็อก ≤860) ต้องบางพอให้สองใบวางคู่กัน
   · ถอด tile (สีหมวดยังอยู่ที่จุดหน้าป้าย) — tile 44px + ช่องไฟกินครึ่งความกว้างของการ์ดครึ่งจอ
   · ตัวเลขห้ามตัดบรรทัด/ล้น (G10) — ขนาดตามความกว้างของการ์ดเอง (cqi) ไม่เกิน --t-heading
     ยอด 123,456,789.00 (14 ตัว) ต้องพอดีการ์ดครึ่งจอ 360px — ยอดเงินห้ามย่อเป็น "ล้าน" (แผน W5) */
@media screen and (max-width: 560px) {
  .stat { --stat-py: var(--s-3); --stat-px: var(--s-4); }
  .stat:has(> .mtile) { padding-left: var(--stat-px); }
  .stat .label { margin-bottom: var(--s-1); }
  .stat .meta { margin-top: var(--s-1); }
  .stat > .mtile { display: none; }
  .stat .value:not(.text) { font-size: clamp(var(--t-body), 12.5cqi, var(--t-heading)); white-space: nowrap; }
}
/* ⚠️ การ์ดสรุปที่แคบจนลิงก์ในบรรทัดรองไม่พอดี ยอมให้ลิงก์ตัดบรรทัด — ดีกว่าล้นกรอบจนทั้งหน้าเลื่อนแนวนอน
   ที่มาจริง (device:sweep 360 · แดชบอร์ด): การ์ดครึ่งจอ 156px แต่ "เปิดแบบ ภ.พ.30 ของงวดนี้ →" กว้าง 169px (nowrap)
   ⚠️ ห้ามแก้ด้วยการยุบกลุ่มที่มีลิงก์เป็นคอลัมน์เดียว — ลองแล้ว การ์ดสรุปของหน้าขาย/ซื้อมีลิงก์สั้น ("ยกเลิกแล้ว 3 ใบ →")
   จึงเรียงสามใบยาวลงมา ดันแถวแรกของรายการตกจอแรกบนมือถือ (design:audit C3 ตก) · .stat เป็น container อยู่แล้ว */
@container (max-width: 210px) {
  .stat .meta a, .stat .meta .cta { white-space: normal; }
}

/* ---- แถบ "งานที่ต้องทำวันนี้" (ดีไซน์ V3.1) ---------------------------------
   ยกแบบจากแถบ "งานค้างวันนี้" ของ YourHR — จอเขาม่วง ของเราเขียวแบรนด์
   ขอบซ้ายการ์ดเป็น "สีสถานะ" (ความเร่งด่วนคือข่าว ไม่ใช่ที่อยู่) ส่วน tile
   ไอคอนเป็นสีหมวดของปลายทาง — สองภาษาสีอยู่ร่วมกันคนละบทตามกติกา V3 */
/* V4 (P2 · G9): แถบเป็นหัวข้อกลุ่ม ไม่ใช่กล่อง — ของเดิมเป็นกล่องสีเขียวอ่อนมีขอบ แล้วการ์ดงานแต่ละใบ
   (มีขอบของตัวเอง) วางอยู่ข้างใน = กล่องซ้อนกล่องบนจอแรกของแดชบอร์ดและหน้าเงินเดือน
   ความเป็นกลุ่มมาจากหัวข้อ + ระยะห่าง เหมือนกลุ่ม "เดือนนี้" / "ยอดคงค้าง" ข้างล่าง */
.todo-band {
  margin-bottom: var(--s-6);
}
.todo-head {
  display: flex; align-items: center; gap: var(--s-2);
  font-size: var(--t-label); font-weight: 600; color: var(--brand-ink);
  margin-bottom: var(--s-3);
}
.todo-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--s-3); }
.todo-card {
  display: flex; align-items: center; gap: var(--s-3);
  min-height: 44px; padding: var(--s-3) var(--s-4);
  background: var(--surface); color: var(--ink);
  /* V4: ถอดขอบซ้ายสี (แถบบนการ์ดมุมมนคือหน้าตาของดีไซน์สำเร็จรูป) — กลับคำตัดสิน V3.1 โดยตั้งใจ */
  border: 1px solid var(--card-line); border-radius: var(--r);
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}
@media (hover: hover) { .todo-card:hover { box-shadow: var(--sh-sm); } }
/* ความเร่งด่วนยังบอกด้วย "คำ" ในป้ายเสมอ (ห้ามใช้สีอย่างเดียว · WCAG 1.4.1) สีของตัวเลขเป็นสัญญาณรอง */
/* ⚠️ เฉพาะเมื่อมีงานค้างจริง (:not(.is-zero)) — "0 เกินกำหนด" สีแดงคือเตือนเท็จ (W12) และสีสถานะทับการหรี่ .55
   ได้คอนทราสต์ 2.3–2.7:1 ต่ำกว่า 3:1 (ผู้ตรวจ V4) · ศูนย์ใช้สี --muted (ดู .todo-card.is-zero) */
.todo-card.t-danger:not(.is-zero) .n { color: var(--danger); }
.todo-card.t-warn:not(.is-zero) .n   { color: var(--warn); }
.todo-card.t-info:not(.is-zero) .n   { color: var(--info); }
.todo-card .n {
  font-family: var(--font-display); font-size: var(--t-heading); font-weight: 600;
  font-variant-numeric: tabular-nums lining-nums; min-width: 1.6ch; text-align: center;
}
.todo-card .tx { flex: 1 1 auto; font-size: var(--t-label); line-height: 1.45; }
/* จำนวนเงินใต้จำนวนใบ — "14 ใบ" ที่ไม่มีตัวเลขเงินตอบไม่ได้ว่าต้องไล่เก็บเท่าไร (ข้อค้นพบ 14) */
.todo-card .todo-amt { display: block; font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums lining-nums; }
/* ศูนย์งานค้าง = ข่าวดี — หรี่การ์ดแบบ .is-zero ของ HR ไม่ใช่ซ่อน
   (ตำแหน่งคงที่ทุกวัน คนถึงเชื่อว่าแถบนี้ "เฝ้าให้จริง")
   ⚠️ หรี่ด้วยสี --muted ไม่ใช่ opacity — opacity .55 ทำตัวอักษรเหลือ 3.62:1 ต่ำกว่า AA
   (ตัววัดคอนทราสต์เพิ่งนับ opacity เป็นครั้งแรก · ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · C17)
   .todo-amt ตั้งสี --ink ของตัวเอง จึงต้องหรี่ตามตรง ๆ */
.todo-card.is-zero,
.todo-card.is-zero .n,
.todo-card.is-zero .todo-amt { color: var(--muted); }
.todo-card > .ic:last-child { transform: rotate(-90deg); color: var(--faint); flex: 0 0 auto; }

/* หัวกลุ่มการ์ดมีไอคอนนำ — จังหวะอ่านแบบ section header ของ HR */
.grid-label .ic { vertical-align: -2px; margin-right: 5px; color: var(--muted); }
/* หน่วยของทั้งกลุ่ม ("· บาท") — ย้ายขึ้นมาจากบรรทัดรองของการ์ดทุกใบ (ดีไซน์ V4 W5) พูดครั้งเดียวต่อกลุ่ม */
.grid-label-unit { font-weight: 400; color: var(--muted); }
/* ตัดเหลือบรรทัดเดียวพร้อม … (ข้อความเต็มต้องอยู่ใน title หรือหน้าปลายทางเสมอ — ห้ามใช้กับยอดเงิน)
   ⚠️ screen เท่านั้น (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · C9) — กระดาษไม่มี title ไม่มีหน้าปลายทาง
   เดิมกฎนี้ไม่มีด่าน สมุดรายวันที่สั่งพิมพ์จึงเหลือคำอธิบาย/เลขอ้างอิงบรรทัดเดียวทุกแถว ทั้งที่ก่อน V4 พิมพ์ครบ
   บนกระดาษ span กลับเป็นข้อความในบรรทัดตามปกติ = หน้าตาเดียวกับก่อน V4 */
@media screen {
  .u-clamp-1 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; overflow: hidden; }
}

/* ---- แผง "วิธีใช้หน้านี้" (ดีไซน์ V3 ข้อ C) --------------------------------
   คอมโพเนนต์ใหม่ทั้งตัว (ระบบไม่เคยมี slide-over — ทีมสำรวจยืนยันแล้ว
   ห้ามฝืนยัดเป็น dropdown) · เลเยอร์ต่ำกว่าโมดัล (90) เพราะโมดัลคืองานหลัก
   คู่มือคือของเสริม · กฎ media ของคอมโพเนนต์อยู่ในบล็อกนี้ (บทเรียน 18) */
.help-backdrop {
  position: fixed; inset: 0; z-index: 80;
  background: color-mix(in srgb, var(--n-950) 34%, transparent);
}
.help-drawer {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 85;
  width: min(400px, 94vw);
  background: var(--surface); color: var(--ink);
  border-left: 1px solid var(--line);
  box-shadow: var(--sh-lg);
  display: flex; flex-direction: column;
  animation: help-in var(--mid) var(--ease) both;
}
@keyframes help-in { from { transform: translateX(24px); opacity: 0; } to { transform: none; opacity: 1; } }
.help-head {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--line);
}
.help-head-tx { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.help-head-tx strong { font-size: var(--t-section); }
.help-head-tx span { font-size: var(--t-micro); color: var(--muted); }
.help-body { padding: var(--s-4) var(--s-5) var(--s-6); overflow-y: auto; }
.help-what { font-size: var(--t-body); line-height: 1.7; color: var(--ink-2); margin-bottom: var(--s-4); }
.help-sec {
  font-size: var(--t-micro); font-weight: 600; color: var(--muted);
  margin: var(--s-4) 0 var(--s-2);
}
.help-steps { margin: 0; padding-left: var(--s-5); display: grid; gap: var(--s-2); }
.help-steps li { font-size: var(--t-body); line-height: 1.65; }
.help-links { display: grid; gap: var(--s-2); }
.help-link {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-2) var(--s-3); min-height: 44px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--ink); font-size: var(--t-label); line-height: 1.5;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
@media (hover: hover) { .help-link:hover { border-color: var(--line-strong); background: var(--surface-2); } }
/* จุดชวนสังเกตครั้งแรก — pulse หนึ่งจังหวะแล้วเงียบ (JS ถอดคลาสเมื่อเคยเปิดแล้ว) */
.help-btn.is-fresh { position: relative; }
.help-btn.is-fresh::after {
  content: ""; position: absolute; top: 6px; right: 6px;
  width: 7px; height: 7px; border-radius: 50%; background: var(--brand);
  animation: help-pulse 1.8s var(--ease) 2;
}
@keyframes help-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--brand) 45%, transparent); }
  55% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--brand) 0%, transparent); }
}

/* ---- แถบ "ไปต่อจากหน้านี้" (ดีไซน์ V3 ข้อ D) ------------------------------
   ท้ายเนื้อหาเสมอ — ตัวเลขคือพระเอก ห้ามดันลง · จอแคบเลื่อนแนวนอน
   ในกล่องของตัวเอง (กติกาห้ามทั้งหน้าล้นแนวนอน) */
.related-rail { margin-top: var(--s-7); padding-top: var(--s-5); border-top: 1px solid var(--line); }
.related-head { font-size: var(--t-micro); font-weight: 600; color: var(--muted); margin-bottom: var(--s-3); }
.related-cards { display: flex; gap: var(--s-3); overflow-x: auto; padding-bottom: var(--s-1); }
.related-card {
  display: flex; align-items: center; gap: var(--s-3);
  flex: 0 0 auto; min-width: 230px; max-width: 340px; min-height: 44px;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--line); border-radius: var(--r);
  background: var(--surface); color: var(--ink);
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}
@media (hover: hover) { .related-card:hover { border-color: var(--line-strong); box-shadow: var(--sh-sm); } }
/* ลูกศรท้ายการ์ดต้องชี้ "ขวา" (ไปต่อ) — ไอคอน caret ชุดเราวาดชี้ลง */
.related-card > .ic:last-child { transform: rotate(-90deg); color: var(--faint); flex: 0 0 auto; }
.related-tx { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.related-tx strong { font-size: var(--t-label); }
.related-why { font-size: var(--t-label); color: var(--muted); line-height: 1.5; }
@media (max-width: 640px) {
  .related-card { min-width: 200px; }
  .help-drawer { width: 100vw; border-left: 0; }
}

.empty { text-align: center; padding: var(--s-9) var(--s-5); color: var(--muted); }
/* ⚠️ ลูกตรงเท่านั้น — ไม่ใช่ไอคอนทุกตัวใน .empty
   ดูเหตุผลเต็มที่กฎ `.empty > .ic` ในหมวด empty states ข้างล่าง */
.empty > .ic { color: var(--n-300); margin-bottom: var(--s-3); }
:root[data-theme="dark"] .empty > .ic { color: var(--line-strong); }
.empty h3 { color: var(--ink-2); font-size: var(--t-heading); margin-bottom: var(--s-2); }
.empty p { max-width: 44ch; margin: 0 auto var(--s-5); font-size: var(--t-body); line-height: 1.65; }

/* ⚠️ หน้าจอว่างคือหน้าที่ไม่มีอะไรให้ทำเลยนอกจากปุ่มนี้ปุ่มเดียว (ใช้ใน 91 ไฟล์)
   ปุ่มจึงควรมีน้ำหนักมากกว่าปุ่มทั่วไป — ให้ขนาดจากที่นี่ที่เดียว
   ไม่ต้องแก้ 91 ไฟล์ และไม่ต้องมีคลาสใหม่ให้ใครจำว่าต้องใส่ */
.empty .btn {
  position: relative;
  min-height: 46px;
  padding: 12px var(--s-6);
  font-size: var(--t-section);
}

/* ⚠️ ตัวหนังสือต้องอยู่กึ่งกลาง "ปุ่ม" ไม่ใช่กึ่งกลาง "ไอคอน+ตัวหนังสือ"
 *
 * เจ้าของทักว่า "ข้อความมันไม่อยู่ตรงกลาง" — วัดแล้วเยื้องขวา 12px จริง
 * เพราะ flex จัดกึ่งกลางทั้งกลุ่ม ไอคอน 17px + ช่องไฟ 7px จึงดันตัวหนังสือ
 * ไปทางขวาครึ่งหนึ่งของ 24px พอดี
 *
 * บนหน้าจอว่าง ทุกอย่างจัดกึ่งกลางเป็นแกนเดียวกัน (ไอคอนวงกลม · หัวข้อ
 * · คำอธิบาย) ตาจึงเทียบตัวหนังสือบนปุ่มกับบรรทัดข้างบนแล้วเห็นว่าเหลื่อม
 * — บนแถบเครื่องมือที่ปุ่มเรียงกันเป็นแถว การจัดกึ่งกลางทั้งกลุ่มถูกต้องแล้ว
 * จึงแก้เฉพาะที่นี่ ไม่ใช่ทั้งระบบ
 *
 * ⚠️ แก้ด้วย CSS ที่เดียว — ปุ่มแบบนี้มี 17 จุดใน 91 ไฟล์ที่ใช้ .empty
 * การไล่แก้มาร์กอัปทีละจุดคือการรอให้จุดที่ 18 พลาดซ้ำ (บทเรียนข้อ 85)
 */
.empty .btn:has(> .ic:first-child) {
  padding-inline-end: calc(var(--s-6) + 17px + 7px);
}
/* หน้าว่างที่มีสองทางเลือก (ออกใบแรก · เริ่มจากใบเสนอราคา) — ปุ่มกว้างขั้นต่ำเท่ากัน
   UAT ดีไซน์ V4 (มือถือ 390): ตัดลงคนละบรรทัดแล้วกว้าง 185 กับ 179px ซ้อนกันดูเหลื่อม
   บนจอกว้างวางคู่กันก็ยาวเท่ากัน · min() กันจอแคบกว่าปุ่ม */
.empty:has(> .btn ~ .btn) > .btn { min-width: min(100%, 15rem); }

/*
 * ⚠️ ทำไมเป็นช่องว่างด้านขวา ไม่ใช่ position: absolute หรือ grid
 *
 * · `position: absolute` ใช้ไม่ได้ — `.ic { flex: 0 0 auto }` คือสิ่งเดียว
 *   ที่ทำให้ไอคอนคงขนาดตามแอตทริบิวต์ พอถอดออกจากสายการจัดวาง กฎนั้นหมดผล
 *   แล้ว svg กางเต็มพื้นที่ (วัดได้จริง: 17px กลายเป็น 57px ทับตัวหนังสือ)
 *
 * · `display: grid` สามคอลัมน์จัดกึ่งกลางได้ถูกต้อง (วัดแล้วเยื้อง 0)
 *   แต่มันเปลี่ยนปุ่มจากระดับ inline เป็นระดับบล็อก ซึ่งเปลี่ยนพฤติกรรม
 *   ความกว้างของปุ่มทุกใบใน .empty — แก้เรื่องหนึ่งแล้วเปลี่ยนอีกเรื่อง
 *
 * · `::after` เป็นช่องว่างก็ใช้ไม่ได้ — วัดแล้วมันไม่กินพื้นที่จริง
 *   (computed บอกว่ากว้าง 17px แต่ความกว้างปุ่มไม่เปลี่ยน)
 *
 * เพิ่มระยะขอบด้านขวาเท่ากับ (ไอคอน + ช่องไฟ) แทน ซึ่งเป็นกลไกที่แน่นอน:
 *   ซ้าย 24 + [ไอคอน 17 + ช่องไฟ 7 + ตัวหนังสือ] + ขวา 48
 *   ⇒ กึ่งกลางของ "ตัวหนังสือ" ตรงกับกึ่งกลางของ "ปุ่ม" พอดี
 *
 * ⚠️ 17px ตรงกับขนาดไอคอนที่ปุ่มในหน้าจอว่างใช้ทั้งหมด (icon(..., 17))
 * ถ้าวันหนึ่งมีคนใช้ 16 หรือ 20 ความเยื้องจะเป็น 0.5–1.5px ซึ่งมองไม่เห็น
 * — ต่างจากของเดิมที่เยื้อง 12px และเจ้าของมองเห็นทันที
 */

/* ==========================================================================
   Controls
   ========================================================================== */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  min-height: 40px; padding: 9px var(--s-4);
  /* ⚠️ 12px ไม่ใช่ 9px — ปุ่มสูง 40px ที่มุมมน 9px อ่านว่าเป็นกล่องเหลี่ยม
     ค่านี้อยู่ในสเกลเดิม (--r) จึงไม่เพิ่มขั้นความมนให้ระบบ */
  border-radius: var(--r); border: 1px solid transparent;
  font-family: var(--font-body); font-size: var(--t-body); font-weight: 600; line-height: 1.55;
  cursor: pointer; white-space: nowrap; text-decoration: none;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease),
              box-shadow var(--fast) var(--ease), color var(--fast) var(--ease);
}
.btn:active { transform: translateY(.5px); }
.btn:disabled, .btn.disabled { opacity: .5; cursor: not-allowed; }

/* ⚠️ สีทึบเรียบ ไม่ใช่ไล่สีแนวตั้ง
   ไล่สีจากอ่อนบนลงเข้มล่าง + ไฮไลต์ขาวขอบบน คือภาษาปุ่มยุค 2010
   ที่จำลองแสงตกกระทบวัตถุจริง — เจ้าของอ่านออกทันทีว่า "โบราณมาก"
   ความลึกมาจากเงาสีเดียวกับปุ่มที่ตกลงด้านล่างแทน (ดู --glow-brand) */
.btn-primary {
  background: var(--brand);
  color: var(--on-brand);
}
@media (hover: hover) { .btn-primary:hover {
  background: var(--brand-ink);
  color: var(--on-brand);
} }
.btn-ghost { background: var(--surface); border-color: var(--line-strong); color: var(--ink-2); }
@media (hover: hover) { .btn-ghost:hover { background: var(--surface-2); color: var(--ink); border-color: var(--n-300); } }
@media (hover: hover) { :root[data-theme="dark"] .btn-ghost:hover { border-color: var(--line-strong); } }
.btn-soft { background: var(--brand-soft); color: var(--brand-ink); border-color: transparent; }
@media (hover: hover) { .btn-soft:hover { background: color-mix(in srgb, var(--brand-soft) 72%, var(--brand) 28%); color: var(--brand-ink); } }
.btn-danger { background: var(--danger); color: var(--on-danger); box-shadow: var(--sh-xs); }
@media (hover: hover) { .btn-danger:hover { filter: brightness(1.07); color: var(--on-danger); } }
.btn-sm { min-height: 32px; padding: 4px var(--s-3); font-size: var(--t-label); border-radius: var(--r-xs); }
.btn-lg { min-height: 46px; padding: 12px var(--s-6); font-size: var(--t-section); }
.btn-block { width: 100%; }
.btn-icon { padding: 9px; min-width: 40px; }
/* ปุ่มไอคอนในแถวตาราง (ปุ่มพิมพ์ของขาย/ซื้อ) ไม่มีขอบบนเครื่องที่มีเมาส์ (แผน V4 W4 · ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · C22)
   30 แถว × กรอบเส้นขอบ = 30 กล่องต่อจอที่ไม่ได้บอกอะไรเพิ่ม · ขอบกลับมาตอนชี้ (บอกว่ากดได้) · วงโฟกัสยังเป็นของกลางตามเดิม
   ⚠️ เฉพาะ (hover: hover) และ (pointer: fine) — จอสัมผัสไม่มีสถานะชี้ ปุ่มที่ไม่มีขอบจะอ่านเป็นไอคอนประดับ
   ⚠️ คลาสระบุตรง ๆ ที่ปุ่ม ไม่เดาจาก :has(> svg:only-child) — CSS มองไม่เห็นโหนดข้อความ ปุ่มที่มีทั้งไอคอนและคำจะโดนไปด้วย */
@media (hover: hover) and (pointer: fine) {
  table.tbl td .btn-ghost.btn-row-icon { border-color: transparent; background: transparent; }
  table.tbl td .btn-ghost.btn-row-icon:hover { border-color: var(--line-strong); background: var(--surface-2); }
}

.field { margin-bottom: var(--s-4); }
.field label, .form-label {
  display: block; font-size: var(--t-label); font-weight: 600;
  color: var(--ink-2); margin-bottom: 6px;
}

/* ⚠️ แถวป้ายของทุกช่องในแถวเดียวกันต้องสูงเท่ากัน — ไม่งั้นช่องกรอกเหลื่อม

   ต้นเหตุที่เจอจริง (31 ส.ค. 2569): ป้าย "ลูกค้า" มีปุ่ม "+ ลูกค้าใหม่"
   อยู่ข้างใน แถวป้ายเลยสูง 32px ขณะที่ป้ายเปล่าข้าง ๆ สูง 21px
   → ช่องกรอกสองคอลัมน์เริ่มคนละระดับ ต่างกัน 11px ทั้งแถว

   แก้ที่กฎเดียว: ป้ายทุกใบเป็นกล่อง flex สูงขั้นต่ำเท่ากัน (--s-6 = 24px)
   และปุ่มที่อยู่ "ในป้าย" ถูกกดให้พอดีความสูงนั้น
   ⚠️ ยกเว้น .check — ป้ายติ๊กเป็นคนละคอมโพเนนต์ มีจังหวะของตัวเอง */
.field > label:not(.check) {
  display: flex; align-items: center; gap: var(--s-2);
  min-height: var(--s-6);
}
.field > label .btn {
  min-height: var(--s-6); height: var(--s-6);
  padding-top: 0; padding-bottom: 0;
}
/* ป้ายติ๊กที่อยู่ในช่องฟอร์ม — `.field label` (0,1,1) ชนะ `.check` (0,1,0)
   ป้ายจึงเคยเป็น block ธรรมดา กล่องติ๊กนั่งบนเส้นฐานของตัวหนังสือ แล้วเหลื่อม
   ลงล่างราว 12px ทุกจุด (เห็นชัดที่ "จดจำฉันไว้" หน้าเข้าสู่ระบบ) · ยังเป็นแถวเต็ม
   ความกว้างเหมือนเดิม (เรียงลงล่าง · แตะได้ทั้งแถว) แต่กล่องตรงกับบรรทัดแรก:
   ป้ายยาวหลายบรรทัดกล่องอยู่บรรทัดแรก ไม่ลอยกลางย่อหน้า · ป้ายบรรทัดเดียว
   ถูกจัดกลางในความสูง 44px ของจอสัมผัสด้วย align-content */
.field label.check {
  display: grid; grid-template-columns: auto minmax(0, 1fr); grid-auto-flow: column;
  align-items: start; align-content: center; column-gap: var(--s-2);
}
.field .hint { font-size: var(--t-label); color: var(--muted); margin-top: 4px; line-height: 1.5; }
.req { color: var(--danger); font-weight: 700; }

.input, .select, textarea.input, select.select {
  width: 100%; min-height: 40px; padding: 9px var(--s-3);
  font-family: inherit; font-size: var(--t-body); color: var(--ink);
  background: var(--surface);
  /* V4: ขอบตัวควบคุม ≥3:1 ทั้งตอนพักและตอนชี้ (WCAG 1.4.11 · ลูกค้า 40+ ต้องเห็นว่าตรงไหนพิมพ์ได้ · C32 ยกจาก 2:1)
     ⛔ ห้ามถอดขอบช่องกรอกเพื่อให้ตัวชี้วัด "กล่อง" ลดลง */
  border: 1px solid var(--ctl-line);
  border-radius: var(--r-sm);
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}
@media (hover: hover) { .input:hover, .select:hover { border-color: var(--ctl-line-hi); } }
.input:focus, .select:focus, textarea.input:focus {
  outline: none; border-color: var(--brand);
}
.input::placeholder { color: var(--faint); }
.input:disabled, .input[readonly] { background: var(--surface-2); color: var(--muted); box-shadow: none; }
.input.num, input.money { text-align: right; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
textarea.input { min-height: 88px; resize: vertical; line-height: 1.6; }

/* ---- input[type=file] — ปุ่ม "Choose File" เป็นของเบราว์เซอร์ ไม่ใช่ของเรา ----
   Chrome วาด ::file-selector-button เป็นก้อนเทาอ่อนตัวหนังสือดำ
   (rgb 240,240,240 / 0,0,0) โดยไม่สนธีมของหน้า และ font ของมันไม่ inherit
   (บทเรียนข้อ 15) — วัดก่อนแก้: 8 จุดใน 7 หน้าโผล่เป็นก้อนสว่างกลางฟอร์มมืด
   ทุกอย่างที่ตาเห็นต้องถูกตั้งทับที่ pseudo-element เอง ไม่มีทางสืบทอดถึง */
.input[type="file"] { padding: var(--s-1) var(--s-3) var(--s-1) var(--s-1); cursor: pointer; line-height: 1.6; }
.input[type="file"]::file-selector-button {
  font: inherit; font-weight: 600; line-height: 1.5;
  color: var(--ink-2); background: var(--surface-2);
  border: 1px solid var(--line-strong); border-radius: var(--r-xs);
  padding: var(--s-1) var(--s-3); margin: 0 var(--s-3) 0 0; cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.input[type="file"]:focus::file-selector-button { color: var(--ink); background: var(--surface-3); }
@media (hover: hover) { .input[type="file"]:hover::file-selector-button { color: var(--ink); background: var(--surface-3); } }
.input[type="file"]:disabled::file-selector-button { color: var(--muted); cursor: not-allowed; }

/* ---- .dropzone — พื้นที่ลากไฟล์มาวาง (รับบิลเข้าระบบ · นำเข้าข้อมูล) ----
   ⚠️ ไม่ใช่ชั้นวาดทับแบบ controls.js — input[type=file] ตัวจริงอยู่ "ใน" ป้ายนี้
   ซ่อนด้วย .sr-only แต่ยังโฟกัสได้ จึงเป็นจุด Tab เพียงจุดเดียว (บทเรียนข้อ 17
   กลับด้าน: ไม่มีของซ่อนที่โฟกัสได้ซ้อนกับของจริง) กรอบสะท้อนโฟกัสด้วย
   :focus-within และเพราะเป็น <label> ครอบ input กดตรงไหนของกรอบก็เปิด
   ตัวเลือกไฟล์ได้โดยไม่ต้องใช้ JS · forms.js เติมแค่การรับไฟล์ที่วาง + ชิปชื่อไฟล์
   ⚠️ selector ที่สองต้องอยู่ เพราะ `.field > label:not(.check)` (0,2,1) ตั้ง
   min-height 24px และ gap ไว้ให้ป้ายทุกใบ — `.dropzone` เปล่า ๆ (0,1,0) แพ้ */
.dropzone, .field > label.dropzone {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--s-1);
  position: relative; min-height: var(--s-9); margin: 0; padding: var(--s-6) var(--s-4);
  text-align: center; cursor: pointer; letter-spacing: 0;
  font-size: var(--t-body); font-weight: 600; color: var(--ink);
  background: var(--surface-2);
  border: 2px dashed var(--line-strong); border-radius: var(--r);
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease), color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}
@media (hover: hover) { .dropzone:hover { border-color: var(--brand); background: var(--surface-3); } }
.dropzone:focus-within { border-color: var(--brand); border-style: solid; box-shadow: var(--ring); }
.dropzone.is-over { border-color: var(--brand); border-style: solid; color: var(--brand-ink); background: var(--brand-soft); }
.dropzone .ic { color: var(--brand); }
.dropzone.is-over .ic { color: var(--brand-ink); }
.dz-title { font-size: var(--t-body); font-weight: 600; color: inherit; }
.dropzone .dz-hint { font-size: var(--t-micro); font-weight: 400; color: var(--muted); }
.dropzone.is-over .dz-hint { color: inherit; }
/* ชิปชื่อไฟล์ที่เลือก — forms.js สร้างกล่องนี้ต่อท้ายกรอบเมื่อมีไฟล์ */
.dz-files { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-top: var(--s-2); }
.dz-files:empty { display: none; }
.dz-files .chip { max-width: 100%; min-width: 0; }
.dz-files .dz-name { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Specificity note: the shared field rule above matches this element through
   `select.select` (0,1,1). A bare `.select` (0,1,0) would lose, and the chevron
   would tile across the control — so the chevron rules must match it at least
   as strongly. */
select.select, .select {
  appearance: none; -webkit-appearance: none; cursor: pointer;
  padding-right: 34px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236B7A6B' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 16px 16px;
}
:root[data-theme="dark"] select.select, :root[data-theme="dark"] .select {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2393A398' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 16px 16px;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) select.select,
  :root:not([data-theme="light"]) .select {
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2393A398' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 11px center;
    background-size: 16px 16px;
  }
}

/* ⚠️ ไม่ตัดบรรทัด (UAT งานคุมเงิน · 29 ก.ย. 2569) — ค่าของโล่ Bootstrap (flex-wrap: wrap) + .input กว้าง 100% ที่ไม่มีกฎ flex
   ⇒ ส่วนต่อท้าย (ปุ่มคัดลอก · คำว่า "บาท") ตกไปอยู่บรรทัดใต้ช่องทุกที่ (15 มุมมอง) และปุ่มคัดลอกสูงแค่ 26px
   ช่องหดได้ (min-width: 0) · ส่วนต่อท้ายคงขนาด · สูงเท่าช่องด้วย stretch */
.input-group { display: flex; flex-wrap: nowrap; }
.input-group > .input, .input-group > .select { flex: 1 1 0%; min-width: 0; }
.input-group > .addon { flex: 0 0 auto; }
.input-group .input { border-radius: var(--r-sm) 0 0 var(--r-sm); }
.input-group .addon {
  display: inline-flex; align-items: center; padding: 0 var(--s-3);
  background: var(--surface-2); border: 1px solid var(--line-strong); border-left: 0;
  border-radius: 0 var(--r-sm) var(--r-sm) 0; font-size: var(--t-body); color: var(--muted); white-space: nowrap;
}
/* An addon that is actually a button (copy to clipboard) must look pressable. */
button.addon {
  cursor: pointer; font-family: inherit;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
@media (hover: hover) { button.addon:hover { background: var(--surface-3); color: var(--ink); } }
button.addon:active { background: var(--brand-soft); color: var(--brand-ink); }
.is-invalid { border-color: var(--danger) !important; }
.invalid-msg { color: var(--danger); font-size: var(--t-label); margin-top: 5px; }

.check { display: inline-flex; align-items: center; gap: var(--s-2); cursor: pointer; font-size: var(--t-body); min-height: 24px; }
/* WCAG 2.2 target size (minimum) is 24×24 CSS px, and a 17px box misses it on
   every device. The label is the real target, so it carries the height. */
.check input,
input[type="checkbox"], input[type="radio"] {
  width: 20px; height: 20px; accent-color: var(--brand); cursor: pointer; flex: 0 0 auto;
}

/* ตารางช่องติ๊กหลายช่อง — ใช้กับรายการที่ยาวไม่แน่นอน เช่น กิจการที่ผู้ใช้เข้าได้
   ⚠️ กฎของคอมโพเนนต์ต้องอยู่ด้วยกันทั้งชุดรวม media query ของมันเอง
   ไม่งั้นคนต่อกฎท้ายไฟล์จะเขียนทับแบบเงียบ ๆ (บทเรียนข้อ 18) */
.check-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: var(--s-2) var(--s-4);
  max-height: 190px; overflow-y: auto;
  padding: var(--s-3); border: 1px solid var(--line); border-radius: var(--r);
  background: var(--surface-2);
}

@media (max-width: 560px) {
  .check-grid { grid-template-columns: 1fr; max-height: 240px; }
}

.alert {
  display: flex; gap: var(--s-3); align-items: flex-start;
  padding: var(--s-3) var(--s-4); border-radius: var(--r);
  font-size: var(--t-body); line-height: 1.6; margin-bottom: var(--s-4);
  border: 1px solid transparent; font-weight: 500;
}
.alert-ok     { background: var(--ok-bg);     color: var(--ok);     border-color: var(--ok-line); }
.alert-warn   { background: var(--warn-bg);   color: var(--warn);   border-color: var(--warn-line); }
.alert-danger { background: var(--danger-bg); color: var(--danger); border-color: var(--danger-line); }
.alert-info   { background: var(--info-bg);   color: var(--info);   border-color: var(--info-line); }
/* The "go fix it" link that belongs to an alert always sits at its far end. */
.alert .cta { margin-left: auto; font-weight: 700; color: inherit; white-space: nowrap; }
@media (hover: hover) { .alert .cta:hover { color: inherit; text-decoration: underline; } }
/*
 * ⚠️ เป้ากดบนจอสัมผัส — ลิงก์นี้สูงเท่าบรรทัดข้อความ (วัดจริงบนมือถือ 390px ได้ 22px)
 *    ต่ำกว่าเกณฑ์ 44px ครึ่งหนึ่ง และมันคือปุ่มเดียวที่พาลูกค้าไปจ่ายเงิน
 *    ตอนระบบอยู่ในโหมดอ่านอย่างเดียว/ถูกล็อก — กดไม่โดนคือค้างอยู่ตรงนั้นทั้งวัน
 *
 *    ขยาย "พื้นที่กด" ด้วย ::after แทนการทำให้ตัวลิงก์สูงจริง เพื่อไม่ให้ความสูง
 *    ของแถบขยับบนทุกหน้า (บทเรียนข้อ 114) — ในแถบเตือนไม่มีอะไรทับพื้นที่นี้
 *    เพราะลิงก์อยู่ปลายแถวและส่วนที่ขยายออกไปตกอยู่ในช่องว่างของแถบเอง
 */
@media (any-pointer: coarse) {
  .alert .cta { position: relative; }
  .alert .cta::after {
    content: ''; position: absolute; left: 0; right: 0;
    top: 50%; height: 44px; transform: translateY(-50%);
  }
}
/* ⭐ แถบประกาศจากส่วนกลาง — ปุ่มปิดเป็น <button> ให้หน้าตาเหมือน .cta ที่เป็นลิงก์ · ที่มาของประกาศเป็นตัวเล็กจาง */
.alert button.cta { background: none; border: 0; padding: 0; cursor: pointer; font: inherit; font-weight: 700; color: inherit; }
.alert .hub-ann-from { display: block; font-size: var(--t-label); }
/* ⚠️ เดิม opacity .75 = 3.07–3.74:1 ในทุกโทนของธีมสว่าง ต่ำกว่า AA — ตัวเล็กกว่าพออยู่แล้วที่จะบอกว่าเป็นบรรทัดรอง (C17) */
/*
 * ⭐ แถบลิงก์ตั้งรหัสผ่านจากทีมกลาง (24 ก.ย. 2569) — ชื่อ/อีเมล/เหตุผลมาจากส่วนกลาง ยาวและไม่มีช่องว่างได้
 *    ต้องตัดบรรทัดได้บนมือถือ · ปุ่มขึ้นบรรทัดใหม่เมื่อจอแคบ ไม่ดันแถบกว้างเกินจอ (ผลตรวจ F11 · บทเรียนข้อ 39)
 */
.alert.owner-reset-bar { flex-wrap: wrap; align-items: center; }
/* [UAT3 A-9] กล่องลิงก์ลูกค้า (sales/view) — ช่อง URL ตั้งฐาน 320px แล้วขึ้นบรรทัดของตัวเองเมื่อที่ไม่พอ (เดิมถูกบีบเหลือ 112px บนจอ 390) */
.alert.share-box { flex-wrap: wrap; align-items: center; }
.owner-reset-bar .owner-reset-text { flex: 1 1 16rem; min-width: 0; overflow-wrap: anywhere; }
.owner-reset-bar .owner-reset-form { margin: 0 0 0 auto; }
/*
 * ⭐ ผลค้นเลขเอกสาร (26 ก.ย. 2569 · GoBen POS · plan/MASTER_PROMPT_EXTERNAL_REF_EXACT_TH.md)
 *    กล่องสภาพผลค้น (partials/doc_search_state.php) — หัวข้อ + คำอธิบาย + รายการใบที่ตัวกรองซ่อนในกล่องเดียว ตัดบรรทัดได้
 *    · บรรทัด "ตรงด้วยเลขอะไร" ใต้ชื่อคู่ค้า (doc_search_hit) — เลขเป็นฟอนต์ตัวเลข อ่านเทียบกับคำค้นทีละตัว
 */
.search-state .search-state-body { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.search-state .search-state-hint { display: block; font-weight: 400; }
.search-state .search-state-list { margin: var(--s-2) 0 0; padding-left: var(--s-5); font-weight: 400; }
/* [UAT3] กล่องใบสำคัญนอกช่วงวันที่ (accounting/journal) มีลิงก์ขยายช่วง — ลิงก์ขึ้นบรรทัดของตัวเองเมื่อจอแคบ ไม่บีบคำอธิบาย */
.alert.search-state:has(> .cta) { flex-wrap: wrap; align-items: center; }
.search-hit { display: block; margin-top: 2px; color: var(--muted); font-size: var(--t-label); font-weight: 400; }
/* ชิปในบรรทัดนี้เตี้ยกว่าชิปสถานะ — มันเป็นป้ายกำกับของบรรทัด ไม่ใช่สถานะของแถว และบรรทัดสามบนมือถือต้องไม่ดันแถวเกินเพดาน A5 */
.search-hit .chip { margin-right: var(--s-2); padding: 1px var(--s-2); line-height: 1.5; }
.search-hit-v { color: var(--ink-2); font-family: var(--font-num); white-space: nowrap; } /* เลขอ่านเป็นก้อนเดียวแบบคอลัมน์เลขที่ (UAT เดสก์ท็อป: "GBN-UAT0926-" / "12-R1") */

/* ---- code sample ---- */
.code, pre.code {
  margin: 0; padding: var(--s-3) var(--s-4);
  background: var(--surface-sunk); border: 1px solid var(--line);
  border-radius: var(--r-sm); overflow-x: auto; white-space: pre;
  font-family: var(--font-num); font-size: var(--t-label); line-height: 1.6; color: var(--ink-2);
}
/* Views write <code> naturally for URLs, header names and field names, so the
   bare element carries the style rather than requiring a class. */
.code-inline, code {
  font-family: var(--font-num); font-size: var(--t-micro);
  background: var(--surface-sunk); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r-xs);
  padding: 1px 6px;
  /* Long values (webhook URLs, JSON samples) wrap instead of widening the row */
  word-break: break-word; overflow-wrap: anywhere;
}
pre.code code, .code code { background: none; border: 0; padding: 0; font-size: inherit; }

/* ---- proportion bar (share of total, stock level, budget usage) ---- */
.bar {
  height: 8px; border-radius: var(--r-pill); overflow: hidden;
  background: var(--surface-3); min-width: 64px;
}
.bar-fill {
  height: 100%; border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--brand-hi), var(--brand));
  transition: width .4s var(--ease);
}
/* Light-to-dark like the main bar, rather than fading out to nothing. Fading
   the far end made the bar look shorter than the figure it stands for. */
.bar-fill.warn {
  background: linear-gradient(90deg, color-mix(in srgb, var(--warn) 72%, var(--surface)), var(--warn));
}
.bar-fill.danger {
  background: linear-gradient(90deg, color-mix(in srgb, var(--danger) 72%, var(--surface)), var(--danger));
}

/* ==========================================================================
   Auth
   ========================================================================== */
.auth-wrap { min-height: 100vh; display: grid; grid-template-columns: 1.02fr .98fr; }
.auth-art {
  position: relative; overflow: hidden; color: #fff;
  background: linear-gradient(155deg, #07281F 0%, #0B503B 42%, #14795A 100%);
  display: flex; flex-direction: column; justify-content: space-between;
  padding: var(--s-8) var(--s-9);
}
.auth-art::before {
  content: ""; position: absolute; inset: 0; opacity: .55;
  background:
    radial-gradient(820px 460px at 92% 8%, rgba(122,220,180,.20), transparent 60%),
    radial-gradient(620px 520px at 4% 96%, rgba(0,0,0,.42), transparent 62%);
}
/* A faint ledger rule pattern — the subject's own material */
/* ⚠️ กฎนี้เคยถูกประกาศซ้ำอีกครั้งท้ายไฟล์ แล้วผสมกันเป็นผลที่ไม่มีใครตั้งใจ
   ตัวล่างแทนที่ background ทั้งก้อน (เท็กซ์เจอร์หาย) แต่ opacity: .05
   ของตัวบนยังอยู่ (ไฟถูกกดเหลือ 5%) — รวมกันแล้วเสียทั้งสองอย่าง
   ตอนนี้รวมเป็นกฎเดียว เท็กซ์เจอร์เป็นเลเยอร์บนสุดที่มีความจางของตัวเอง */
.auth-art::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
}
.auth-art > * { position: relative; z-index: 1; }
.auth-art .brand-mark {
  width: 44px; height: 44px; flex: 0 0 44px; font-size: var(--t-heading);
  border-radius: var(--r);
  background: rgba(255,255,255,.14); backdrop-filter: blur(8px);
  box-shadow: var(--edge);
  color: #fff;
}
/* The mark sits inside a tile here, so it wants to be smaller than the
   free-standing 34px version used on the pale pages. */
.auth-art .brand-mark svg { width: 26px; height: 26px; }
.auth-art h2 {
  /* V4 (W10ข): 18ch — วลีห้ามแยก (span.u-nowrap) "บัญชี ภาษี และเงินเดือน" ต้องพอดีหนึ่งบรรทัดที่ 32px */
  color: #fff; font-size: var(--t-hero); line-height: 1.26; max-width: 18ch;
  margin: 0 0 var(--s-3); letter-spacing: -.01em; font-weight: 600;
}
.auth-art p { color: rgba(255,255,255,.78); max-width: 44ch; font-size: var(--t-section); line-height: 1.68; }
.auth-points { list-style: none; padding: 0; margin: var(--s-6) 0 0; display: grid; gap: var(--s-3); }
.auth-points li { display: flex; align-items: flex-start; gap: var(--s-3); color: rgba(255,255,255,.93); font-size: var(--t-section); }
.auth-points .tick {
  flex: 0 0 21px; width: 21px; height: 21px; border-radius: 50%; margin-top: 1px;
  background: rgba(255,255,255,.17); display: grid; place-items: center;
  box-shadow: var(--edge-glass);
}
.auth-foot { font-size: var(--t-label); color: rgba(255,255,255,.72); }
/* V4 (G3): ลิงก์ในส่วนท้ายเคยได้สีลิงก์ปกติ (เขียวแบรนด์) บนพื้นเขียวเข้ม — คอนทราสต์ต่ำกว่า 2:1 */
.auth-foot a { color: rgba(255,255,255,.92); text-decoration: underline; text-underline-offset: 2px; }

.auth-panel { display: flex; align-items: center; justify-content: center; padding: var(--s-8) var(--s-6); background: var(--bg); }
.auth-card { width: 100%; max-width: 404px; }
.auth-card h1 { font-size: var(--t-display); margin-bottom: 5px; }
.auth-card .lead { color: var(--muted); margin-bottom: var(--s-6); font-size: var(--t-section); }

.demo-users {
  margin-top: var(--s-6); border: 1px dashed var(--line-strong);
  border-radius: var(--r); padding: var(--s-3) var(--s-4); background: var(--surface);
}
.demo-users .eyebrow { margin-bottom: var(--s-2); }
.demo-user {
  display: flex; align-items: center; gap: var(--s-2); width: 100%; text-align: left;
  padding: 7px var(--s-2); border-radius: var(--r-xs); border: 0; background: transparent;
  cursor: pointer; font-family: inherit; font-size: var(--t-label); color: var(--ink-2);
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
@media (hover: hover) { .demo-user:hover { background: var(--brand-soft); color: var(--brand-ink); } }
.demo-user .who { font-weight: 600; }
.demo-user .mail { color: var(--faint); font-size: var(--t-label); margin-left: auto; }

/* ==========================================================================
   Overlays
   ========================================================================== */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 90;
  background: color-mix(in srgb, var(--n-950) 52%, transparent);
  backdrop-filter: blur(3px);
  display: none; align-items: flex-start; justify-content: center;
  padding: 6vh var(--s-4); overflow-y: auto;
}
/* ⚠️ fill-mode: both คือตัวกันกระพริบ — วัดจริง 31 ส.ค. 2569:
   แอนิเมชัน fade-in ของ backdrop เริ่มช้าได้ถึง ~120ms (คอมโพสิต
   backdrop-filter) ระหว่างรอ ธาตุแสดงค่าปกติ opacity:1 → ผู้ใช้เห็น
   โมดัลเต็ม แล้ววูบหายไปเฟรมแรกของแอนิเมชัน (0) แล้วค่อยเฟดกลับ
   = "กระพริบตื้ด" ที่เจ้าของรายงาน · both ตรึงธาตุที่เฟรมแรกตั้งแต่
   ก่อนแอนิเมชันเริ่ม จึงเฟดขึ้นครั้งเดียวเนียน ๆ */
.modal-backdrop.show { display: flex; opacity: 1; animation: fade-in var(--mid) var(--ease) both; }

/* ⚠️ ขาออกต้องมี ไม่งั้นของที่ค่อย ๆ มาจะหายวับ ซึ่งรู้สึกเหมือนถูกตัดบท
   และต้องเร็วกว่าขาเข้าราวสองในสาม — ขาออกที่ช้าเท่าขาเข้าทำให้รู้สึกหน่วง
   เพราะคนตัดสินใจปิดไปแล้ว ไม่ได้กำลังรอดูอะไร */
@keyframes fade-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes modal-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(-8px) scale(.99); } }
.modal-backdrop.closing { animation: fade-out 160ms var(--ease) forwards; }
.modal-backdrop.closing .modal { animation: modal-out 160ms var(--ease) forwards; }
.modal {
  background: var(--surface); border-radius: var(--r-lg); width: 100%; max-width: 640px;
  border: 1px solid var(--line); overflow: hidden;
  animation: modal-in .24s var(--ease) both;

  /* ⚠️ Bootstrap ก็มีคลาส .modal ของมันเอง (display:none · position:fixed
     · pointer-events:none ฯลฯ) — การชนะของ CSS เป็นรายพร็อพเพอร์ตี้
     พร็อพเพอร์ตี้ที่เราไม่เคยประกาศจึงรั่วจาก Bootstrap มาเงียบ ๆ
     ผลจริง: โมดัลทุกตัวทั้งแอปเปิดแล้วได้จอมืดเปล่า (เจ้าของกดลูกค้าใหม่
     แล้วไม่มีอะไรขึ้น 31 ส.ค. 2569) — ประกาศทับให้ครบทุกตัวที่มันตั้ง
     เกต qa:bootstrap-collision เฝ้าการรั่วชนิดนี้ทั้งไฟล์แล้ว */
  display: block;
  position: static;
  top: auto; left: auto;
  height: auto;
  z-index: auto;
  margin: 0;
  outline: 0;
  pointer-events: auto;
}
.modal.wide { max-width: 960px; }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-in { from { opacity: 0; transform: translateY(-12px) scale(.985); } to { opacity: 1; transform: none; } }
.modal-head { display: flex; align-items: center; gap: var(--s-3); padding: var(--s-5) var(--s-6); border-bottom: 1px solid var(--line); }
.modal-head h3 { margin: 0; font-size: var(--t-heading); }
.modal-body { padding: var(--s-6); }
.modal-foot {
  display: flex; gap: var(--s-2); justify-content: flex-end;
  padding: var(--s-4) var(--s-6); border-top: 1px solid var(--line); background: var(--surface-2);
}

.dropdown { position: relative; }
.dropdown-menu {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 50; min-width: 226px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); padding: 5px; display: none;
}
.dropdown-menu.show { display: block; animation: modal-in .16s var(--ease); }

/* ⚠️ บนจอแคบ แผงที่ยึดขอบขวาของปุ่ม จะไหลออกนอกจอด้านซ้าย
   (เจ้าของเจอเองบนมือถือ 6 ก.ย. 2569 — แผงสลับบริษัทกว้าง 275px
   ยึดขวาที่ x=193 บนจอ 390 ⇒ ขอบซ้ายอยู่ที่ -82px คำว่า "สลับบริษัท"
   เหลือ "ลับบริษัท" และไอคอนหน้ารายการหายไปทั้งคอลัมน์)

   ⚠️ ไม่มีเกตไหนจับได้ตามนิยาม: แผงที่ยื่นออกไปทางซ้าย **ไม่เพิ่ม
   scrollWidth** ตัวตรวจการเลื่อนแนวนอนจึงเงียบสนิท และตอนที่เกตวัดหน้า
   แผงยังปิดอยู่ ⇒ วัดผิดสถานะ (บทเรียนข้อ 20)

   ทางแก้: ให้กล่องอ้างอิงเป็นแถบบน ซึ่งกว้างเต็มจอ แล้วยึดสองข้าง
   แผงจึงกว้างเท่าจอเสมอ ไม่ว่าเนื้อหาจะยาวแค่ไหนหรือปุ่มจะอยู่ตรงไหน
   (`.topbar` เป็น position: sticky อยู่แล้ว จึงเป็น containing block ได้
   และ overflow: visible จึงไม่ตัดแผงทิ้ง)

   ⚠️ สองคลาสชนะคลาสเดียวเสมอ กฎนี้จึงชนะ `.dropdown-menu-wide`
   ที่ตั้ง max-width ไว้ 320px ได้ ไม่ว่าจะเขียนไว้บรรทัดไหนของไฟล์
   (บทเรียนข้อ 18 · 29 — แต่รอบนี้พึ่งน้ำหนัก selector ไม่ใช่ลำดับ) */
@media (max-width: 640px) {
  .topbar .dropdown { position: static; }
  .topbar .dropdown-menu {
    left: var(--s-2); right: var(--s-2);
    width: auto; min-width: 0; max-width: none;
  }
}
.dropdown-item {
  display: flex; align-items: center; gap: var(--s-2); width: 100%; text-align: left;
  padding: 9px var(--s-3); border-radius: var(--r-xs); border: 0; background: transparent;
  font-family: inherit; font-size: var(--t-body); color: var(--ink-2); cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
@media (hover: hover) { .dropdown-item:hover { background: var(--surface-2); color: var(--ink); } }
.dropdown-item.danger { color: var(--danger); }
@media (hover: hover) { .dropdown-item.danger:hover { background: var(--danger-bg); } }
.dropdown-divider { height: 1px; background: var(--line); margin: 5px 0; }
.dropdown-head { padding: var(--s-3) var(--s-3) var(--s-2); border-bottom: 1px solid var(--line); margin-bottom: 5px; }
.dropdown-head strong { display: block; font-size: var(--t-body); color: var(--ink); }
.dropdown-head span { font-size: var(--t-micro); color: var(--faint); }

.toast-host { position: fixed; right: var(--s-5); bottom: var(--s-5); z-index: 100; display: flex; flex-direction: column; gap: var(--s-2); }
.toast {
  background: var(--surface); border: 1px solid var(--line); border-left: 3px solid var(--brand);
  border-radius: var(--r); padding: var(--s-3) var(--s-4);
  font-size: var(--t-body); min-width: 268px; max-width: 400px; color: var(--ink);
  animation: toast-in .24s var(--ease); transition: opacity var(--mid) var(--ease);
}
.toast { display: flex; align-items: baseline; gap: var(--s-3); }
/* ทางไปต่อของ toast อยู่ท้ายสุดเสมอ — รูปแบบเดียวกับ .alert .cta ในหน้าเต็ม
   เพื่อให้คนที่เห็นทั้งสองแบบเรียนรู้ครั้งเดียวพอ */
.toast-cta {
  margin-left: auto; white-space: nowrap; font-weight: 700;
  color: var(--brand); text-decoration: underline;
}
@media (hover: hover) { .toast-cta:hover { color: var(--brand-ink); } }
/* เป้ากดบนจอสัมผัส — เหตุผลเดียวกับ .alert .cta (toast หายเองใน 4 วินาที ยิ่งต้องกดโดนครั้งแรก) */
@media (any-pointer: coarse) {
  .toast-cta { position: relative; }
  .toast-cta::after {
    content: ''; position: absolute; left: 0; right: 0;
    top: 50%; height: 44px; transform: translateY(-50%);
  }
}
.toast.ok { border-left-color: var(--ok); }
.toast.danger { border-left-color: var(--danger); }
.toast.warn { border-left-color: var(--warn); }
@keyframes toast-in { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: none; } }

/* ==========================================================================
   Navigation bits
   ========================================================================== */
.filters { display: flex; gap: var(--s-3); flex-wrap: wrap; align-items: flex-end; margin-bottom: var(--s-5); }
.filters .field { margin: 0; min-width: 156px; }

/* ⚠️ เส้นจัดแถวของแถบตัวกรอง = ขอบล่างของช่องกรอก เสมอ

   .filters จัดลูกชิดขอบล่าง (flex-end) แต่ field ที่มีคำอธิบายใต้ช่อง
   (.hint หรือ .small) จะสูงกว่าเพื่อนด้วยความสูงของคำอธิบาย → ช่องกรอก
   ของมันลอยขึ้นเหนือเส้นแถวเท่านั้นพอดี วัดจริง: ar-aging เหลื่อม 29px
   tax/corporate 23px devconsole 23px (เจ้าของชี้ว่าทั้งโปรเจกต์เป็นแบบนี้
   — ถูกของเขา มี 6 หน้า)

   แก้โดยให้คำอธิบายลอย absolute ใต้เส้นแถว ไม่กินความสูงของ field
   และเผื่อระยะใต้แถบด้วย :has เฉพาะแถบที่มีคำอธิบายจริง */
.filters .field { position: relative; }
.filters .field > .hint,
.filters .field > .small {
  position: absolute; top: 100%; left: 0; right: 0;
  margin-top: var(--s-1);
}
.filters:has(.field > .hint),
.filters:has(.field > .small) { margin-bottom: calc(var(--s-5) + var(--s-6)); }

/* ⚠️ ตั้งบนกล่องที่ใช้ ไม่ใช่ :root ใน media query — Bootstrap ประกาศ :root ใน media ของมันเอง
   แล้วเกต qa_bootstrap_collision (ถูกต้อง) ถือว่าเราประกาศ :root ชนแต่ไม่ทับ scroll-behavior */
@media (any-pointer: coarse) { .filters, .page-head-actions { --ctl-h: 44px; } }
/* V4 (W7): จอกว้าง — แถวตัวกรองต้องจบในแถวเดียวที่ 1440 (ช่อง · ปุ่มลัดช่วงวันที่ · ปุ่มความหนาแน่น)
   ช่องวันที่ "วว/ดด/ปปปป" ต้องการ ~130px ไม่ใช่ 235px ที่มันยืดเอง · ช่องค้นหารับที่เหลือ
   ปุ่มความหนาแน่นเหลือไอคอน (ป้ายยังอยู่สำหรับโปรแกรมอ่านหน้าจอ + title) — ป้ายเต็มแสดงในช่วงแถวกระชับ
   ที่มันเปลี่ยนความหมายเป็น "แสดงทุกช่อง" */
@media (min-width: 861px) {
  .filters { column-gap: var(--s-2); }
  .filters .field { flex: 0 1 150px; min-width: 140px; }
  .filters .search-box { flex: 1 1 180px; }
}
@media (min-width: 1025px), (min-width: 861px) and (any-pointer: fine) {
  .density-btn { width: var(--ctl-h); padding-left: 0; padding-right: 0; justify-content: center; }
  .density-btn .density-label {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
}
/* ⚠️ height ไม่ใช่ min-height — ช่องกรอก (padding 9 + บรรทัด 22.5 + ขอบ) สูงเอง ~43px ขณะที่ปุ่มสูง 40
   ค่าต่ำสุดเท่ากันจึงยังได้แถวที่ต่างกัน 3–4px (วัดจริงบนหน้าขายที่ 1440) · ช่องกรอกถอด padding แนวตั้ง
   ให้เบราว์เซอร์จัดข้อความกลางกล่องเอง ตัวอักษรไทยที่มีสระบน/ล่างจึงไม่ถูกตัด */
.filters .input, .filters .select, .filters .btn, .filters .cbx-btn, .filters .datefield-text,
.filters .icon-btn, .page-head-actions .btn { min-height: var(--ctl-h); height: var(--ctl-h); }
.filters .input, .filters .select, .filters .datefield-text { padding-top: 0; padding-bottom: 0; }

/* V4 (W7): จอกว้าง — ป้ายของช่องตัวกรองนั่งบนเส้นขอบบนของช่อง (ป้ายยังเป็นข้อความจริง ผูก for= เดิม
   ไม่ใช่ placeholder ที่หายตอนพิมพ์) · แถวป้ายเดิมกินความสูง ~30px ทุกหน้ารายการ
   พื้นครึ่งล่างของป้าย = พื้นช่อง (กลบเส้นขอบ) ครึ่งบนโปร่ง = อยู่บนพื้นหน้าหรือในการ์ดก็ถูก */
@media (min-width: 861px) {
  .filters { margin-top: var(--s-2); }
  .filters .field > label:not(.check) {
    position: absolute; top: 0; left: 10px; z-index: 1;
    transform: translateY(-50%);
    display: block; min-height: 0; margin: 0; padding: 0 4px;
    max-width: calc(100% - 20px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: var(--t-micro); font-weight: 500; line-height: 1.3; color: var(--muted);
    background: linear-gradient(to bottom, transparent 50%, var(--surface) 50%);
  }
}
/* คำอธิบายใต้ช่องของแถบตัวกรองบนจอกว้าง — กลับเข้าลำดับปกติ (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · C10)
   กฎ "ลอย absolute + เผื่อที่ใต้แถบ 44px ตายตัว" ข้างบนออกแบบไว้ตอนช่องกว้างตามเนื้อ (~235px) และคำอธิบาย 12px
   V4 บีบช่องเหลือ 150px และยกคำอธิบายเป็น 14px → คำอธิบายงบแสดงฐานะการเงินตัดสามบรรทัด (63px)
   บรรทัดที่สามจมใต้การ์ดสรุปข้างล่าง · คอนโซลนักพัฒนาทับป้ายช่องถัดไป — ที่เผื่อตายตัวเดาความสูงคำอธิบายไม่ได้
   ทางที่ไม่ต้องเดา: จอกว้างป้ายลอยบนเส้นขอบแล้ว (บล็อกข้างบน) กล่องของทุกช่องจึงเริ่มที่ขอบบนของช่องกรอกเท่ากัน
   จัดแถบชิดบน (flex-start) แทนชิดล่าง ช่องกรอก/ปุ่ม (สูง --ctl-h เท่ากัน) ยังตรงแนวเดียวกัน
   แล้วคำอธิบายอยู่ในลำดับปกติใต้ช่อง แถบสูงตามคำอธิบายจริง ไม่ทับอะไรอีก
   · เฉพาะแถบที่มีคำอธิบาย (6 หน้า — ทุกลูกของแถบพวกนี้คือช่องกรอก/ปุ่มสูง --ctl-h) แถบอื่นชิดล่างตามเดิม
   · ช่องที่มีคำอธิบายได้ฐาน 240px (ใกล้ ~235px ก่อน V4) คำอธิบายจึงไม่ตัดเป็นเส้นแคบ ๆ สามสี่บรรทัด
   · คืน u-grow / u-fixed ที่ .filters .field { flex } ข้างบนทับไปเงียบ ๆ (น้ำหนัก selector สูงกว่าคลาสเดี่ยว)
   ⚠️ screen เท่านั้น — กระดาษแนวนอนกว้างเกิน 861 ห้ามเปลี่ยนรูป (แถบตัวกรองถูกซ่อนบนกระดาษอยู่แล้ว แต่ไม่พึ่งข้อนั้น) */
@media screen and (min-width: 861px) {
  .filters:has(.field > .hint),
  .filters:has(.field > .small) { align-items: flex-start; margin-bottom: var(--s-5); }
  .filters.u-mb-0:has(.field > .hint),
  .filters.u-mb-0:has(.field > .small) { margin-bottom: 0; }
  .filters .field > .hint,
  .filters .field > .small { position: static; }
  .filters .field:has(> .hint),
  .filters .field:has(> .small) { flex-basis: 240px; }
  .filters .field.u-grow { flex-grow: 1; }
  .filters .field.u-fixed { flex: 0 0 auto; }
}
.search-box { position: relative; flex: 1 1 260px; max-width: 380px; }
.search-box .ic { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--faint); pointer-events: none; }
.search-box .input { padding-left: 38px; }

.tabs {
  display: flex; gap: 2px; border-bottom: 1px solid var(--line);
  /* V4 (W7): s-4 — แท็บกับแถวตัวกรองข้างล่างเป็นชุดควบคุมเดียวกัน ไม่ต้องห่างเท่าระหว่างส่วน */
  margin-bottom: var(--s-4); overflow-x: auto; scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
/* V4 (W8): เลือนเฉพาะข้างที่ยังเลื่อนไปได้ (ux.js ติด data-overflow) — mask ไม่ต้องรู้สีพื้นข้างหลัง
   แถบแท็บจึงอยู่บนพื้นหน้าหรือในการ์ดก็ถูกเหมือนกัน */
.tabs[data-overflow="right"] { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent); mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent); }
.tabs[data-overflow="left"]  { -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 40px), transparent); mask-image: linear-gradient(to left, #000 calc(100% - 40px), transparent); }
.tabs[data-overflow="both"]  { -webkit-mask-image: linear-gradient(to right, transparent, #000 40px, #000 calc(100% - 40px), transparent); mask-image: linear-gradient(to right, transparent, #000 40px, #000 calc(100% - 40px), transparent); }

/* ⚠️ .tabs เป็นกล่องเลื่อน จึงตัดทุกอย่างที่ล้นออกนอกขอบ
   วงแหวนโฟกัสปกติวาดอยู่ "นอก" ขอบของแท็บ จึงถูกตัดหายเกือบหมด
   คนที่เดินด้วยคีย์บอร์ดแทบไม่เห็นว่าตัวเองอยู่แท็บไหน
   วงแหวนแบบเข้าใน (offset ติดลบ) วาดในขอบของตัวเอง จึงไม่มีอะไรให้ตัด */
.tab:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -3px;
  border-radius: var(--r-xs);
}
.tab {
  padding: 10px var(--s-4); border: 0; background: transparent; cursor: pointer;
  font-family: inherit; font-size: var(--t-body); font-weight: 500; color: var(--muted);
  border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
@media (hover: hover) { .tab:hover { color: var(--ink); } }
.tab.active { color: var(--brand-ink); border-bottom-color: var(--brand); font-weight: 600; }

.pagination { display: flex; gap: 4px; align-items: center; justify-content: center; padding: var(--s-4); flex-wrap: wrap; }
.pagination a, .pagination span {
  min-width: 36px; height: 36px; padding: 0 10px; border-radius: var(--r-xs);
  display: inline-grid; place-items: center; font-size: var(--t-body); font-weight: 500;
  border: 1px solid var(--line); color: var(--ink-2); background: var(--surface);
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
@media (hover: hover) { .pagination a:hover { background: var(--surface-2); border-color: var(--line-strong); } }
.pagination .current { background: var(--brand); color: var(--on-brand); border-color: var(--brand); font-weight: 600; }

/* ==========================================================================
   Document editor
   ========================================================================== */
.line-table { width: 100%; border-collapse: collapse; font-size: var(--t-body); }
.line-table th {
  font-size: var(--t-micro); text-transform: uppercase;
  color: var(--muted); font-weight: 600; padding: var(--s-2);
  border-bottom: 1px solid var(--line); text-align: left; white-space: nowrap;
}
.line-table td { padding: 5px 4px; border-bottom: 1px solid var(--line); vertical-align: top; }
/* The computed amount is text next to inputs — centre it on their line rather
   than nudging it down with padding in every view. */
.line-table td.amount { vertical-align: middle; font-weight: 600; color: var(--ink); }
.line-table .input-narrow { max-width: 132px; }
.line-table .input, .line-table .select { min-height: 36px; font-size: var(--t-body); padding: 6px 9px; box-shadow: none; }
.line-table .row-del {
  color: var(--faint); border: 0; background: transparent; cursor: pointer;
  padding: 8px; border-radius: var(--r-xs); line-height: 1;
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease);
}
@media (hover: hover) { .line-table .row-del:hover { color: var(--danger); background: var(--danger-bg); } }

.totals { margin-left: auto; width: 100%; max-width: 360px; }
.totals .row {
  display: flex; justify-content: space-between; align-items: center;
  padding: 6px 0; font-size: var(--t-body); color: var(--ink-2);
}
.totals .row span:last-child { font-variant-numeric: tabular-nums; font-weight: 500; color: var(--ink); }
.totals .row.grand {
  border-top: 2px solid var(--line-strong); margin-top: var(--s-2); padding-top: var(--s-3);
  font-size: var(--t-heading); font-weight: 600; color: var(--ink);
}
.totals .row.grand span:last-child {
  color: var(--brand-ink); font-family: var(--font-display);
  font-size: var(--t-title); letter-spacing: -.028em; font-weight: 600;
}

/* ==========================================================================
   Utilities

   The design system had no utility layer, so every screen reached for a style
   attribute and invented its own spacing — 5px here, 14px there, 18px on the
   next page. Same intent, different result, and the product read as assembled
   rather than designed.

   These are the only spacing values a page may use, and they are all on the
   4px scale. Prefix u- because component names are already taken (.row lives
   inside .totals, .num is a number cell): a utility must never collide with a
   component or the cascade decides the layout instead of the author.
   ========================================================================== */
.u-m-0  { margin: 0 !important; }
.u-mt-0 { margin-top: 0; }
.u-mt-1 { margin-top: var(--s-1); }
.u-mt-2 { margin-top: var(--s-2); }
.u-mt-3 { margin-top: var(--s-3); }
.u-mt-4 { margin-top: var(--s-4); }
.u-mt-5 { margin-top: var(--s-5); }
.u-mt-6 { margin-top: var(--s-6); }
.u-mb-0 { margin-bottom: 0; }
.u-mb-1 { margin-bottom: var(--s-1); }
.u-mb-2 { margin-bottom: var(--s-2); }
.u-mb-3 { margin-bottom: var(--s-3); }
.u-mb-4 { margin-bottom: var(--s-4); }
.u-mb-5 { margin-bottom: var(--s-5); }
.u-mb-6 { margin-bottom: var(--s-6); }
.u-ml-1 { margin-left: var(--s-1); }
.u-ml-2 { margin-left: var(--s-2); }
.u-ml-auto { margin-left: auto; }
.u-pt-1 { padding-top: var(--s-1); }
.u-pt-2 { padding-top: var(--s-2); }

/* Groups are laid out by their container, not by margins on each child, so
   nothing collapses or doubles when a row is conditionally rendered. */
.u-row     { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
.u-irow    { display: inline-flex; align-items: center; gap: var(--s-1); }
.u-row-1   { gap: var(--s-1); }
.u-row-3   { gap: var(--s-3); }
.u-row-top { align-items: flex-start; }
.u-row-end { justify-content: flex-end; }
.u-row-between { justify-content: space-between; }
.u-stack   { display: grid; gap: var(--s-2); }
.u-stack-1 { display: grid; gap: var(--s-1); }
.u-stack-3 { display: grid; gap: var(--s-3); }
.u-stack-4 { display: grid; gap: var(--s-4); }
.u-grow    { flex: 1 1 auto; min-width: 0; }

/* แถบสวิตช์นโยบายท้ายการ์ด — เส้นคั่นบอกว่าเป็นคนละเรื่องกับเนื้อหาข้างบน
   ⚠️ กฎทั้งชุดอยู่ด้วยกัน รวม media query ของตัวเอง (บทเรียนข้อ 18)
   การแยกกฎฐานไปไว้ท้ายไฟล์จะฆ่า media query นี้แบบเงียบสนิท */
.autopost  { border-top: 1px solid var(--line); }

@media (max-width: 560px) {
  /* ปุ่มเต็มความกว้างบนจอแคบ — ปุ่มแคบข้างข้อความยาวคือปุ่มที่กดยาก */
  .autopost > .btn { width: 100%; justify-content: center; }
}
.u-fixed   { flex: 0 0 auto; }

.u-hide    { display: none; }
.u-inline  { display: inline; }
.u-nowrap  { white-space: nowrap; }
.u-center  { text-align: center; }
.u-right   { text-align: right; }
.u-full    { width: 100%; }
.u-wide    { max-width: none; }
.u-upper   { text-transform: uppercase; }
.u-brand   { color: var(--brand); }
.u-bold    { font-weight: 700; }

/* One vocabulary for table columns. A date column is the same width on the
   invoice list as on the journal, so the eye lands in the same place when a
   person moves between screens. */
.w-check  { width: 44px; }
.w-act    { width: 68px; }
.w-qty    { width: 90px; }
.w-status { width: 110px; }
.w-date   { width: 120px; }
.w-money  { width: 130px; }
.w-doc    { width: 150px; }
.w-key    { width: 200px; }
.w-wide   { min-width: 200px; }

/* คอลัมน์ของ "ตารางบรรทัดสินค้า" — ชุดเดียวใช้ทั้งฟอร์มขายและฟอร์มซื้อ
   ⚠️ ของเดิมเป็น style="width:N%" ติดในแท็ก และบวกกันได้ 108% (ขาย) / 104% (ซื้อ)
   ตารางแบบ auto จึงไปบีบคอลัมน์ที่ "ดันได้ง่ายที่สุด" ทิ้ง ผลคือช่องจำนวนเงิน
   เหลือ 72px ทั้งที่เป็นตัวเลขที่คนอ่านเป็นอันดับแรกของบรรทัด ส่วนช่องข้อความ
   กินไป 353px — วัดจากจอจริงวันที่เจ้าของบอกว่า "หน้านี้ยังไม่สวยเลย"
   ตอนนี้รวมกันได้ 100% พอดี และเงินได้ที่ยืนของมันคืน */
.line-table .col-item   { width: 21%; }
.line-table .col-desc   { width: 19%; }
.line-table .col-qty    { width: 8%; }
.line-table .col-unit   { width: 9%; }
.line-table .col-price  { width: 12%; }
.line-table .col-disc   { width: 9%; }
.line-table .col-vat    { width: 10%; }
.line-table .col-amount { width: 12%; min-width: 96px; }

/* ==========================================================================
   Small components that pages were re-inventing inline
   ========================================================================== */
/* Navigation tile: a whole card that is one link (settings hub, report index) */
.tile-ic { color: var(--brand); margin-bottom: var(--s-2); line-height: 1; }
.tile-title { font-size: var(--t-heading); margin: var(--s-1) 0 var(--s-2); }

/* A read-only value standing where an input would be, so labels stay aligned
   whether the row is editable or not. */
.field-value {
  display: flex; align-items: center; gap: var(--s-2);
  min-height: 38px; color: var(--ink); font-weight: 500;
}

/* Compact page title for focused screens (auth, errors, single-purpose pages) */
.h-sm { font-size: var(--t-title); }

/* A page that is one centred panel: company picker, password reset. */
.center-page {
  min-height: 100vh; display: grid; place-items: center;
  padding: var(--s-7) var(--s-4); background: var(--bg);
}

/* ⚠️ ลูกของกริดมี min-width: auto ตามค่าปริยาย จึงย่อต่ำกว่าเนื้อหาไม่ได้
   หน้าราคาที่มีการ์ดสี่ใบข้างในจึงดันหน้าให้ล้นแนวนอน 321px ที่จอ 390px
   วัดได้จริงตอนกวาด 102 เส้นทาง × 5 ความกว้าง — เป็นจุดเดียวใน 510 การทดสอบ
   min-width: 0 ปลดล็อกให้ย่อได้ · max-width: 100% กันไม่ให้เกินกรอบพ่อ */
.center-page > * { min-width: 0; max-width: 100%; }

.panel-narrow { width: 100%; max-width: 480px; }

/* A choice in a list where the whole row is the button (company picker) */
.demo-user.boxed { padding: var(--s-3) var(--s-4); border: 1px solid var(--line); border-radius: var(--r); }

/* Column chart. The axis, the grid lines and the columns share one height and
   one bottom inset, so the baseline of the chart and its month labels line up
   instead of drifting apart whenever one of the three is edited. */
.chart { display: flex; gap: var(--s-2); --chart-h: 196px; --chart-foot: var(--s-6); }
.chart-axis {
  display: flex; flex-direction: column; justify-content: space-between;
  height: var(--chart-h); padding-bottom: var(--chart-foot); text-align: right;
}
.chart-plot { position: relative; flex: 1 1 auto; min-width: 0; }
.chart-grid {
  position: absolute; inset: 0 0 var(--chart-foot) 0;
  display: flex; flex-direction: column; justify-content: space-between; pointer-events: none;
}
.chart-grid > div { border-top: 1px solid var(--line); }
.chart-cols {
  position: relative; display: flex; align-items: flex-end; gap: var(--s-3);
  height: var(--chart-h); padding-bottom: var(--chart-foot);
}

/* Same code block, but a long payload wraps instead of scrolling out of view */
.code-soft { white-space: pre-wrap; word-break: break-word; }

/* Bulleted list inside body copy: the indent is the marker's gutter, so it is
   set once here rather than guessed per page. */
.bullets { margin: 0; padding-left: var(--s-5); }
.bullets li { margin-bottom: var(--s-1); }
.bullets li:last-child { margin-bottom: 0; }

/* Centred message inside a card — empty states, confirmations, sign-out */
.pad-center { text-align: center; padding: var(--s-8) var(--s-6); }


/* ==========================================================================
   Application shell v3 — a menu you can put away, and a bar that is clearly
   not the menu

   Two complaints drove this: the sidebar could not be collapsed, and the left
   menu and the top bar read as the same surface so people could not tell them
   apart. Both are fixed structurally rather than with decoration:

   - The sidebar has three states (full / rail / hidden) and remembers which.
   - The sidebar sits on its own tinted ground with a real edge, so "where I can
     go" and "where I am" are visibly different materials.
   ========================================================================== */

:root {
  --sidebar-rail: 76px;
  --shell-gap: 0px;
}

/* The menu is a different material from the workspace: slightly cooler and
   darker in light mode, slightly lifted in dark mode, with a hard edge. */
.sidebar {
  background: var(--sidebar-bg);
  border-right: 1px solid var(--line-strong);
  box-shadow: 1px 0 0 color-mix(in srgb, var(--brand) 6%, transparent);
  width: var(--sidebar-w);
  transition: width .22s var(--ease), transform .28s var(--ease);
}

/* ---- rail: icons only, name on hover ---------------------------------- */
body.rail .sidebar { width: var(--sidebar-rail); }
body.rail .main { margin-left: var(--sidebar-rail); }
body.rail .brand-text,
/* ⚠️ ยกเว้นตราไว้ — มันเป็น <span> ลูกตรงของ .nav-link เหมือนกัน ถ้าไม่ยกเว้น
   กฎนี้จะซ่อนมันอยู่ดี แม้จะเขียนกฎให้แสดงไว้ข้างล่างแล้วก็ตาม
   (เจอมาแล้ว: แก้กฎที่คิดว่าเป็นตัวซ่อน แต่ตัวจริงคือกฎนี้) */
body.rail .nav-section,
body.rail .nav-link > span:not(.nav-badge),
body.rail .menu-search { display: none; }
body.rail .sidebar-brand { justify-content: center; padding: 0; }
body.rail .nav-link { justify-content: center; padding-left: 0; padding-right: 0; }
body.rail .nav-link.active::before { left: 0; }

/*
 * ⚠️ โหมดย่อเคยซ่อนตราทั้งหมด สัญญาณ "มีงานค้าง" จึงหายไปเลย
 *
 * คนที่ย่อเมนูเพื่อเอาพื้นที่จอคือคนที่ทำงานหนักที่สุด และเป็นคนที่ต้องรู้มากที่สุด
 * ว่ามีใบแจ้งหนี้เกินกำหนดค้างอยู่ ตัวเลขไม่พอที่จริง แต่ "มีหรือไม่มี" พอที่
 *
 * ข้อความเต็มยังอยู่ใน title กับ .sr-only ข้างใน โปรแกรมอ่านหน้าจอจึงยังได้ยินครบ
 */
body.rail .nav-badge {
  position: absolute; top: 6px; right: 6px;
  width: 8px; height: 8px; min-width: 0;
  padding: 0; margin: 0;
  background: var(--danger); border-radius: var(--r-pill);
  box-shadow: 0 0 0 2px var(--sidebar-bg);
}

/* ซ่อนเฉพาะตัวเลข — .sr-only ต้องอยู่ต่อ ไม่งั้นโปรแกรมอ่านหน้าจอจะได้ยินแค่ความว่างเปล่า
   (ใช้ display แทน font-size: 0 เพราะขนาดตัวอักษรต้องมาจากสเกลเสมอ) */
body.rail .nav-badge-n { display: none; }

                                                                              
                                        
@media (hover: hover) { body.rail .nav-link[data-label]:hover::after {
  content: attr(data-label);
  position: fixed; left: calc(var(--sidebar-rail) + 8px);
  transform: translateY(-6px);
  background: var(--n-900); color: var(--n-0);
  padding: 6px var(--s-3); border-radius: var(--r-sm);
  font-size: var(--t-label); font-weight: 500; white-space: nowrap;
  box-shadow: var(--sh-md); z-index: 60; pointer-events: none;
} }
@media (hover: hover) { :root[data-theme="dark"] body.rail .nav-link[data-label]:hover::after { background: var(--surface-3); color: var(--ink); } }

/* ---- brand lockup ------------------------------------------------------ */
.brand-mark {
  background: none;
  color: var(--brand); width: 34px; height: 34px; flex: 0 0 34px;
  display: grid; place-items: center;
}
.brand-mark svg { width: 34px; height: 34px; display: block; }

.sidebar-brand { gap: var(--s-3); padding: 0 var(--s-4); }
.sidebar-brand a { display: flex; align-items: center; gap: var(--s-3); color: inherit; min-width: 0; }

/* ---- collapse control -------------------------------------------------- */
.rail-toggle {
  display: flex; align-items: center; gap: var(--s-2);
  width: 100%; min-height: 34px; padding: 0 var(--s-2);
  border: 0; background: transparent; font: inherit; font-size: var(--t-label);
  color: var(--muted); border-radius: var(--r-xs); cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
@media (hover: hover) { .rail-toggle:hover { background: var(--surface-2); color: var(--ink); } }
/* Folded: icon only, centred, and pointing the other way — the control stays
   exactly where it was, which is the whole point of moving it here. */
body.rail .rail-toggle { justify-content: center; padding: 0; }
body.rail .rail-toggle .ic { transform: rotate(180deg); }
body.rail .rail-toggle-text { display: none; }
/* แท็บเล็ตแนวนอน (≥ 993px) เห็นแถบข้างและปุ่มนี้ — นิ้วต้องแตะโดน (device:sweep วัดได้ 34px ที่ 1024) */
@media (any-pointer: coarse) { .rail-toggle { min-height: 44px; } }

/* ---- menu search ------------------------------------------------------- */
.menu-search { padding: var(--s-3) var(--s-3) 0; position: relative; }
.menu-search input {
  width: 100%; height: 34px; padding: 0 var(--s-3) 0 30px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface-2); color: var(--ink); font: inherit; font-size: var(--t-body);
}
.menu-search input::placeholder { color: var(--faint); }
.menu-search input:focus { outline: none; border-color: var(--brand); background: var(--surface); box-shadow: var(--ring); }
.menu-search .ic { position: absolute; left: 21px; top: 50%; transform: translateY(-2px); color: var(--faint); pointer-events: none; }
.nav-link.hidden-by-search, .nav-section.hidden-by-search { display: none; }

/* ---- collapsible groups ------------------------------------------------ */
.nav-section {
  display: flex; align-items: center; gap: var(--s-2);
  cursor: pointer; user-select: none;
  border: 0; background: transparent; width: 100%; text-align: left;
  font-family: inherit;
  /* These headings became controls when the groups learned to fold, so they
     have to be big enough to hit: WCAG 2.2 puts the floor at 24px, and a
     thumb on a phone is not a mouse pointer. */
  min-height: 32px; padding-top: var(--s-1); padding-bottom: var(--s-1);
  border-radius: var(--r-xs);
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
@media (hover: hover) { .nav-section:hover { background: var(--surface-2); color: var(--muted); } }
.nav-section .caret { margin-left: auto; transition: transform var(--fast) var(--ease); opacity: .7; }
.nav-section[aria-expanded="false"] .caret { transform: rotate(-90deg); }
.nav-group[hidden] { display: none; }

/* ---- foot -------------------------------------------------------------- */
.sidebar-foot {
  border-top: 1px solid var(--line); padding: var(--s-2) var(--s-3);
  display: flex; align-items: center; gap: var(--s-2);
  font-size: var(--t-micro); color: var(--faint);
}

/* ==========================================================================
   Top bar — context and identity, never navigation
   ========================================================================== */
.topbar {
  background: var(--topbar-bg);
  border-bottom: 1px solid var(--line);
  gap: var(--s-2);
}
.topbar-title { font-size: var(--t-section); }
/* ⚠️ ต้องตัดคำเหมือนหัวข้อหลัก — ช่องนี้เพิ่งมีข้อมูลครั้งแรก จึงยังไม่เคย
   เจอกรณีข้อความยาว ชื่อกลุ่มบวกชื่อเมนูยาว ๆ จะดันปุ่มบนแถบบนจนล้นออกไป */
.topbar-crumb {
  font-size: var(--t-micro); color: var(--faint); line-height: 1.1; margin-bottom: 1px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.topbar-titles { min-width: 0; display: flex; flex-direction: column; justify-content: center; }

/* A divider makes the identity cluster read as a separate zone from the page
   context on its left. */
.topbar-sep { width: 1px; height: 24px; background: var(--line); margin: 0 var(--s-1); flex: 0 0 1px; }

/* ==========================================================================
   Navigation progress + skeleton (AJAX)
   ========================================================================== */
#ya-progress {
  position: fixed; inset: 0 0 auto 0; height: 2px; z-index: 90;
  opacity: 0; transition: opacity .2s var(--ease); pointer-events: none;
}
#ya-progress.active { opacity: 1; }
#ya-progress span {
  display: block; height: 100%; width: 0;
  background: linear-gradient(90deg, var(--brand), var(--brand-hi));
  transition: width .28s var(--ease);
}

.skel-page { padding: var(--s-2) 0; }
.skel-card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: var(--s-5); margin-bottom: var(--s-5);
}
.skel {
  height: 12px; border-radius: var(--r-pill); margin-bottom: var(--s-3);
  background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 37%, var(--surface-2) 63%);
  background-size: 400% 100%; animation: skel 1.3s ease-in-out infinite;
}
@keyframes skel { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@media (prefers-reduced-motion: reduce) { .skel { animation: none; } }

/* The content region swaps during AJAX navigation; a hair of fade keeps the
   change from reading as a flicker. */
#mainContent { animation: content-in .18s var(--ease); }
@keyframes content-in { from { opacity: .55; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { #mainContent { animation: none; } }

/* ค่าฐานของปุ่มเปิดเมนูกับฉากหลัง — ต้องอยู่ "ก่อน" ทุก media query ที่จะทับมัน

   ⚠️ ของเดิมอยู่ท้ายไฟล์ ซึ่งทำให้กฎในบล็อก 992 ตายเงียบทั้งหมด
   น้ำหนัก selector เท่ากัน ตัวที่อยู่ล่างกว่าชนะเสมอ (บทเรียนข้อ 18)
   ผลจริงที่วัดได้: ที่ความกว้าง 861-992px เมนูข้างถูกเลื่อนออกนอกจอแล้ว
   แต่ปุ่มเปิดเมนูยังซ่อนอยู่ = ไม่มีทางเปิดเมนูได้เลยทั้งช่วง

   ⚠️ และการอ่านไฟล์อย่างเดียวมองไม่เห็นเรื่องนี้ ต้องวัดที่ความกว้างจริง
   ด้วย Emulation.setDeviceMetricsOverride เท่านั้นถึงจะเจอ */
.sidebar-toggle { display: none; }
.backdrop { display: none; }

/* ==========================================================================
   Responsive shell
   ========================================================================== */
@media (max-width: 992px) {
  /* V4 (W8): ลิ้นชักที่ปิดต้อง visibility: hidden (หลังเลื่อนออกจบ) — ไม่งั้นปุ่ม Tab เดินเข้าเมนูที่มองไม่เห็น
     ~40 จุดก่อนถึงเนื้อหา (บทเรียนข้อ 17) และเงาของแผงนอกจอยังถูกวาดอยู่ขอบซ้าย
     visibility เปลี่ยนทันทีตอนเปิด · หน่วงเท่าแอนิเมชันตอนปิด แผงจึงยังเห็นตอนเลื่อนออก */
  .sidebar {
    transform: translateX(-100%); width: var(--sidebar-w); box-shadow: none;
    visibility: hidden;
    transition: width .22s var(--ease), transform .28s var(--ease), visibility 0s linear .28s;
  }
  body.nav-open .sidebar {
    transform: none; box-shadow: var(--sh-lg);
    visibility: visible;
    transition: width .22s var(--ease), transform .28s var(--ease), visibility 0s;
  }
  body.rail .sidebar { width: var(--sidebar-w); }
  body.rail .brand-text, body.rail .nav-section, body.rail .nav-link > span,
  body.rail .menu-search { display: revert; }
  body.rail .nav-link { justify-content: flex-start; padding-left: var(--s-3); padding-right: var(--s-3); }
  .main, body.rail .main { margin-left: 0; }
  .rail-toggle { display: none; }

  /* ⚠️ ปุ่มเปิดเมนูต้องโผล่ตั้งแต่จุดที่ sidebar ถูกซ่อน ไม่ใช่ที่ 860
     ของเดิม sidebar หายที่ ≤992 แต่ปุ่มมาที่ ≤860 → ช่วง 861–992px
     ไม่มีทางเปิดเมนูได้เลย (โน้ตบุ๊กจอเล็กและแท็บเล็ตแนวนอนอยู่ในช่วงนี้) */
  .sidebar-toggle { display: inline-grid; }
  body.nav-open .backdrop {
    display: block; position: fixed; inset: 0; z-index: 35;
    background: color-mix(in srgb, var(--n-950) 45%, transparent);
    animation: fade-in var(--mid) var(--ease);
  }
}

/* ==========================================================================
   Responsive
   ========================================================================== */

/* Very wide screens get more columns rather than more empty space. */
@media (min-width: 1800px) {
  .grid-4 { grid-template-columns: repeat(4, minmax(0,1fr)); }
  .content { padding-left: var(--s-8); padding-right: var(--s-8); }
}

@media (max-width: 1200px) {
  .grid-4 { grid-template-columns: repeat(2, minmax(0,1fr)); }
}
@media (max-width: 1024px) {
  .grid-3 { grid-template-columns: repeat(2, minmax(0,1fr)); }
  .content { padding: var(--s-5) var(--s-5) var(--s-9); }
}
/* V4 (W7 · W8): การ์ดสรุป "สามใบพอดี" ช่วง 861–1200 วางแถวเดียว — สองคอลัมน์ทิ้งใบที่สามลอยเดี่ยว
   ครึ่งแถว (รูในกริด) และกินจอแรกแท็บเล็ตแนวนอนไปอีก ~150px · ถอด tile + ตัวเลข --t-title ให้พอดีสามคอลัมน์
   (ยอด 123,456,789.00 กว้าง ~152px ที่ 22px · การ์ดกว้าง ~190px ที่ 1024)
   จำนวนอื่นที่เป็นคี่ใช้กติกา "ใบแรกเต็มแถว" ในบล็อกเดียวกัน */
/* ⚠️ กฎกริดการ์ดสรุปทั้งชุดของ V4 (บล็อกนี้ + บล็อก ≤860 ข้างล่าง):
   · ต้องกันกระเบื้องรายงาน (a.stat.report-tile = ไอคอน + ชื่อ + คำอธิบาย ไม่ใช่ตัวเลขสรุป) — ไม่งั้นมือถือบีบกระเบื้อง
     เหลือ ~150px สองคอลัมน์ และจำนวนคี่ได้รูในกริด (ผู้ตรวจ V4 · ชนกับ .grid-2 > .report-tile:last-child เต็มแถว)
   · ต้องเป็น screen เท่านั้น — กระดาษ A4 กว้าง ~688px เข้าช่วงจอพวกนี้ แล้วใบสำคัญ/งบที่พิมพ์เปลี่ยนรูป (แผน V4: กระดาษต้องไม่เปลี่ยน) */
@media screen and (min-width: 561px) and (max-width: 1200px) {
  .grid:has(> .stat:nth-child(3):last-child):not(:has(> .card)):not(:has(> .report-tile)) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  /* ⚠️ ต้องชนะกติกา "ใบแรกเต็มแถว" ของบล็อก ≤860 (สามเป็นเลขคี่) — น้ำหนักสูงกว่าเสมอ (มี :nth-child(3):last-child เพิ่ม) */
  .grid:has(> .stat:nth-child(3):last-child):not(:has(> .card)):not(:has(> .report-tile)) > .stat:first-child { grid-column: auto; }
  .grid:has(> .stat:nth-child(3):last-child):not(:has(> .card)):not(:has(> .report-tile)) > .stat:has(> .mtile) { padding-left: var(--stat-px, var(--s-5)); }
  .grid:has(> .stat:nth-child(3):last-child):not(:has(> .card)):not(:has(> .report-tile)) > .stat > .mtile { display: none; }
  .grid:has(> .stat:nth-child(3):last-child):not(:has(> .card)):not(:has(> .report-tile)) > .stat .value:not(.text) { font-size: var(--t-title); white-space: nowrap; }
}
@media screen and (min-width: 861px) and (max-width: 1200px) {
  /* เฉพาะกริดที่ช่วงนี้เป็นสองคอลัมน์จริง (grid-2 · grid-4 ≤1200 · grid-3 ≤1024) */
  :is(.grid-2, .grid-4):has(> .stat):not(:has(> .card)):not(:has(> .report-tile)) > .stat:first-child:nth-last-child(odd):not(:only-child):not(:nth-last-child(3)) { grid-column: 1 / -1; }
}
@media screen and (min-width: 861px) and (max-width: 1024px) {
  .grid-3:has(> .stat):not(:has(> .card)):not(:has(> .report-tile)) > .stat:first-child:nth-last-child(odd):not(:only-child):not(:nth-last-child(3)) { grid-column: 1 / -1; }
}
/* .grid-fill — กริดการ์ดที่จำนวนใบขึ้นกับสิทธิ์ของผู้ดู: ใบสุดท้ายกินที่ที่เหลือของแถว ไม่ทิ้งรู
   (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · C37 — หน้าตั้งค่า: บทบาทที่ตั้งเองอาจเห็นห้าใบ = 3 + 2)
   สามคอลัมน์: เศษหนึ่ง = เต็มแถว · เศษสอง = ใบสุดท้ายกว้างสองช่อง · สองคอลัมน์ (861–1024): เศษหนึ่ง = เต็มแถว
   ≤860 เป็นคอลัมน์เดียวอยู่แล้ว · ⚠️ screen เท่านั้น (กระดาษ A4 อยู่ในช่วงจอพวกนี้ ห้ามเปลี่ยนรูป) */
@media screen and (min-width: 1025px) {
  .grid-3.grid-fill > :last-child:nth-child(3n+1) { grid-column: 1 / -1; }
  .grid-3.grid-fill > :last-child:nth-child(3n+2) { grid-column: span 2; }
}
@media screen and (min-width: 861px) and (max-width: 1024px) {
  .grid-3.grid-fill > :last-child:nth-child(odd) { grid-column: 1 / -1; }
}
@media (max-width: 992px) {
  .auth-wrap { grid-template-columns: 1fr; }
  .auth-art { display: none; }
}
/* V4 (W8): กริดของการ์ดสรุปไม่ยุบเหลือคอลัมน์เดียว — แท็บเล็ตเคยเรียงการ์ดสรุปทีละใบเต็มจอ
   กริดของการ์ดเนื้อหา (ตาราง/กราฟ) ยังคอลัมน์เดียวตามเดิม (กันด้วย :not(:has(> .card))) · กระเบื้องรายงานไม่ใช่การ์ดสรุป
   จำนวนคี่ = ใบแรกเต็มแถว ไม่ให้ใบสุดท้ายลอยเดี่ยวทิ้งรูไว้ครึ่งแถว
   ⚠️ screen เท่านั้น (กระดาษ A4 เข้าช่วงนี้) · น้ำหนัก selector สูงกว่า .grid-2/3/4 { 1fr } ของบล็อกข้างล่างจึงวางก่อนได้ */
@media screen and (max-width: 860px) {
  .grid:has(> .stat):not(:has(> .card)):not(:has(> .report-tile)) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid:has(> .stat):not(:has(> .card)):not(:has(> .report-tile)) > .stat:first-child:nth-last-child(odd) { grid-column: 1 / -1; }
}
@media (max-width: 860px) {
  .sidebar { transform: translateX(-100%); box-shadow: none; }
  body.nav-open .sidebar { transform: none; box-shadow: var(--sh-lg); }
  body.nav-open .backdrop {
    display: block; position: fixed; inset: 0; z-index: 35;
    background: color-mix(in srgb, var(--n-950) 45%, transparent);
    animation: fade-in var(--mid) var(--ease);
  }
  .main { margin-left: 0; }
  .sidebar-toggle { display: inline-grid; }
  .content { padding: var(--s-4) var(--s-4) 72px; }
  .topbar { padding: 0 var(--s-3); gap: var(--s-2); }
  .topbar-title { font-size: var(--t-section); }
  /* minmax(0, 1fr) ไม่ใช่ 1fr (UAT งานคุมเงิน · 29 ก.ย. 2569) — 1fr = minmax(auto, 1fr) คอลัมน์โตตามเนื้อที่ห้ามตัดบรรทัด
     ช่องเลือกบัญชีรับเงินชื่อยาว (412px) ในการ์ด 316px ⇒ ช่องวันที่/บัญชีของฟอร์มรับชำระล้นการ์ด ~96px บนมือถือ
     ค่าฐานของไฟล์ (บรรทัด .grid-2 ด้านบน) เป็น minmax(0,1fr) อยู่แล้ว บล็อกนี้เคยถอยกลับเป็น 1fr เงียบ ๆ */
  .grid-2, .grid-3, .grid-4 { grid-template-columns: minmax(0, 1fr); }
  .page-head-actions { width: 100%; margin-left: 0; }
  .page-head-actions .btn { flex: 1 1 auto; }
  .filters { flex-direction: column; align-items: stretch; }
  /* คู่ของกฎ hint-ลอยด้านบน: แถบเป็นคอลัมน์แล้ว คำอธิบายต้องกลับมาอยู่ใน
     ลำดับปกติ ไม่งั้นมันจะลอยทับ field ตัวถัดไป */
  .filters .field > .hint,
  .filters .field > .small { position: static; margin-top: var(--s-1); }
  .filters:has(.field > .hint),
  .filters:has(.field > .small) { margin-bottom: var(--s-5); }
  /* ⚠️ แถบตัวกรองบนจอแคบเป็น "แนวตั้ง" — ในแกนตั้ง flex-basis คือ "ความสูง"
     .search-box มี flex: 1 1 260px (ตั้งไว้ให้กว้าง 260 ตอนเป็นแถวแนวนอน)
     พอเป็นคอลัมน์ 260 จึงกลายเป็นความสูง → ช่องค้นหาสูง 260px มีช่องว่างตาย
     ใต้ช่องกรอกทุกหน้าที่มีแถบนี้ (36 หน้า) วัดจริงบนจอ 500px: กล่อง 260 / ช่องกรอก 42
     แก้ที่ flex ไม่ใช่ที่ height เพราะต้นเหตุคือ "เลขเดียวทำหน้าที่สองแกน" */
  .filters .field, .filters .search-box { width: 100%; max-width: none; min-width: 0; }
  .filters .search-box { flex: 0 0 auto; }
  .filters .btn { width: 100%; }
  .company-pill { max-width: 154px; }
  table.tbl { min-width: 640px; }
  table.tbl th { position: static; }
  /* Fingers are less precise than a mouse */
  .btn, .btn-sm { min-height: 44px; }
  .btn-sm { padding: 6px var(--s-4); }
  /* flex-shrink คือค่าเริ่มต้น 1 — แถบบนบีบปุ่มเหล่านี้เหลือ 34px ทั้งที่
     ประกาศ 44px ไว้ ความกว้างที่ประกาศจึงเป็นแค่คำขอ ไม่ใช่พื้น */
  .icon-btn { width: 44px; height: 44px; min-width: 44px; flex: 0 0 auto; }
  .company-pill { min-height: 44px; flex-shrink: 1; }
  /* The document editor is the one screen a person fills in on a phone, so its
     controls need the same reach as everything else. */
  /* เก้าคอลัมน์เบียดกันใน 555px ทำให้ช่องเลือก VAT เหลือกว้าง 30px ซึ่งแคบกว่า
     ข้อความที่ต้องแสดงเสียอีก กรอบนี้เลื่อนแนวนอนได้อยู่แล้ว จึงให้พื้นความกว้าง
     กับตารางไป แล้วปล่อยให้เลื่อนแทนการบีบ */
  .line-table { min-width: 760px; }
  .line-table .input, .line-table .select { min-height: 44px; }
  .line-table .select { min-width: 104px; }
  .line-table .row-del { min-width: 44px; min-height: 44px; }
  .check { min-height: 44px; }
  input[type="checkbox"], input[type="radio"] { width: 24px; height: 24px; }
  .tab { padding: 13px var(--s-4); }
  .nav-link { padding: 13px var(--s-3); }
  /* หัวข้อกลุ่มกลายเป็นปุ่มพับเมนูแล้ว บนมือถือจึงต้องสูงเท่าลิงก์ที่ขนาบมันอยู่
     บนจอใหญ่คงไว้ 32px เพราะใช้เมาส์ ไม่ใช่นิ้ว */
  .nav-section { min-height: 44px; }
  .totals { max-width: none; }
}
@media (max-width: 480px) {
  /* ⚠️ บนจอโทรศัพท์ ทุกพิกเซลของป้ายกิจการคือตัวอักษรที่อ่านออกเพิ่มขึ้น
     ลูกศรบอกว่า "กดได้" ซึ่งบนจอสัมผัสไม่ได้เพิ่มอะไร (ไม่มี hover ให้ค้นพบ)
     และป้ายทั้งใบก็กดได้อยู่แล้ว — ยอมทิ้งลูกศรเพื่อแลกกับชื่อที่อ่านออก
     ⚠️ ต้องซ่อนด้วย display: none ไม่ใช่ opacity — ของที่มองไม่เห็นแต่ยัง
     กินที่ ไม่ได้คืนพิกเซลให้ใคร (และยังโฟกัสได้ ตามบทเรียนข้อ 17) */
  .company-pill svg:last-child { display: none; }
  .company-pill { padding-left: 8px; padding-right: 10px; gap: 6px; }
  /* V4: เดิม --t-section (15) · ตอนนี้ --t-section คือหัวการ์ด 17px — เนื้อความมือถือใช้ขนาดเดียวกับจอใหญ่ */
  body { font-size: var(--t-body); }
  h1 { font-size: var(--t-title); }
  .stat .value { font-size: var(--t-title); }
  .modal-foot { flex-direction: column-reverse; }
  .modal-foot .btn { width: 100%; }
  .modal-body { padding: var(--s-5); }
  .toast-host { left: var(--s-3); right: var(--s-3); bottom: var(--s-3); }
  .toast { min-width: 0; max-width: none; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* ==========================================================================
   Print — the customer's face to their customer
   ========================================================================== */
@media print {
  /* ⚠️ สเกลของกระดาษตรึงไว้ที่ค่าก่อนดีไซน์ V4 — ในบล็อกนี้มีแค่ body ที่ใช้ --t-print ส่วนหัวตาราง ชิป ป้าย
     ใช้โทเคนของจอ ถ้าไม่ตรึง ใบสำคัญรายวัน/รายงานที่สั่งพิมพ์จากจอจะโตตามจอ (ผู้ตรวจ V4 มุมวิศวกรรม ข้อ 7)
     เกต qa_design_system ข้อ 5ก ข้ามบล็อกนี้โดยตั้งใจ — กระดาษเป็นคนละสื่อ ตัวเล็กกว่าจอได้ */
  :root {
    --t-hero: 2rem; --t-display: 1.625rem; --t-title: 1.3125rem; --t-heading: 1.0625rem;
    --t-section: .9375rem; --t-body: .875rem; --t-label: .8125rem; --t-micro: .75rem;
  }
  /* กฎที่ V4 เปลี่ยน "ตัวที่ใช้" (ไม่ใช่แค่ค่าของโทเคน) — ตรึงโทเคนอย่างเดียวไม่พอ ต้องคืนของเดิมบนกระดาษด้วย
     พิสูจน์ด้วย design:audit --print เทียบพิกเซลกับเส้นฐาน before-print */
  table.tbl td, table.tbl th { padding: var(--s-3) var(--s-4); line-height: 1.6; }
  .stat .meta, .field .hint, .related-why { font-size: var(--t-micro); color: var(--faint); }
  /* ชิป · ป้ายการ์ดสรุป · สีช่องตาราง — V4 เปลี่ยนตัวที่ใช้ (--t-micro → --t-label · ink-2 → ink) ต้องคืนบนกระดาษ (ผู้ตรวจ V4) */
  .chip { font-size: var(--t-micro); font-weight: 600; line-height: 1.75; padding: 3px 10px; }
  .stat .label { font-size: var(--t-micro); }
  table.tbl td { color: var(--ink-2); }
  small { font-size: var(--t-micro); }
  h1 { font-size: var(--t-display); }
  .sidebar, .topbar, .page-head-actions, .no-print, .toast-host, .filters, .tabs, .pagination { display: none !important; }
  .main { margin-left: 0; }
  .content { padding: 0; max-width: none; }
  body { background: #fff; font-size: var(--t-print); color: #000; }
  .card { border: 0; box-shadow: none; margin-bottom: 12pt; break-inside: avoid; }
  .card-head { background: none; border-bottom: 1pt solid #000; padding: 0 0 6pt; }
  .table-wrap { overflow: visible; }
  table.tbl th { background: none; border-bottom: 1pt solid #000; color: #000; }
  table.tbl td { border-bottom: .5pt solid #999; }
  @page { size: A4; margin: 14mm; }
}

/* ==========================================================================
   Reading comfort

   Everything here exists because these screens are read for hours, not
   glanced at: a resting place for the eye between rows, a hover that says
   "this row is clickable" without shouting, and numbers that never jitter.
   ========================================================================== */
table.tbl tbody tr { transition: background var(--fast) var(--ease); }
table.tbl tbody tr:nth-child(even) { background: color-mix(in srgb, var(--surface-2) 55%, transparent); }
@media (hover: hover) { table.tbl tbody tr:hover { background: var(--brand-softer); } }
@media (hover: hover) { :root[data-theme="dark"] table.tbl tbody tr:hover { background: color-mix(in srgb, var(--brand) 10%, transparent); } }

/* Money columns must never re-align as values change. */
table.tbl td.num, table.tbl th.right, .stat .value, .totals .row span:last-child {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* Cards sit on the page rather than being cut out of it. */
/* เงาของ .card ย้ายไปอยู่ชั้น v4 (--sh-card) ทั้งหมดแล้ว สองบรรทัดที่เคยอยู่ตรงนี้
   ถูกเขียนทับจนไม่มีผลกับสิ่งที่ตาเห็นมาตั้งแต่ชั้น v4 เกิด จึงลบทิ้ง */

/* Section headings need air above them more than they need weight. */
.card + .card { margin-top: var(--s-2); }

/* ⚠️ การ์ดที่อยู่ในกริด ต้องไม่มีระยะขอบของตัวเอง — กริดจัดระยะให้แล้วด้วย gap

   ที่มา (วัดบนเครื่องจริง 4 ก.ย. 2569 หน้า /tax): .card มี margin-bottom 20px
   และกฎ .card + .card ข้างบนเติม margin-top 8px ให้การ์ดใบที่สองเป็นต้นไป
   เมื่อการ์ดกลายเป็นลูกของ .grid ระยะพวกนี้ไม่ถูกล้าง ผลคือ
     · การ์ดใบที่สองในแถวเริ่มต่ำกว่าใบแรก 8px (top 579 เทียบ 587)
       ทั้งสองใบจบที่ก้นเดียวกัน ความสูงจึงเป็น 421 กับ 413 ในแถวเดียวกัน
     · ระยะระหว่างแถวกลายเป็น 36px (16 + 20) ทั้งที่แนวนอนเป็น 16px ตาม gap
   เจ้าของอ่านออกทันทีว่า "หลาย ๆ อย่างยังไม่เท่ากัน" ซึ่งถูกต้องทุกพิกเซล

   ⚠️ ต้องอยู่ "ต่อจาก" กฎ .card + .card เท่านั้น
   น้ำหนัก selector ของ .grid > .card สูงกว่าอยู่แล้ว แต่การวางไว้ท้ายไฟล์
   จะทำให้กฎนี้ไปทับ media query ที่เขียนไว้ก่อนหน้าโดยไม่ตั้งใจ
   (บทเรียนข้อ 18 และ 29 ของโปรเจกต์) */
.grid > .card,
.grid > .stat { margin-top: 0; margin-bottom: 0; }
.page-head { margin-bottom: var(--s-5); }
.page-head .sub { max-width: 72ch; }
/* มือถือ: หัวหน้ารายการเป็นชุดควบคุมเดียวกับรายการ (ค้นเลขเอกสาร 26 ก.ย. 2569 · design:audit C3)
   ระยะเดิมไม่เท่ากันเลย 20 · 20 · 16 · 12 — แถบสรุป → แท็บ → ปุ่มตัวกรอง → รายการ ใช้ s-3 ชุดเดียว
   (สิ่งที่ทำงานด้วยกันชิดกัน) · หัวข้อหน้าห่างชุดนั้น s-4
   เหตุที่ต้องทำ: รายการที่มีใบร่าง (บิล POS ในโหมดรับเป็นร่างคือเรื่องปกติ) ขึ้นแถบ "เลือกทั้งหมดที่เป็นร่าง" 44px
   บนการ์ด ⇒ ที่ 390×844 แถวแรกจบที่ y=800 ใต้แถบเมนูล่าง (788) · เดโมไม่เคยมีใบร่างบนหน้าแรกจึงไม่มีใครเห็น
   ⚠️ แถบนั้นย่อหรือย้ายไปแถบเลือกหลายใบไม่ได้ — เป้ากด 44px และแถบล่างเต็มแล้วที่ 390 (ใช้ 322 จาก 324px)
   ⚠️ screen เท่านั้น — กระดาษ A4 (~688px) ตกอยู่ในช่วงนี้ (บทเรียนข้อ 343) */
@media screen and (max-width: 720px) {
  .page-head { margin-bottom: var(--s-4); }
  .grid:has(> .stat):is(:has(+ .tabs), :has(+ .filters), :has(+ .filters-toggle), :has(+ .date-preset), :has(+ .summary-line)) { margin-bottom: var(--s-3); }
  .tabs:has(+ .filters-toggle) { margin-bottom: var(--s-3); }
}

/* Focus has to be obvious for keyboard work, without a halo on mouse clicks. */
:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ==========================================================================
   Reach and reassurance

   A list row that only responds on a small link, a page that gives no way back
   to the top, a toast a screen reader never announces — none of these are
   visible in a screenshot, and all of them are felt every day.
   ========================================================================== */

/* Whole-row targets. The cursor and a quiet lift say "this opens". */
table.tbl tbody tr.row-link { cursor: pointer; }
table.tbl tbody tr.row-link:focus-visible {
  outline: 2px solid var(--brand); outline-offset: -2px;
  background: var(--brand-softer);
}

/* แถวเอกสารที่ยกเลิกแล้ว — จางลงและขีดฆ่าเลขที่ ให้ตาแยกจากใบที่มีชีวิตได้
   โดยไม่ต้องอ่านชิป (ชิป "ยกเลิกแล้ว" เป็นเทาเดียวกับ "ร่าง"/"รอชำระ" โดยตั้งใจ
   เพราะเทา = ไม่ต้องสนใจ · ฝั่งซื้อเคยมี 25 จาก 30 แถวเป็นใบยกเลิกที่หน้าตาเหมือน
   ใบปกติทุกประการ)
   ⚠️ ยังมองเห็นและยังโฟกัสได้ จึงไม่ต้องถอด tabindex — บทเรียน 17 ว่าด้วย opacity 0
   ⚠️ หรี่ด้วยสี --muted ไม่ใช่ opacity (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · C17)
   เดิมเขียนว่า "ใช้ opacity เพื่อไม่ให้คู่สีใหม่หลุดจากเกตคอนทราสต์" = หลบเกต ไม่ใช่ผ่านเกต
   opacity .55 ทำตัวอักษรเหลือ 3.62:1 (ink) · 2.51:1 (muted) · ชิป 2.2–2.5:1 — ใบยกเลิกยังเป็นเนื้อหาที่ต้องอ่านได้
   WCAG 1.4.3 ยกเว้นเฉพาะตัวควบคุมที่ปิดใช้งาน · สัญญาณจริงคือเส้นขีดฆ่า + ชิป ส่วนสีจางเป็นสัญญาณรอง
   --muted บน --surface = 7.05:1 (สว่าง) · 6.58:1 (มืด) และบนกระดาษชนะ table.tbl td ของบล็อก print ด้วยน้ำหนัก selector */
table.tbl tbody tr.row-void > td,
table.tbl tbody tr.row-reversed > td { color: var(--muted); }
table.tbl tbody tr.row-void a > strong { text-decoration: line-through; }
/* เลขในบรรทัดผลค้น (.search-hit-v) ตั้งสีของตัวเอง ⇒ ไม่รับความจางของแถว (บทเรียนข้อ 381 · ผู้ตรวจรอบสอง 26 ก.ย. 2569) */
table.tbl tbody tr.row-void .search-hit-v,
table.tbl tbody tr.row-reversed .search-hit-v { color: inherit; }
/* แถวที่ลิงก์ตรงพามา (?mv|cs|sc= ของหน้าช่องทางรับเงิน · ผู้ตรวจอิสระรอบสอง A3) — พื้นโทน info + ขีดซ้าย
   ไม่ตั้งสีตัวอักษร (คู่สีของแถวยกเลิกข้างบนยังมีผล) · โทเคนถูกตั้งใหม่ในโหมดมืดแล้ว */
table.tbl tbody tr.row-target > td { background: var(--info-bg); }
table.tbl tbody tr.row-target > td:first-child { box-shadow: inset 3px 0 0 var(--info); }
/* ที่หมายของ #fragment (pjax.js revealTarget · โหลดเต็มหน้า) ไม่จมใต้แถบบน — การ์ดและแถวที่มี id */
#mainContent .card[id] { scroll-margin-top: calc(var(--topbar-h) + var(--s-3)); }
#mainContent tr[id] { scroll-margin-top: calc(var(--topbar-h) + var(--s-3)); }
/* แถวรายการบัญชีที่ "ถูกกลับรายการแล้ว" (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · C12/C24 · คำตัดสิน §14.2)
   สีจางแบบเดียวกับใบยกเลิก (กฎคู่อยู่ข้างบน) + ขีดฆ่า "ยอด" (ไม่ใช่เลขที่) — ใบนี้ยังอยู่ในสมุดตลอดไป (กลับรายการไม่ลบของเดิม)
   แต่ยอดของมันถูกหักล้างด้วยใบกลับแล้ว คนไล่ยอดต้องเห็นทันทีว่าไม่ต้องนับซ้ำ
   ⚠️ ยอด = ช่อง td.right.num เท่านั้น — วันที่ก็เป็น td.num (ไม่มี .right) ห้ามขีดฆ่าวันที่
   ⚠️ แถวกระชับ (มือถือ/แท็บเล็ต) มีคู่ของมันในบล็อกแถวกระชับ (ช่องบทบาท amount) · การ์ดป้าย:ค่าใช้กฎนี้ตรง ๆ */
table.tbl tbody tr.row-reversed > td.right.num { text-decoration: line-through; }
/* การ์ดป้าย:ค่า (มือถือ · แท็บเล็ต): ป้าย "ยอด" เป็น ::before ที่เป็นลูกในลำดับปกติ เส้นขีดฆ่าของช่องจึงลามไปขีดป้ายด้วย
   (วัดบนจอ 768 — ป้ายถูกขีดทั้งที่ป้ายไม่ได้ถูกยกเลิก) · ของที่อยู่นอกลำดับปกติไม่รับเส้นที่ลามมา (สเปก text-decoration)
   จึงยกป้ายของช่องนี้เป็น absolute ชิดซ้าย ค่ายังชิดขวาตามเดิม · ตารางจอกว้างไม่มีป้าย กฎนี้จึงไม่มีผลที่นั่น */
table.tbl tbody tr.row-reversed > td.right.num[data-label] { position: relative; }
table.tbl tbody tr.row-reversed > td.right.num[data-label]::before { position: absolute; left: 0; top: 2px; }

/* Keyboard users should not have to walk 25 menu items to reach the page. */
.skip-link {
  position: fixed; top: -100px; left: var(--s-3); z-index: 100;
  background: var(--brand); color: var(--on-brand);
  padding: 10px var(--s-4); border-radius: var(--r-sm);
  font-weight: 600; box-shadow: var(--sh-md);
  transition: top var(--fast) var(--ease);
}
.skip-link:focus { top: var(--s-3); color: var(--on-brand); }

/* Long reports need a way home without a scroll marathon. */
.to-top {
  position: fixed; right: var(--s-5); bottom: var(--s-5); z-index: 45;
  width: 42px; height: 42px; border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: var(--surface); color: var(--ink-2);
  border: 1px solid var(--line-strong);
  cursor: pointer; opacity: 0; transform: translateY(8px); pointer-events: none;
  /* ⚠️ opacity:0 ทำให้มองไม่เห็นแต่ "ยังโฟกัสได้" = จุด Tab ตาย
     pointer-events:none กันแค่เมาส์ ไม่ได้กันคีย์บอร์ดเลย (บทเรียนข้อ 17)
     visibility:hidden เอาออกจากทั้งสายตา ลำดับ Tab และโปรแกรมอ่านหน้าจอ */
  visibility: hidden;
  transition: opacity var(--mid) var(--ease), transform var(--mid) var(--ease), background var(--fast) var(--ease);
  /* ⚠️ ห้ามใส่ visibility ในรายการทรานซิชัน
     visibility เป็นคุณสมบัติแบบ "ไม่ต่อเนื่อง" ค่าจะพลิกที่กึ่งกลางของช่วงเวลา
     ผลคือปุ่มที่เพิ่งโผล่ยังโฟกัสไม่ได้ราวหนึ่งในสิบวินาทีแรก ซึ่งคนกด Tab
     พอดีจังหวะนั้นจะข้ามปุ่มไปโดยไม่รู้ว่าทำไม
     ปล่อยให้พลิกทันที ส่วนการจางยังทำโดย opacity ตามเดิม */
}
.to-top.show { opacity: 1; transform: none; pointer-events: auto; visibility: visible; }
@media (hover: hover) { .to-top:hover { background: var(--surface-2); color: var(--ink); } }
@media (max-width: 860px) { .to-top { right: var(--s-3); bottom: 72px; } }
@media print { .to-top, .skip-link { display: none !important; } }

/* Present to assistive technology, absent from the page. Used for table
   captions and any other label that would be noise on screen but is the only
   context a screen reader gets. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ==========================================================================
   Accounting figures

   A column of numbers is read by shape before it is read by value. Brackets
   and colour do the work a minus sign cannot: a leading "-" sits at the far
   edge of a right-aligned column where the eye is not looking, and vanishes
   next to a comma.
   ========================================================================== */
.num-neg { color: var(--danger); font-variant-numeric: tabular-nums; white-space: nowrap; }
.num-pos { color: var(--ok); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Nothing happened here — say so quietly rather than printing 0.00, which
   reads as a figure somebody has to check. */
.num-zero { color: var(--faint); font-variant-numeric: tabular-nums; }

/* Totals earn a rule above them, the way they do on paper. */
table.tbl tfoot td, table.tbl tr.total td {
  border-top: 2px solid var(--line-strong);
  font-weight: 600; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
table.tbl tr.grand td { border-top: 3px double var(--line-strong); }

/* ==========================================================================
   Working posture

   Two states for the same table, because closing a month and reviewing one
   document are different jobs with different needs.
   ========================================================================== */
body.dense table.tbl th,
body.dense table.tbl td { padding-top: 6px; padding-bottom: 6px; font-size: var(--t-body); }
body.dense .card { margin-bottom: var(--s-3); }
body.dense .card-body { padding: var(--s-4); }

/* The total must stay in view while lines are being typed: an invoice with
   twenty lines otherwise pushes it off screen exactly when it matters. */
/* เฉพาะ "แถบท้ายฟอร์มเอกสาร" (สั่งด้วย co-class .totals-bar ใน view) —
   ไม่ใช่ .totals ทุกใบ: กล่องสรุปในการ์ด (tax/import) เคยได้ sticky ติดมา
   แล้วกลายเป็นกล่องลอยทับเนื้อการ์ดตัวเอง (ux-critic 31 ส.ค.) · แยกที่
   "ผู้ประกาศ" ไม่ใช้ specificity ทับ — เกต sticky-integrity ห้ามวิธีนั้นถูกแล้ว */
.totals.totals-bar {
  position: sticky; bottom: 0; z-index: 5;
  background: var(--surface);
  padding-top: var(--s-2);
  /* V4 (W10): พื้นทึบ + เส้นขอบบนเส้นเดียว แทนเงาลอย — เงาทำให้แถบกลายเป็นกล่องที่สองในการ์ดฟอร์ม
     (G9) · พื้นทึบยังกันตัวเลขบรรทัดสินค้าไม่ให้ทะลุขึ้นมาใต้ยอดรวมตอนเลื่อน */
  border-top: 1px solid var(--line-strong);
}
/* ธีมมืดไม่ต้องเขียนซ้ำแล้ว --sh-up เปลี่ยนค่าตามธีมเอง */
@media print { .totals { position: static; box-shadow: none; } }
/* กล่องสรุปในการ์ด — ปลดเฉพาะระยะชิดขวา 360px (ของฟอร์ม) ให้เต็มการ์ด
   ส่วนการลอย sticky ไม่ต้องปลดแล้ว เพราะมันอยู่ที่ .totals-bar เท่านั้น */
.card-body > .totals, .card-foot > .totals { margin-left: 0; max-width: none; }

/* ==========================================================================
   Refinement layer v4 — depth, motion, and the small things

   The system before this layer was orderly but inert: every surface was a
   white rectangle with a hairline border and a shadow too faint to see, and
   nothing on the screen ever moved. Order without material reads as
   unfinished rather than restrained.

   The material this reaches for is fine stationery and precision instruments,
   because that is what the product actually is: paper with a warm bloom in it,
   ink-crisp numerals, hairline rules, a green that behaves like fountain-pen
   ink. So the motion decelerates and settles — it never springs or bounces,
   except under a finger on a button, where a little give is the point.

   Everything here is presentation only. No figure, no colour meaning and no
   piece of copy changes: a bar that stands for zero still draws nothing, and a
   number that finishes counting lands on the exact string the server rendered.
   ========================================================================== */

:root {
  /* Deceleration curves. --ease-out settles; --ease-spring is used on press
     only, where overshoot reads as give rather than as decoration. */
  --ease-out:    cubic-bezier(.22, 1, .36, 1);
  --ease-spring: cubic-bezier(.34, 1.28, .48, 1);
  --d-1: 120ms; --d-2: 190ms; --d-3: 320ms; --d-4: 460ms; --d-5: 700ms;

  /* Elevation as three stacked layers — contact, spread and ambient — tinted
     with the palette's green rather than neutral grey, so a card looks lit by
     the same light as the page instead of pasted onto it. */
  /* V4: การ์ดพักตัวไม่มีเงา — ขอบ --card-line รับหน้าที่แยกชั้นแทน · เงามีไว้บอกว่า "ลอยอยู่" เท่านั้น
     ⚠️ เงาศูนย์ต้องเขียน 0 0 #0000 ไม่ใช่ none: โทเคนนี้อยู่ในรายการเงาคั่นจุลภาค none ทำให้ทั้งประกาศใช้ไม่ได้ */
  --sh-card:    0 0 #0000;
  /* ⚠️ ต้องเบากว่า --sh-float (.22) เสมอ — การ์ดที่ชี้อยู่คือชั้นล่างสุดที่ยกขึ้น
     ไม่ใช่ของที่ลอยเหนือทุกอย่าง ของเดิม .24/blur 60 หนักกว่าทั้งดรอปดาวน์
     (.22) และโมดัล (.18) ทำให้ไวยากรณ์ความลึกกลับหัวทั้งชุด */
  --sh-card-hi: 0 2px 4px rgba(16,32,24,.06),  0 8px 18px -6px rgba(16,32,24,.11), 0 18px 38px -18px rgba(16,32,24,.15);
  --sh-float:   0 4px 10px rgba(16,32,24,.07), 0 18px 44px -14px rgba(16,32,24,.22);
  /* A one-pixel highlight along the top edge is what makes a surface read as
     coated stock rather than a filled rectangle. */
  /* V4: ไฮไลต์ขอบบน ("กระดาษเคลือบ") ถอดออก — พื้นผิวแบนแยกด้วยขอบ ไม่ใช่แสง · เงาศูนย์ไม่ใช่ none (ดู --sh-card) */
  --edge:       0 0 #0000;
  /* ขอบแก้วบนพื้นโปร่ง (หน้าเข้าสู่ระบบ) */
  --edge-glass: inset 0 0 0 1px rgba(255,255,255,.16);
  /* เงาที่ทอดขึ้น — ใช้กับแถบที่ติดอยู่ก้นจอเท่านั้น */
  --sh-up:      0 -8px 12px -10px rgba(16,32,24,.28);
  /* พื้นที่ยุบลงไป — แถบตัวกรอง */
  /* V4: แถบตัวกรองไม่ยุบลงอีกแล้ว — เป็นแถวเปล่า */
  --sh-inset:   0 0 #0000;
  /* V4: ถอดแสงเคลือบของหัวการ์ด/ชิป/กล่องแจ้งเตือน · ใช้กับ background-image ตัวเดียวเสมอ none จึงถูกต้อง */
  --sheen:      none;
  /* ⚠️ เงาของปุ่มหลัก — ตกลงด้านล่าง ไม่ใช่เรืองรอบตัว
     สองชั้น: ชั้นแรกคมติดขอบให้ปุ่มมีน้ำหนัก ชั้นสองฟุ้งให้ลอยขึ้นจากพื้น
     ใช้สีของปุ่มเอง จึงอ่านว่า "ยกขึ้นมา" ไม่ใช่ "มีเงาดำทาบ" */
  --glow-brand: 0 1px 2px color-mix(in srgb, var(--brand) 32%, transparent),
                0 8px 20px -8px color-mix(in srgb, var(--brand) 48%, transparent);
  --glow-brand-hi: 0 2px 4px color-mix(in srgb, var(--brand) 34%, transparent),
                   0 14px 28px -10px color-mix(in srgb, var(--brand) 56%, transparent);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --sh-card:    0 0 #0000;
    --sh-card-hi: 0 2px 4px rgba(0,0,0,.5),  0 12px 28px -8px rgba(0,0,0,.6), 0 34px 70px -28px rgba(0,0,0,.6);
    --sh-float:   0 6px 16px rgba(0,0,0,.5), 0 22px 52px -16px rgba(0,0,0,.7);
    --edge:       0 0 #0000;
  --edge-glass: inset 0 0 0 1px rgba(255,255,255,.10);
  --sh-up:      0 -8px 12px -10px rgba(0,0,0,.6);
  --sh-inset:   0 0 #0000;
    --sheen:      none;
  }
}
:root[data-theme="dark"] {
  --sh-card:    0 0 #0000;
  --sh-card-hi: 0 2px 4px rgba(0,0,0,.5),  0 12px 28px -8px rgba(0,0,0,.6), 0 34px 70px -28px rgba(0,0,0,.6);
  --sh-float:   0 6px 16px rgba(0,0,0,.5), 0 22px 52px -16px rgba(0,0,0,.7);
  --edge:       0 0 #0000;
  --edge-glass: inset 0 0 0 1px rgba(255,255,255,.10);
  --sh-up:      0 -8px 12px -10px rgba(0,0,0,.6);
  --sh-inset:   0 0 #0000;
  --sheen:      none;
}

/* --------------------------------------------------------------- ground ----
   A flat fill behind everything is the single biggest reason a screen full of
   white cards looks like a wireframe. Two very wide, very weak washes give the
   page a light source; at this strength they never touch text contrast.

   The wash is a fixed pseudo-element rather than a background-attachment:fixed
   on <body>. They look identical and they are not the same thing: a fixed
   background attachment makes the browser repaint the gradient on every scroll
   event, which is exactly the wrong cost to pay on a five-hundred-row ledger.
   A fixed element is composited once and then simply stays where it is.

   It can sit behind the content on z-index -1 because <body> carries the
   background colour, and a background set on <body> with none on <html>
   propagates to the canvas — so nothing of the body's own paints over it. */
/* V4: ถอดแสงไล่สีหลังหน้า — พื้นเรียบสีเดียวคือพื้นที่สงบที่สุดให้ตัวเลขวางอยู่ (ผู้ตรวจ V4 ข้อ 10)
   กฎคงไว้แต่ไม่วาด เพื่อให้รู้ว่าเคยมีและถอดโดยตั้งใจ */
body::before {
  content: none; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background-image:
    radial-gradient(1200px 620px at 10% -12%, color-mix(in srgb, var(--brand) 7%, transparent), transparent 68%),
    radial-gradient(1000px 560px at 102% -6%, color-mix(in srgb, var(--info) 5%, transparent), transparent 62%);
  background-repeat: no-repeat;
}

/* ------------------------------------------------------------ scrollbars ---
   The default Windows scrollbar is a grey slab with nothing to do with the
   rest of the palette, and it is on screen the whole time. */

/*
 * ⚠️ จองที่ให้แถบเลื่อนไว้เสมอ ไม่ว่าหน้านั้นจะต้องเลื่อนหรือไม่
 *
 * ที่วัดได้จริงบนแดชบอร์ด: ตอนวาดครั้งแรกหน้าพอดีจอ ยังไม่มีแถบเลื่อน
 * แล้วเนื้อหาที่เหลือมาถึง หน้าสูงเกินจอ แถบเลื่อนโผล่ และกินความกว้างไป
 * 11px ทำให้ "ทั้งหน้า" กระโดดไปทางซ้าย 11px หลังรีเฟรชทุกครั้ง
 *   ความกว้างที่ใช้ได้: 1600 → 1589 px (วัดที่จอสูง 1200/1400/1600 ตรงกันทุกครั้ง)
 *
 * ⚠️ และอาการนี้เกิดกับการสลับหน้าด้วย ไม่ใช่แค่ตอนรีเฟรช
 * pjax เปลี่ยนเนื้อหาในที่เดิม หน้าสั้นไปหน้ายาวจึงกระโดดทุกครั้งเช่นกัน
 *
 * scrollbar-gutter: stable จองที่ไว้ตั้งแต่ต้น ตำแหน่งของทุกอย่างจึงนิ่ง
 * ตั้งแต่พิกเซลแรกจนจบ -- แลกกับขอบขวา 11px บนหน้าที่ไม่ต้องเลื่อน
 * ซึ่งเป็นราคาที่ถูกกว่าการเห็นทั้งหน้าขยับทุกครั้งที่โหลด
 */
html { scrollbar-gutter: stable; }

::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--n-400) 40%, transparent);
  border: 3px solid transparent; background-clip: padding-box;
  border-radius: var(--r-pill);
}
@media (hover: hover) { ::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--n-400) 70%, transparent); background-clip: padding-box; } }
::-webkit-scrollbar-corner { background: transparent; }

/* ========================================================================== */
@media (prefers-reduced-motion: no-preference) {

  /* ------------------------------------------------------------ entrance ---
     Content arrives in reading order instead of appearing all at once. The
     rise is 10px and the stagger tops out at eight steps: past that it stops
     reading as sequence and starts reading as lag.

     ⚠️ fill = backwards ไม่ใช่ both (UAT งานคุมเงิน P-1 ข · 29 ก.ย. 2569 · M6)
     แอนิเมชันที่ "ค้างผลหลังจบ" (both/forwards) บน opacity/transform ทำให้กล่องเป็น stacking context ของตัวเองตลอดไป
     ⇒ z-index 50 ของเมนูในหัวหน้า (แปลงเป็นเอกสารอื่น · ส่งออก ภ.ง.ด.) ติดอยู่ใน .page-head แล้วการ์ดที่มาทีหลังวาดทับ
     กดตัวเลือกไม่ได้เลยบนเครื่องที่ไม่ได้ตั้งลดการเคลื่อนไหว — ตัวกวาดกับ UAT รอบก่อนรันในโหมดลด จึงไม่เคยเห็น (บทเรียนข้อ 360)
     backwards = ยังซ่อนระหว่างรอ delay ได้ (สถานะ from) และหลังจบไม่เหลือผลใด ๆ ค้าง · ค่าสุดท้ายเท่าค่าปกติอยู่แล้ว
     · ช่วงที่แอนิเมชันยังวิ่ง ตัวเปิดเมนูใน app.js สั่งให้แอนิเมชันเข้าจบทันที (finishEntrance) · device:sweep โพรบเมนูในเนื้อหน้า */
  @keyframes ya-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

  #mainContent > .page-head,
  #mainContent > .alert,
  #mainContent > .filters,
  #mainContent > .tabs,
  #mainContent > .card,
  #mainContent > .table-wrap,
  #mainContent > .empty,
  #mainContent > .grid > * {
    animation: ya-rise var(--d-4) var(--ease-out) backwards;
  }
  #mainContent > *:nth-child(2),   #mainContent > .grid > *:nth-child(2)   { animation-delay: 40ms; }
  #mainContent > *:nth-child(3),   #mainContent > .grid > *:nth-child(3)   { animation-delay: 80ms; }
  #mainContent > *:nth-child(4),   #mainContent > .grid > *:nth-child(4)   { animation-delay: 120ms; }
  #mainContent > *:nth-child(5),   #mainContent > .grid > *:nth-child(5)   { animation-delay: 155ms; }
  #mainContent > *:nth-child(6),   #mainContent > .grid > *:nth-child(6)   { animation-delay: 185ms; }
  #mainContent > *:nth-child(7),   #mainContent > .grid > *:nth-child(7)   { animation-delay: 210ms; }
  #mainContent > *:nth-child(n+8), #mainContent > .grid > *:nth-child(n+8) { animation-delay: 230ms; }

  /* Rows settle in behind the card that holds them. Only the first screenful
     is animated — a five-hundred-row ledger must paint, not perform.
     ⚠️ ห้ามหรี่แถวด้วย opacity — เดิม ya-rise ค้างผลแบบ fill both ซึ่งชนะกฎปกติเสมอ (ความจางของ 12 แถวแรกหายถาวร ·
     วัดจริงตอนตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569) · ตอนนี้เป็น backwards แล้ว แต่โหมดลดการเคลื่อนไหวยังบังคับ
     opacity: 1 !important ทุกแถว (ข้างล่าง) ⇒ แถวยกเลิก/ถูกกลับรายการยังต้องหรี่ด้วยสีตัวอักษรเท่านั้น */
  #mainContent table.tbl tbody tr:nth-child(-n+12) {
    animation: ya-rise 380ms var(--ease-out) backwards;
  }
  #mainContent table.tbl tbody tr:nth-child(1) { animation-delay: 90ms; }
  #mainContent table.tbl tbody tr:nth-child(2) { animation-delay: 120ms; }
  #mainContent table.tbl tbody tr:nth-child(3) { animation-delay: 148ms; }
  #mainContent table.tbl tbody tr:nth-child(4) { animation-delay: 174ms; }
  #mainContent table.tbl tbody tr:nth-child(5) { animation-delay: 198ms; }
  #mainContent table.tbl tbody tr:nth-child(6) { animation-delay: 220ms; }
  #mainContent table.tbl tbody tr:nth-child(n+7) { animation-delay: 240ms; }

  /* Proportion bars draw themselves from their own baseline. */
  @keyframes ya-grow-x { from { transform: scaleX(0); } to { transform: scaleX(1); } }
  @keyframes ya-grow-y { from { transform: scaleY(0); } to { transform: scaleY(1); } }
  /* backwards ด้วยเหตุผลเดียวกับ ya-rise (M6) — transform ที่ค้างหลังจบ = stacking context ค้าง */
  .bar-fill { transform-origin: left center; animation: ya-grow-x var(--d-5) var(--ease-out) backwards; }
}

@media (prefers-reduced-motion: reduce) {
  /* Nothing here may depend on motion to become visible.
     ⚠️ opacity: 1 !important ทับทุกแถว — ความจางของแถวจึงต้องมาจากสีตัวอักษรเท่านั้น (ดู tr.row-void) */
  #mainContent > *,
  #mainContent table.tbl tbody tr,
  .bar-fill, .chart-bar {
    animation: none !important; opacity: 1 !important; transform: none !important;
  }
}

/* --------------------------------------------------------------- cards -----
   Specificity note: the "Reading comfort" block earlier in this file sets
   .card box-shadow at (0,1,0) and appears above, so a plain .card rule here
   wins on order. Its dark override is (0,2,0) and would still win, which is
   why the dark shadow is restated below at that same weight. */
.card {
  box-shadow: var(--sh-card), var(--edge);
  transition: box-shadow var(--d-3) var(--ease-out), border-color var(--d-3) var(--ease-out), transform var(--d-3) var(--ease-out);
}
:root[data-theme="dark"] .card { box-shadow: var(--sh-card), var(--edge); }
.card-head { background-image: var(--sheen); }

                                                               
@media (hover: hover) { .card-link:hover, a.card:hover, a.stat:hover {
  transform: translateY(-2px);
  box-shadow: var(--sh-card-hi), var(--edge);
} }
.card-link:active, a.card:active, a.stat:active { transform: translateY(-1px); transition-duration: var(--d-1); }

/* ---------------------------------------------------------- KPI tiles ------
   A whisper of the tile's own category, top-left, where the light falls. At 6%
   it is felt rather than seen — the point of the earlier fix stands: an
   expense figure is reported, not alarmed about. */
.stat {
  box-shadow: var(--sh-card), var(--edge);
  /* V4: ถอดวงแสงมุมซ้ายบน — สถานะยังบอกด้วยจุดหน้าป้าย (สีสถานะ) ตามภาษาเดิมของ V3 */
  background-image: none;
  transition: box-shadow var(--d-3) var(--ease-out), border-color var(--d-3) var(--ease-out), transform var(--d-3) var(--ease-out);
}
@media (hover: hover) { a.stat:hover { transform: translateY(-2px); box-shadow: var(--sh-card-hi), var(--edge); border-color: var(--line-strong); } }
.stat .value { letter-spacing: -.032em; }
/* A figure mid-count must not shuffle the layout under itself. */
/* ตัวเลขทุกใบใช้ความกว้างต่อหลักเท่ากัน หลักหน่วยจะได้ตรงกันทุกบรรทัด
   (เดิมผูกกับ [data-count] ที่อนิเมชันนับขึ้นใส่ให้ ซึ่งถอดออกไปแล้ว) */
.stat .value { font-variant-numeric: tabular-nums lining-nums; }

/* -------------------------------------------------------------- buttons ----
   Give under the finger, and a light that follows it. --rx/--ry are written by
   motion.js on pointerdown; with no JS the sheen simply blooms from centre. */
.btn {
  position: relative; overflow: hidden; isolation: isolate;
  transition: background var(--d-1) var(--ease), border-color var(--d-1) var(--ease),
              box-shadow var(--d-2) var(--ease-out), color var(--d-1) var(--ease),
              transform var(--d-2) var(--ease-spring);
}
.btn::after {
  content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit;
  pointer-events: none; opacity: 0;
  background: radial-gradient(circle at var(--rx, 50%) var(--ry, 50%),
              rgba(255,255,255,.42), rgba(255,255,255,0) 46%);
  transition: opacity 520ms var(--ease-out);
}
.btn:active::after { opacity: 1; transition-duration: 0ms; }
@media (hover: hover) { .btn:hover { transform: translateY(-1px); } }
.btn:active { transform: translateY(0); }
@media (hover: hover) { .btn:disabled:hover, .btn.disabled:hover { transform: none; } }
/* ⚠️ ไม่มี inset ขาวขอบบนอีกแล้ว — นั่นคือ "ความมันวาว" ที่ทำให้ดูเก่า */
.btn-primary { box-shadow: var(--glow-brand); }
@media (hover: hover) { .btn-primary:hover {
  box-shadow: var(--glow-brand-hi);
  transform: translateY(-1px);
} }
/* กดแล้วต้องกลับลงมาสัมผัสพื้น ไม่งั้นจะรู้สึกว่าปุ่มไม่ตอบสนอง */
.btn-primary:active { transform: translateY(0); box-shadow: var(--glow-brand); }

/* คนที่ตั้งค่าว่าไม่อยากเห็นการเคลื่อนไหว ต้องไม่เห็นการยกตัว */
@media (prefers-reduced-motion: reduce) {
  @media (hover: hover) { .btn-primary:hover { transform: none; } }
}
/* V4: ปุ่มรอง = เส้นขอบ ไม่มีเงา — เงาสงวนให้ปุ่มหลักจุดเดียวของจอ (P5) */
.btn-ghost { box-shadow: none; }
@media (hover: hover) { .btn-ghost:hover { border-color: var(--ctl-line-hi); } }
/* On a dark ground a white bloom over a pale button blows out; it is dimmed
   to the same amount of light the surface itself carries. */
:root[data-theme="dark"] .btn-ghost::after,
:root[data-theme="dark"] .btn-soft::after {
  background: radial-gradient(circle at var(--rx, 50%) var(--ry, 50%), rgba(255,255,255,.16), rgba(255,255,255,0) 46%);
}

.icon-btn { transition: background var(--d-1) var(--ease), border-color var(--d-1) var(--ease), color var(--d-1) var(--ease), transform var(--d-2) var(--ease-spring); }
@media (hover: hover) { .icon-btn:hover { transform: translateY(-1px); } }
.icon-btn:active { transform: translateY(0); }

/* --------------------------------------------------------------- inputs ----
   The focused field is the only place on the screen that matters at that
   moment, so it gets both the ring and a shadow that lifts it off the card. */
.input:focus, .select:focus, textarea.input:focus {
  box-shadow: var(--ring), 0 6px 18px -10px color-mix(in srgb, var(--brand) 60%, transparent);
}
.input, .select, textarea.input, select.select {
  transition: border-color var(--d-1) var(--ease), box-shadow var(--d-2) var(--ease-out), background var(--d-1) var(--ease);
}

/* --------------------------------------------------------------- chips -----
   Coated, like every other surface — and the coat inverts on a dark ground
   because --sheen is a token, not a hard-coded white. */
.chip { background-image: var(--sheen); }

/* -------------------------------------------------------------- sidebar ----
   A faint brand bloom at the top ties the menu to the brand mark sitting in
   it, and the active rail glows just enough to be found without scanning. */
.sidebar {
  background-image: linear-gradient(180deg, color-mix(in srgb, var(--brand) 4%, transparent), transparent 260px);
}
/* V4 (W12): ถอดการขยับลิงก์เมนูตอนชี้ — เมนู 20 รายการที่ขยับตามเมาส์ตลอดเวลาที่เลื่อนผ่าน คือการเคลื่อนไหวที่ไม่บอกอะไร
   พื้นอ่อนตอนชี้ (กฎฐาน) บอกเป้าหมายได้ครบแล้ว */
.nav-link { transition: background var(--d-1) var(--ease), color var(--d-1) var(--ease); }
.nav-link.active {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--brand) 16%, transparent);
  background-image: var(--sheen);
}
.nav-link.active::before {
  height: 20px;
  box-shadow: 0 0 12px color-mix(in srgb, var(--brand) 60%, transparent);
}
.brand-mark {
  box-shadow: 0 4px 12px -3px color-mix(in srgb, var(--g-700) 48%, transparent),
              0 0 0 1px color-mix(in srgb, var(--g-700) 18%, transparent),
              inset 0 1px 0 rgba(255,255,255,.26);
}

/* --------------------------------------------------------------- topbar ----
   Flat until the page moves under it, then it earns a shadow — the standard
   signal that something is floating above the content. .is-stuck is set by
   motion.js. */
.topbar {
  transition: box-shadow var(--d-3) var(--ease-out), background var(--d-3) var(--ease-out);
  backdrop-filter: saturate(180%) blur(14px);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
}
.topbar.is-stuck { box-shadow: 0 1px 0 var(--line), 0 10px 28px -20px rgba(16,32,24,.55); }
:root[data-theme="dark"] .topbar.is-stuck { box-shadow: 0 1px 0 var(--line), 0 12px 30px -18px rgba(0,0,0,.8); }

/* --------------------------------------------------------------- tables ----
   The hovered row grows an accent at its leading edge. It is drawn as an inset
   shadow rather than a border or a pseudo-element so nothing in the row moves
   by even a pixel when the pointer crosses it. */
table.tbl tbody tr td:first-child {
  box-shadow: inset 3px 0 0 -3px var(--brand);
  transition: box-shadow var(--d-2) var(--ease-out);
}
@media (hover: hover) { table.tbl tbody tr:hover td:first-child { box-shadow: inset 3px 0 0 0 var(--brand); } }
table.tbl thead th {
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  background: color-mix(in srgb, var(--surface-2) 90%, transparent);
}

/* ------------------------------------------------------------- overlays ----
   Dialogs and menus come from a little further away now, and land softly. */
.modal { box-shadow: var(--sh-lg), var(--edge); animation: modal-in var(--d-3) var(--ease-out) both; }
.modal-backdrop { backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); }
.dropdown-menu { box-shadow: var(--sh-float), var(--edge); }
.dropdown-menu.show { animation: modal-in var(--d-2) var(--ease-out); }
.toast { box-shadow: var(--sh-float), var(--edge); animation: toast-in var(--d-3) var(--ease-spring); }
.to-top { box-shadow: var(--sh-float), var(--edge); }
@media (hover: hover) { .to-top:hover { transform: translateY(-2px); } }

/* ------------------------------------------------------------- progress ----
   The navigation bar is the only thing on screen during a slow request, so it
   gets a comet tail rather than a flat 2px rule. */
#ya-progress { height: 3px; }
#ya-progress span {
  background: linear-gradient(90deg, color-mix(in srgb, var(--brand) 45%, transparent), var(--brand-hi));
  box-shadow: 0 0 12px color-mix(in srgb, var(--brand-hi) 85%, transparent);
}
.skel-card { box-shadow: var(--sh-card), var(--edge); }

/* ---------------------------------------------------------- theme swap -----
   Only the large surfaces cross-fade. Transitioning every element on a page
   holding a thousand table cells drops frames on exactly the machines that can
   least afford it. The class is removed by motion.js once the swap is done. */
:root.theme-anim body,
:root.theme-anim .card,
:root.theme-anim .stat,
:root.theme-anim .sidebar,
:root.theme-anim .topbar,
:root.theme-anim .table-wrap,
:root.theme-anim .input,
:root.theme-anim .btn {
  transition: background-color var(--d-3) var(--ease-out),
              border-color     var(--d-3) var(--ease-out),
              color            var(--d-3) var(--ease-out);
}

/* ==========================================================================
   Column chart

   The six-month comparison was the weakest thing on the dashboard: flat
   rectangles with hard tops, an axis floating free of them, and a native
   tooltip that took a second to appear and could only ever show one figure at
   a time. The rules that matter for correctness are unchanged — a month with
   no money still draws no bar, and a real figure is still floored at 1.5% so
   that a small one stays visible.
   ========================================================================== */
.chart-col {
  position: relative; flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; height: 100%;
}
.chart-slot {
  flex: 1 1 auto; display: flex; align-items: flex-end; justify-content: center;
  gap: 6px; padding: 0 var(--s-1);
  border-radius: var(--r-sm) var(--r-sm) 0 0;
  transition: background var(--d-2) var(--ease-out);
}
.chart-col:focus-within .chart-slot {
  background: color-mix(in srgb, var(--brand) 7%, transparent);
}
@media (hover: hover) { .chart-col:hover .chart-slot {
  background: color-mix(in srgb, var(--brand) 7%, transparent);
} }
.chart-bar {
  width: 42%; min-width: 8px;
  border-radius: var(--r-xs) var(--r-xs) 0 0;
  transform-origin: bottom center;
  transition: filter var(--d-2) var(--ease-out);
}
.chart-bar.rev {
  background: linear-gradient(180deg, var(--brand-hi) 0%, var(--brand) 62%, color-mix(in srgb, var(--brand) 84%, #000) 100%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.28), 0 6px 14px -9px color-mix(in srgb, var(--brand) 82%, transparent);
}
.chart-bar.exp {
  background: linear-gradient(180deg, color-mix(in srgb, var(--warn) 78%, #fff) 0%, var(--warn) 62%, color-mix(in srgb, var(--warn) 86%, #000) 100%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.26), 0 6px 14px -9px color-mix(in srgb, var(--warn) 78%, transparent);
}
@media (hover: hover) { .chart-col:hover .chart-bar { filter: saturate(1.12) brightness(1.05); } }
.chart-x { font-size: var(--t-micro); color: var(--muted); text-align: center; padding-top: var(--s-2); transition: color var(--d-2) var(--ease-out); }
@media (hover: hover) { .chart-col:hover .chart-x { color: var(--ink-2); } }

/* The readout sits over the plot, never outside it: .card clips its overflow,
   so a tooltip that left the card would be cut in half. */
.chart-tip {
  /* Anchored to the top of the column and growing downwards, not upwards from
     it: measured from the bottom the readout climbed out of the plot and sat
     across the card's own heading. */
  position: absolute; left: 50%; top: var(--s-1); z-index: 6;
  transform: translate(-50%, -6px);
  opacity: 0; pointer-events: none;
  /* Wide enough that a seven-figure amount and its label stay on one line —
     a wrapped readout changes height as the pointer moves along the row. */
  min-width: 200px; white-space: nowrap; padding: var(--s-2) var(--s-3);
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r-sm); box-shadow: var(--sh-float);
  font-size: var(--t-micro); line-height: 1.7; color: var(--ink-2); text-align: left;
  transition: opacity var(--d-2) var(--ease-out), transform var(--d-2) var(--ease-out);
}
.chart-col:focus-within .chart-tip { opacity: 1; transform: translate(-50%, 0); }
@media (hover: hover) { .chart-col:hover .chart-tip { opacity: 1; transform: translate(-50%, 0); } }
.chart-tip strong { display: block; color: var(--ink); font-size: var(--t-label); margin-bottom: 3px; }
.chart-tip .r { display: flex; align-items: center; gap: 6px; }
.chart-tip .r b { margin-left: auto; padding-left: var(--s-4); color: var(--ink); font-variant-numeric: tabular-nums; }
.chart-tip i { flex: 0 0 auto; display: inline-block; width: 7px; height: 7px; border-radius: var(--r-xs); }
.chart-tip i.rev { background: var(--brand); }
.chart-tip i.exp { background: var(--warn); }
/* Centred on the first column the readout hangs off the card's left edge, and
   on the last two it hangs off the right. Each end anchors to its own side.
   The right-hand rule is written second on purpose: in a chart narrow enough
   for one column to match both, keeping the right edge in wins, because that
   is the edge the card clips against the page. */
.chart-col:first-child .chart-tip { left: 0; transform: translate(0, -6px); }
.chart-col:first-child:focus-within .chart-tip { transform: translate(0, 0); }
@media (hover: hover) { .chart-col:first-child:hover .chart-tip { transform: translate(0, 0); } }
.chart-col:nth-last-child(-n+2) .chart-tip { left: auto; right: 0; transform: translate(0, -6px); }
.chart-col:nth-last-child(-n+2):focus-within .chart-tip { transform: translate(0, 0); }
@media (hover: hover) { .chart-col:nth-last-child(-n+2):hover .chart-tip { transform: translate(0, 0); } }

.chart-grid > div { border-top-color: color-mix(in srgb, var(--line) 80%, transparent); }
.chart-grid > div:last-child { border-top-color: var(--line-strong); }

/* A chart sharing a row with a taller card grows into the space instead of
   sitting at a fixed height above a hole. The axis, the grid and the columns
   are flex siblings with the default stretch alignment, so clearing the fixed
   height is all it takes for the three to keep sharing one baseline. */
.chart-card { display: flex; flex-direction: column; }
.chart-card > .card-body { flex: 1 1 auto; display: flex; flex-direction: column; }
.chart-card .chart { flex: 1 1 auto; min-height: 196px; }
/* The axis is a direct flex child of .chart, so clearing its height lets the
   default stretch alignment size it. .chart-cols is one level deeper, inside
   the positioned plot, and has to be told to fill that plot explicitly —
   "auto" there collapses it to nothing and the columns vanish. */
.chart-card .chart-axis { height: auto; }
.chart-card .chart-cols { height: 100%; }

@media (prefers-reduced-motion: no-preference) {
  .chart-bar { animation: ya-grow-y var(--d-5) var(--ease-out) backwards; } /* M6 — ไม่ค้างผลหลังจบ (ดู ya-rise) */
  .chart-col:nth-child(1) .chart-bar { animation-delay: 60ms; }
  .chart-col:nth-child(2) .chart-bar { animation-delay: 105ms; }
  .chart-col:nth-child(3) .chart-bar { animation-delay: 150ms; }
  .chart-col:nth-child(4) .chart-bar { animation-delay: 195ms; }
  .chart-col:nth-child(5) .chart-bar { animation-delay: 240ms; }
  .chart-col:nth-child(n+6) .chart-bar { animation-delay: 285ms; }
  .chart-bar.exp { animation-duration: calc(var(--d-5) + 60ms); }
}

/* ----------------------------------------------------------------- auth ----
   The sign-in panel is the first thing anyone sees and it was a flat green
   fill. Two soft lights give it depth without putting anything in front of the
   words. */
/* แสงสองวงบนภาพฝั่งซ้ายของหน้าเข้าสู่ระบบ + เท็กซ์เจอร์เส้นสแกนบาง ๆ
   ⚠️ เท็กซ์เจอร์ต้องเป็นเลเยอร์แรก (บนสุด) และมีความจางในตัวมันเอง
   ห้ามใช้ opacity ของทั้ง ::after มาคุม เพราะจะกดไฟสองวงลงไปด้วย
   ซึ่งเป็นบั๊กเดิมที่ทำให้ไฟเหลือความทึบ 5% มาตลอด */
.auth-art::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    repeating-linear-gradient(to bottom, rgba(255,255,255,.045) 0 1px, transparent 1px 34px),
    radial-gradient(680px 460px at 82% 8%, rgba(255,255,255,.16), transparent 62%),
    radial-gradient(520px 520px at 6% 96%, color-mix(in srgb, var(--g-300) 26%, transparent), transparent 64%);
}
.auth-art > * { position: relative; z-index: 1; }

/* The demo sign-in list was one flex line of role, description and address.
   The moment a role needed more words than "การเงิน" the line wrapped, the
   addresses stopped lining up, and the first thing anyone sees of the product
   was a broken list. Two rows on the left, the address in its own column on
   the right: it cannot wrap out of shape whatever the roles are called.
   Scoped to .demo-users, because the company picker reuses .demo-user for a
   genuinely single-line choice. */
.demo-users .demo-user {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "who mail" "desc mail";
  column-gap: var(--s-4); row-gap: 1px;
  align-items: center;
}
.demo-users .demo-user .who { grid-area: who; line-height: 1.4; }
.demo-users .demo-user .who + span {
  grid-area: desc; font-size: var(--t-label); line-height: 1.4;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.demo-users .demo-user .mail {
  grid-area: mail; margin-left: 0; white-space: nowrap;
  font-family: var(--font-num); font-size: var(--t-label);
}

@media print {
  *, *::before, *::after { animation: none !important; transition: none !important; }
  .card, .stat { box-shadow: none !important; background-image: none !important; }
  body { background-image: none !important; }
}

/* ==========================================================================
   Refinement layer v4, second pass — the components the first pass left plain

   Depth on the cards made the rest of the vocabulary look unfinished by
   comparison: a bare icon floating at the top of a navigation tile, an empty
   state that is a grey glyph and an apology, a filter row of controls with
   nothing holding them together, a grand total set in the same ink as the
   subtotal above it. Each of these appears on dozens of screens, so each is
   worth one rule here rather than a patch per page.
   ========================================================================== */

/* ------------------------------------------------------- เส้นทางนำทาง ----
   "หน้าแรก › กลุ่ม › เมนู › หน้าปัจจุบัน" — ตอบคำถามสองข้อที่ผู้ใช้ถามบ่อยที่สุด
   บนหน้าลูก: ตอนนี้ฉันอยู่ไหน และจะกลับขึ้นไปหนึ่งขั้นได้อย่างไร

   ⚠️ อยู่ใน #mainContent เสมอ ไม่ใช่บนแถบบน — pjax สลับเฉพาะเนื้อหน้า
   ของที่อยู่นอกนั้นจะค้างบอกเส้นทางของหน้าก่อนหน้า (บทเรียนข้อ 93)

   ⚠️ กฎของคอมโพเนนต์นี้อยู่ด้วยกันทั้งชุด รวม media query ของมันเอง
   การต่อกฎฐานท้ายไฟล์ทีหลังจะทับ media query ที่อยู่ก่อนหน้าแบบเงียบสนิท
   (บทเรียนข้อ 18 · 29) */
.crumbs { margin: 0 0 var(--s-3); font-size: var(--t-label); line-height: 1.5; }
.crumbs ol {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s-1) 0; margin: 0; padding: 0; list-style: none;
}
.crumbs li { display: inline-flex; align-items: center; min-width: 0; }
.crumbs a, .crumbs .crumb-flat {
  display: inline-flex; align-items: center; gap: 5px;
  max-width: 22ch; overflow: hidden; white-space: nowrap;  /* "…" อยู่ที่ span ลูก (กฎถัดไป) — กล่อง flex ตัดเองไม่ได้ · qa_ellipsis_flex */
  border-radius: var(--r-sm);
}
.crumbs a { color: var(--muted); padding: 3px 5px; margin: -3px -5px; }
@media (hover: hover) { .crumbs a:hover { color: var(--brand); background: var(--brand-softer); text-decoration: none; } }
.crumbs a:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
/* ข้อความในชั้นต้องตัดด้วย "…" ที่ตัวมันเอง (ตรวจทานรอบสุดท้าย 26 ก.ย. 2569)
   กล่องชั้นเป็น inline-flex — text-overflow ของกล่อง flex ไม่มีผลกับลูก ข้อความจึงถูกตัดขาดเงียบ ๆ
   วัดจริงที่ใบสำคัญ 1440: "JV202609-10648" เหลือ "JV202609-1064" ไม่มี "…" = อ่านเป็นเลขที่ของอีกใบ
   min-width: 0 ให้ลูก flex หดได้จริง (ค่าปริยาย auto = ไม่หดต่ำกว่าเนื้อ) */
.crumbs a > span, .crumbs .crumb-flat > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.crumbs .crumb-flat { color: var(--muted); padding: 3px 0; }
.crumbs li[aria-current="page"] .crumb-flat { color: var(--ink-2); font-weight: 600; }
.crumb-sep { color: var(--faint); padding: 0 6px; user-select: none; }

/* ปุ่มย้อนกลับหนึ่งขั้น — ชี้ไปชั้นก่อนหน้าใน trail ไม่ใช่ history.back()
   คนที่เปิดจากลิงก์ในอีเมลหรือบุ๊กมาร์กไม่มีประวัติให้ย้อน */
.crumb-back {
  display: none; align-items: center; gap: 6px;
  margin: 0 0 var(--s-3); padding: 8px 12px 8px 8px;
  border: 1px solid var(--line); border-radius: var(--r);
  background: var(--surface-2); color: var(--ink-2);
  font-size: var(--t-label); font-weight: 600;
  min-height: 44px; box-sizing: border-box;
}
@media (hover: hover) { .crumb-back:hover { border-color: var(--brand); color: var(--brand); text-decoration: none; } }
.crumb-back:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* ชุดไอคอนไม่มีลูกศรซ้าย — caret ชี้ลง หมุน 90 องศาได้ลูกศรซ้ายพอดี */
.crumb-back .ic { transform: rotate(90deg); flex: 0 0 auto; }

/* ⚠️ บนจอแคบ เส้นทางยาวเกินจะดันหน้าให้เลื่อนแนวนอน
   จึงย่อป้ายลงและให้ปุ่มย้อนกลับขึ้นมาแทน ซึ่งเป็นทางกลับที่นิ้วกดง่ายกว่า
   ⛔ ไม่ตัดชั้นทิ้ง — ชั้นที่หายคือทางกลับที่หายไปด้วย */
@media (max-width: 700px) {
  /*
   * ⚠️ บนมือถือ ลิงก์ในเส้นทางต้องเป็นเป้ากดขนาดเต็ม 44px
   *
   * device:sweep วัดจากหน้าจริงแล้วพบว่าสูงแค่ 25.5px (กฎ 6ข ของเจ้าของ)
   * ⛔ ห้ามแก้ด้วย ::after เหมือนตราบนเมนู — ชั้นในเส้นทางอยู่ติดกันเป็นแถว
   * พื้นที่ที่ขยายออกไปจะทับชั้นข้างเคียงจนแตะโดนชั้นผิด (บทเรียนข้อ 114
   * ตอนท้าย: ปุ่ม ? ในตารางยอดรวมพิสูจน์แล้วว่า ::after ไม่ได้ผลเสมอไป)
   * ต้องสูงจริง แล้วกันไม่ให้กินพื้นที่แนวตั้งด้วยการให้แถวเลื่อนแทนการตัดบรรทัด
   *
   * ⛔ ห้ามลดจำนวนชั้น — ชั้นที่หายคือทางกลับที่หายไปด้วย
   */
  .crumbs ol {
    flex-wrap: nowrap;
    overflow-x: auto;
    /* ปัดขอบแล้วปัดต่อ ต้องไม่ลากหน้าข้างหลังหรือสั่ง "ย้อนกลับ" ของ iOS
       (บทเรียนข้อ 117 — เคยทำให้คนหลุดออกจากหน้าที่กำลังกรอกอยู่) */
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .crumbs ol::-webkit-scrollbar { display: none; }
  .crumbs li { flex: 0 0 auto; }
  .crumbs a, .crumbs .crumb-flat {
    max-width: 14ch;
    min-height: 44px;
    padding-top: 0; padding-bottom: 0;
    margin-top: 0; margin-bottom: 0;
  }
  .crumb-sep { padding: 0 3px; }
  .crumb-back { display: inline-flex; }
}

/* ดีไซน์ V4 (W6): มือถือ ≤560 เหลือปุ่มกลับอย่างเดียว — แถวเส้นทางที่เลื่อนได้ + ปุ่มกลับ + ชื่อหน้า
   กินจอแรก ~150px ก่อนเห็นเนื้อหาสักบรรทัด · ทางกลับไม่หาย: ปุ่มกลับชี้ชั้นก่อนหน้า
   หน้าแรกอยู่บนแถบล่าง และหน้าชั้นแรกไม่มีปุ่มกลับเพราะแถบล่างคือทางกลับของมัน (breadcrumb.php) */
@media screen and (max-width: 560px) {
  .crumbs { display: none; }
}

/*
 * ⚠️ แท็บเล็ตก็ใช้นิ้ว (24 ก.ย. 2569) — บล็อกข้างบนครอบเฉพาะจอ ≤ 700px
 * device:sweep ที่แท็บเล็ต 768/1024 วัดลิงก์ในเส้นทางได้ 25.5px ⇒ ต้องสูงจริงตามเหตุผลเดียวกับข้างบน
 * (ห้าม ::after — ชั้นอยู่ติดกันเป็นแถว) · ไม่บังคับเลื่อนแนวนอนบนแท็บเล็ต เพราะจอกว้างพอให้ตัดบรรทัดได้
 * ⚠️ ตัวกวาดเคยไม่เห็นข้อนี้เพราะนับ "จุดที่ตกบน <li>/<ol> ที่ห่อลิงก์" เป็นการแตะโดนลิงก์
 */
@media (any-pointer: coarse) {
  .crumbs a, .crumbs .crumb-flat {
    min-height: 44px;
    padding-top: 0; padding-bottom: 0;
    margin-top: 0; margin-bottom: 0;
  }
}

/* ------------------------------------------------------------ hub tiles ----
   A navigation tile is chosen by its icon before its words are read, so the
   icon is given a ground of its own. On hover it takes the brand at full
   strength — the tile says "this one" before the pointer has stopped. */
.tile-ic {
  display: inline-grid; place-items: center;
  width: 46px; height: 46px; margin-bottom: var(--s-3);
  border-radius: var(--r-lg);
  background: linear-gradient(150deg, color-mix(in srgb, var(--brand) 13%, transparent), color-mix(in srgb, var(--brand) 5%, transparent));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--brand) 15%, transparent), var(--edge);
  transition: background var(--d-3) var(--ease-out), color var(--d-3) var(--ease-out), box-shadow var(--d-3) var(--ease-out);
}
@media (hover: hover) { .card-link:hover .tile-ic, a.card:hover .tile-ic {
  background: linear-gradient(150deg, var(--brand-hi), var(--brand));
  color: var(--on-brand);
  box-shadow: var(--glow-brand), inset 0 1px 0 rgba(255,255,255,.22);
} }
                                                                 
                                                                     
                                                                           
                                                                     
                                                                  
                                                                
                                                                      
@media (hover: hover) { .card-link:hover .tile-ic .ic, a.card:hover .tile-ic .ic { color: inherit; } }

/* --------------------------------------------------------- empty states ----
   Seventy-five screens can be empty, and an empty screen is where somebody
   new to the product spends their first minute. The glyph gets a ground so
   the state reads as a designed page rather than a page that failed to load.
   Padding and background apply to an <svg> like any other element, so this
   needs no wrapper and no change to a single view. */
/*
 * ⚠️ ลูกตรงของ .empty เท่านั้น (`>`) ไม่ใช่ไอคอนทุกตัวข้างใน
 *
 * กฎนี้เขียนไว้ให้ "ไอคอนใหญ่กลางหน้า" แต่เดิมเขียนเป็น `.empty .ic`
 * ซึ่งเล่นงาน **ไอคอนในปุ่ม** ด้วย — ปุ่ม "เพิ่มรายการแรก" จึงมีวงกลม
 * พื้นหลังขนาด 57×57 อยู่ข้างใน ทั้งที่ไอคอนตั้งไว้ 17px
 *
 * วัดจริงบนหน้า /recurring: ปุ่มสูง 95px (ควรเป็น 49) และตัวหนังสือ
 * เยื้องขวาจากกึ่งกลางปุ่ม 32px — เจ้าของทักว่า "ข้อความมันไม่อยู่ตรงกลาง"
 *
 * ⚠️ ผมหลงทางอยู่สองรอบเพราะประกอบ empty state ขึ้นมาเองเพื่อดู
 * ตัวอย่างที่ประกอบเองไม่มีไอคอนในปุ่ม จึงวัดได้ว่า "เยื้อง 0" ทุกครั้ง
 * ต้องเปิดหน้าที่ empty state แสดงผลจริงเท่านั้น (บทเรียนข้อ 86)
 */
.empty > .ic {
  box-sizing: content-box;
  padding: 20px; border-radius: 50%;
  color: var(--brand);
  background: radial-gradient(circle at 32% 28%, color-mix(in srgb, var(--brand) 14%, transparent), color-mix(in srgb, var(--brand) 5%, transparent) 72%);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--brand) 12%, transparent);
}
:root[data-theme="dark"] .empty > .ic { color: var(--brand); }
.empty h3 { font-size: var(--t-heading); }

/* ---------------------------------------------------------------- tabs -----
   The underline alone was the only thing separating the current view from the
   five beside it. A resting ground on hover and a lit rule under the active
   tab give the row a shape the eye can follow. */
.tab { border-radius: var(--r-xs) var(--r-xs) 0 0; }
@media (hover: hover) { .tab:hover { background: color-mix(in srgb, var(--brand) 6%, transparent); } }
/* V4: แท็บที่เลือกอยู่ = เส้นใต้สีแบรนด์ + ตัวหนา (มีอยู่แล้วในกฎฐาน) — ถอดแสงเรืองและไล่สี
   เส้นใต้พอบอก "อยู่ตรงนี้" แล้ว แสงคือเสียงรบกวนชั้นที่สอง */
.tab.active { background: none; box-shadow: none; }

/* -------------------------------------------------------------- filters ----
   A filter bar is one control, not six. Sat directly on the page background
   the fields floated free and the page had no place where "narrow this down"
   lived. A sunken panel is the standard way to say a group belongs together
   and is not part of the data below it. */
/* V4: แถวเปล่า ไม่มีแผงครอบ — แผงจมเป็นกล่องที่สองเหนือตาราง และดันแถวข้อมูลแรกลงไปอีก ~40px (ผู้ตรวจ V4 ข้อ 7)
   ความเป็น "กลุ่มเดียวกัน" มาจากการอยู่แถวเดียวกันและระยะห่าง ไม่ใช่กรอบ */
.filters {
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
}
/* A filter bar that is the whole content of a card is already inside a panel;
   a second one nested in the first reads as a mistake. */
.card-body > .filters:only-child, .card > .filters {
  padding: 0; background: none; border: 0; box-shadow: none; border-radius: 0;
}

/* --------------------------------------------------------------- totals ----
   The grand total is what the whole document is for, and it was set in the
   same weight as the subtotal above it with a rule between them. */
.totals .row.grand {
  margin-top: var(--s-3);
  /* V4: ถอดกรอบไล่สี — ยอดรวมเด่นด้วยขนาดและสีแบรนด์ของตัวเลขอยู่แล้ว กรอบคือกล่องซ้อนในการ์ด */
  padding-top: var(--s-3);
}

/* ---------------------------------------------------------- pagination -----
   The page you are on should look pressed, and the ones you are not should
   look pressable. */
.pagination a { transition: background var(--d-1) var(--ease), border-color var(--d-1) var(--ease), transform var(--d-2) var(--ease-spring), box-shadow var(--d-2) var(--ease-out); }
@media (hover: hover) { .pagination a:hover { transform: translateY(-1px); box-shadow: var(--sh-sm); } }
/* ⚠️ ไม่มีไฮไลต์ขาวขอบบน — ใช้ภาษาเดียวกับ .btn-primary
   เลขหน้าที่เปิดอยู่คือปุ่มสถานะ ไม่ใช่วัตถุมันวาว */
.pagination .current { box-shadow: var(--glow-brand); }

/* ---------------------------------------------------------- search box -----
   The magnifier sat on the same line as the placeholder and read as part of
   the text. It steps back until the field is being used. */
.search-box .ic { transition: color var(--d-2) var(--ease-out); }
.search-box:focus-within .ic { color: var(--brand); }

/* -------------------------------------------------------------- alerts -----
   An alert is a raised note on the page, not a coloured background behind it. */
.alert { box-shadow: none; background-image: none; }

@media (prefers-reduced-motion: reduce) {
  .tile-ic, .pagination a, .btn, .stat, .card-link, a.card, a.stat, .nav-link, .icon-btn, .to-top {
    transition: none !important;
  }
  @media (hover: hover) { .btn:hover, .stat:hover, .pagination a:hover, .card-link:hover, a.card:hover, a.stat:hover, .nav-link:hover, .icon-btn:hover, .to-top:hover { transform: none !important; } }
}

@media print {
  .tile-ic, .empty > .ic, .filters, .totals .row.grand, .alert {
    background: none !important; box-shadow: none !important;
  }
}

/* ==========================================================================
   Global search results

   Anchored under the sidebar's search box rather than opening as a dialog: the
   box is where the person is already looking, and a panel that grows out of it
   keeps the menu visible behind, so the search reads as an extension of the
   navigation instead of a mode you have to get out of.
   ========================================================================== */
.menu-search { position: relative; }
/* The field has to keep clear of the badge, or the placeholder runs under it
   and is cut off mid-word. */
.menu-search input { padding-right: 62px; }
.menu-search-kbd {
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
  font-family: var(--font-num); font-size: var(--t-micro); letter-spacing: .02em;
  color: var(--faint); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--r-xs);
  padding: 2px 5px; pointer-events: none; white-space: nowrap;
}
.menu-search input:focus + .menu-search-kbd { opacity: 0; }

.search-results {
  /* Wider than the menu that launches it — a result is only useful if its
     document number and counterparty are both readable, and 264px truncates
     the number to "TX202…", which is the half the eye is looking for. */
  position: absolute; z-index: 60;
  top: calc(100% + 6px); left: 0;
  width: min(520px, calc(100vw - 2 * var(--s-4)));
  max-height: min(60vh, 460px); overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: var(--r);
  box-shadow: var(--sh-float), var(--edge);
  padding: 5px;
}
.search-results[hidden] { display: none; }

.search-group {
  font-size: var(--t-micro); font-weight: 600; text-transform: uppercase;
  color: var(--faint); padding: var(--s-3) var(--s-3) var(--s-1);
}
.search-group:first-child { padding-top: var(--s-2); }

.search-item {
  display: flex; align-items: center; gap: var(--s-2);
  padding: 7px var(--s-3); border-radius: var(--r-xs);
  color: var(--ink-2); text-decoration: none;
  transition: background var(--d-1) var(--ease), color var(--d-1) var(--ease);
}
.search-item.is-active {
  background: var(--brand-soft); color: var(--brand-ink); text-decoration: none;
}
@media (hover: hover) { .search-item:hover {
  background: var(--brand-soft); color: var(--brand-ink); text-decoration: none;
} }
.search-item-main { display: grid; gap: 1px; min-width: 0; flex: 1 1 auto; }
.search-item-title {
  font-size: var(--t-body); font-weight: 500; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.search-item.is-active .search-item-title { color: var(--brand-ink); }
@media (hover: hover) { .search-item:hover .search-item-title { color: var(--brand-ink); } }
.search-item-meta {
  font-size: var(--t-micro); color: var(--faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.search-item-right { flex: 0 0 auto; font-size: var(--t-label); font-weight: 600; color: var(--ink); }

.search-empty { padding: var(--s-5) var(--s-4); text-align: center; }
.search-empty-title { font-size: var(--t-body); color: var(--ink-2); }
.search-empty-hint { font-size: var(--t-micro); color: var(--faint); margin-top: var(--s-1); }

/* ---- keyboard shortcut panel -------------------------------------------- */
kbd {
  display: inline-block; min-width: 22px; text-align: center;
  font-family: var(--font-num); font-size: var(--t-micro); line-height: 1.5;
  color: var(--ink-2); background: var(--surface-2);
  border: 1px solid var(--line-strong); border-bottom-width: 2px;
  border-radius: var(--r-xs); padding: 2px 6px;
}
.shortcut-group + .shortcut-group { margin-top: var(--s-5); }
.shortcut-group .eyebrow { margin-bottom: var(--s-2); }
.shortcut-row {
  display: flex; align-items: baseline; gap: var(--s-4);
  padding: 6px 0; border-bottom: 1px solid var(--line);
}
.shortcut-row:last-child { border-bottom: 0; }
.shortcut-keys { flex: 0 0 122px; display: flex; align-items: center; gap: 4px; }
.shortcut-plus { color: var(--faint); font-size: var(--t-micro); }
.shortcut-what { flex: 1 1 auto; font-size: var(--t-body); color: var(--ink-2); }

/* ---- plain-language explainer under a page title ------------------------
   Four of the core accounting screens are named entirely in terms only a
   bookkeeper knows. The people using this product are often the owner or the
   admin, and the .sub line above is already carrying the company, the counts
   and the date range — so the explanation gets its own line, set quieter than
   the data so it can be skipped once it has been read. */
.page-explain {
  margin: var(--s-2) 0 0;
  max-width: 76ch;
  font-size: var(--t-label); line-height: 1.6; color: var(--faint);
}
@media print { .page-explain { display: none; } }
/* V4: คงไว้บนมือถือโดยตั้งใจ — แผนเดิมจะซ่อนเพราะเชื่อว่าแผงวิธีใช้พูดเรื่องเดียวกัน
   ตรวจแล้วไม่จริง: แผงของสมุดรายวัน/ผังบัญชี/ปิดงวด/งบทดลองใช้คู่มือระดับหมวด ("accounting" · "reports")
   คำอธิบายศัพท์เฉพาะหน้าจึงอยู่ที่นี่ที่เดียว — ซ่อนแล้วผู้ใช้มือถือจะไม่มีทางอ่านเจอ */

/* An alert's label is a fixed phrase — the message beside it wraps freely, but
   the label itself must not break mid-word. Thai has no spaces, so a browser
   will happily split "ข้อควรรู้" into "ข้อควร" and "รู้", which is not a word. */
.alert > strong:first-child,
.alert > div > strong:first-child,
.alert > .alert-text > strong:first-child { white-space: nowrap; }  /* ux.js mendAlerts() ห่อประโยคเป็น span.alert-text (V4 · G7) */
/* หัวข้อกล่องสภาพผลค้นเลขเอกสารเป็นประโยคที่มีคำค้นของผู้ใช้อยู่ข้างใน ไม่ใช่ป้ายสั้น — ต้องตัดบรรทัดได้ทุกความกว้าง
   (ผู้ตรวจ diff 26 ก.ย. 2569: เลขอ้างอิงผู้ขาย ~24 ตัวอักษรทำหัวข้อทะลุขอบกล่องที่ 768–1100px) · น้ำหนัก 0,3,1 ชนะกฎบน */
.alert.search-state .search-state-body > strong { white-space: normal; }

/* ==========================================================================
   ตารางกลายเป็นการ์ดบนจอแคบ
   --------------------------------------------------------------------------
   ระบบนี้มีตาราง 104 หน้า ตารางหลักกว้าง 9 คอลัมน์ ซึ่งบนมือถือเลื่อนซ้ายขวา
   อย่างเดียว อ่านจริงไม่ได้ ที่ 720px ลงมาจึงพับเป็นการ์ดแถวละใบ

   ป้ายกำกับมาจาก data-label ที่ tablecards.js อ่านมาจากหัวตาราง — CSS เป็น
   คนตัดสินใจว่าจะแสดงแบบไหน สคริปต์แค่เตรียมข้อมูลให้ ถ้าสคริปต์ไม่ทำงาน
   ตารางก็เลื่อนแนวนอนเหมือนเดิม ไม่มีอะไรพัง
   ========================================================================== */
@media (max-width: 720px) {
  /* ⚠️ กฎ "ห้ามตัดบรรทัด" สองข้อในไฟล์นี้ตั้งอยู่บนสมมติฐานว่าข้อความสั้น
     — ป้ายกำกับของกล่องแจ้งเตือน (.alert > strong) กับป้ายสถานะ (.chip)
     สมมติฐานนั้นจริงบนจอกว้าง และผิดทันทีที่มีคนใส่ประโยคเต็มลงไป
     ซึ่งเกิดขึ้นแล้วจริงหลายที่: หน้าแพ็กเกจมี <strong> ยาว 623px
     หน้าภาษีนิติบุคคลมี 377px หน้าเมตริกสาขามีป้ายสถานะยาว 258px

     ผลบนจอ 360px คือทั้งหน้าเลื่อนซ้ายขวา 307px โดยกล่องแจ้งเตือน
     ที่ตั้งใจจะ "เตือน" กลับเป็นตัวดันเนื้อหาที่เหลือออกนอกจอ

     บนจอที่ไม่มีที่พอ ป้ายที่ตัดบรรทัดอ่านได้เสมอ ดีกว่าหน้าที่เลื่อนได้
     ทั้งใบ — จึงคลายเฉพาะช่วงนี้ ไม่แตะพฤติกรรมบนจอกว้างที่ยังถูกอยู่ */
  .alert > strong:first-child,
  .alert > div > strong:first-child,
  .alert > .alert-text > strong:first-child,
  .chip { white-space: normal; }

  /* กรอบเลื่อนไม่ต้องทำหน้าที่แล้ว การ์ดพอดีจอตั้งแต่ต้น */
  .table-wrap { overflow-x: visible; }
  .table-wrap::after { display: none; }

  /* ⚠️ ข้อยกเว้นที่ขาดไม่ได้ — ตารางบรรทัดสินค้าไม่ได้พับเป็นการ์ด
     มันเป็นตารางกรอกข้อมูล 9 ช่องต่อบรรทัดที่ยังตั้ง min-width: 760px ไว้
     โดยตั้งใจ (ดูบล็อก 860px) เพราะช่องเลือก VAT ที่ถูกบีบเหลือ 30px
     แคบกว่าข้อความในตัวมันเอง

     กฎ overflow-x: visible ด้านบนจึงถอดกรอบเลื่อนออกจากตารางที่ยัง
     ต้องการมันอยู่ ผลคือหน้าสร้างเอกสารดันทั้งหน้าให้เลื่อนแนวนอน 418px
     ที่ 390px — ทั้งแถบบน เมนู และปุ่มบันทึก เลื่อนตามไปด้วยทั้งหมด
     ⚠️ วัดจากหน้าจริงเท่านั้นถึงจะเห็น เพราะกฎทั้งสองข้อ "ถูก" ทั้งคู่
     เมื่ออ่านแยกกัน */
  .table-wrap:has(.line-table) { overflow-x: auto; }
  .table-wrap:has(.line-table)::after { display: block; }

  /* min-width ที่บังคับให้เลื่อนแนวนอนต้องหายไป ไม่งั้นค่าจะถูกดันออกนอกจอ
     โดยที่ป้ายยังอยู่ในจอ — เห็นแต่ชื่อคอลัมน์ ไม่เห็นตัวเลข */
  table.tbl[data-cards] { display: block; min-width: 0; width: auto; background: none; }

  /* ตารางแบบ "หัวข้อ–ค่า" สองคอลัมน์ ไม่ต้องกว้าง 640px — มันอยู่ในรูปทรง
     การ์ดอยู่แล้ว การบังคับความกว้างทำให้หน้ารายละเอียดเลื่อนซ้ายขวาเปล่า ๆ */
  table.tbl[data-kv] { min-width: 0; }
  table.tbl[data-kv] th,
  table.tbl[data-kv] td { white-space: normal; }
  table.tbl[data-cards] > thead { display: none; }
  table.tbl[data-cards] > tbody { display: block; }

  table.tbl[data-cards] > tbody > tr {
    display: block;
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    /* --surface เท่ากับพื้นหลังกล่องที่ครอบอยู่พอดี การ์ดจะมองไม่เห็นเลย
       --surface-2 คือระดับถัดไปที่ออกแบบไว้สำหรับการซ้อนชั้นแบบนี้ */
    background: var(--surface-2);
    /* V4: การ์ดแถวไม่มีเงา — พื้น + ขอบพอแยกชั้นแล้ว (เงาคือกล่องที่สามในการ์ด) */
    box-shadow: none;
  }

  table.tbl[data-cards] > tbody > tr + tr { margin-top: var(--s-3); }

  table.tbl[data-cards] > tbody > tr > td {
    display: flex;
    align-items: baseline;
    /* ป้ายถูกดันไปซ้ายด้วย margin-right:auto ของ ::before ที่เหลือจึงเกาะกลุ่ม
       ชิดขวาเสมอ ไม่ว่าช่องนั้นจะมีกี่ชิ้น — ตัวเลขทุกบรรทัดตรงแนวกัน */
    justify-content: flex-end;
    gap: var(--s-2);
    width: auto;
    padding: 2px 0;
    border: 0;
    /* ค่าชิดขวา และตัดบรรทัดได้ — ชื่อบริษัทไทยยาวเกินหนึ่งบรรทัดเป็นปกติ */
    text-align: right;
    white-space: normal;
  }
  /* ผลค้นเลขเอกสาร (26 ก.ย. 2569) — บรรทัด "ตรงด้วยเลขอะไร" ต้องอยู่ "ใต้" ชื่อ ไม่ใช่คอลัมน์ข้างกัน
     โหมดแสดงทุกช่อง (การ์ดป้าย:ค่า) และกระดาษ A4 (~688px ตกบล็อกนี้) เคยบีบชื่อเหลือคอลัมน์ 128px ข้างชิป (ผู้ตรวจ diff) */
  table.tbl[data-cards] > tbody > tr > td:has(> .search-hit) { flex-wrap: wrap; }
  table.tbl[data-cards] > tbody > tr > td > .search-hit { flex: 1 0 100%; text-align: right; }

  /* ป้ายซ้าย ค่าขวา — รูปแบบเดียวกับสลิปธนาคารและใบเสร็จ */
  table.tbl[data-cards] > tbody > tr > td[data-label]::before {
    content: attr(data-label);
    /* ป้ายไม่ยอมหด ป้ายที่หดจะกลายเป็น "ลูกค้ ..." ซึ่งอ่านไม่ออก */
    flex: 0 0 auto;
    margin-right: auto;
    color: var(--muted);
    font-size: var(--t-label);
    font-weight: 500;
    text-align: left;
  }

  /* ค่าตัวเลขชิดขวาเสมอ และกว้างต่อหลักเท่ากันทุกบรรทัด */
  table.tbl[data-cards] > tbody > tr > td.num,
  table.tbl[data-cards] > tbody > tr > td.right {
    font-variant-numeric: tabular-nums lining-nums;
  }

  /* ช่องที่ไม่มีป้าย (คอลัมน์ปุ่ม) กินเต็มความกว้างและอยู่ท้ายการ์ด */
  table.tbl[data-cards] > tbody > tr > td:not([data-label]) {
    justify-content: flex-end;
    gap: var(--s-2);
    margin-top: var(--s-2);
    padding-top: var(--s-2);
    border-top: 1px solid var(--line);
  }

  /* ช่องว่างจริง ๆ ไม่ต้องกินที่ */
  table.tbl[data-cards] > tbody > tr > td:empty { display: none; }

  /* แถว "ไม่พบข้อมูล" กินเต็มการ์ด ไม่ต้องมีป้าย */
  table.tbl[data-cards] > tbody > tr[data-card-full] > td {
    display: block;
    text-align: center;
  }

  /* ยอดรวมท้ายตารางยังต้องอ่านออกว่าเป็นยอดรวม
   *
   * ⚠️ ของเดิมเรียงเป็นแถวเดียว (flex + justify-content: flex-end) ซึ่งพอดี
   * เฉพาะตารางที่มียอดรวมคอลัมน์เดียว ตารางเงินเดือนมีห้าคอลัมน์
   * ตัวเลขจึงกว้างเกินจอ ล้นออกไปทางซ้าย ป้ายหายไปนอกจอ
   * และตัวเลขตัวแรกถูกตัดหัวจนอ่านผิดความหมาย
   *
   * ตอนนี้ใช้รูปทรงเดียวกับการ์ดของแถวข้อมูล — ป้ายซ้าย ค่าขวา บรรทัดละคู่
   * ซึ่งอ่านออกทุกบรรทัดและไม่มีวันล้น ไม่ว่าตารางจะมีกี่คอลัมน์
   */
  table.tbl[data-cards] > tfoot { display: block; }
  table.tbl[data-cards] > tfoot > tr {
    display: block;
    margin-top: var(--s-3);
    padding: var(--s-3) var(--s-4);
    border-radius: var(--r-lg);
    background: var(--surface);
    font-weight: 600;
  }
  table.tbl[data-cards] > tfoot > tr > td {
    display: flex;
    align-items: baseline;
    gap: var(--s-2);
    width: auto;
    padding: 2px 0;
    border: 0;
    text-align: right;
    white-space: normal;
  }
  table.tbl[data-cards] > tfoot > tr > td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 auto;
    margin-right: auto;
    color: var(--muted);
    font-size: var(--t-label);
    font-weight: 500;
    text-align: left;
  }
  table.tbl[data-cards] > tfoot > tr > td.num,
  table.tbl[data-cards] > tfoot > tr > td.right {
    font-variant-numeric: tabular-nums lining-nums;
  }
  /* ป้ายรวม ("รวมในหน้านี้ N รายการ") เป็นหัวข้อของการ์ด ไม่ใช่คู่ป้าย–ค่า
   *
   * ⚠️ ต้องซ่อนป้ายที่สคริปต์แปะให้ด้วย ไม่งั้นหัวการ์ดจะอ่านว่า
   * "งวดรวมในหน้านี้ (2 รอบ)" — ป้ายของคอลัมน์แรกไปเกาะหน้าข้อความสรุป
   * ช่องนี้เป็นหัวข้อของทั้งการ์ด ไม่ได้เป็นค่าของคอลัมน์ "งวด"
   */
  table.tbl[data-cards] > tfoot > tr > td:first-child {
    display: block;
    margin-bottom: var(--s-2);
    padding-bottom: var(--s-2);
    border-bottom: 1px solid var(--line);
    text-align: left;
  }
  table.tbl[data-cards] > tfoot > tr > td:first-child::before { content: none; }
  table.tbl[data-cards] > tfoot > tr > td:empty { display: none; }
}
/*
 * ตารางรายการของหน้าเอกสารสาธารณะ (/d/<token> · public/doc.php) — M9 (UAT งานคุมเงิน P-1 ข · 29 ก.ย. 2569)
 *
 * หน้าเดียวของระบบที่ลูกค้าภายนอกเห็น: เลย์เอาต์ auth ไม่โหลด tablecards.js ตารางจึงไม่พับเป็นการ์ด
 * แล้วสามกฎที่ถูกเมื่ออ่านแยกกันรวมกันตัดคอลัมน์ทิ้ง — table.tbl min-width 640 (≤860) · .table-wrap overflow visible (≤720)
 * · .card overflow hidden ⇒ ลูกค้าบนมือถือเห็นแต่ชื่อรายการ ไม่เห็นจำนวน/ราคา/ยอด และหน้าไม่เลื่อนแนวนอน ตัววัดการล้นจึงเงียบ
 * ⇒ แต่ละบรรทัดเป็นการ์ดป้าย:ค่าแบบเดียวกับการ์ดในแอป (ชื่อรายการเป็นหัวการ์ด) · ป้ายเขียนในมุมมองเอง (data-label)
 * ⚠️ screen เท่านั้น — กระดาษ A4 (~688–718px ที่ขอบปกติ) เข้าช่วง ≤720 และหน้าพิมพ์ของหน้านี้ต้องไม่เปลี่ยน (บทเรียนข้อ 400)
 * ⚠️ เดสก์ท็อป/แท็บเล็ต (>720) ไม่แตะ · device:sweep วัดว่าทุกเซลล์มองเห็นได้ที่ 390 (โพรบหน้าเอกสารสาธารณะ)
 */
@media screen and (max-width: 720px) {
  table.tbl[data-doc-items] { display: block; min-width: 0; width: auto; background: none; }
  table.tbl[data-doc-items] > thead { display: none; }
  table.tbl[data-doc-items] > tbody { display: block; }
  table.tbl[data-doc-items] > tbody > tr {
    display: block;
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface-2);
  }
  table.tbl[data-doc-items] > tbody > tr + tr { margin-top: var(--s-3); }
  table.tbl[data-doc-items] > tbody > tr > td {
    display: flex; align-items: baseline; justify-content: flex-end; gap: var(--s-2);
    padding: 2px 0; border: 0; text-align: right; white-space: normal;
  }
  table.tbl[data-doc-items] > tbody > tr > td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 auto; margin-right: auto;
    color: var(--muted); font-size: var(--t-label); font-weight: 500; text-align: left;
  }
  /* ชื่อรายการ = หัวการ์ด (ไม่มีป้าย · ชิดซ้าย · ตัดบรรทัดได้) */
  table.tbl[data-doc-items] > tbody > tr > td:first-child {
    display: block; text-align: left; font-weight: 600;
    padding-bottom: var(--s-2); margin-bottom: var(--s-1); border-bottom: 1px solid var(--line);
  }
  table.tbl[data-doc-items] > tbody > tr > td:first-child .small { font-weight: 400; }
}
/*
 * ตารางที่ "ยังเป็นตาราง" บนจอ ≤720 ต้องมีกรอบเลื่อนของตัวเอง (ผู้ทวน G4 · 29 ก.ย. 2569 · พี่น้องของ M9 · บทเรียนข้อ 44)
 *
 * กฎ .table-wrap { overflow-x: visible } ของบล็อก ≤720 ข้างบนตั้งอยู่บนสมมติฐานว่า "ทุกตารางพับเป็นการ์ดแล้ว"
 * ซึ่งจริงเฉพาะเลย์เอาต์หลัก (tablecards.js ติด data-cards ให้ทุกตาราง) — หน้าในเลย์เอาต์ auth ไม่มีสคริปต์นั้น
 * วัดจริงที่ 390: หน้าแพ็กเกจ /pricing ตารางกว้าง 640 ใน .card (overflow hidden · ขอบขวา 374) เซลล์แพ็กเกจที่ 2–5 อยู่ที่ x 412–657
 * ถูกตัดทิ้งทั้งหมด · หน้าสถานะ /status คอลัมน์รายละเอียดถูกตัดที่ 374 · หน้าไม่เลื่อนแนวนอน ตัววัดการล้นจึงเงียบ
 * ⇒ ตารางที่ไม่มี data-cards (ไม่ได้พับ) · ไม่ใช่ตารางรายการของหน้าเอกสารสาธารณะ (พับเองข้างบน) · ไม่ใช่หัวข้อ–ค่า (ไม่กว้าง)
 *   ได้กรอบเลื่อนคืน — เงาขอบของ .table-wrap (background-attachment) บอกว่ายังเลื่อนต่อได้ทางไหน
 * ⚠️ ตารางในเลย์เอาต์หลักมี data-cards หลัง tablecards.js ทำงาน จึงไม่โดนกฎนี้ (พฤติกรรมเดิมทุกหน้าในแอป)
 * ⚠️ screen เท่านั้น — กระดาษ A4 เข้าช่วง ≤720 (บทเรียนข้อ 400) · device:sweep (CLIP_PROBE) ตรวจทุกเซลล์ของทุกตาราง
 */
@media screen and (max-width: 720px) {
  .table-wrap:has(> table.tbl:not([data-cards]):not([data-doc-items]):not([data-kv])) { overflow-x: auto; }
}
/*
 * [UAT3] หน้าสถานะระบบบนมือถือ — ตารางสามคอลัมน์ (ส่วนของระบบ · สถานะ · รายละเอียด) ในเลย์เอาต์ auth (ไม่มี tablecards.js)
 * กฎข้างบนให้กรอบเลื่อนคืนแล้ว แต่คอลัมน์ "รายละเอียด" — เหตุผลที่ส่วนนั้นขึ้น "เฝ้าดู" — อยู่นอกจอเสมอ ต้องปัดเลื่อนถึงจะเห็น
 * (UAT 30 ก.ย. 2569 · 390) ⇒ ≤560 พับเป็นแถวละสองบรรทัด: ชื่อส่วน + ชิปสถานะ / รายละเอียดเต็มความกว้าง
 * หัวตารางถอยแบบตัดขอบ (โปรแกรมอ่านหน้าจอยังอ่านชื่อคอลัมน์) · มุมมองติด data-stack เอง (ไม่เดาจากจำนวนคอลัมน์)
 * ⚠️ screen เท่านั้น — กระดาษ A4 เข้าช่วง ≤720 (บทเรียนข้อ 400) · device:sweep (STACK) วัดว่าทุกเซลล์อยู่ในจอ
 */
@media screen and (max-width: 560px) {
  /* ตารางที่พับแล้วไม่ต้องมีกรอบเลื่อน — กฎข้างบนทำให้ .table-wrap เป็นกล่องเลื่อน (overflow-y ถูกคิดเป็น auto ตามไปด้วย)
     แล้วแถบเลื่อนแนวตั้ง 11px กินขอบขวาของตาราง · selector ต้องหนักกว่ากฎข้างบน (:not ชุดเดียวกัน + [data-stack]) */
  .table-wrap:has(> table.tbl[data-stack]:not([data-cards]):not([data-doc-items]):not([data-kv])) { overflow: visible; }
  table.tbl[data-stack] { min-width: 0; }
  table.tbl[data-stack] > thead {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }
  table.tbl[data-stack],
  table.tbl[data-stack] > tbody { display: block; }
  table.tbl[data-stack] > tbody > tr {
    display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--s-3); align-items: center;
    border-bottom: 1px solid var(--line);
  }
  table.tbl[data-stack] > tbody > tr > td { display: block; border: 0; }
  table.tbl[data-stack] > tbody > tr > td:nth-child(3) { grid-column: 1 / -1; padding-top: 0; }
}
/*
 * หน้าเอกสารของลูกค้าบนกระดาษ (ผู้ทวน G4 · 29 ก.ย. 2569 · วัดด้วย design:audit --print ของหน้านี้) — ลูกค้าที่ได้ลิงก์สั่งพิมพ์ใบเองได้
 * กฎ table.tbl { min-width: 640px } ของบล็อก ≤860 ไม่มี screen จึงมีผลบนกระดาษ A4 ด้วย แล้วตารางรายการใน .card-body (~605px)
 * ล้นการ์ดที่ตัดขอบ ⇒ คอลัมน์ "รวม" ถูกตัดบนกระดาษ (มีมาก่อน M9 — ตารางสี่คอลัมน์เดิมก็ล้นเท่ากัน)
 * ⇒ บนกระดาษตารางนี้หดตามความกว้างของการ์ด (ห้าคอลัมน์พอดีโดยไม่ต้องบังคับ) · จอไม่เปลี่ยน · device:sweep วัดเซลล์บนกระดาษของหน้านี้
 */
@media print {
  table.tbl[data-doc-items] { min-width: 0; }
}
/* การ์ดของมือถือแบบแน่น — คู่ของกฎเดียวกันในบล็อกแท็บเล็ตข้างล่าง
   (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · รีวิว RV3 · บทเรียนข้อ 44 · 340)
   หน้าที่ไม่ประกาศบทบาทบนมือถือได้ปุ่ม "แถวแน่น" มาตลอด แต่ช่องของการ์ด (0,2,4) ชนะ body.dense table.tbl td (0,2,3)
   ⇒ กดแล้วจอไม่เปลี่ยน · แก้พร้อมแท็บเล็ตในรอบเดียว ไม่ปล่อยพี่น้องไว้
   ⚠️ แยกบล็อก screen ออกมา ไม่ใส่ในบล็อก ≤720 ข้างบน (บล็อกนั้นไม่มี screen) — กระดาษ A4 ~688px ต้องไม่เปลี่ยนตามค่าที่ผู้ใช้เลือกบนจอ
   ⚠️ :where(:not([data-cards-compact])) กันแถวกระชับ — body.dense ... > tr (0,3,4) หนักกว่าแถวกระชับ (0,3,3)
      ไม่กันแล้วระยะของแถวกระชับถูกบีบตาม ทั้งที่บนแถวกระชับปุ่มนี้คือ "แสดงทุกช่อง" ไม่ใช่ความหนาแน่น */
@media screen and (max-width: 720px) {
  /* ยอดรวมท้ายการ์ด: ล้างพื้นเทาของ <td> (table.tbl tfoot td) — คู่ของกฎเดียวกันในบล็อกพับบนแท็บเล็ตและแถวกระชับ
     (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · ข้อ 1) ไม่ล้างแล้วยอดแตกเป็นแถบเทาสองแถบต่อยอด · อยู่ในบล็อก screen เพราะบล็อกการ์ด
     ≤720 ข้างบนไม่มี screen และกระดาษ A4 (~688px) ต้องเหมือนเดิมทุกประการ */
  table.tbl[data-cards]:not(.line-table) > tfoot > tr > td { background: none; }
  /* แถวยอดรวมที่มียอดตัวเดียว และมุมมองขอให้ช่องยอดไม่มีป้าย (data-label="") = "ชื่อยอด ····· ยอด" บรรทัดเดียว
     (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · ข้อ 1 — ค่าเผื่อหนี้สงสัยจะสูญ: ที่ควรมี · ที่ตั้งไว้แล้ว · ต้องบันทึกเพิ่ม)
     รูปหัวการ์ด + ค่าบรรทัดใหม่ของยอดรวมหลายคอลัมน์ ทำให้ยอดเดี่ยวสามแถวกลายเป็นสามกล่องห่างกันครึ่งจอ
     แถวแบบเดียวกันที่ต่อกันอยู่รวมเป็นกล่องเดียว (ไม่เว้นระยะคั่น) · ⚠️ คู่ของกฎนี้อยู่ในบล็อกพับบนแท็บเล็ต */
  table.tbl[data-cards]:not(.line-table) > tfoot > tr:has(> td:first-child + td[data-label=""]:last-child) { display: flex; align-items: baseline; gap: var(--s-3); }
  table.tbl[data-cards]:not(.line-table) > tfoot > tr:has(> td:first-child + td[data-label=""]:last-child) > td:first-child { flex: 1 1 auto; margin: 0; padding: 2px 0; border-bottom: 0; }
  table.tbl[data-cards]:not(.line-table) > tfoot > tr:has(> td:first-child + td[data-label=""]:last-child) + tr:has(> td:first-child + td[data-label=""]:last-child) { margin-top: 0; padding-top: 0; }
  table.tbl[data-cards]:not(.line-table) > tfoot > tr:has(> td:first-child + td[data-label=""]:last-child):has(+ tr > td:first-child + td[data-label=""]:last-child) { padding-bottom: var(--s-1); }
  body.dense table.tbl[data-cards]:where(:not([data-cards-compact])) > tbody > tr { padding: var(--s-2) var(--s-3); }
  body.dense table.tbl[data-cards]:where(:not([data-cards-compact])) > tbody > tr + tr { margin-top: var(--s-2); }
  body.dense table.tbl[data-cards]:where(:not([data-cards-compact])) > tbody > tr > td[data-label] { padding-top: 0; padding-bottom: 0; }
}
/* ช่อง "เลือกทั้งหมด" ของการ์ดมือถือทั่วไป (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569) — คู่ของกฎเดียวกันในแถวกระชับ
   การ์ดซ่อนหัวตารางทั้งแถบ ⇒ ช่องนี้หาย ต้องติ๊กทีละใบ (ไฟล์โอนเงิน · ขาย/ซื้อ/เงินเดือนในโหมดแสดงทุกช่อง)
   แสดงเฉพาะช่องติ๊กพร้อมป้ายจาก aria-label (tablecards.js ติด data-select-label + data-select-all) · แตะทั้งแถบ 44px ได้
   ⚠️ เฉพาะเมื่อมีแถวที่ติ๊กได้จริง — "เลือกทั้งหมด" ที่ไม่มีอะไรให้เลือกคือตัวควบคุมตาย
   ⚠️ บล็อก screen แยก — บล็อกการ์ด ≤720 ข้างบนไม่มี screen และกระดาษ A4 ต้องไม่เปลี่ยน */
@media screen and (max-width: 720px) {
  table.tbl[data-cards]:not([data-cards-compact]):has(> tbody > tr > td input[type="checkbox"]:not([disabled])) > thead:has(> tr > th[data-select-all] input) { display: block; }
  table.tbl[data-cards]:not([data-cards-compact]) > thead:has(> tr > th[data-select-all] input) > tr { display: flex; }
  table.tbl[data-cards]:not([data-cards-compact]) > thead:has(> tr > th[data-select-all] input) > tr > th:not([data-select-all]) { display: none; }
  table.tbl[data-cards]:not([data-cards-compact]) > thead > tr > th[data-select-all]:has(input) {
    display: flex; align-items: center; gap: var(--s-3);
    width: auto; min-height: 44px; padding: var(--s-2) var(--s-4);
    position: static; background: none; border: 0; cursor: pointer;
    font-size: var(--t-label); font-weight: 500; color: var(--ink-2); text-transform: none; letter-spacing: 0;
  }
  table.tbl[data-cards]:not([data-cards-compact]) > thead > tr > th[data-select-all]:has(input)::after { content: attr(data-select-label); }

  /* ตารางกรอกข้อมูลที่พับเป็นการ์ด (data-card-form — เตรียมไฟล์โอนเงิน) (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · T2)
     ช่องของการ์ดเป็นแถวเดียว "ป้ายซ้าย ค่าขวา" ซึ่งถูกกับค่าที่อ่าน แต่ไม่ใช่กับช่องกรอก: ป้ายยาว
     ("ชื่อผู้รับเงิน (ต้องตรงกับหน้าสมุดบัญชี)") บีบช่องชื่อเหลือ "สมช" บนจอ 390 และคำอธิบายใต้ช่อง
     ("เงินเดือนสุทธิ") ถูกบีบเป็นสามบรรทัดข้างช่องกรอก
     ⇒ ช่องกรอกยืดเต็มที่ที่เหลือ และขึ้นบรรทัดใหม่เต็มความกว้างเมื่อเหลือไม่ถึง 12rem · คำอธิบายขึ้นบรรทัดของตัวเองใต้ค่า */
  table.tbl[data-cards][data-card-form]:not([data-cards-compact]) > tbody > tr > td { flex-wrap: wrap; }
  table.tbl[data-cards][data-card-form]:not([data-cards-compact]) > tbody > tr > td > .input { flex: 1 1 12rem; min-width: 0; }
  table.tbl[data-cards][data-card-form]:not([data-cards-compact]) > tbody > tr > td > .small { flex: 1 1 100%; }

  /* ช่องติ๊กที่เป็นช่องแรกของการ์ด (ไม่มีป้าย) = หัวการ์ดชิดซ้าย — ไม่ใช่ "แถบปุ่มท้ายการ์ด" ที่มีเส้นคั่นด้านบน
     (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · T2) กฎช่องไม่มีป้ายข้างบนเขียนไว้ให้คอลัมน์ปุ่มซึ่งอยู่ท้ายแถว
     ช่องติ๊กอยู่ต้นแถว ⇒ ได้เส้นคั่นลอยเหนือช่องติ๊กทุกการ์ด (ไฟล์โอนเงิน · ขาย/ซื้อในโหมด "แสดงทุกช่อง")
     · ช่องของคอลัมน์ติ๊กที่ไม่มีช่องติ๊ก (ใบที่ไม่ใช่ร่าง — มีแต่ช่องว่างของเทมเพลต ซึ่ง :empty ไม่นับว่าว่าง) ไม่ต้องกินที่ */
  table.tbl[data-cards]:not([data-cards-compact]) > tbody > tr > td:first-child:not([data-label]):has(input[type="checkbox"]) {
    justify-content: flex-start; margin-top: 0; padding-top: 0; border-top: 0;
  }
  table.tbl[data-cards]:not([data-cards-compact]) > tbody > tr > td.w-check:not(:has(input)) { display: none; }
}

/* ==========================================================================
   ตารางที่ไม่ประกาศบทบาท บนแท็บเล็ต — พับเป็นการ์ด ป้าย:ค่า เหมือนมือถือ
   (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · C33 · แผน V4 W8 · คำตัดสิน §14.2)
   --------------------------------------------------------------------------
   แผน V4 ข้อวินิจฉัยที่ 1: "ตารางบนแท็บเล็ต 768 แย่ที่สุดในระบบ" — ช่วง 721–860 และแท็บเล็ตสัมผัสถึง 1024
   ตารางที่ไม่ประกาศ data-card-role (105 มุมมอง) ยังเป็นตารางเต็ม บังคับกว้าง 640px (บล็อก ≤860)
   ⇒ ผู้ใช้/บริษัทในเครือ/ตลาดออนไลน์ เลื่อนข้าง · ชื่อพนักงาน ภ.ง.ด.1/สปส. ตัด 4–5 บรรทัด · พยากรณ์เงินสด 6–8 บรรทัด
   แผนสั่งให้พับเหมือนมือถือ แต่รอบส่งมอบทำเฉพาะตารางที่มีบทบาท (บล็อกแถวกระชับข้างล่าง)

   ช่วงจอ = ช่วงเดียวกับแถวกระชับ (ตารางสองแบบบนอุปกรณ์เดียวกันต้องเปลี่ยนรูปพร้อมกัน)
   แต่เริ่มที่ 721 — ≤720 มีบล็อกการ์ดข้างบนอยู่แล้ว (ไม่ซ้อนสองบล็อกบนมือถือ มือถือไม่เปลี่ยนแม้แต่พิกเซลเดียว)
   ⚠️ screen เท่านั้น — กระดาษ A4 (~688px) อยู่ในช่วงนี้ งบ/ใบสำคัญที่สั่งพิมพ์ต้องเหมือนเดิมทุกประการ

   ไม่พับ (ตัดสินที่ตาราง ไม่ใช่ที่หน้า):
   · [data-cards-compact] — ตารางที่ประกาศบทบาทมีแถวกระชับของตัวเองอยู่แล้ว (งบทดลอง · งบกำไรขาดทุน · งบดุล อยู่กลุ่มนี้)
   · [data-kv] — ตารางไม่มีหัว (หัวข้อ–ค่า) = รูปการ์ดอยู่แล้ว (ตัวงบกระแสเงินสด · งบแสดงการเปลี่ยนแปลงส่วนของเจ้าของ อยู่กลุ่มนี้)
     tablecards.js ไม่ติด data-cards ให้ตารางพวกนี้อยู่แล้ว — กันซ้ำไว้ไม่ให้พังวันที่สคริปต์เปลี่ยน
   · .line-table — ตารางกรอกบรรทัดสินค้ามีการ์ดของตัวเอง (linecards) และตั้งใจเลื่อนแนวนอนที่ช่วงนี้
   · [data-statement] — ตารางงบ/แบบภาษีที่มีหัวตาราง และผู้อ่านต้องเทียบตัวเลขแนวตั้งกับแบบกระดาษ/ข้ามคอลัมน์
     ⚠️ CSS เดาเองไม่ได้ว่าตารางไหนคืองบ (ไม่มีอะไรบนหน้าบอกได้) — มุมมองต้องติดแอตทริบิวต์นี้ที่แท็ก <table> เอง
        ติดแล้ว (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · รีวิว RV1): รายการในแบบ ภ.พ.30 · ขั้นภาษีและรายการปรับปรุง
        ภ.ง.ด.50 · เมทริกซ์งบรวมแยกกิจการ · เงินเข้า–ออกรายเดือน · ยอดขายรายเดือน · งบทดลอง/กำไรขาดทุน/งบดุล
        (สามใบหลังเป็นแถวกระชับอยู่แล้ว ป้ายนี้มีผลตอนกด "แสดงทุกช่อง" บนแท็บเล็ต — กลับเป็นตารางเต็ม ไม่ใช่การ์ด)
        ⚠️ เกต qa_statement_tables บังคับให้ตารางทุกตัวในหน้ารายงาน/ภาษี/งบรวมถูกตัดสินอย่างใดอย่างหนึ่ง
           (ติดป้ายนี้ หรืออยู่ในรายชื่อ "พับ" ของเกตพร้อมเหตุผล) — ตารางใหม่ที่ไม่มีใครตัดสินจะแดงทันที
        (งบอายุลูกหนี้/เจ้าหนี้เป็นรายการรายคน ผู้ตรวจ C33 ให้พับ จึงไม่อยู่ในกลุ่มนี้)
   · ⚠️ ตารางที่หัวมีช่องติ๊ก "เลือกทั้งหมด" พับด้วย — ข้อยกเว้นนี้ถูกถอดแล้ว (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · T2)
     เดิม (รีวิว RV2) ยกเว้นไว้ด้วยเหตุผลว่า "ป้ายของแถบเลือกทั้งหมดมาจาก data-select-label ซึ่ง tablecards.js
     ทำให้เฉพาะตาราง [data-cards-compact]" — ไม่จริงแล้วตั้งแต่คอมมิตเดียวกัน: tablecards.js ติด data-select-label
     + data-select-all ให้ทุกตารางที่หัวมีช่องติ๊ก และตัวรับแตะครอบ th[data-select-all] ของทุกตาราง
     ผลของข้อยกเว้นที่อ้างเหตุผลเก่า: เตรียมไฟล์โอนเงิน (bankfiles/create) บน iPad แนวตั้ง 768 ยังเป็นตารางกว้าง ~924px
     ในกรอบเลื่อนข้าง — ยอดโอนรายคนและยอดรวมที่จะสั่งธนาคารโอนอยู่นอกจอ ขณะที่มือถือได้การ์ดที่ใช้ได้ครบ
     และขาย/ซื้อ/เงินเดือนในโหมด "แสดงทุกช่อง" ของผู้อนุมัติเป็นตาราง 10–11 คอลัมน์ ขณะที่ผู้ไม่มีสิทธิ์อนุมัติ
     (หัวไม่มีช่องติ๊ก) ได้การ์ด — ปุ่มเดียวกันให้ผลสองแบบตามสิทธิ์ ซึ่งไม่มีใครตั้งใจ
     ⇒ พับเหมือนมือถือ พร้อมแถบ "เลือกทั้งหมด" แบบเดียวกับบล็อกมือถือ (กฎคู่อยู่ท้ายบล็อกนี้)
     ⚠️ ตัววัดของ design:audit นับกรอบเลื่อนข้างเฉพาะในจอแรก — ตารางโอนเงินอยู่ใต้จอแรก เกต C5/W8 จึงเขียวมาตลอด
   · ตารางสองคอลัมน์ (ไม่มีช่องที่สามในหัว) — พอดีจอแท็บเล็ตเสมอ พับแล้วได้แต่ความสูงสองเท่า
     (หลักเดียวกับเกต qa_table_cards: ตารางสองคอลัมน์ "อยู่ในรูปทรงการ์ดอยู่แล้วโดยธรรมชาติ")
     ⚠️ ยกเว้นเมื่อมุมมองติด data-fold — "พอดีจอเสมอ" ไม่จริงเมื่อคอลัมน์หนึ่งเป็นข้อความยาว
        (รายการเครื่องมือของผู้ช่วย AI: ชื่อ + รหัส | คำอธิบาย — วัดได้สามบรรทัดต่อช่องบน 768 · W8)
        เงื่อนไข "มีช่องที่สาม หรือ data-fold" เขียนแบบ :not(:not(A):not(B)) เพราะเกตหลายตัวแยก selector ด้วยจุลภาค

   ⚠️ เงื่อนไขทั้งหมดอยู่ใน :where() — น้ำหนัก selector ของทุกกฎจึงเท่ากับบล็อก ≤720 ข้างบนพอดี
      กฎอื่นที่เคยชนะ/แพ้การ์ดบนมือถือ (โฟกัสแถว · ความหนาแน่น · ยอดรวมท้าย) ได้ผลเดียวกันบนแท็บเล็ต
      และ min-width: 640px ของบล็อก ≤860 (0,1,1) แพ้ table.tbl[data-cards] (0,2,1) ⇒ ไม่เลื่อนข้างอีก
   ========================================================================== */
@media screen and (min-width: 721px) and (max-width: 860px), screen and (min-width: 721px) and (max-width: 1024px) and (any-pointer: coarse) {
  /* กรอบไม่ต้องเลื่อนข้างแล้ว · และไม่ต้องเป็นกล่องเลื่อนแนวตั้ง (เพดาน 100vh − 230px ของจอ ≥861 มีไว้ให้หัวตารางลอย
     ซึ่งการ์ดไม่มีหัวตาราง) — รายการการ์ดเลื่อนไปกับหน้า ไม่ซ้อนกล่องเลื่อนบนจอสัมผัส
     ⚠️ :has() ซ้อนใน :has() ไม่ได้ (ทั้งกฎถูกทิ้งเงียบ ๆ — วัดแล้วเพดานยังอยู่) จึงเขียนเงื่อนไขเป็นเส้นทางเดียว
        ไล่ลงไปถึงช่องที่สามของหัวตาราง แทนการเรียก selector ของตารางข้างล่างซ้ำ
     ⚠️ ตารางที่ไม่พับต้องได้กรอบเลื่อนข้างคืนครบ ไม่งั้นล้นจอ — เงื่อนไขของกรอบกับของตารางต้องตรงกันเสมอ
        (ข้อยกเว้น "หัวมีช่องติ๊ก" เคยอยู่ทั้งสองที่ ถอดพร้อมกันทั้งสองที่ · T2) */
  .table-wrap:has(> table.tbl[data-cards]:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]) > thead > tr > :nth-child(3)) { overflow-x: visible; max-height: none; }
  .table-wrap:has(> table.tbl[data-cards]:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]) > thead > tr > :nth-child(3))::after { display: none; }
  /* ตารางสองคอลัมน์ที่มุมมองขอให้พับ (data-fold) — เงื่อนไข "ช่องที่สาม" ข้างบนไม่ครอบ จึงเขียนแยก (ไม่มีจุลภาคใน :has) */
  .table-wrap:has(> table.tbl[data-cards][data-fold]) { overflow-x: visible; max-height: none; }
  .table-wrap:has(> table.tbl[data-cards][data-fold])::after { display: none; }

  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) { display: block; min-width: 0; width: auto; background: none; }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > thead { display: none; }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody { display: block; }

  /* การ์ดแถวละใบ — รูปเดียวกับมือถือ (คำอธิบายเต็มอยู่ในบล็อก ≤720 ข้างบน) */
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr {
    display: block;
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface-2);
    box-shadow: none;
  }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr + tr { margin-top: var(--s-3); }

  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr > td {
    display: flex;
    align-items: baseline;
    justify-content: flex-end;
    gap: var(--s-2);
    width: auto;
    padding: 2px 0;
    border: 0;
    text-align: right;
    white-space: normal;
  }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr > td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 auto;
    margin-right: auto;
    color: var(--muted);
    font-size: var(--t-label);
    font-weight: 500;
    text-align: left;
  }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr > td.num,
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr > td.right {
    font-variant-numeric: tabular-nums lining-nums;
  }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr > td:not([data-label]) {
    justify-content: flex-end;
    gap: var(--s-2);
    margin-top: var(--s-2);
    padding-top: var(--s-2);
    border-top: 1px solid var(--line);
  }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr > td:empty { display: none; }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr[data-card-full] > td {
    display: block;
    text-align: center;
  }

  /* ปุ่ม "แถวแน่น" ต้องมีผลกับการ์ดที่พับด้วย (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · รีวิว RV3 · บทเรียนข้อ 340)
     หน้าที่ไม่ประกาศบทบาท ux.js cardsMode() ยังถือเป็นโหมดตาราง ปุ่มจึงเป็น "แถวแน่น — เห็นหลายแถวขึ้น"
     แต่ body.dense table.tbl td (0,2,3) แพ้ช่องของการ์ด (0,2,4) — กดแล้ว aria-pressed สลับ จอไม่เปลี่ยน = ปุ่มตาย
     ⇒ แบบแน่นของการ์ด: ขอบในการ์ด · ระยะระหว่างการ์ด · ระยะบรรทัดป้าย:ค่า แคบลง (เห็นหลายใบขึ้นต่อจอจริง)
     ⚠️ ขึ้นต้นด้วย body.dense ⇒ ชนะกฎการ์ดข้างบนด้วยน้ำหนัก ไม่พึ่งลำดับบรรทัด
     ⚠️ ช่องไม่มีป้าย (แถบปุ่มท้ายการ์ด) ไม่แตะ — ระยะบนของมันคือที่วางเส้นคั่น ถ้าบีบเส้นจะชิดปุ่ม
     คู่ของมือถือ ≤720 อยู่ต่อท้ายบล็อกการ์ดของมือถือ (มือถือเป็นปุ่มตายแบบเดียวกันมาก่อนรอบนี้) */
  body.dense table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr { padding: var(--s-2) var(--s-3); }
  body.dense table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr + tr { margin-top: var(--s-2); }
  body.dense table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr > td[data-label] { padding-top: 0; padding-bottom: 0; }

  /* ยอดรวมท้ายตาราง — ป้ายซ้าย ค่าขวา บรรทัดละคู่ (เหตุผลในบล็อก ≤720) */
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tfoot { display: block; }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tfoot > tr {
    display: block;
    margin-top: var(--s-3);
    padding: var(--s-3) var(--s-4);
    border-radius: var(--r-lg);
    background: var(--surface);
    font-weight: 600;
  }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tfoot > tr > td {
    display: flex;
    align-items: baseline;
    gap: var(--s-2);
    width: auto;
    padding: 2px 0;
    border: 0;
    /* พื้นเทาของ tfoot มาจาก <td> (table.tbl tfoot td) ไม่ใช่ <tr> — ไม่ล้างแล้วยอดรวมแตกเป็นแถบเทาสองแถบต่อยอด
       (หัว + ค่า มีช่องขาวคั่น) · แถวกระชับล้างไว้แล้วตั้งแต่ผู้ตรวจ V4 รอบสอง (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · ข้อ 1) */
    background: none;
    text-align: right;
    white-space: normal;
  }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tfoot > tr > td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 auto;
    margin-right: auto;
    color: var(--muted);
    font-size: var(--t-label);
    font-weight: 500;
    text-align: left;
  }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tfoot > tr > td.num,
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tfoot > tr > td.right {
    font-variant-numeric: tabular-nums lining-nums;
  }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tfoot > tr > td:first-child {
    display: block;
    margin-bottom: var(--s-2);
    padding-bottom: var(--s-2);
    border-bottom: 1px solid var(--line);
    text-align: left;
  }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tfoot > tr > td:first-child::before { content: none; }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tfoot > tr > td:empty { display: none; }
  /* ยอดเดี่ยวที่ช่องยอดไม่มีป้าย = บรรทัดเดียว และรวมเป็นกล่องเดียว — คู่ของกฎในบล็อก screen ≤720 (เหตุผลอยู่ที่นั่น · ข้อ 1) */
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tfoot > tr:has(> td:first-child + td[data-label=""]:last-child) { display: flex; align-items: baseline; gap: var(--s-3); }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tfoot > tr:has(> td:first-child + td[data-label=""]:last-child) > td:first-child { flex: 1 1 auto; margin: 0; padding: 2px 0; border-bottom: 0; }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tfoot > tr:has(> td:first-child + td[data-label=""]:last-child) + tr:has(> td:first-child + td[data-label=""]:last-child) { margin-top: 0; padding-top: 0; }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tfoot > tr:has(> td:first-child + td[data-label=""]:last-child):has(+ tr > td:first-child + td[data-label=""]:last-child) { padding-bottom: var(--s-1); }

  /* แถบ "เลือกทั้งหมด" ของการ์ดที่พับบนแท็บเล็ต — คู่ของกฎเดียวกันในบล็อกมือถือ (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · T2)
     หัวตารางถูกซ่อนทั้งแถบ ⇒ แสดงเฉพาะช่องติ๊กของหัว + ป้ายจาก aria-label (tablecards.js ติด data-select-label/data-select-all)
     แตะทั้งแถบ 44px ได้ (ตัวรับแตะใน tablecards.js ครอบ th[data-select-all] ทุกตาราง) · เฉพาะเมื่อมีแถวที่ติ๊กได้จริง
     ⚠️ :has() ของ thead หนักกว่า > thead { display: none } ข้างบน — ชนะโดยไม่พึ่งลำดับบรรทัด */
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))):has(> tbody > tr > td input[type="checkbox"]:not([disabled])) > thead:has(> tr > th[data-select-all] input) { display: block; }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > thead:has(> tr > th[data-select-all] input) > tr { display: flex; }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > thead:has(> tr > th[data-select-all] input) > tr > th:not([data-select-all]) { display: none; }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > thead > tr > th[data-select-all]:has(input) {
    display: flex; align-items: center; gap: var(--s-3);
    width: auto; min-height: 44px; padding: var(--s-2) var(--s-4);
    position: static; background: none; border: 0; cursor: pointer;
    font-size: var(--t-label); font-weight: 500; color: var(--ink-2); text-transform: none; letter-spacing: 0;
  }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > thead > tr > th[data-select-all]:has(input)::after { content: attr(data-select-label); }

  /* ตารางกรอกข้อมูลที่พับ (data-card-form) — คู่ของกฎในบล็อกมือถือ (เหตุผลอยู่ที่นั่น · T2) */
  table.tbl[data-cards][data-card-form]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr > td { flex-wrap: wrap; }
  table.tbl[data-cards][data-card-form]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr > td > .input { flex: 1 1 12rem; min-width: 0; }
  table.tbl[data-cards][data-card-form]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr > td > .small { flex: 1 1 100%; }
  /* ช่องติ๊กต้นแถว = หัวการ์ด · ช่องคอลัมน์ติ๊กที่ไม่มีช่องติ๊กไม่กินที่ — คู่ของกฎในบล็อกมือถือ (T2) */
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr > td:first-child:not([data-label]):has(input[type="checkbox"]) {
    justify-content: flex-start; margin-top: 0; padding-top: 0; border-top: 0;
  }
  table.tbl[data-cards]:where(:not([data-cards-compact]):not([data-kv]):not(.line-table):not([data-statement]):not(:not(:has(> thead > tr > :nth-child(3))):not([data-fold]))) > tbody > tr > td.w-check:not(:has(input)) { display: none; }
}

/* ==========================================================================
   แถวกระชับ (ดีไซน์ V4 · W8) — รายการหลักบนมือถือและแท็บเล็ต
   --------------------------------------------------------------------------
   การ์ด "ป้าย:ค่า" ข้างบนอ่านออกทุกช่อง แต่ใบขายหนึ่งใบกิน 7 บรรทัด (~310px)
   เห็น 2.5 ใบต่อจอ — คนที่เปิดมือถือมาหา "ใบของลูกค้ารายนี้" ต้องเลื่อนผ่าน
   ป้ายที่อ่านซ้ำทุกใบ · แถวกระชับเหลือสองบรรทัดแบบรายการในแอปธนาคาร:
       [ ] ลูกค้า ····························· ยอดรวม
           เลขที่ · วันที่ · ค้างชำระ 1,200.00      [สถานะ]

   กลไก: หัวตารางประกาศ <th data-card-role> · tablecards.js คัดลงทุกช่องแล้ว
   ติดธง data-cards-compact · ตารางที่ไม่ประกาศ = การ์ดแบบเดิมทุกอย่าง
   กดทั้งแถวใช้ tr.row-link ของ ux.js (ช่องบทบาท link คือปลายทาง)

   ช่วงที่ใช้: มือถือ + แท็บเล็ตสัมผัสถึง 1024 · แล็ปท็อปที่ใช้เมาส์ได้ตารางตามเดิม
   ⚠️ `screen` เท่านั้น — กระดาษ A4 กว้าง ~688px ซึ่งตกอยู่ในช่วงนี้ (แผน V4 W1)
   ⚠️ น้ำหนัก selector ต้องชนะกฎการ์ดข้างบนโดยไม่พึ่งลำดับบรรทัด จึงขึ้นต้นด้วย
      [data-cards][data-cards-compact] และช่องใช้ [data-card-role] เสมอ
   ========================================================================== */
@media screen and (max-width: 860px), screen and (max-width: 1024px) and (any-pointer: coarse) {
  table.tbl[data-cards][data-cards-compact] { display: block; min-width: 0; width: auto; background: none; }
  table.tbl[data-cards][data-cards-compact] > thead { display: none; }
  /* ⚠️ ช่อง "เลือกทั้งหมด" ต้องยังอยู่ในแถวกระชับ (ผู้ตรวจ V4) — ขาย/ซื้อ/เงินเดือนบนแท็บเล็ตสัมผัส 721–1024
     เคยเป็นตารางปกติที่มีช่องนี้ ซ่อนหัวตารางทั้งแถบ = ต้องติ๊กทีละใบ · แสดงเฉพาะช่องติ๊กพร้อมป้าย (tablecards.js ใส่ป้ายจาก aria-label) */
  /* เฉพาะเมื่อมีแถวที่ติ๊กได้จริง — "เลือกทั้งหมด" ที่ไม่มีอะไรให้เลือก (ไม่มีใบร่างเลย) คือตัวควบคุมตาย */
  table.tbl[data-cards][data-cards-compact]:has(> tbody > tr > td[data-card-role~="check"] input:not([disabled])) > thead:has(> tr > th[data-card-role~="check"] input) { display: block; }
  table.tbl[data-cards][data-cards-compact] > thead:has(> tr > th[data-card-role~="check"] input) > tr { display: flex; }
  table.tbl[data-cards][data-cards-compact] > thead:has(> tr > th[data-card-role~="check"] input) > tr > th:not([data-card-role~="check"]) { display: none; }
  table.tbl[data-cards][data-cards-compact] > thead > tr > th[data-card-role~="check"]:has(input) {
    display: flex; align-items: center; gap: var(--s-3);
    width: auto; min-height: 44px; padding: var(--s-2) var(--s-4);
    position: static; background: none; border: 0; cursor: pointer;
    font-size: var(--t-label); font-weight: 500; color: var(--ink-2); text-transform: none; letter-spacing: 0;
  }
  table.tbl[data-cards][data-cards-compact] > thead > tr > th[data-card-role~="check"]:has(input)::after { content: attr(data-select-label); }
  table.tbl[data-cards][data-cards-compact] > tbody { display: block; }
  .table-wrap:has(> table.tbl[data-cards-compact]) { overflow-x: visible; }
  .table-wrap:has(> table.tbl[data-cards-compact])::after { display: none; }

  /* แถว = รายการในการ์ด คั่นด้วยเส้น ไม่ใช่กล่องซ้อนกล่อง (P2) */
  table.tbl[data-cards][data-cards-compact] > tbody > tr {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    /* ช่องว่างระหว่างช่อง = ที่วางตัวคั่น "·" ของบรรทัดรอง (ตัวคั่นลอยอยู่กลางช่องว่าง ไม่กินที่ในบรรทัด)
       ⚠️ gap มีเฉพาะระหว่างช่องในบรรทัดเดียวกัน — ช่องที่ตัดลงบรรทัดใหม่จึงไม่มีระยะนำหน้า
       และ tablecards.js ซ่อนจุดของช่องต้นบรรทัดโดยไม่เปลี่ยนขนาดอะไรเลย (ไม่แกว่งกลับขึ้นบรรทัดบน) */
    /* ⚠️ ระยะนี้เป็นตัวแปร (--card-gap) เพราะตัวคั่นต้องลอยอยู่กลางช่องว่างเสมอ — จอ ≤560 แคบลง (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · T1)
       ถ้าเขียนค่าตรง ๆ สองที่ ตัวคั่นจะหลุดไปทับตัวอักษรทันทีที่มีคนแก้ที่หนึ่งแล้วลืมอีกที่ */
    --card-gap: calc(var(--s-4) + var(--s-1));
    column-gap: var(--card-gap);
    row-gap: 2px;
    margin: 0;
    padding: var(--s-3) var(--s-4);
    border: 0;
    border-bottom: 1px solid var(--line);
    border-radius: 0;
    background: none;
    box-shadow: none;
  }
  table.tbl[data-cards][data-cards-compact] > tbody:last-of-type > tr:last-child { border-bottom: 0; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr.row-link:active { background: var(--surface-2); }

  /* ตัวขึ้นบรรทัดสอง — ::after ของ flex container เป็นลูกที่รับ order ได้ */
  table.tbl[data-cards][data-cards-compact] > tbody > tr::after {
    content: '';
    order: 3;
    flex-basis: 100%;
    height: 0;
  }

  /* ค่าตั้งต้นของทุกช่อง: ล้างรูปทรง "ป้ายซ้าย ค่าขวา" ของการ์ดเดิม
     ช่องที่ลืมประกาศบทบาทยังแสดงเป็นรายละเอียด — ห้ามซ่อนข้อมูลเงียบ ๆ */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td {
    order: 4;
    display: block;
    flex: 0 1 auto;
    min-width: 0;
    width: auto;
    margin: 0;
    padding: 0;
    border: 0;
    text-align: left;
    white-space: normal;
    color: var(--muted);
    /* --t-body ไม่ใช่ --t-label — บรรทัดรายละเอียดคือข้อมูลที่อ่านจริง (เลขที่ · วันที่ · ค้างชำระ) ไม่ใช่คำอธิบาย
       ลำดับชั้นในแถวมาจากน้ำหนักและสี (ชื่อ 600 สีหลัก · รายละเอียด 400 สีรอง) — ลูกค้าหลักอายุ 40+ */
    font-size: var(--t-body);
  }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td::before { content: none; }

  /* บรรทัด 1 ซ้าย: ชื่อ — ตัวหนา ตัดได้สองบรรทัด */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="title"] {
    order: 1;
    flex: 1 1 0;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    color: var(--ink);
    font-size: var(--t-body);
    font-weight: 600;
  }

  /* บรรทัดรองที่ซ้อนในช่องชื่อ (อ้างอิง · หมายเหตุ) ไม่แสดงในแถวกระชับ — อยู่ในหน้ารายละเอียดครบ
     ไม่งั้นมันกินบรรทัดที่สองของ line-clamp แล้วชื่อยาวถูกตัดเหลือบรรทัดเดียว */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="title"] > .small:not(a):not(:has(.chip)) { display: none; }
  /* ⚠️ ช่องชื่อที่มีชิปเตือน ("รอบล่าสุดล้มเหลว" · "สินค้าถูกปิด") — ตัดสองบรรทัดเฉพาะชื่อ ไม่ใช่ทั้งช่อง
     ไม่งั้นชื่อยาวสองบรรทัดดันชิปตกขอบที่ถูกตัดทิ้ง คำเตือนหายเงียบ ๆ (ผู้ตรวจ V4) — ข้อยกเว้นยอมให้แถวสูงขึ้น */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="title"]:has(> .small .chip) {
    display: block; -webkit-line-clamp: unset; overflow: visible;
  }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="title"]:has(> .small .chip) > strong {
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
  }
  /* ผลค้นเลขเอกสาร (26 ก.ย. 2569) — บรรทัด "ตรงด้วยเลขอะไร" ต้องเห็นบนมือถือ: มันคือสิ่งเดียวที่แยก GBN-1-12 ออกจาก GBN-1-123
     ⚠️ ไม่ใช่ .small (กฎข้างบนซ่อน .small ที่ไม่มีชิป) · ช่องชื่อเลิกตัดทั้งช่อง ตัดชื่อหนึ่งบรรทัด + บรรทัดเลขหนึ่งบรรทัด
     · ellipsis อยู่ที่ลูกที่เป็นบล็อก ไม่ใช่กล่อง flex (บทเรียนข้อ 390) */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="title"]:has(> .search-hit) {
    display: block; -webkit-line-clamp: unset; overflow: hidden;
  }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="title"] > .hit-name {
    display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  }
  /* ⚠️ เลขห้ามถูกตัด — "UAT-POS-17…" เทียบกับคำค้นไม่ได้ ซึ่งคือเหตุผลเดียวที่บรรทัดนี้มีอยู่ (UAT 390px · 26 ก.ย. 2569)
     ป้าย "เลขภายนอก/อ้างอิง" ถอยแบบตัดขอบ (โปรแกรมอ่านหน้าจอยังอ่าน) ให้ที่แก่ตัวเลข · เลขยาวเกินบรรทัดตัดบรรทัดได้ ไม่ใช่ตัดทิ้ง */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="title"] > .search-hit {
    display: block; white-space: normal; overflow-wrap: anywhere; text-align: left; margin-top: 0;
  }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="title"] > .search-hit .search-hit-v { white-space: normal; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="title"] > .search-hit .search-hit-k {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }
  /* แถวผลค้นที่มีบรรทัดเลขเป็นแถวสามบรรทัด — บีบระยะบน/ล่างแบบแถวค้างชำระ ทุกความกว้างของแถวกระชับ
     (แท็บเล็ต 768/1024 วัดได้ 103.8px ตอนกฎนี้อยู่แค่บล็อก ≤560 · น้ำหนัก :has ชนะกฎ tr ของบล็อก ≤560 ที่อยู่ล่างกว่า) */
  table.tbl[data-cards][data-cards-compact] > tbody > tr:has(> td[data-card-role~="title"] > .search-hit) { padding-top: var(--s-2); padding-bottom: var(--s-2); }

  /* บรรทัด 1 ขวา: ยอด — ไม่ตัดบรรทัด ตรงหลักทุกแถว */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="amount"] {
    order: 2;
    flex: 0 0 auto;
    margin-left: auto;
    color: var(--ink);
    font-size: var(--t-body);
    font-weight: 500;
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums lining-nums;
  }
  table.tbl[data-cards][data-cards-compact] > tbody > tr.row-void > td[data-card-role~="amount"] { text-decoration: line-through; }
  /* C12/C24: รายการที่ถูกกลับแล้ว — ยอดของแถวกระชับขีดฆ่าเหมือนใบยกเลิก */
  table.tbl[data-cards][data-cards-compact] > tbody > tr.row-reversed > td[data-card-role~="amount"] { text-decoration: line-through; }
  /* สีจางของใบยกเลิก/ถูกกลับรายการในแถวกระชับ (UAT ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569)
     ช่องชื่อ/ยอดของแถวกระชับตั้ง --ink ของตัวเองหนักกว่ากฎของแถวในส่วนตาราง — เดิม opacity ของทั้งแถวครอบให้
     พอเปลี่ยนเป็นสีแล้ว สองช่องที่เด่นที่สุดของแถวกลับเข้มเท่าใบปกติ ⇒ ต้องตามมาหรี่ที่นี่เอง */
  table.tbl[data-cards][data-cards-compact] > tbody > tr:is(.row-void, .row-reversed) > td[data-card-role~="title"],
  table.tbl[data-cards][data-cards-compact] > tbody > tr:is(.row-void, .row-reversed) > td[data-card-role~="amount"] { color: var(--muted); }

  /* บรรทัด 2 ซ้าย: รายละเอียด คั่นด้วย · — เลขที่ (meta link) ขึ้นก่อน แล้วรายละเอียด แล้วยอดที่มีป้าย
     ⚠️ ลำดับนี้ต้องตรงกับ rank() ใน tablecards.js ที่เลือกช่องแรกที่ไม่มีตัวคั่น */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta"][data-card-role~="link"] { order: 4; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta"]:not([data-card-role~="link"]) { order: 5; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta-l"] { order: 6; }
  /* ตัวคั่น "·" ลอยอยู่กลางช่องว่างของแถว (column-gap) — ไม่ใช่ตัวอักษรนำหน้าช่อง
     ⚠️ UAT ดีไซน์ V4 (24 ก.ย. 2569 · มือถือ 390): เดิมจุดเป็นตัวอักษรต้นช่อง พอบรรทัดรองยาวเกิน
     ("TX202609-0001 · ใบกำกับภาษี · ค้างชำระ 15,100.00") ช่องสุดท้ายตัดลงบรรทัดสามพร้อม "·" นำหน้าลอย ๆ
     ตอนนี้ช่องต้นบรรทัด (tablecards.js ติด data-card-linestart จากตำแหน่งจริง) ไม่วาดจุด
     และเพราะจุดไม่กินที่ในบรรทัด การซ่อนมันจึงไม่ทำให้ช่องเด้งกลับขึ้นบรรทัดบน */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta"],
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta-l"] { position: relative; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta"]:not([data-card-first]):not([data-card-linestart])::after,
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta-l"]:not([data-card-first]):not([data-card-linestart])::after {
    content: '·';
    position: absolute;
    top: 0;
    left: calc(var(--card-gap) / -2);
    transform: translateX(-50%);
    color: var(--muted);
    pointer-events: none;
  }
  /* meta-l = มีป้ายนำหน้าค่า ("ค้างชำระ 1,200.00") — ค่าตัวเลขเปล่าในบรรทัดรองไม่บอกว่าเป็นยอดอะไร */
  /* ⚠️ ป้ายจบด้วยช่องว่างธรรมดา ไม่ใช่ \00a0 และไม่มี margin-right — nbsp ไม่ยุบกับช่องว่างที่ขึ้นต้นข้อความของเทมเพลต
     เดิมได้ nbsp + margin 8px + ช่องว่างของเทมเพลต ≈ 16px ระหว่าง "เกินมา" กับ "16 วัน" จนสถานะตกบรรทัดที่สาม
     (คิวทวงหนี้บนมือถือ 101px/แถว) · ช่องนี้ nowrap อยู่แล้ว ป้ายกับค่าจึงไม่แยกบรรทัด */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta-l"][data-label]::before {
    content: attr(data-label) ' ';
  }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta-l"] { white-space: nowrap; font-variant-numeric: tabular-nums; }
  /* ช่องที่เป็นชุดชิป (งวดที่ค้างของแม่แบบ — ค้างได้หลายงวด) ต้องตัดบรรทัดได้ ไม่งั้นชิปที่เกินการ์ดถูกตัดทิ้งเงียบ ๆ */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta-l"]:has(.chip) { white-space: normal; max-width: 100%; }
  /* ช่องรายละเอียดเป็นข้อความบรรทัดเดียว: บรรทัดย่อย (.small — ส่วนต่างเทียบงวด · วันที่ย่อย) ไม่แสดง
     ⚠️ ตัวเลข "+354" ที่ไม่มีป้ายบอกว่าเทียบกับอะไรอ่านแล้วเข้าใจผิดได้ (แผน V4 W5) — หน้ารายละเอียดมีครบ
     ส่วนกล่องย่อยอื่น (ชิปที่ห่อใน div) ต่อบรรทัดเดียวกัน */
  /* ⚠️ :not(:has(.chip)) — บรรทัดย่อยที่ห่อชิปเตือน ("ครบรอบแล้ว" · "รอบล่าสุดล้มเหลว") ต้องยังแสดง
     ชิปเตือนคือข้อยกเว้นที่แถวกระชับมีไว้ให้เห็น ซ่อนมันคือซ่อนสิ่งเดียวที่ต้องเห็น */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta"] > .small:not(:has(.chip)),
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta-l"] > .small:not(:has(.chip)),
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="status"] > .small:not(:has(.chip)) { display: none; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta"] br,
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta-l"] br { display: none; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta"] > div,
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta-l"] > div { display: inline; margin: 0 0 0 var(--s-1); }
  /* กล่องที่ต่อจากบรรทัดย่อยที่ถูกซ่อน = สิ่งแรกที่ตาเห็นในช่อง ไม่มีอะไรข้างหน้าให้เว้นระยะ
     (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · ข้อ 7) — เงินเดือน: ชื่อเดือนภาษี (.small) ถูกซ่อน "จ่าย 30 ส.ค. 2569" จึงขึ้นต้นช่อง
     แต่ margin ซ้าย 4px ของกฎข้างบนยังอยู่ บรรทัดสองเยื้องขวากว่าชื่องวด 4px ทุกแถว (หน้าขาย/ซื้อตรงแนว)
     ⚠️ น้ำหนัก :not(:has()) หนักกว่ากฎข้างบน — ชนะโดยไม่พึ่งลำดับบรรทัด · ตัวเชื่อม + ข้ามโหนดข้อความช่องว่างให้เอง
     ⚠️ เขียนสองกฎแยก ไม่ใช้ :is(A, B) — เกตหลายตัวแยก selector ด้วยจุลภาค (บทเรียนข้อ 383) */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta"] > .small:not(:has(.chip)) + div { margin-left: 0; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta-l"] > .small:not(:has(.chip)) + div { margin-left: 0; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="meta"] strong { font-weight: 500; }

  /* บรรทัด 2 ขวา: สถานะ */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="status"] {
    order: 7;
    margin-left: auto;
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--s-1);
  }
  /* สถานะปกติของหน้านั้น ("ลงบัญชีแล้ว" · "ใช้งาน") ไม่ต้องพูดทุกแถว — แสดงเฉพาะข้อยกเว้น */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="status"] .chip-normal { display: none; }
  /* [UAT2] แถวใบตัดชำระ/เอกสารอย่างเดียว: ช่องยอดเหลือ "—" บรรทัดเดียว (บรรทัดรองถอยออก) · ชิปชนิดใบ + ยอดที่รับมาอยู่บรรทัดสอง
     แทนชิปสถานะปกติ (chip-normal · ร่าง/ยกเลิกยังแสดง) — วัดจริงบน 390: ชิป 173px ข้างยอดบีบชื่อลูกค้าจนตัดสองบรรทัด แถวสูง 101px (แถวปกติ 74px) */
  /* [UAT3 A-4] ช่องยอดคง "—" ไว้เหมือนจอกว้าง (ยอดที่รับวางตรงช่องยอดอ่านเป็นยอดขายของใบ · การ์ดนับแถวนี้เป็นศูนย์)
     ถอยเฉพาะบรรทัดรอง (ชิปชนิดใบ + ยอดที่รับ) ไปบรรทัดสอง */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-settle-kind] > .settle-sub { display: none; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="status"] .settle-kind-line2 {
    display: inline-flex; align-items: center; gap: var(--s-1);
  }
  /* [UAT3 R8-X2 · R12] ใบตัดชำระที่ยกเลิกแล้ว: ชิป "ยกเลิกแล้ว" + ชิปชนิดใบ + ยอดที่รับ ไม่พอดีบรรทัดสองบนจอ 390
     (วัดจริง: ทั้งกลุ่มตกเป็นบรรทัดสาม แถวสูง 98px) ⇒ ชิปชนิดใบถอยแบบตัดขอบ — สถานะ "ยกเลิกแล้ว" คือข้อมูลที่ต้องเห็น
     (โปรแกรมอ่านหน้าจอยังอ่านชิปชนิดใบ · จอกว้างเห็นครบ) · ยอดที่รับขีดฆ่าเหมือนยอดของใบยกเลิกอื่นบนแถวกระชับ (เดิมไม่ขีด) */
  table.tbl[data-cards][data-cards-compact] > tbody > tr.row-void > td[data-card-role~="status"] .settle-kind-line2 > .chip {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
    margin: 0; padding: 0; border: 0;
  }
  table.tbl[data-cards][data-cards-compact] > tbody > tr.row-void > td[data-card-role~="status"] [data-settle-amt] { text-decoration: line-through; }
  /* ขั้น lean ของใบตัดชำระที่ยกเลิก: ทั้งชิปชนิดใบและยอดที่รับถอยแล้ว — กล่องที่เหลือว่างยังกินระยะห่าง (gap) ดันชิป "ยกเลิกแล้ว"
     ไม่ชิดขอบขวาเหมือนแถวอื่น ⇒ ถอยทั้งกล่องแบบตัดขอบ (โปรแกรมอ่านหน้าจอยังอ่านทั้งคู่) */
  table.tbl[data-cards][data-cards-compact] > tbody > tr.row-void[data-card-squeeze="lean"] > td[data-card-role~="status"] .settle-kind-line2 {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }
  /* ชิปที่มีป้ายสั้น (คิวทวงหนี้ — ป้ายเต็มยาวจนชิปตกเป็นบรรทัดที่สาม): แสดงป้ายสั้น
     ป้ายเต็มซ่อนแบบ clip ไม่ใช่ display:none — โปรแกรมอ่านหน้าจอยังอ่านป้ายเต็ม (ป้ายสั้นเป็น aria-hidden) */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td .chip-short { display: inline; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td .chip:has(> .chip-short) > .chip-full {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }
  /* ชิปที่ป้ายสั้นของมันถูกรวมไว้ในชิปก่อนหน้าแล้ว (เงินเดือน: ยื่นทั้ง ภ.ง.ด.1 และ สปส. = "✓ ภ.ง.ด.1 · สปส.1-10" ใบเดียว)
     (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · ข้อ 6) — ชิปสองใบเต็มดันบรรทัดสองของแถวเดือนที่ยื่นแล้วเป็นสองบรรทัด (106px)
     ถอยทั้งใบแบบตัดขอบ ไม่ใช่ display:none — โปรแกรมอ่านหน้าจอยังอ่านป้ายเต็มของทั้งสองใบ · นอกแถวกระชับเห็นสองใบตามเดิม */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td .chip-merged {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
    margin: 0; padding: 0; border: 0;
  }

  /* บรรทัดสาม: ปุ่มที่เป็นหน้าที่หลักของตารางนั้น (ไม่มีหน้ารายละเอียดให้ไปกดแทน) */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="action"] {
    order: 8;
    flex: 1 1 100%;
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--s-2);
    margin-top: var(--s-1);
  }
  /* แถวที่มีบรรทัดปุ่ม — ปุ่ม 44px คือความสูงส่วนใหญ่ของแถวอยู่แล้ว ระยะบน/ล่างของแถวบีบลงได้ */
  table.tbl[data-cards][data-cards-compact] > tbody > tr:has(> td[data-card-role~="action"]) { padding-top: var(--s-2); padding-bottom: var(--s-2); }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="action"] > .u-row { justify-content: flex-end; flex-wrap: wrap; }

  /* ช่องติ๊ก: อยู่ต้นแถว ทั้งช่อง 44px เป็นเป้ากด (ux.js ส่งการแตะในช่องไปที่ช่องติ๊ก)
     ⛔ ห้ามซ่อน — อนุมัติร่างหลายใบ/ลงบัญชีหลายรอบบนมือถือใช้ช่องนี้ */
  /* เว้นที่เฉพาะแถวที่ติ๊กได้จริง (ร่าง) — แถวที่ไม่มีช่องติ๊กไม่ต้องเสียที่ 56px ทุกแถว */
  table.tbl[data-cards][data-cards-compact] > tbody > tr:has(> td[data-card-role~="check"] input) { padding-left: calc(44px + var(--s-3)); }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="check"]:not(:has(input)) { display: none; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="check"] {
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--s-2);
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
  }

  /* ไม่แสดงในแถวกระชับ: ปุ่มในแถว (ทำได้ที่หน้ารายละเอียด) · ช่องที่เป็นแค่ลิงก์ซ้ำกับการแตะแถว · ช่องว่าง */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="hide"],
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role="link"],
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-omit],
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-empty] { display: none; }
  /* ช่อง fit ของแถวที่บรรทัดสองล้น (tablecards.js วัดหลังจัดวางแล้วติด data-card-squeeze เฉพาะแถวนั้น)
     (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · T1) — ร่างที่มีช่องติ๊ก · ร่างจาก POS · ชิปยาว ดันชิปสถานะตกบรรทัดสาม
     ชิปสถานะ (ข้อมูลที่ใช้ตัดสินใจ) ต้องอยู่บรรทัดสองเสมอ ⇒ ย่อวันที่ทีละขั้น ละเป็นทางสุดท้าย (ผู้ทวน T1 —
     ฉบับแรกละทั้งช่องทันที จอ 360 เสียวันที่ 13 จาก 14 สภาพ ขัดกับสเปก W8/C23)
     ⚠️ ชื่อขั้นต้องตรงกับ SQUEEZE_STEPS ใน tablecards.js · แต่ละขั้นมีผลกับแถวที่ยังล้นหลังขั้นก่อนหน้าเท่านั้น
     short: สองหลักแรกของปี (.fit-cut ที่มุมมองห่อไว้) ถอยไป ⇒ "28 ก.ย. 69" — วันที่ยังอยู่ที่เดิม
       display:none ได้ เพราะโปรแกรมอ่านหน้าจอจะอ่าน "28 ก.ย. 69" ซึ่งเป็นวันที่ที่ครบความหมาย
     day: ทั้งปี (.fit-yr) ถอยแบบตัดขอบ ⇒ "28 ก.ย." บนจอ · โปรแกรมอ่านหน้าจอยังอ่านปีครบ
     drop: ถอยทั้งช่องแบบตัดขอบ ไม่ใช่ display:none — โปรแกรมอ่านหน้าจอยังอ่านวันที่ของแถวนั้นครบ (ผู้ทวน T1)
       tablecards.js ไม่นับช่องนี้ตอนวางตัวคั่น "·" (กล่อง 1px ที่ลอยอยู่ต้นแถว) */
  table.tbl[data-cards][data-cards-compact] > tbody > tr[data-card-squeeze="short"] > td[data-card-role~="fit"] .fit-cut { display: none; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr[data-card-squeeze="day"] > td[data-card-role~="fit"] .fit-yr {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }
  table.tbl[data-cards][data-cards-compact] > tbody > tr[data-card-squeeze="drop"] > td[data-card-role~="fit"] {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }
  /* [UAT3 R8-X2] lean (ขั้นหลังสุด): แถวที่ยังล้นหลังละวันที่ — ละวันที่ต่อ (เหมือน drop) และถอยของรองที่มุมมองติด data-card-lean
     แบบตัดขอบ (โปรแกรมอ่านหน้าจอยังอ่าน) · วัดจริงจอ 360: ใบตัดชำระที่ยกเลิก "เลขที่ + ยกเลิกแล้ว + ยอดที่รับ" ตกบรรทัดสาม (98px) */
  table.tbl[data-cards][data-cards-compact] > tbody > tr[data-card-squeeze="lean"] > td[data-card-role~="fit"],
  table.tbl[data-cards][data-cards-compact] > tbody > tr[data-card-squeeze="lean"] [data-card-lean] {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }

  /* แถว "ไม่พบข้อมูล" กินเต็มแถว */
  table.tbl[data-cards][data-cards-compact] > tbody > tr[data-card-full] { padding-left: var(--s-4); }
  table.tbl[data-cards][data-cards-compact] > tbody > tr[data-card-full] > td {
    flex: 1 1 100%;
    text-align: center;
    font-size: var(--t-body);
  }
  /* หัวหมวดของงบ (แถวเต็มที่มีตัวหนา: "สินทรัพย์หมุนเวียน") · หัวกลุ่มของงบทดลอง — ชิดซ้าย พื้นอ่อน
     แยกจากแถว "ไม่พบข้อมูล" ด้วยตัวหนาที่มุมมองใส่อยู่แล้ว ไม่ต้องแก้มุมมอง */
  table.tbl[data-cards][data-cards-compact] > tbody > tr[data-card-full]:has(> td > strong),
  table.tbl[data-cards][data-cards-compact] > tbody > tr[data-group-row] { background: var(--surface-2); }
  table.tbl[data-cards][data-cards-compact] > tbody > tr[data-card-full] > td:has(> strong) { text-align: left; color: var(--ink); }
  table.tbl[data-cards][data-cards-compact] > tbody > tr[data-group-row] > td { background: none; border: 0; }

  /* ยอดรวมท้ายตาราง — แบบ "ป้าย:ค่า" เดิม (ช่วง 721–1024 ไม่มีบล็อกการ์ดข้างบนให้พึ่ง) */
  table.tbl[data-cards][data-cards-compact] > tfoot { display: block; }
  /* ⚠️ ต้องล้างค่าของการ์ดเดิม (≤720: margin-top · มุมมน · พื้น) ด้วย — ไม่งั้นยอดรวมดูเป็นกล่องซ้อนในการ์ด (ผู้ตรวจ V4 · P2) */
  table.tbl[data-cards][data-cards-compact] > tfoot > tr {
    display: block;
    margin-top: 0;
    padding: var(--s-3) var(--s-4);
    border-radius: 0;
    background: none;
    border-top: 1px solid var(--line-strong);
    font-weight: 600;
  }
  /* ⚠️ พื้นเทาของ tfoot มาจาก <td> (table.tbl tfoot td { background: --surface-2 }) ไม่ใช่ <tr> — ล้างแค่ tr แล้ว
     ยอดรวมยังเป็นกล่องเทาเยื้องในการ์ด (ผู้ตรวจ V4 รอบสอง) · margin ของ td แรกจากการ์ดเดิม (≤720) ก็ต้องล้าง
     ไม่งั้นกล่องเทาแตกเป็นสองก้อนมีช่องขาวคั่น */
  table.tbl[data-cards][data-cards-compact] > tfoot > tr > td {
    display: flex;
    align-items: baseline;
    gap: var(--s-2);
    width: auto;
    padding: 2px 0;
    border: 0;
    background: none;
    text-align: right;
    white-space: normal;
    font-variant-numeric: tabular-nums lining-nums;
  }
  table.tbl[data-cards][data-cards-compact] > tfoot > tr > td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 auto;
    margin-right: auto;
    color: var(--muted);
    font-size: var(--t-label);
    font-weight: 500;
    text-align: left;
  }
  table.tbl[data-cards][data-cards-compact] > tfoot > tr > td:first-child { display: block; text-align: left; margin: 0 0 var(--s-1); padding: 2px 0; border-bottom: 0; }
  table.tbl[data-cards][data-cards-compact] > tfoot > tr > td:first-child::before { content: none; }
  table.tbl[data-cards][data-cards-compact] > tfoot > tr > td:empty { display: none; }
}
/* opt = รายละเอียดที่ละได้บนมือถือ — บรรทัดสองที่ 390px ใส่ได้ ~320px (เลขที่ + วันที่ + ชิปสถานะพอดี)
   ช่องที่สามจะดันชิปลงบรรทัดที่สาม · แท็บเล็ตมีที่พอจึงยังแสดง */
@media screen and (max-width: 560px) {
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="opt"] { display: none; }
  /* บรรทัดรองในช่องชื่อที่ละได้บนมือถือแคบ — คู่ของ opt สำหรับของที่อยู่ในช่องเดียวกับชื่อบนเดสก์ท็อป
     (เลขรอบเงินเดือน · ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · A5) */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="title"] > .card-opt { display: none; }
  /* [UAT2 F1-4] บรรทัดรอง "หลังหักใบลดหนี้" ใต้ยอดค้างของหน้ารายการขาย — ละบนแถวกระชับ (เพดาน A5 · ยอดในแถวยังเป็นยอดของใบ
     ที่บวกกับแถวใบลดหนี้แล้วเท่าการ์ด) · จอกว้างและโหมดแสดงทุกช่องยังเห็น */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="sub"] > .card-opt { display: none; }

  /* ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · T1 — แถวร่าง/ค้างชำระบนมือถือ 390 สูง 101–123px (เพดาน A5 = 96)
     เกต A5 ผ่านมาตลอดเพราะกิจการที่ตัววัดใช้ไม่มีแถวสภาพนี้เลย · วัดด้วยหน้าจำลองที่โหลด app.css + tablecards.js ตัวจริง
     บรรทัดสองที่ 390 ใส่ได้ ~324px (มีช่องติ๊ก ~284px): เลขที่ 114 + วันที่ 86 + ชิป 76 + ระยะ 20×2 = 316 — เฉียดขอบ
     ชิปที่ยาวกว่า "รอชำระ" ("เกินกำหนด" 95 · "ชำระบางส่วน" 108) หรือช่องติ๊ก ทำให้ชิปสถานะตกบรรทัดสาม

     (1) ระยะระหว่างช่องแคบลง 20 → 12px — ตัวคั่น "·" ยังลอยกลางช่องว่าง (--card-gap ตัวเดียวกับตำแหน่งตัวคั่น)
         ⇒ ร่างที่มีช่องติ๊ก · เกินกำหนดเต็มจำนวน กลับมาสองบรรทัด (102 → 78px) โดยไม่ละอะไรเลย
     (1ข) ระยะบน/ล่างของแถว 12 → 10px — ชื่อลูกค้า/ผู้ขายสองบรรทัด ("ห้างหุ้นส่วนจำกัด …" ปกติของลูกค้า B2B)
         เดิมสูง 100px ทั้งแถวร่างและแถวเปิด · 10px = 96px พอดีเพดาน · แถวบรรทัดเดียว 78 → 74px
         ⚠️ แถวที่มีบรรทัดปุ่ม/ยอดรอง บีบเป็น --s-2 ด้วยกฎที่หนักกว่า (:has) — ไม่ถูกกฎนี้ขยายกลับ */
  table.tbl[data-cards][data-cards-compact] > tbody > tr { --card-gap: var(--s-3); padding-top: 10px; padding-bottom: 10px; }

  /* (2) ยอดรอง (sub — "ค้างชำระ X" ของหน้าขาย) ขึ้นไปอยู่ใต้ยอดรวมชิดขวา แทนการแย่งที่บรรทัดสอง
         ใบที่หัก ณ ที่จ่าย · ชำระบางส่วน · สกุลต่างประเทศ แสดงยอดค้างทุกใบ — เลขที่ + วันที่ + ยอดค้าง + ชิป (≈430px) ไม่มีวันพอ 324px
         ยอดคู่กัน (ยอดรวม / ค้างชำระ) อยู่แนวเดียวกัน อ่านเป็นคู่ได้ทันที · บรรทัดสองยังมีเลขที่ · วันที่ — ชิปสถานะครบ
         ⚠️ ช่องนี้กินทั้งบรรทัด (flex-basis 100%) จึงตัดบรรทัดเองทั้งก่อนและหลัง — ตัวขึ้นบรรทัด (::after ของแถว) ไม่จำเป็น
            ถอดออกเพื่อไม่ให้ระยะระหว่างบรรทัด (row-gap) ถูกนับสองครั้ง
         ⚠️ แถวสามบรรทัดบีบระยะบน/ล่างลงเป็น --s-2 แบบเดียวกับแถวที่มีบรรทัดปุ่ม (action) ⇒ ≤96px
         ⚠️ ยอดค้างเต็มจำนวนถูกละอยู่แล้ว (data-card-omit — ชิป "รอชำระ" บอกแทน) กฎนี้จึงมีผลเฉพาะแถวที่ยอดค้างเป็นข้อมูลใหม่ */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="sub"] {
    order: 2;
    flex: 1 1 100%;
    text-align: right;
  }
  /* ⚠️ น้ำหนักต้องเท่ากฎวาดตัวคั่นของ meta-l (…:not([data-card-first]):not([data-card-linestart])::after) และอยู่ล่างกว่า
     ไม่งั้นแพ้เงียบ ๆ — วันนี้รอดเพราะ tablecards.js ติด data-card-linestart ให้ช่องแรกที่ตาเห็นอยู่แล้ว แต่ไม่ควรพึ่งแค่นั้น */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="sub"]:not([data-card-first]):not([data-card-linestart])::after { content: none; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr:has(> td[data-card-role~="sub"]:not([data-card-omit]):not([data-card-empty]))::after { content: none; }
  table.tbl[data-cards][data-cards-compact] > tbody > tr:has(> td[data-card-role~="sub"]:not([data-card-omit]):not([data-card-empty])) { padding-top: var(--s-2); padding-bottom: var(--s-2); }

  /* (3) ร่างจาก POS: ชิป "รอตรวจ (จาก POS)" แสดงเฉพาะใบร่างอยู่แล้ว — ชิป "ร่าง" คู่กันพูดซ้ำและกินที่ 58px
         ถอยแบบตัดขอบ (ไม่ใช่ display:none) โปรแกรมอ่านหน้าจอยังอ่าน "ร่าง" ครบ · แท็บเล็ต/เดสก์ท็อปเห็นทั้งสองชิปตามเดิม */
  table.tbl[data-cards][data-cards-compact] > tbody > tr > td[data-card-role~="status"]:has(> .chip-pos) > .chip-draft {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
    margin: 0; padding: 0; border: 0;
  }
}

/* ==========================================================================
   แถบเมนูล่างสำหรับมือถือ
   --------------------------------------------------------------------------
   บนจอเล็ก เมนูข้างต้องเปิดก่อนทุกครั้ง = สองแตะสำหรับงานที่ทำทั้งวัน
   แถบล่างเหลือแตะเดียว และอยู่ในระยะที่นิ้วโป้งเอื้อมถึงโดยไม่ขยับมือ

   ซ่อนบนจอกว้างเพราะที่นั่นเมนูข้างเปิดค้างอยู่แล้ว การมีสองเมนูพร้อมกัน
   ทำให้ไม่รู้ว่าควรใช้อันไหน
   ========================================================================== */
.bottom-nav { display: none; }

@media (max-width: 860px) {
  .bottom-nav {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 40;
    display: flex;
    align-items: stretch;
    justify-content: space-around;
    gap: var(--s-1);
    padding: 4px var(--s-2);
    /* เผื่อแถบบ้านของระบบบนเครื่องจอไร้ขอบ ไม่งั้นปุ่มไปทับแล้วกดโดนสลับแอป */
    padding-bottom: calc(4px + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--line);
    background: var(--surface);
    box-shadow: var(--sh-up);
  }

  .bottom-nav-item {
    display: flex;
    flex: 1 1 0;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    /* 44px คือขนาดเป้าแตะขั้นต่ำที่นิ้วโป้งกดไม่พลาด */
    min-height: 44px;
    padding: 4px 2px;
    border-radius: var(--r);
    color: var(--muted);
    font-size: var(--t-micro);
    font-weight: 500;
    line-height: 1.2;
    text-align: center;
    text-decoration: none;
  }

  .bottom-nav-item svg { flex: 0 0 auto; }

  .bottom-nav-item.active {
    color: var(--brand);
    background: var(--brand-soft);
  }

  .bottom-nav-item:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: -2px;
  }

  /* ปุ่มสร้างยกขึ้นจากแถว เพื่อให้เห็นว่าเป็นการกระทำ ไม่ใช่ปลายทาง */
  .bottom-nav-create {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    width: 52px;
    height: 52px;
    margin-top: -14px;
    border-radius: 50%;
    background: var(--brand);
    /* ⚠️ --on-brand ไม่ใช่ #fff เสมอ — โหมดมืดเป็นสีเข้ม (#04231A)
       เพราะ --brand ของโหมดมืดสว่างกว่า ขาวบนสีนั้นอ่านไม่ออก */
    color: var(--on-brand);
    box-shadow: var(--sh-float);
    text-decoration: none;
  }

  .bottom-nav-create:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 3px;
  }

  /* เนื้อหาท้ายหน้าต้องเลื่อนพ้นแถบได้ ไม่งั้นปุ่มบรรทัดสุดท้ายกดไม่ถึง */
  body { padding-bottom: calc(60px + env(safe-area-inset-bottom, 0px)); }

}

@media print {
  .bottom-nav { display: none !important; }
}

/* ==========================================================================
   รายการตรวจ "เริ่มต้นใช้งาน"
   --------------------------------------------------------------------------
   ผู้ใช้ใหม่เปิดระบบมาเจอหน้าเปล่า ไม่มีอะไรบอกว่าควรทำอะไรก่อน การ์ดนี้
   ตอบคำถามนั้นในที่ที่คนเปิดอยู่แล้วทุกวัน และหายไปเองเมื่อครบทุกข้อ
   ========================================================================== */
.onboarding-bar {
  height: 6px;
  margin: 0 var(--s-4) var(--s-3);
  border-radius: var(--r-pill);
  background: var(--surface-3, var(--surface-2));
  overflow: hidden;
}

.onboarding-bar > span {
  display: block;
  height: 100%;
  border-radius: inherit;
  /* ไล่สีเขียว→เทียล (--grad-brand) — ลูกเล่นดีไซน์ V3: แถบเดินแล้ว "มีชีวิต"
     แถบไม่ใช่ตัวหนังสือ เกณฑ์คอนทราสต์ตัวอักษรจึงไม่บังคับกับพื้นไล่สีนี้ */
  background: var(--grad-brand);
  transition: width .3s ease;
}

.onboarding-steps {
  margin: 0;
  padding: 0 var(--s-4) var(--s-4);
  list-style: none;
}

.onboarding-steps > li {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) 0;
}

.onboarding-steps > li + li { border-top: 1px solid var(--line); }

/* วงกลมบอกสถานะ — ว่างคือยังไม่ทำ ติ๊กคือทำแล้ว */
.onboarding-mark {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border: 2px solid var(--line-strong);
  border-radius: 50%;
  color: var(--on-brand, #fff);
}

.onboarding-steps > li.is-done .onboarding-mark {
  border-color: var(--brand);
  background: var(--brand);
}

.onboarding-text { display: flex; flex: 1 1 0; flex-direction: column; gap: 2px; min-width: 0; }
.onboarding-text > strong { font-weight: 600; }

/* ข้อที่ทำแล้วจางลง ไม่ใช่หายไป — คนต้องเห็นว่าคืบหน้าไปแค่ไหน */
/* ⚠️ สี --muted ไม่ใช่ opacity .62 (= 4.48:1 ต่ำกว่า AA · C17) · บรรทัดคำใบ้เป็น --muted อยู่แล้ว หรี่เฉพาะหัวข้อ */
.onboarding-steps > li.is-done .onboarding-text > strong { color: var(--muted); }

@media print { .onboarding { display: none !important; } }

/* รายการคำเตือนก่อนอนุมัติเอกสาร — หัวข้อกับเหตุผลอยู่บรรทัดเดียวกัน
   เพราะคนอ่านต้องตัดสินใจทันทีว่าเรื่องนี้เกี่ยวกับบิลใบนี้หรือเปล่า */
.doc-warn-list { margin: var(--s-2) 0 0; padding-left: var(--s-4); }
.doc-warn-list > li + li { margin-top: var(--s-1); }
.doc-warn-list > li > strong { margin-right: var(--s-1); }

/* ==========================================================================
   ประวัติการแก้ไข — ตัวเลขนี้เคยเป็นเท่าไหร่ ใครแก้ เมื่อไหร่
   ค่าเดิมขีดฆ่า ค่าใหม่เน้น เพื่อให้เห็นทิศทางโดยไม่ต้องอ่านทั้งบรรทัด
   ========================================================================== */
.audit-trail { margin: 0; padding: 0 var(--s-4) var(--s-4); list-style: none; }
.audit-trail > li { padding: var(--s-3) 0; }
.audit-trail > li + li { border-top: 1px solid var(--line); }

.audit-trail-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-2);
}

.audit-trail-changes { margin: var(--s-2) 0 0; padding: 0; list-style: none; }

.audit-trail-changes > li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-2);
  padding: 2px 0;
  font-size: var(--t-body);
}

.audit-field { min-width: 9rem; color: var(--muted); }
.audit-from  { color: var(--muted); text-decoration: line-through; }
.audit-to    { font-weight: 600; font-variant-numeric: tabular-nums lining-nums; }

@media (max-width: 560px) {
  .audit-field { min-width: 100%; }
}

/* ข้อเสนอจับคู่ธนาคาร — ความมั่นใจอยู่บน เหตุผลอยู่ล่าง เพื่อให้ตัดสินใจ
   ได้โดยไม่ต้องเปิดดูใบสำคัญทีละใบ */
.match-confidence { display: flex; align-items: center; gap: var(--s-2); margin-bottom: var(--s-1); }
.match-reasons { margin: var(--s-1) 0 0; padding-left: var(--s-4); font-size: var(--t-label); color: var(--muted); }
.match-reasons > li { padding: 1px 0; }

/* ==========================================================================
   Controls v5 — ของที่เราวาดเอง แทนของที่ Windows วาดให้

   เหตุผลทั้งหมดอยู่ใน public/assets/js/controls.js หัวไฟล์
   สรุปสั้น ๆ: <select> 185 ตัวและ input[type=date] 58 ตัวคือเหตุผลข้อเดียว
   ที่ใหญ่ที่สุดที่ทำให้จอนี้ "ไม่เหมือนของที่ออกแบบมา"

   element เดิมยังอยู่ครบและยังถือค่าเดิม ตรงนี้แค่ซ่อนมันแล้ววาดทับ
   ========================================================================== */

/* min-width: 0 + max-width: 100% (UAT งานคุมเงิน · 29 ก.ย. 2569) — ช่องเลือกเป็นลูกของกล่อง flex/grid บ่อย
   (เซลล์ป้าย:ค่าของการ์ดมือถือ · แถวฟอร์ม) แล้ว min-width: auto = ความยาวชื่อที่ห้ามตัดบรรทัด ⇒ ชื่อยาวดันช่องล้นการ์ด
   ด้านซ้าย (เซลล์ชิดขวา) โดยหน้าไม่เลื่อน · ให้ช่องหดได้แล้วตัดข้อความด้วย … ที่ .cbx-value เอง (บทเรียนข้อ 390) */
.cbx { position: relative; display: block; min-width: 0; max-width: 100%; }

/* ของเดิมต้องอยู่ในเอกสารต่อ (ฟอร์มยังส่งค่าจากมัน และการตรวจ required ยังใช้มัน)
   แต่ต้องไม่กินพื้นที่และไม่รับคลิก */
.cbx-native,
.datefield-native {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  opacity: 0; pointer-events: none;
  border: 0; padding: 0; margin: 0;
  z-index: -1;
}

/* ชื่อช่องที่ปุ่มอ่านผ่าน aria-labelledby (ชื่อช่อง + ค่า · ตรวจความครบหลังดีไซน์ V4 · C5)
   ⚠️ ซ่อนด้วย display:none ไม่ใช่ .sr-only — ของที่ aria-labelledby ชี้ตรง ๆ ยังให้ชื่อได้แม้ถูกซ่อน (accname 1.2)
   แต่ .sr-only ยังอยู่ในต้นไม้การเข้าถึง โปรแกรมอ่านหน้าจอจึงอ่านชื่อช่องซ้ำเป็นข้อความลอยอีกรอบ */
.cbx-name { display: none; }

.cbx-btn {
  display: flex; align-items: center; gap: var(--s-2);
  width: 100%; min-height: 40px;
  padding: 9px var(--s-3);
  font-family: inherit; font-size: var(--t-body); color: var(--ink);
  text-align: left; cursor: pointer;
  background: var(--surface);
  /* V4: ขอบเดียวกับ .input (--ctl-line) — ฟอร์มเดียวต้องไม่มีขอบสองแบบ */
  border: 1px solid var(--ctl-line);
  border-radius: var(--r-sm);
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}

@media (hover: hover) { .cbx-btn:hover:not(:disabled) { border-color: var(--ctl-line-hi); } }
.cbx-btn:focus-visible { outline: none; border-color: var(--brand); box-shadow: var(--ring); }
.cbx.is-open .cbx-btn { border-color: var(--brand); box-shadow: var(--ring); }
.cbx.is-invalid .cbx-btn { border-color: var(--danger); }
.cbx-btn:disabled { background: var(--surface-2); color: var(--muted); cursor: not-allowed; box-shadow: none; }

.cbx-value {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ยังไม่ได้เลือก = ข้อความชี้ทาง ไม่ใช่ค่า จึงต้องเบากว่าค่าจริง */
.cbx.is-empty .cbx-value { color: var(--faint); }

.cbx-caret {
  flex: 0 0 auto; display: inline-flex; color: var(--muted);
  transition: transform var(--fast) var(--ease);
}

.cbx.is-open .cbx-caret { transform: rotate(180deg); }

/* ---- แผงลอย (ใช้ร่วมกันทั้งรายการเลือกและปฏิทิน) ----
   ต่อไว้ที่ body ไม่ใช่ในเซลล์ เพราะตารางรายการสินค้ามี overflow:auto
   ซึ่งจะตัดแผงขาดครึ่ง */
.pop {
  /* .modal-backdrop อยู่ที่ 90 และ .toast-host อยู่ที่ 100
     แผงต้องอยู่เหนือกล่องโต้ตอบ (ไม่งั้นเลือกอะไรในกล่องไม่ได้เลย)
     แต่ต้องอยู่ใต้ข้อความแจ้งเตือน (ซึ่งสำคัญกว่าเสมอ) */
  position: fixed; z-index: 95;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--sh-float);
  opacity: 0; transform: translateY(-4px);
  transition: opacity var(--fast) var(--ease), transform var(--fast) var(--ease);
}

.pop.is-in { opacity: 1; transform: none; }

.cbx-pop { display: flex; flex-direction: column; max-height: min(320px, 60vh); overflow: hidden; }

.cbx-search { padding: var(--s-2); border-bottom: 1px solid var(--line); }

.cbx-search input {
  width: 100%; min-height: 34px; padding: 6px var(--s-3);
  font: inherit; font-size: var(--t-body); color: var(--ink);
  background: var(--surface-2);
  border: 1px solid transparent; border-radius: var(--r-xs);
}

/* ปิด outline ของเบราว์เซอร์ได้ก็ต่อเมื่อให้อะไรที่เห็นชัดกว่าแทน
   ขอบ 1px เปลี่ยนสีอย่างเดียวยังบางเกินไปสำหรับคนที่ใช้คีย์บอร์ดล้วน */
.cbx-search input:focus { outline: none; border-color: var(--brand); background: var(--surface); box-shadow: var(--ring); }

.cbx-list { overflow-y: auto; padding: var(--s-1); }

.cbx-opt {
  display: flex; align-items: center;
  min-height: 36px; padding: 7px var(--s-3) 7px var(--s-6);
  font-size: var(--t-body); color: var(--ink-2);
  border-radius: var(--r-xs); cursor: pointer;
  white-space: nowrap; overflow: hidden;  /* "…" อยู่ที่ .cbx-opt-main (controls.js ห่อทุกแถว) — กล่อง flex ตัดเองไม่ได้ · qa_ellipsis_flex */
}

.cbx-opt.is-active { background: var(--brand-soft); color: var(--brand-ink); }
.cbx-opt.is-disabled { color: var(--faint); cursor: not-allowed; }

/* ---- ตัวเลือกที่มีข้อมูลประกอบ (data-hint) เช่น ยอดคงเหลือของสินค้า ----
   ชื่อกินพื้นที่ที่เหลือทั้งหมดและถูกตัดด้วย … ส่วนยอดชิดขวาและไม่ยอมถูกตัด
   เพราะเลขที่ถูกตัดครึ่งอันตรายกว่าไม่แสดงเลย */
.cbx-opt-main {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.cbx-opt-hint {
  flex: 0 0 auto; margin-left: var(--s-3);
  font-size: var(--t-label); color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* ⚠️ ไม่ต้องเขียนทับสีตอนแถวถูกชี้ (.cbx-opt.is-active)
   วัดแล้วอ่านออกทั้งสองธีมบนพื้น --brand-soft: --muted 6.26:1 (สว่าง) / 5.80:1 (มืด)
   · --warn 4.85:1 (สว่าง) / 6.93:1 (มืด) — ผ่าน 4.5:1 ทุกช่อง
   และกฎที่เขียนทับจะมีน้ำหนัก 0,3,0 ชนะ .cbx-opt-hint.is-low ที่ 0,2,0
   ทำให้สีเตือนของสินค้าใกล้หมดหายไปทันทีที่เอาเมาส์ไปชี้ ซึ่งคือตอนที่กำลังจะกดเลือก */

/* ต่ำกว่าจุดสั่งซื้อ = เรื่องที่ต้องรู้ก่อนกดเลือก ไม่ใช่หลังกดบันทึกแล้วโดนปฏิเสธ
   บอกด้วยจุดนำหน้าด้วย ไม่ใช่สีอย่างเดียว — สีอย่างเดียวคนตาบอดสีแยกไม่ออก */
.cbx-opt-hint.is-low { color: var(--warn); font-weight: 600; }
.cbx-opt-hint.is-low::before { content: "\2022 "; }

/* ค่าที่เลือกอยู่ต้องบอกด้วยเครื่องหมาย ไม่ใช่บอกด้วยสีอย่างเดียว —
   สีอย่างเดียวคนตาบอดสีแยกไม่ออก */
.cbx-opt.is-on { font-weight: 600; color: var(--ink); position: relative; }

.cbx-opt.is-on::before {
  content: ""; position: absolute; left: 10px; top: 50%;
  width: 5px; height: 9px; margin-top: -6px;
  border: solid var(--brand); border-width: 0 1.8px 1.8px 0;
  transform: rotate(45deg);
}

.cbx-empty { padding: var(--s-4); font-size: var(--t-label); color: var(--muted); text-align: center; }

/* ---- ช่องวันที่ ---- */

.datefield { position: relative; display: block; }

.datefield-text {
  width: 100%; min-height: 40px;
  padding: 9px 38px 9px var(--s-3);
  font-family: inherit; font-size: var(--t-body); color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--ctl-line);
  border-radius: var(--r-sm);
  font-variant-numeric: tabular-nums;
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}

@media (hover: hover) { .datefield-text:hover:not(:disabled) { border-color: var(--ctl-line-hi); } }
.datefield-text:focus { outline: none; border-color: var(--brand); box-shadow: var(--ring); }
.datefield-text::placeholder { color: var(--faint); }
.datefield.is-invalid .datefield-text { border-color: var(--danger); }
.datefield-text:disabled { background: var(--surface-2); color: var(--muted); box-shadow: none; }

.datefield-btn {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  color: var(--muted); background: none; border: 0; border-radius: var(--r-xs);
  cursor: pointer;
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease);
}

@media (hover: hover) { .datefield-btn:hover { color: var(--brand); background: var(--surface-2); } }
.datefield-btn:focus-visible { outline: none; box-shadow: var(--ring); }

/* ---- ปฏิทิน ---- */

.cal { width: 268px; padding: var(--s-3); }

.cal-head { display: flex; align-items: center; gap: var(--s-2); margin-bottom: var(--s-2); }

/* ⚠️ หัวปฏิทินเป็น <button> ที่ controls.js สร้าง ไม่ใช่ข้อความ
   ปุ่มที่ไม่ตั้ง background/border เองจะได้ ButtonFace ของเบราว์เซอร์ (เทา 240 + 2px outset)
   ในโหมดมืดตัวหนังสือสีอ่อนบนพื้นเทาอ่อน = อ่านไม่ออก (เคยวัดได้คอนทราสต์ 1.03:1) */
.cal-title {
  flex: 1 1 auto; min-height: 30px; padding: 0 var(--s-2);
  text-align: center;
  font-family: inherit; font-size: var(--t-section); font-weight: 600; color: var(--ink);
  background: none; border: 0; border-radius: var(--r-xs); cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

@media (hover: hover) { .cal-title:hover { background: var(--surface-2); color: var(--ink); } }
.cal-title:focus-visible { outline: none; box-shadow: var(--ring); }

.cal-nav {
  flex: 0 0 auto; width: 30px; height: 30px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--t-heading); line-height: 1; color: var(--muted);
  background: none; border: 0; border-radius: var(--r-xs); cursor: pointer;
}

@media (hover: hover) { .cal-nav:hover { background: var(--surface-2); color: var(--ink); } }
.cal-nav:focus-visible { outline: none; box-shadow: var(--ring); }

.cal-dow, .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }

.cal-dow span {
  text-align: center; padding: var(--s-1) 0;
  font-size: var(--t-micro); font-weight: 600; color: var(--faint);
}

.cal-pad { display: block; }

.cal-day {
  min-height: 32px;
  font-family: inherit; font-size: var(--t-label); color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  background: none; border: 0; border-radius: var(--r-xs); cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

@media (hover: hover) { .cal-day:hover { background: var(--brand-soft); color: var(--brand-ink); } }
.cal-day:focus-visible { outline: none; box-shadow: var(--ring); }

/* วันนี้บอกด้วยขอบ วันที่เลือกบอกด้วยพื้น — สองความหมายต้องแยกกันได้ */
.cal-day.is-today { box-shadow: inset 0 0 0 1px var(--line-strong); font-weight: 600; }
.cal-day.is-on { background: var(--brand); color: var(--on-brand); font-weight: 600; }
@media (hover: hover) { .cal-day.is-on:hover { background: var(--brand-hi); color: var(--on-brand); } }

/* ---- ตาราง 12 เดือน (กดหัวปฏิทินแล้วได้โหมดเลือกเดือน/ปี) ----
   ภาษาเดียวกับปุ่มทางลัดด้านล่าง: พื้นรอง + ขอบเส้น · ชี้ = เขียวอ่อน · เดือนที่อยู่ = เขียวเต็ม
   controls.js สร้าง .cal-months/.cal-month เอง — กฎต้องมีคู่กันเสมอ (เกตดีไซน์ข้อ 12) */
.cal-months { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s-1); }

.cal-month {
  min-height: 36px; padding: 0 var(--s-1);
  font-family: inherit; font-size: var(--t-label); font-weight: 500; color: var(--ink-2);
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r-xs); cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

@media (hover: hover) { .cal-month:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand-soft); } }
.cal-month:focus-visible { outline: none; box-shadow: var(--ring); }
.cal-month.is-on { background: var(--brand); color: var(--on-brand); border-color: var(--brand); font-weight: 600; }
@media (hover: hover) { .cal-month.is-on:hover { background: var(--brand-hi); color: var(--on-brand); border-color: var(--brand-hi); } }

.cal-quick {
  display: flex; gap: var(--s-1);
  margin-top: var(--s-3); padding-top: var(--s-3);
  border-top: 1px solid var(--line);
}

.cal-quick-btn {
  flex: 1 1 0; min-height: 30px;
  font-family: inherit; font-size: var(--t-micro); font-weight: 500; color: var(--ink-2);
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r-xs); cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

@media (hover: hover) { .cal-quick-btn:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand-soft); } }
.cal-quick-btn:focus-visible { outline: none; box-shadow: var(--ring); }

/* ---- นิ้วต้องกดโดนตั้งแต่ครั้งแรก ---- */
@media (any-pointer: coarse) {
  .cbx-opt { min-height: 44px; }
  /* ⚠️ รายการในแผงดรอปดาวน์สูง 40px มาตลอด และไม่มีเกตไหนเห็น
     เพราะตอนที่เกตวัดหน้า แผงยังปิดอยู่ (display: none) ของที่ซ่อนอยู่
     ไม่มีกล่องให้วัด — เกตจึงเงียบตามนิยาม ไม่ใช่เพราะพลาด
     เจอตอนวัดแผงที่ "เปิดอยู่จริง" ในรอบแก้แผงล้นจอ 6 ก.ย. 2569 */
  .dropdown-item { min-height: 44px; }
  /* เดิม 40px ซึ่งต่ำกว่าเกณฑ์ที่โปรเจกต์ตั้งไว้เอง — ยกให้ครบ */
  .cal-day { min-height: 44px; }
  .cal-title { min-height: 44px; }
  .cal-month { min-height: 44px; }
  .cal-quick-btn { min-height: 44px; }
  .datefield-btn { width: 40px; height: 40px; }
}

/* ---- คนที่ตั้งค่าว่าไม่อยากเห็นการเคลื่อนไหว ต้องไม่เห็นเลย ---- */
@media (prefers-reduced-motion: reduce) {
  .pop { transition: none; opacity: 1; transform: none; }
  .cbx-caret { transition: none; }
}

/* ปุ่มที่มีแต่ไอคอนไม่เคยตั้งขนาดตัวอักษร จึงตกไปใช้ค่าปริยายของเบราว์เซอร์
   (13.3333px) ซึ่งเป็นขนาดที่ไม่มีใครเลือก และเป็นขั้นที่ 9 ที่ไม่ควรมี */
.icon-btn, .cbx-native, .datefield-native { font-size: var(--t-body); }

/* ==========================================================================
   แถบสรุปที่ติดก้นจอ (หน้าแก้ไขเอกสาร)

   เหตุผลอยู่ใน public/assets/js/docbar.js
   สั้น ๆ: ปุ่มบันทึกกับยอดรวมเคยอยู่ก้นหน้า เอกสาร 20 บรรทัดจึงต้องเลื่อน
   ลงไปหาทุกครั้ง และระหว่างกรอกก็มองไม่เห็นว่ายอดเป็นเท่าไร
   ========================================================================== */

.card-foot.is-sticky {
  position: sticky; bottom: 0; z-index: 20;
  /* V4 (W10): พื้นทึบ + เส้นขอบบน (ของ .card-foot) — ถอดเบลอ/โปร่ง/เงาลอย
     แถบบันทึกคือส่วนหนึ่งของการ์ดฟอร์ม ไม่ใช่กล่องลอยอีกชั้น และพื้นโปร่ง+เบลอทำให้ตัวเลข
     บรรทัดสินค้าที่เลื่อนผ่านข้างหลังเป็นเงาจาง ๆ ใต้ยอดรวม (อ่านยาก · G9) */
  background: var(--surface);
  border-top-color: var(--line-strong);
}

/* ยอดรวมอยู่ซ้ายสุด ปุ่มถูกดันไปขวาสุด — ตาอ่านเลขก่อน แล้วค่อยไปหาปุ่ม */
.docbar-total {
  display: flex; align-items: baseline; gap: var(--s-2);
  margin-right: auto;
}

.docbar-total-label { font-size: var(--t-label); color: var(--muted); }

.docbar-total-value {
  font-size: var(--t-title); font-weight: 600; color: var(--ink);
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
  /* จุดตั้งต้นของการกะพริบ (@keyframes docbar-bump) — สถานะเสียงสะท้อนเปลี่ยนค่าตรงนี้ ไม่ใช่เปลี่ยน animation */
  --bump-from: var(--brand);
  --bump-y: -1px;
}

/* ยอดจริงอยู่ในสายตาแล้ว — ยอดบนแถบถอยเป็นเสียงสะท้อน ไม่ใช่ตัวเลขที่สอง
   (docbar.js สลับคลาสนี้ด้วย IntersectionObserver) */
/* ⚠️ ถอยด้วยสี --muted ไม่ใช่ opacity .38 (= 2.28:1 ต่ำกว่า AA · ตรวจความครบหลังดีไซน์ V4 · C17)
   ป้ายเป็น --muted อยู่แล้ว ตัวเลขลดจาก --ink ลงมาเท่าป้าย = ยังอ่านได้ แต่ไม่แย่งสายตากับยอดจริง */
/* ตอนเป็นเสียงสะท้อนไม่กะพริบ (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · ข้อ 2) — เดิมทุกครั้งที่พิมพ์ ตัวเลขบนแถบเป็นสีแบรนด์
   → เข้มเต็ม → กระตุกกลับเป็นสีจาง กลายเป็นยอดตัวที่สองที่แย่งสายตากับยอดจริง ซึ่งขัดกับเหตุผลของ is-echo โดยตรง
   ⇒ แอนิเมชันเดิมยังวิ่ง แต่เริ่มจากสีจางและไม่ขยับ (มองไม่เห็นอะไรเลย)
   ⛔ ห้ามกลับไปใช้ `animation: none` ในสถานะนี้ (ผู้ทวนข้อ 2) — docbar.js ติด is-bumped ค้างไว้จนยอดเปลี่ยนครั้งถัดไป
      ถอด is-echo (เลื่อนจอจนยอดจริงพ้นตา) = ชื่อแอนิเมชันเปลี่ยนจาก none กลับเป็น docbar-bump = เบราว์เซอร์เริ่มแอนิเมชันใหม่
      ตัวเลขกะพริบสีแบรนด์ทุกครั้งที่เลื่อนขึ้น ทั้งที่ยอดไม่ได้เปลี่ยน · การเปลี่ยนค่าตัวแปรไม่ทำให้แอนิเมชันเริ่มใหม่ */
.docbar-total.is-echo .docbar-total-value { color: var(--muted); --bump-from: var(--muted); --bump-y: 0px; }
.docbar-total-value { transition: color var(--mid) var(--ease); }

/* ตัวเลขขยับตามที่พิมพ์ — บอกว่ามันมีชีวิต ไม่ใช่ป้ายค้าง */
.docbar-total-value.is-bumped { animation: docbar-bump 320ms var(--ease); }

/* ⚠️ เฟรมจบไม่ระบุสี — เบราว์เซอร์วิ่งกลับไปหาค่าฐานของตัวเลขเอง (--ink ปกติ · --muted ตอนเป็นเสียงสะท้อน)
   เดิมจบที่ --ink ตายตัว ไม่มี fill ⇒ ตอนเป็นเสียงสะท้อนตัวเลขเข้มเต็มแล้วค่อยกระตุกกลับ (ข้อ 2)
   สีของสถานะต้องอยู่ที่เดียว (กฎของสถานะนั้น) ไม่ใช่จำซ้ำไว้ในแอนิเมชัน */
@keyframes docbar-bump {
  0%   { color: var(--bump-from); transform: translateY(var(--bump-y)); }
}

@media (prefers-reduced-motion: reduce) {
  .docbar-total-value.is-bumped { animation: none; }
}

/*
 * ⚠️ แถบเมนูล่างบนมือถือเกาะขอบล่างเหมือนกัน แถบนี้ต้องหลบขึ้นมาให้พ้น
 *
 * วัดจริงบนจอ 390px ขณะเลื่อนอยู่กลางเอกสาร (สถานะที่ sticky ทำงานจริง):
 * ทับกัน 54px และ elementFromPoint ที่กึ่งกลางปุ่ม "บันทึกและอนุมัติ"
 * คืน .bottom-nav-item — **ปุ่มบันทึกกดไม่โดนเลยบนมือถือ**
 *
 * ⚠️ สองอย่างที่เกือบทำให้สรุปผิด:
 *   1. ตอนเลื่อนสุดหน้าสองแถบไม่ทับกัน (ห่าง 39px) ต้องวัดตอน sticky ทำงานจริง
 *   2. กฎนี้ต้องอยู่ **หลัง** กฎฐาน .card-foot.is-sticky { bottom: 0 }
 *      น้ำหนัก selector เท่ากัน ตัวที่อยู่ล่างกว่าชนะ — เคยวางไว้กลางไฟล์แล้วไม่มีผล
 */
@media (max-width: 860px) {
  .card-foot.is-sticky {
    bottom: calc(var(--bottom-nav-h) + env(safe-area-inset-bottom, 0px));
  }
}

/*
 * toast ก็ลอยอยู่ล่างจอเหมือนกัน จึงต้องหลบแถบล่างด้วยกฎเดียวกัน
 *
 * ⚠️ เพิ่งเห็นปัญหานี้ได้หลังแก้ให้ toast แสดงผลจริง (โล่ชั้นที่ห้า)
 * ตอนที่มันยัง display: none ไม่มีใครเห็นว่ามันทับอะไร
 * วัดบนจอ 500px: toast ใบที่สองล่างสุดที่ 760px แถบล่างเริ่มที่ 726px
 * = ทับปุ่มนำทาง 34px และ toast ซ้อนกันหลายใบยิ่งทับลึกขึ้น
 *
 * ต้องอยู่ "หลัง" กฎ .toast-host ใน @media (max-width: 480px) ที่ตั้ง bottom
 * ไว้ก่อนหน้า ไม่งั้นน้ำหนักเท่ากันแล้วตัวบนแพ้ (บทเรียนข้อ 18/29)
 */
@media (max-width: 860px) {
  .toast-host {
    bottom: calc(var(--bottom-nav-h) + var(--s-3) + env(safe-area-inset-bottom, 0px));
  }
}

/*
 * ของที่เบราว์เซอร์เลื่อนมาให้เห็น (โฟกัสจากปุ่ม Tab · scrollIntoView · ลิงก์ #id)
 * ต้องไม่ไปจบใต้แถบเมนูล่าง — WCAG 2.2 ข้อ 2.4.11 (โฟกัสต้องไม่ถูกบัง)
 * ดีไซน์ V4 · UAT 24 ก.ย. 2569: ช่องติ๊กที่ถูกเลื่อนมาให้เห็นไปอยู่ใต้แถบเมนูพอดี
 * แตะแล้วโดนปุ่ม "หน้าแรก" แทน
 */
@media screen and (max-width: 860px) {
  html { scroll-padding-bottom: calc(var(--bottom-nav-h) + var(--s-3) + env(safe-area-inset-bottom, 0px)); }
}
/* แถบเลือกหลายรายการ (.bulk-bar) ก็ติดขอบล่างเหมือนกัน — ตอนมันแสดงอยู่
   ของที่ถูกเลื่อนมาให้เห็นต้องหยุดเหนือแถบ ไม่ใช่จบใต้แถบ (แถบสูงราว 74px + ระยะ 12px) */
@media screen {
  html:has(.bulk-bar:not([hidden])) { scroll-padding-bottom: calc(var(--s-4) + 96px); }
}
@media screen and (max-width: 860px) {
  html:has(.bulk-bar:not([hidden])) {
    scroll-padding-bottom: calc(var(--bottom-nav-h) + var(--s-3) + 96px + env(safe-area-inset-bottom, 0px));
  }
}

/*
 * ปุ่ม "กลับขึ้นบน" ลอยอยู่มุมล่างขวาเหมือนกัน ต้องหลบแถบสรุป
 * วัดจริงบนจอ 390px ก่อนแก้: ทับปุ่มบันทึก 17px และขอบขวาของปุ่มกดไม่โดน
 * --docbar-h ถูกวัดและตั้งค่าโดย docbar.js เพราะแถบสูงไม่เท่ากันทุกหน้า
 */
body.has-docbar .to-top { bottom: calc(var(--docbar-h, 72px) + var(--s-4)); }

@media (max-width: 860px) {
  body.has-docbar .to-top {
    bottom: calc(var(--bottom-nav-h) + var(--docbar-h, 72px) + var(--s-3));
  }
}

/* บนจอแคบ ปุ่มต้องได้ทั้งบรรทัด ไม่ใช่ถูกบีบจนกดพลาด */
@media (max-width: 640px) {
  /* ⚠️ ยกที่ของชื่อหน้าบนแถบบนให้ชื่อกิจการ เฉพาะบนจอโทรศัพท์

     ชื่อหน้าซ้ำกับ <h1> ในเนื้อหาที่อยู่ต่ำลงมาไม่ถึงนิ้ว — วัดแล้วว่า
     ทุกหน้าที่มีแถบบน (96 หน้า) มีหัวข้อของตัวเองครบ จึงไม่มีหน้าไหน
     เสียชื่อเรื่องไปจากกฎนี้
     ส่วนชื่อกิจการไม่ซ้ำกับที่ใดเลยบนหน้าจอ และเป็นข้อมูลที่ผิดแล้วแพงที่สุด
     ในโปรแกรมบัญชี — คนที่มองไม่เห็นว่าตัวเองอยู่กิจการไหน จะลงเอกสารผิดกิจการ
     ก่อนกฎนี้ ป้ายเหลือ 74px = เห็นแต่จุดไข่ปลา ซึ่งแย่กว่าไม่แสดงเสียอีก */
  .topbar-titles { display: none; }
  .docbar-total { width: 100%; margin-right: 0; }
  .card-foot.is-sticky .btn { flex: 1 1 auto; }
}

/* ==========================================================================
   สองอย่างที่เห็นชัดตอนเอกสารมี 12 บรรทัด (ก่อนหน้านี้ทดสอบกันแค่บรรทัดเดียว)
   ========================================================================== */

/* 1) overflow:hidden ของการ์ดฆ่า position:sticky ทั้งใบ
      ตัวที่ overflow ไม่ใช่ visible จะกลายเป็น "กล่องเลื่อน" ของลูกที่ sticky
      และกล่องนี้เลื่อนไม่ได้ ลูกจึงไม่มีวันติดขอบจอ
      การ์ดที่มีแถบติดก้นจอจึงต้องปล่อย overflow — docbar.js ติดคลาสนี้ให้ */
.card.has-sticky-foot { overflow: visible; }
.card.has-sticky-foot .card-foot.is-sticky {
  border-bottom-left-radius: var(--r-lg);
  border-bottom-right-radius: var(--r-lg);
}

/* 2) ช่อง "ชื่อรายการ" ทำให้ทุกแถวสูงสองบรรทัด — ครึ่งหนึ่งของความสูงตาราง
      หายไปกับกล่องขาวที่ส่วนใหญ่ว่างเปล่า

      มันเป็นช่องเสริมของช่องเลือกสินค้า ไม่ใช่ช่องคู่ขนาน จึงไม่ควรมีน้ำหนัก
      เท่ากัน ทำให้มันเป็นบรรทัดต่อเนื่องที่จางลง แล้วค่อยชัดขึ้นเมื่อถูกใช้ */
.line-table .r-name {
  min-height: 26px; padding: 2px 8px;
  font-size: var(--t-label); color: var(--ink-2);
  background: none; border: 1px solid transparent; box-shadow: none;
  border-radius: var(--r-xs);
}

.line-table .r-name::placeholder { color: var(--faint); }
@media (hover: hover) { .line-table .r-name:hover { background: var(--surface-2); } }

.line-table .r-name:focus {
  background: var(--surface); border-color: var(--brand);
  box-shadow: var(--ring); color: var(--ink);
}

/* กรอกแล้วต้องอ่านออกชัดเหมือนข้อมูลจริง ไม่ใช่จางค้างไว้ */
.line-table .r-name:not(:placeholder-shown) { color: var(--ink); }

/* แถวเตี้ยลง = เห็นบรรทัดพร้อมกันมากขึ้น = เทียบราคาข้ามบรรทัดได้ในสายตาเดียว */
.line-table td { padding: 3px 4px; }

/* แถวสลับเฉดอ่อน ๆ ให้ตาไม่หลุดบรรทัดตอนกวาดจากซ้ายไปขวา
   เอกสาร 20 บรรทัดที่ไม่มีอะไรให้เกาะคือที่มาของการกรอกผิดช่อง */
.line-table tbody tr:nth-child(even) { background: color-mix(in srgb, var(--surface-2) 55%, transparent); }
@media (hover: hover) { .line-table tbody tr:hover { background: var(--brand-softer); } }
.line-table tbody tr:focus-within { background: var(--brand-soft); }

/* คลาสเดิมของ <select>/<input> ถูกเก็บไว้ที่ data-of ไม่ใช่คลาสของกล่องครอบ
   (เหตุผลอยู่ใน controls.js — เคยคัดคลาสไปใส่ปุ่มแล้วเกิดบั๊กเงินจริง)
   ตัวปรับความกว้างจึงต้องเลือกด้วย attribute แทน */
.cbx[data-of~="input-narrow"] { max-width: 132px; }
.line-table .cbx[data-of~="select"] .cbx-btn { min-height: 36px; padding: 6px 9px; box-shadow: none; }

/* หัวข้อกลุ่มเหนือแถวการ์ด — เบา ไม่แย่งสายตาจากตัวเลข แต่บอกว่ากำลังดูอะไรอยู่ */
.grid-label {
  margin: var(--s-5) 0 var(--s-3);
  font-size: var(--t-micro); font-weight: 600;
  text-transform: uppercase; color: var(--faint);
}

.grid-label:first-child { margin-top: 0; }

/* ขีดบอกค่าบนแกน Y — เคยเขียนขนาดติดใน style= ที่ .7rem (11.2px)
   ซึ่งต่ำกว่าพื้นล่าง และหลบเกตได้เพราะเกตอ่านแต่ไฟล์ CSS */
.chart-tick { font-size: var(--t-micro); line-height: 1; color: var(--faint); }

/* ==========================================================================
   คลาสที่มาแทนขนาดตัวอักษรที่เคยเขียนติดใน style= ของ view

   ของพวกนั้นหลบเกตได้ทั้งหมด เพราะเกตอ่านแต่ไฟล์ CSS จึงเป็นทางที่ขนาด
   นอกสเกลไหลกลับเข้าระบบได้เรื่อย ๆ — ตอนนี้เกตตรวจ view ด้วยแล้ว
   ========================================================================== */

.h-xs { font-size: var(--t-section); font-weight: 600; letter-spacing: -.01em; }

.brand-wordmark { font-size: var(--t-heading); font-weight: 700; letter-spacing: -.01em; }

/* คำอธิบายสีใต้กราฟ — เป็นกุญแจอ่านกราฟ ไม่ใช่หัวข้อ จึงเบาและอยู่กลาง */
.chart-legend {
  display: flex; gap: var(--s-5); justify-content: center;
  font-size: var(--t-label); color: var(--muted);
}

/* หน้า 404 — ตัวเลขใหญ่คือของชิ้นเดียวบนจอ ปล่อยให้มันใหญ่ได้เต็มที่ */
.err-code {
  font-size: var(--t-hero); font-weight: 700;
  color: var(--brand); letter-spacing: -.04em; line-height: 1;
}

.err-title { font-size: var(--t-heading); font-weight: 600; margin-top: var(--s-2); }

/* ตัวช่วยยื่นภาษี — ยอดที่ต้องนำส่งคือตัวเลขที่คนเปิดหน้านี้มาดู */
.wizard-figure { font-size: var(--t-title); font-weight: 600; letter-spacing: -.02em; }
.wizard-sub    { font-size: var(--t-heading); font-weight: 600; }

/* ==========================================================================
   ตัวควบคุมฟอร์มต้องรับตัวอักษรจากหน้า ไม่ใช่จากเบราว์เซอร์

   ⚠️ ทำไมกฎนี้ถึงมีอยู่

   เบราว์เซอร์ตั้ง font-size ของ <button>/<input>/<select>/<textarea> ไว้เองที่
   13.3333px และค่านั้นไม่ inherit จากหน้า ตัวไหนที่เราลืมตั้งขนาดจึงหลุดไปใช้
   ค่านี้เงียบ ๆ — เป็นขั้นที่ 9 ที่ไม่มีใครเลือก และไม่โผล่ในไฟล์ CSS ให้เกตเห็น

   วัดจากหน้า sales/create/invoice จริง: ปุ่มลบบรรทัด (.row-del) เป็นตัวเดียว
   ที่ยังหลุดอยู่ กฎนี้อุดทั้งชั้น ไม่ใช่อุดทีละตัวที่บังเอิญเจอ
   ========================================================================== */
button, input, select, textarea { font-family: inherit; font-size: inherit; }

/* ปุ่มลบบรรทัดในตารางสินค้า — เครื่องหมายเดียวในเซลล์แคบ ต้องอ่านออกและกดโดน */
.line-table .row-del { font-size: var(--t-body); line-height: 1; }

/* โครงร่างระหว่างรอผลค้นหา — กว้างไม่เท่ากันเพื่อให้ดูเหมือนรายการจริง ไม่ใช่แถบเรียง */
.search-skel { margin: var(--s-2) auto; max-width: 220px; }
.search-skel:nth-child(3) { max-width: 160px; }
.search-skel:nth-child(4) { max-width: 190px; }

/* บรรทัดบอกความจริงใต้ช่องเลือกบัญชีของแต่ละรายการซื้อ
   บอกว่า "บรรทัดนี้จะลงบัญชีอะไรจริง ๆ" ตามกฎเดียวกับ DocumentService
   เพราะช่องเลือกอย่างเดียวโกหกได้ — สินค้าที่ตัดสต๊อกลง 1040 เสมอไม่ว่าเลือกอะไร */
.r-acct-note {
  margin-top: 3px; padding-left: 2px;
  font-size: var(--t-micro); line-height: 1.35; color: var(--muted);
}

/* ยอดคงเหลือใต้ช่องเลือกสินค้าของแต่ละบรรทัด — ใช้ทั้งฟอร์มขายและฟอร์มซื้อ
   ⚠️ เป็นการบอกล่วงหน้าเท่านั้น ด่านจริงอยู่ที่ DocumentService::moveStock() */
.r-stock-note {
  margin-top: 3px; padding-left: 2px;
  font-size: var(--t-micro); line-height: 1.35; color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* ตัดเกินยอดที่มี — เซิร์ฟเวอร์จะปฏิเสธตอนอนุมัติ บอกตั้งแต่ตอนกรอกจะถูกกว่า */
.r-stock-note.is-short { color: var(--danger); font-weight: 600; }

/* ช่องที่ถูกบังคับ ต้องดูเหมือน "ข้อเท็จจริง" ไม่ใช่ "ช่องที่กดไม่ได้เพราะพัง" */
.line-table .select.is-forced,
.select.is-forced { background: var(--surface-2); color: var(--muted); cursor: not-allowed; }

/* ==========================================================================
   ตารางกรอกบนจอแคบ — โหมดการ์ด

   ⚠️ ปัญหาที่แก้: ตารางอ่านอย่างเดียวพับเป็นการ์ดหมดแล้ว (tablecards.js)
   แต่ตาราง **กรอก** ยังเป็นตาราง 9 คอลัมน์ที่มี min-width: 760px
   คนกรอกใบแจ้งหนี้บนมือถือจึงต้องเลื่อนซ้ายขวาทุกบรรทัด ทุกช่อง

   ⚠️ ทำด้วย CSS ล้วน ห้ามแตะ DOM ของแถวเด็ดขาด

   โค้ดคำนวณเงินอ่านค่าด้วย `tr.querySelector('.r-qty')` แล้วเรียก `.value`
   ทันทีโดยไม่เช็ค null (sales:1158-1161 · purchase:1130-1133) และปุ่มลบ
   เช็ค `tbody.children.length > 1` — การใส่ element ใด ๆ เข้าไปในแถว
   หรือใส่ <tr> เข้า tbody จะทำให้ยอดเงินผิดหรือลบบรรทัดสุดท้ายได้
   บทเรียนข้อ 10 ของโปรเจกต์คือใบ 650,000 บาทได้ VAT ขาด 70 บาท
   จากความผิดพลาดชนิดนี้เป๊ะ ๆ

   ⚠️ ป้ายชื่อช่องมาจาก data-label ที่ JS เขียนให้ตอนรัน (linecards.js)
   ไม่ใช่ข้อความในไฟล์ CSS — ป้ายต้องมาจาก <thead> ชุดเดียวกับที่คนเห็น
   บนเดสก์ท็อป ไม่งั้นสองที่จะไม่ตรงกันวันหนึ่ง (บทเรียนข้อ 16)

   ⚠️ ตำแหน่งของบล็อกนี้สำคัญ — ต้องอยู่ "หลัง" กฎฐานทั้งหมดของ .line-table
   (`.line-table td` ที่ 3957 · `.line-table tbody tr` ที่ 3961) ไม่งั้น
   padding/background ของกฎฐานจะเขียนทับโหมดการ์ดเงียบสนิท (บทเรียนข้อ 18)
   ========================================================================== */
@media (max-width: 720px) {
  /* ⚠️ ต้องล้าง min-width ที่ตั้งไว้ในบล็อก 860px (บรรทัด 2201)
     ไม่งั้นการ์ดจะยังกว้าง 760px แล้วหน้ายังเลื่อนแนวนอนเหมือนเดิม */
  .line-table { min-width: 0; width: 100%; display: block; }

  .line-table thead { display: none; }
  .line-table tbody { display: block; }

  /* แต่ละแถว = การ์ดหนึ่งใบ */
  .line-table tbody tr {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--s-2) var(--s-3);
    padding: var(--s-3);
    margin-bottom: var(--s-3);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface);
  }

  .line-table tbody td {
    display: block;
    padding: 0;
    border: 0;
    min-width: 0;
  }

  /* ป้ายชื่อช่อง — มาจาก data-label ที่ linecards.js เขียนจาก <thead> */
  .line-table tbody td[data-label]::before {
    content: attr(data-label);
    display: block;
    margin-bottom: 2px;
    font-size: var(--t-micro);
    font-weight: 600;
    color: var(--muted);
  }

  /*
   * ช่องที่ต้องกินเต็มความกว้าง — ชื่อสินค้าและคำอธิบาย
   *
   * ⚠️ เลือกด้วย :has() ของลูก ไม่ใช่ nth-child เพราะลำดับคอลัมน์ของ
   * ฟอร์มขายกับฟอร์มซื้อไม่เหมือนกัน (คอลัมน์ 2 ของซื้อคือ "บัญชี")
   * และ recurring/entry_form มีคอลัมน์คนละชุดอีก
   */
  .line-table tbody td:has(.r-product),
  .line-table tbody td:has(.r-name),
  .line-table tbody td:has(.r-desc),
  .line-table tbody td:has(.r-account),
  .line-table tbody td:has(textarea) {
    grid-column: 1 / -1;
  }

  /* ยอดบรรทัด — ตัวหนา เต็มความกว้าง ปิดท้ายการ์ด */
  .line-table tbody td:has(.r-amount) {
    grid-column: 1 / -1;
    padding-top: var(--s-2);
    border-top: 1px dashed var(--line);
    font-weight: 700;
  }

  /* ปุ่มลบ — มุมขวาล่างของการ์ด ไม่ใช่คอลัมน์ที่ต้องเลื่อนไปหา */
  .line-table tbody td:has(.row-del) {
    grid-column: 2 / -1;
    justify-self: end;
  }

  /* ช่องติ๊กของแถว (ถ้ามี) อยู่มุมบน */
  .line-table tbody td:has(input[type="checkbox"]) {
    grid-column: 1 / -1;
  }

  /* ทุกช่องกรอกในการ์ดกินเต็มความกว้างของช่องตัวเอง */
  .line-table tbody td .input,
  .line-table tbody td .select,
  .line-table tbody td .cbx,
  .line-table tbody td [data-of~="select"] {
    width: 100%;
    max-width: none;
  }

  /*
   * ⚠️ ต้องเลือกด้วย [data-of~="select"] ไม่ใช่ .r-vat
   *
   * controls.js:132-134 วาง <button class="cbx-btn"> ไว้ "ก่อน" <select>
   * ในแถวเดียวกัน และคลาสเดิมของ select ถูกย้ายไปอยู่ที่ wrap.dataset.of
   * การเลือกด้วย .r-vat จะได้ปุ่มที่วาดทับ ไม่ใช่ช่องจริง — นี่คือรูปแบบ
   * เดียวกับบั๊ก VAT 70 บาท (บทเรียนข้อ 10)
   */
  .line-table tbody td .cbx { display: block; }

  /* แถวรวมท้ายตาราง (ถ้ามี) ยังเป็นแถวเดียว ไม่ต้องพับ */
  .line-table tfoot { display: block; }
  .line-table tfoot tr { display: flex; justify-content: space-between; padding: var(--s-3); }
  .line-table tfoot td { display: block; padding: 0; border: 0; }
}

/* ==========================================================================
   เป้าแตะ 44px — ทุกความกว้างจอ ไม่ใช่เฉพาะจอแคบ
   ==========================================================================

   ⚠️ กฎชุดนี้เคยมีอยู่แล้วและถูกต้องทุกบรรทัด แต่ถูกขังไว้ใน
   `@media (max-width: 860px)` (บรรทัด 1373-1389) ซึ่งเป็นการถามคำถามผิด

   ความกว้างจอไม่ได้บอกว่าคนใช้นิ้วหรือเมาส์ — iPad แนวนอนคือ 1194px
   และเป็นนิ้วเต็มตัว · โน้ตบุ๊กจอสัมผัส 1366px ก็เช่นกัน
   เป้าจริงของปุ่มลบบรรทัดสินค้าบนอุปกรณ์พวกนี้คือ 30 × 28px

   ⚠️ ต้องใช้ `any-pointer` ไม่ใช่ `pointer`
   `pointer` ตอบว่า "อุปกรณ์ชี้ตำแหน่งหลักคืออะไร" — iPad ที่ต่อ Magic Keyboard
   ตอบว่า `fine` ทั้งที่จอสัมผัสยังใช้ได้ · `any-pointer: coarse` ตอบว่า
   "มีอุปกรณ์หยาบให้ใช้ไหม" ซึ่งเป็นคำถามที่ตรงกับเรื่องนี้

   ⚠️ วางไว้ท้ายไฟล์โดยตั้งใจ (บทเรียนข้อ 18)
   กฎที่น้ำหนัก selector เท่ากัน ตัวที่อยู่ล่างกว่าชนะ — บล็อกนี้ต้องชนะ
   ทุกกฎขนาดที่ประกาศไว้ก่อนหน้า ไม่งั้นจะถูกเขียนทับเงียบ ๆ เหมือนที่เคยเกิด
   กับ .row-del ที่บรรทัด 3002 ทับกฎขนาดของมันเองมาแล้ว

   ⚠️ ไม่แตะจอที่ใช้เมาส์อย่างเดียว — ตารางจะไม่บวมบนเดสก์ท็อป */
@media (any-pointer: coarse) {
  .btn, .btn-sm                                   { min-height: 44px; }
  /* ปุ่มในป้ายฟอร์ม — น้ำหนัก selector สูงกว่ากฎฐานของ .btn จึงต้องคืน 44
     ที่นี่เอง ไม่งั้นกฎ 24px ข้างบนจะชนะบนจอสัมผัสแล้วเป้าแตะหดครึ่งหนึ่ง */
  .field > label .btn                             { height: auto; min-height: 44px; }
  .icon-btn                                       { width: 44px; height: 44px; min-width: 44px; flex: 0 0 auto; }
  .tab                                            { min-height: 44px; padding: 13px var(--s-4); }
  .nav-link                                       { min-height: 44px; padding: 13px var(--s-3); }
  .nav-section                                    { min-height: 44px; }
  .check                                          { min-height: 44px; }
  .cbx-btn, .datefield-btn                        { min-height: 44px; }
  /* ปุ่มคัดลอกท้ายช่อง (หน้าแชร์คู่มือ · API · LINE) — วัดได้ 26px ตอนตกบรรทัด (UAT 29 ก.ย. 2569) */
  .input-group > button.addon                     { min-height: 44px; min-width: 44px; justify-content: center; }
  .line-table .input, .line-table .select         { min-height: 44px; }
  /* ⚠️ ช่องกรอกทั่วทั้งระบบ ไม่ใช่เฉพาะในตารางบรรทัดสินค้า
     วัดจากอุปกรณ์สัมผัสจริงที่ 390px (5 ก.ย. 2569): ช่องกรอกสูง 42px
     ทุกหน้าที่มีฟอร์ม — หน้าตั้งค่ามี 11 ช่องที่ต่ำกว่าเกณฑ์พร้อมกัน
     กฎเดิมยกให้เฉพาะ .line-table ซึ่งเป็นส่วนน้อยของช่องกรอกทั้งระบบ */
  .input, .select, select.select, .datefield-text { min-height: 44px; }
  /* ปุ่มสลับกราฟ/ตารางบนแดชบอร์ด — วัดได้ 27px */
  .chart-table-toggle                             { min-height: 44px; padding: var(--s-2) var(--s-3); }
  /* ⚠️ ลิงก์ "ดูทั้งหมด" ท้ายหัวการ์ด เป็นเป้ากดเดี่ยว ไม่ใช่ลิงก์ในย่อหน้า
     จึงไม่ได้รับการยกเว้นแบบลิงก์ข้อความ · วัดได้ 22px บนหน้าการเงิน
     บัญชี และแดชบอร์ด — ยกเป็นกล่องสูง 44 โดยตัวอักษรยังอยู่กึ่งกลาง
     ขนาดตัวอักษรไม่เปลี่ยน เปลี่ยนเฉพาะพื้นที่ที่นิ้วแตะได้ */
  .card-head a.small, .card-head > a,
  .card-foot a.small                               { display: inline-flex; align-items: center; min-height: 44px; }

  /* [UAT2 E11] ลิงก์ "เปิดใบที่มีอยู่ →" ใต้เหตุผลของตัวเลือกที่ปิดในเมนูแปลง — เป้ากดเดี่ยว วัดได้ 25px บนแท็บเล็ต 768
     ⇒ กล่องสูง 44 บนบรรทัดของตัวเอง · ⚠️ ไม่ใช้ ::after แบบตราบนเมนู: รายการถัดไปในเมนูชิดอยู่ข้างใต้
     พื้นที่ที่ยืดลงไปจะบังรายการนั้น (บทเรียนข้อ 114 — ปุ่ม ? ในตารางยอดรวมเคยพังแบบนี้) */
  .dropdown-menu a.convert-open-existing           { display: flex; align-items: center; min-height: 44px; }

  /* โลโก้เป็นลิงก์กลับหน้าแรก และช่องค้นหาเมนูไม่เคยตั้งความสูงเลย
     จึงตกไปใช้ค่าปริยายของเบราว์เซอร์ 34px (บทเรียนข้อ 15) */
  .sidebar-brand > a                               { display: flex; align-items: center; min-height: 44px; }
  .menu-search input                               { min-height: 44px; }

  /*
   * ตราบนเมนูต้องดูเล็กตามหน้าที่ของมัน — ขยาย "พื้นที่กด" ไม่ใช่ขยายตัวตรา
   *
   * ตราลิงก์ไปคนละหน้ากับชื่อเมนูที่อยู่ข้าง ๆ (badge_url) จึงเป็นเป้ากดจริง
   * ที่แยกจากแถว ไม่ใช่ของประดับ · ::after กินพื้นที่กดขึ้นไป 45px
   * โดยหน้าตาบนจอไม่เปลี่ยนเลยแม้แต่พิกเซลเดียว
   * ⚠️ ห้ามเกิน 44px มาก ไม่งั้นจะไปบังพื้นที่กดของชื่อเมนูแถวข้างบน/ล่าง
   */
  .nav-badge::after { content: ''; position: absolute; inset: -12px -8px; }

  /* ปุ่มเลขหน้าตั้ง height: 36px ตายตัว — เป็นปุ่มที่คนกดบ่อยที่สุดในหน้ารายการ
     และเป็นปุ่มที่พลาดแล้วเสียเวลามากที่สุด เพราะกดโดนเลขข้างเคียง */
  .pagination a, .pagination span                  { min-height: 44px; min-width: 44px; }

  /*
   * ปุ่มเครื่องหมายคำถามต้องดูเล็กตามหน้าที่ — มันอยู่ท้ายบรรทัดข้อความ
   * ทำให้ใหญ่จะดันบรรทัดในตารางยอดรวมเสียทั้งตาราง
   *
   * ⚠️ เคยลองขยายด้วย ::after เหมือนตราบนเมนู แล้วเกตยังฟ้อง ซึ่ง "ถูกแล้ว" —
   * ปุ่มนี้อยู่ในตารางที่มีแถวชิดกัน พื้นที่ที่ ::after กินขึ้นไปจึงถูกแถว
   * ข้างเคียงทับจนแตะไม่โดนจริง ต่างจากตราบนเมนูที่รอบตัวเป็นที่ว่าง
   * → ตัวปุ่มต้องสูงจริง แล้วดึงกลับด้วย margin ลบ ไม่ให้ดันบรรทัด
   * (แถวข้างเคียงเป็นตัวเลขล้วน ไม่มีเป้ากดอื่นให้ไปบัง)
   */
  .why-btn {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 44px; min-height: 44px; margin-block: -15px;
  }
  .line-table .cbx[data-of~="select"] .cbx-btn    { min-height: 44px; }
  .line-table .row-del                            { min-width: 44px; min-height: 44px; }
  .line-table .r-name                             { min-height: 44px; }
  .row-del                                        { min-width: 44px; min-height: 44px; }

  /* ช่องติ๊กใช้ 24px ตามเกณฑ์เดิมของโปรเจกต์ — ใหญ่กว่านี้จะเบียดข้อความข้าง ๆ
     และมาตรฐานยอมให้เล็กกว่า 44 ได้เมื่อมีระยะห่างรอบตัวพอ */
  input[type="checkbox"], input[type="radio"]     { width: 24px; height: 24px; }
  .check input                                    { width: 24px; height: 24px; }
}

/* ขนาดของไอคอนแบบฟอนต์ (Bootstrap Icons) — ใช้คลาส ไม่ใช่ style ติดใน HTML
   บทเรียนข้อ 11: ค่าที่เขียนติดใน view หลบเกตได้ทั้งหมด
   ⚠️ ค่าพวกนี้เป็นขนาดของ "รูป" ไม่ใช่ขนาดของ "ตัวอักษร" จึงไม่อยู่ในสเกล --t-*
   และไม่นับเป็นขั้นที่ 9 ของสเกลตัวอักษร */
.bi.ic { display: inline-flex; align-items: center; justify-content: center; }
/* ⚠️ หกขั้นนี้คือสเกลทั้งหมด — helper icon() ดึงทุกค่าเข้าขั้นที่ใกล้ที่สุด
   จึงไม่มีทางเกิดคลาส .ic-N ที่ไม่มีนิยาม (ซึ่งจะหายไปเงียบ ๆ) */
.ic-14 { font-size: var(--ic-14); }
.ic-16 { font-size: var(--ic-16); }
.ic-18 { font-size: var(--ic-18); }
.ic-20 { font-size: var(--ic-20); }
.ic-24 { font-size: var(--ic-24); }
.ic-32 { font-size: var(--ic-32); }

/* กล่องกราฟ Chart.js — canvas ต้องมีความสูงที่แน่นอน
   ⚠️ maintainAspectRatio:false แปลว่า Chart.js ยอมให้กล่องเป็นคนกำหนดความสูง
   ถ้ากล่องไม่มีความสูง canvas จะสูง 0 แล้วกราฟหายไปทั้งใบโดยไม่มี error */
.chart-wrap { position: relative; height: 260px; }
.chart-wrap-lg { height: 320px; }
@media (max-width: 860px) { .chart-wrap, .chart-wrap-lg { height: 240px; } }

/* ปุ่มธีม — ไอคอนสามตัวซ้อนกัน โชว์ตัวที่ตรงกับสถานะปัจจุบัน
   ⚠️ ใช้ display:none ไม่ใช่ opacity:0
   opacity:0 ยังอ่านออกด้วยโปรแกรมอ่านหน้าจอ และยังโฟกัสได้ (บทเรียนข้อ 17)
   ผลคือปุ่มเดียวจะประกาศชื่อไอคอนสามชื่อพร้อมกัน */
.theme-btn .ti { display: none; }
.theme-btn[data-mode="auto"]  .ti-auto,
.theme-btn[data-mode="light"] .ti-light,
.theme-btn[data-mode="dark"]  .ti-dark { display: inline-flex; }

/* รายการหลักฐานใต้แถบเตือน — แถบเตือนที่ชี้ไปหาตัวปัญหาได้
   ⚠️ ต้องเป็นลิงก์จริง ไม่ใช่ข้อความ คำเตือนที่บอกว่ามีปัญหาแต่ไปต่อไม่ได้
   คือคำเตือนที่คนอ่านแล้วทำอะไรไม่ได้นอกจากกังวล */
.alert-list { list-style: none; margin: var(--s-3) 0 0; padding: 0; display: grid; gap: var(--s-2); }
/* ⚠️ เคยเขียนว่า var(--t-small) ซึ่งไม่มีอยู่จริงในระบบ
   CSS ที่อ้าง token ที่ไม่ถูกนิยาม จะทิ้งทั้งประกาศไปเงียบ ๆ ไม่มี error
   ขนาดจึงตกไปใช้ค่าที่สืบทอดมา ซึ่งบังเอิญดูโอเคจนไม่มีใครสังเกต
   ชื่อจริงของขั้นนี้ในสเกลคือ --t-mini */
.alert-list li { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-3); font-size: var(--t-micro); }

/* สองด้านของตัวเลขเดียวกันบนการ์ดสถิติ — บอกที่มาของยอดสุทธิ */
.stat-split { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4); margin-top: var(--s-2); }
.stat-split b { font-weight: 600; }

/* ยอดคงค้างแยกสองส่วนบนการ์ดลูกหนี้/เจ้าหนี้ (ข้อค้นพบ 14)
   บรรทัด "เกินกำหนดแล้ว" เป็นสีเตือนเฉพาะเมื่อมีของจริง ศูนย์ใบคือข่าวดี ไม่ต้องแดง
   แถบสัดส่วนใช้ .bar/.bar-fill ของระบบ — ไม่วาดใหม่ */
.stat-due { display: grid; gap: var(--s-1); margin-top: var(--s-3); font-size: var(--t-micro); color: var(--faint); }
.stat-due b { font-weight: 600; color: var(--ink-2); }
.stat-due .is-late, .stat-due .is-late b { color: var(--danger); }
.stat-due a { color: inherit; text-decoration: none; }
.stat-due a:focus-visible { text-decoration: underline; }
@media (hover: hover) { .stat-due a:hover { text-decoration: underline; } }
.stat-due .bar { min-width: 0; margin-top: var(--s-1); }

/* กล่องรายงานในสารบัญ — ไอคอนซ้าย ข้อความขวา
   ⚠️ .value กับ .meta เดิมเป็น <div> ตอนนี้เป็น <span> เพราะอยู่ในกล่อง
   flex จึงต้องบังคับให้ยังขึ้นบรรทัดใหม่เหมือนเดิม */
.report-tile { display: flex; align-items: flex-start; gap: var(--s-3); }
.report-tile > span:last-child { display: block; min-width: 0; }
.report-tile .value, .report-tile .meta { display: block; }
.report-tile-ic {
  display: inline-grid; place-items: center; flex: none;
  width: 38px; height: 38px; border-radius: var(--r-sm);
  background: var(--brand-softer); color: var(--brand);
}
                                                                      
@media (hover: hover) { .report-tile:hover .report-tile-ic { background: var(--brand-soft); } }
/* V4: กระเบื้องที่อยู่ในการ์ดเป็นแถวแบน ไม่ใช่กล่องในกล่อง (หน้ารวมรายงานมี 15 กล่องซ้อน) — ตอนชี้ได้พื้นอ่อนแทนการยก */
.card .report-tile { border-color: transparent; background: transparent; box-shadow: none; }
/* V4 (W7): กลุ่มรายงานที่มีจำนวนคี่ (5 · 3 · 7) — ใบสุดท้ายกินเต็มแถว ไม่ทิ้งช่องว่างครึ่งแถวท้ายกลุ่ม
   (กระเบื้องเป็นแถวลิงก์แบนอยู่แล้ว การกว้างเต็มแถวไม่ทำให้หน้าตาต่างจากใบอื่น) */
.grid-2 > .report-tile:last-child:nth-child(odd) { grid-column: 1 / -1; }
/* V4 (P2 · G9): การ์ดสรุปที่อยู่ในการ์ด (บัญชีรับจ่ายเงินบนหน้าการเงิน) = ช่องพื้นอ่อน ไม่ใช่กล่องมีขอบซ้อนกล่อง
   กระเบื้องรายงานยกเว้น — มันเป็นแถวลิงก์แบน (กฎข้างบน) */
.card .stat:not(.report-tile) { border-color: transparent; background-color: var(--surface-2); box-shadow: none; }
@media (hover: hover) { .card a.report-tile:hover { transform: none; box-shadow: none; border-color: transparent; background: var(--surface-2); } }

/* ==========================================================================
   หัวตารางลอย — ปลดล็อกของที่เขียนไว้แล้วแต่ตายมาตลอด
   ==========================================================================

   ⚠️ `table.tbl th { position: sticky; top: 0 }` มีอยู่ในไฟล์นี้มานานแล้ว
   แต่ไม่เคยทำงานแม้แต่ครั้งเดียว — วัดบนหน้าจริงแล้วยืนยัน

   สาเหตุ: `.table-wrap` ตั้ง `overflow-x: auto` ไว้เพื่อให้ตารางที่กว้างกว่าจอ
   เลื่อนข้างได้ แต่ CSS บอกว่าถ้าแกนหนึ่งไม่ใช่ visible อีกแกนที่เป็น visible
   จะถูกคำนวณเป็น auto ไปด้วย `.table-wrap` จึงกลายเป็น "กล่องเลื่อน" ของ th
   ที่ sticky อยู่ — และกล่องนั้นไม่มีเพดานความสูง จึงไม่เคยเลื่อนแนวตั้งเลย
   (วัดได้: scrollHeight 1337 = clientHeight 1337 พอดี)

   sticky ที่ไม่มีอะไรให้เลื่อน คือ sticky ที่ไม่มีวันติด — ไม่ใช่เพราะเขียนผิด
   แต่เพราะบริบทมันไม่มีอยู่จริง (บทเรียนข้อ 12 ในอีกรูปแบบหนึ่ง)

   ทางแก้: ให้เพดานความสูง `.table-wrap` มันจึงกลายเป็นกล่องเลื่อนจริง
   ⚠️ ตารางที่สั้นกว่าเพดานไม่ได้รับผลอะไรเลย — ไม่มีแถบเลื่อน หน้าตาเหมือนเดิม
   ทุกพิกเซล เพดานทำงานเฉพาะกับตารางที่ยาวจริง ซึ่งเป็นตารางที่ต้องการหัวลอยพอดี
   ========================================================================== */

@media (min-width: 861px) {
  /* ⚠️ เฉพาะจอกว้าง — จอแคบ tablecards.js เปลี่ยนตารางเป็นการ์ดไปแล้ว
     การซ้อนกล่องเลื่อนบนมือถือทำให้เลื่อนหน้าไม่ได้ ซึ่งแย่กว่าไม่มีหัวลอย */
  .table-wrap {
    max-height: calc(100vh - 230px);
  }

  /* ══ ตารางที่พอดีกรอบ ไม่ต้องมีกล่องเลื่อนของตัวเอง ══

     tablefit.js เป็นคนติดคลาส is-fit ให้ หลังวัดว่าตารางไม่กว้างเกินกรอบ
     กฎชุดนี้จึงมีผลเฉพาะกรณีที่ "พิสูจน์แล้วว่าไม่ต้องเลื่อนข้าง"

     ⚠️ ต้องปลด overflow ของการ์ดด้วย ปลดแค่ที่กรอบไม่พอ
     .card ตั้ง overflow: hidden ไว้เพื่อตัดมุมมน กล่องที่ overflow ไม่ใช่
     visible จะกลายเป็นกล่องเลื่อนของลูกที่ sticky ทั้งหมด และการ์ดนั้นเลื่อน
     ไม่ได้ หัวตารางจึงไม่มีวันติด (บทเรียนข้อ 12)
     วัดแล้ว: ปลดที่กรอบอย่างเดียว หัวตารางตรึงได้ 0 จาก 10 หน้า
              ปลดทั้งสองชั้น ตรึงได้ 8 จาก 8 หน้าที่เลื่อนได้จริง

     ⚠️ ใช้ :has(> .table-wrap.is-fit) ไม่ใช่ :has(.table-wrap.is-fit)
     เพราะการ์ดที่มีตารางซ้อนอยู่ลึก ๆ ไม่ควรถูกปลดมุมมนไปด้วย

     ⚠️ z-index ของหัวตารางต้องต่ำกว่าแถบบน (30) แต่สูงกว่าแถวข้อมูล
     ไม่งั้นหัวจะไปบังแถบบนตอนเลื่อน */
  /* ⚠️ ใช้ clip ไม่ใช่ visible

     ทั้งสองค่าทำให้การ์ดเลิกเป็นกล่องเลื่อนของลูกที่ sticky เหมือนกัน
     (ตามสเปก overflow: clip ไม่สร้าง scroll container) หัวตารางจึงตรึงได้
     ทั้งคู่ — วัดแล้วได้ 5/5 เท่ากัน

     แต่ visible ทำให้การ์ดเลิกตัดมุมโค้งให้ลูกไปด้วย ลูกที่มีพื้นหลังของ
     ตัวเอง (.card-head · .card-foot · ตัวกรอบตารางเอง) จึงวาดทับมุมจน
     กลายเป็นเหลี่ยม — วัดแล้วเป็นทุกใบ 15 จาก 15
     ⚠️ border-radius ที่ใส่ไว้ที่กรอบตารางไม่ช่วย เพราะ radius ไม่คลิปลูก
     เมื่อ overflow เป็น visible

     clip ยังคลิปตามมุมโค้งอยู่ จึงได้ทั้งมุมสวยและ sticky ที่ทำงาน
     ⚠️ ราคาที่จ่าย: ถ้าตัววัดตัดสินผิด ตารางจะถูกตัดเงียบแทนที่จะดันหน้า
     ให้เลื่อนแนวนอน (ซึ่งเป็นอาการที่เกตจับได้) — จึงต้องให้ tablefit.js
     เฝ้าทั้งกล่องพ่อและตัวตารางเอง ไม่ใช่แค่ดัก resize */
  .card:has(> .table-wrap.is-fit) { overflow: clip; }

  .table-wrap.is-fit {
    overflow: visible;
    max-height: none;
    border-radius: 0 0 var(--r-lg) var(--r-lg);
  }

  /* ⚠️ ตรึงกับ "หน้า" ใต้แถบบน ไม่ใช่ตรึงกับกล่องที่เพิ่งถอดทิ้งไป
     ถ้าไม่ตั้ง top ใหม่ หัวจะไปตรึงที่ top: 0 ของหน้า แล้วมุดใต้แถบบนหายไป */
  .table-wrap.is-fit table.tbl th { top: var(--topbar-h); z-index: 3; }
}

/* ⚠️ เวลาพิมพ์ต้องไม่มีเพดาน ไม่งั้นกระดาษจะได้ตารางที่ถูกตัดกลางคัน
   โดยไม่มีอะไรบอก — งบที่พิมพ์ออกมาไม่ครบคือเอกสารที่ใช้ไม่ได้ */
@media print {
  .table-wrap { max-height: none !important; overflow: visible !important; }
}

/* ⚠️ หัวตารางลอยกับความโปร่งใส — ต้องมีทางถอยเสมอ

   `table.tbl thead th` (บรรทัด ~1865) ตั้งกระจกฝ้าไว้ตั้งแต่ต้น:
   พื้นโปร่งราว 81% + backdrop-filter: blur(8px) ซึ่งเป็นดีไซน์ที่ถูกต้อง
   เพราะเบลอทำให้ตัวเลขที่เลื่อนผ่านข้างหลังกลายเป็นรอยเปื้อนที่อ่านไม่ออก
   ไม่ใช่ตัวเลขคมชัดสองชั้นทับกัน

   ⚠️ แต่ของเดิมไม่เคยได้ทำงานเลย เพราะ sticky ตายมาตลอด จึงไม่เคยมีใคร
   เจอกรณีที่เบราว์เซอร์ไม่รองรับ backdrop-filter หรือผู้ใช้ปิดไว้
   ในกรณีนั้นจะเหลือแค่ "พื้นโปร่ง 81%" เปล่า ๆ แล้วตัวเลขเงินข้างหลัง
   จะทะลุขึ้นมาคมชัด ซ้อนกับหัวคอลัมน์ — อ่านผิดได้ง่ายกว่าอ่านไม่ออกเสียอีก

   ตอนนี้ sticky ทำงานจริงแล้ว ความเสี่ยงนั้นจึงเป็นของจริง */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  table.tbl thead th {
    /* ทาสีทึบรองพื้น แล้วทับด้วยสีเดิม — ได้สีเดียวกันเป๊ะแต่ทึบสนิท */
    background-color: var(--surface);
    background-image: linear-gradient(var(--surface-2), var(--surface-2));
  }
}

/* ช่องกรอกจำนวนเงินที่แคบกว่าปกติ (ส่วนลดท้ายบิล ฯลฯ)
   ⚠️ ค่าพวกนี้เคยเขียนติดใน HTML ว่า max-width:130px;min-height:32px
   ซึ่งหลบเกต qa_touch_target ได้ทั้งหมด เพราะเกตอ่านจากไฟล์ CSS
   (บทเรียนข้อ 11 ในเรื่องขนาดเป้ากด แทนที่จะเป็นขนาดตัวอักษร)
   32px ต่ำกว่าเกณฑ์ 44px ที่ระบบนี้ใช้ทั้งระบบ — นิ้วโป้งบนแท็บเล็ต
   กดพลาดไปโดนช่องข้าง ๆ แล้วแก้ยอดผิดใบโดยไม่รู้ตัว */
.input-amount { max-width: 130px; }

/* ปุ่มช่วงเวลาสำเร็จรูปใต้แถบตัวกรอง */
.date-preset { display: flex; flex-wrap: wrap; gap: var(--s-2); margin: calc(var(--s-3) * -1) 0 var(--s-4); }
/* V4 (W7): ปุ่มลัดที่อยู่ในแถวตัวกรองเป็นลูกของแถวนั้น — ไม่ต้องดึงขึ้น/เว้นล่างแบบแถวของตัวเอง */
.filters .date-preset { margin: 0; flex-wrap: nowrap; }
@media (max-width: 860px) {
  /* แผงตัวกรองบนจอแคบเป็นคอลัมน์ ปุ่มทุกปุ่มเต็มแถว — ปุ่มลัดสามปุ่มแบ่งแถวเดียวกันแทนสามแถว */
  .filters .date-preset { width: 100%; }
  .filters .date-preset .btn { flex: 1 1 0; width: auto; min-width: 0; }
}

/* ตรารวมยอดบนหัวกลุ่มเมนูที่ถูกพับอยู่
   ⚠️ margin-left: auto ไม่ได้ เพราะ caret ต้องอยู่ริมขวา
   ตรานี้จึงเบียดชิดขวาด้วยการดัน caret ที่มี margin-left: auto อยู่แล้ว */
.nav-section-badge { margin-left: auto; }
.nav-section .caret { margin-left: var(--s-2); }

/* ⚠️ โหมดย่อ (rail) ซ่อน <span> ลูกตรงของหัวกลุ่มทั้งหมด ตรานี้ก็โดนด้วย
   ซึ่งถูกต้อง เพราะโหมดย่อไม่แสดงชื่อกลุ่มอยู่แล้ว จึงไม่มีที่ให้ตราเกาะ
   (บทเรียนข้อ 27 ของโปรเจกต์ — กฎที่ซ่อนจริงไม่ใช่กฎที่ชื่อตรงกัน) */

/* ปุ่มสร้างบนแถบบน
   ⚠️ จอแคบซ่อนเฉพาะ "คำ" ไม่ใช่ทั้งปุ่ม — เหลือไอคอนบวกไว้
   แถบล่างบนมือถือมีปุ่มสร้างของตัวเองอยู่แล้ว แต่ช่วง 861–992px
   ไม่มีทั้งสองอย่างถ้าซ่อนทั้งปุ่ม (ช่วงเดียวกับที่เมนูเคยเปิดไม่ได้) */
.topbar-create { flex: none; white-space: nowrap; }
/* V4 (P4): ปุ่มสร้างบนแถบบนซ่อนเมื่อหัวหน้ามีปุ่มหลักที่ "สร้างเอกสารหมวดเดียวกัน" อยู่แล้ว — การกระทำเดียวกันไม่ขึ้นสองที่
   ⚠️ เทียบหมวดของปลายทาง ไม่ใช่ "มีปุ่มหลักใดก็ได้" (ผู้ตรวจ V4): หน้าผู้ติดต่อ/สินค้า/การเงิน/ภาษีมีปุ่มหลักของตัวเอง
   ซึ่งเป็นคนละการกระทำ ผู้ใช้เดสก์ท็อปต้องยังมีทางลัดออกเอกสาร · quickCreate() คืนได้แค่สองหมวดนี้ */
/* [UAT3] เทียบ "การกระทำเดียวกัน" ด้วยชนิดเอกสารปลายทาง ไม่ใช่แค่หมวด — ฉบับก่อนเทียบหมวด (/sales/create/) ⇒ หน้ารายการขาย
   (ปุ่มหลัก "+ ใบกำกับภาษี") ซ่อนปุ่ม "+ สร้างใบแจ้งหนี้" ของแถบบน แล้วบนเดสก์ท็อปไม่มีทางออกใบแจ้งหนี้จากหน้านั้นเลย (UAT 30 ก.ย. 2569)
   quickCreate() คืนได้สองปลายทาง (sales/create/invoice · purchase/create/expense) ⇒ ซ่อนเฉพาะเมื่อหัวหน้ามีปุ่มหลักไปปลายทางเดียวกันเป๊ะ */
body:has(#mainContent .page-head a.btn-primary[href$="/sales/create/invoice"]) .topbar-create[href$="/sales/create/invoice"],
body:has(#mainContent .page-head a.btn-primary[href$="/purchase/create/expense"]) .topbar-create[href$="/purchase/create/expense"] { display: none; }
@media (max-width: 992px) { .topbar-create span { display: none; } }
@media (max-width: 560px) { .topbar-create { display: none; } }

/* เมนูสลับกิจการ — ชื่อกิจการยาวกว่าชื่อเมนูทั่วไปมาก
   ⚠️ ป้ายเป็น <button> แล้ว ไม่ใช่ <a> จึงต้องคืนค่าที่เบราว์เซอร์ตั้งให้ปุ่มเอง
   (ปุ่มมี font ของตัวเองที่ไม่ inherit — บทเรียนข้อ 15) */
.dropdown-menu-wide { min-width: 240px; max-width: 320px; }
button.company-pill { font: inherit; cursor: pointer; }

/* กิจการที่อยู่ตอนนี้ — ไม่ใช่ลิงก์ ต้องดูต่างจากตัวที่กดได้
   ของที่หน้าตาเหมือนกดได้แต่กดไม่ได้ ทำให้คนคิดว่าระบบค้าง */
.dropdown-item.is-current { color: var(--brand); font-weight: 600; cursor: default; }
@media (hover: hover) { .dropdown-item.is-current:hover { background: none; } }

/* เส้นคั่นในเมนูดรอปดาวน์ — แยก "ของที่ทำบ่อย" ออกจาก "ของที่ทำนาน ๆ ครั้ง"
   ⚠️ ไม่มีคลาสนี้ในระบบมาก่อน สคริปต์ที่เขียนเมนูสลับกิจการตรวจเจอเองแล้วเตือน
   ถ้าปล่อยไว้ เส้นคั่นจะหายเงียบ ๆ โดยหน้าจอยังดูปกติทุกอย่าง */
.dropdown-sep { height: 1px; background: var(--line); margin: var(--s-2) 0; }

/* แถบสรุป "ดุล · ยอด" (ดีไซน์ V4 W6) — งบทดลอง · สมุดรายวัน · แถวข้อความ ไม่ใช่กล่อง
   ชิป "ดุล" ตอบคำถามแรก ตัวเลขตามหลัง · แทนการ์ดสรุปที่ซ้ำกับแถวยอดรวมท้ายตาราง */
.summary-line {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2) var(--s-5);
  margin: 0 0 var(--s-4);
  font-size: var(--t-body); color: var(--ink-2);
}
.summary-line b { margin-left: var(--s-1); font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums lining-nums; }
.summary-line .is-late, .summary-line .is-late b { color: var(--danger); }
.summary-note { font-size: var(--t-label); color: var(--muted); }
/* แถบสรุปที่เป็นหัวการ์ด (งบทดลอง) — หัวการ์ดต้องตัดบรรทัดได้บนจอแคบ ไม่งั้นช่องค้นหาถูกดันล้นขอบ */
.card-head:has(> .summary-line) { flex-wrap: wrap; row-gap: var(--s-2); }
.card-head > .summary-line { margin: 0; }
/* ซ่อนเฉพาะบนจอ (โปรแกรมอ่านหน้าจอยังอ่าน) — บนกระดาษยังพิมพ์ เช่นชื่อการ์ดที่แถบสรุปมาแทนที่บนจอ */
@media screen {
  .sr-only-screen {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
}

/* หัวหมวดบัญชีในงบทดลอง — เป็นแถวสรุป ไม่ใช่แถวข้อมูล จึงต้องดูต่าง */
.tb-group td { background: var(--surface-2); border-bottom: 1px solid var(--line-strong); }
.tb-group-btn {
  display: inline-flex; align-items: center; gap: var(--s-2);
  background: none; border: 0; padding: 0; color: var(--ink);
  font: inherit; cursor: pointer; min-height: var(--s-6);
}
.tb-group-btn .caret { transition: transform var(--fast) var(--ease); opacity: .7; }
.tb-group-btn[aria-expanded="false"] .caret { transform: rotate(-90deg); }
/* เป้าพับ/กางหมวดของงบทดลองบนจอสัมผัส (ตรวจความครบหลังดีไซน์ V4 · 25 ก.ย. 2569 · R9 — พิสูจน์แล้วว่าจริง)
   ฐานสูง --s-6 = 24px และไม่มีบล็อกสัมผัสไหนยกให้ · app.js ฟังคลิกเฉพาะ [data-tb-toggle] (ตัวปุ่ม)
   แตะส่วนอื่นของแถวหมวดจึงไม่เกิดอะไร อ่านเป็น "ปุ่มพับเสีย" · V4 ขยายแถวกระชับถึงแท็บเล็ต 1024 แต่ปุ่มนี้ยังเท่าเดิม
   สูง 44px + กว้างเต็มช่องชื่อหมวด (ช่องแรก colspan 3 / ช่องบทบาท title ในแถวกระชับ) = แตะตรงไหนของชื่อหมวดก็พับได้
   ⚠️ screen เท่านั้น — งบทดลองที่สั่งพิมพ์จากแท็บเล็ตต้องได้แถวหมวดสูงเท่าเดิม */
@media screen and (any-pointer: coarse) {
  .tb-group-btn { min-height: 44px; width: 100%; }
}

/* ปุ่มเครื่องหมายคำถามข้างบรรทัดที่คนสงสัยบ่อย */
.why-btn {
  background: none; border: 0; padding: 0 var(--s-1); margin-left: var(--s-1);
  color: var(--muted); font: inherit; cursor: pointer; line-height: 1;
  vertical-align: middle;
}
@media (hover: hover) { .why-btn:hover { color: var(--brand); } }

/* ตัวกรองพับได้ — ปุ่มโผล่เฉพาะจอแคบ
   ⚠️ บนจอกว้างต้องบังคับให้ฟอร์มกางเสมอ เพราะคลาส .collapse ของ Bootstrap
   ซ่อนไว้ตั้งแต่ต้น ถ้าไม่บังคับ จอเดสก์ท็อปจะไม่มีตัวกรองเลย
   ซึ่งเป็นการทำให้หน้าที่เคยใช้ได้ กลายเป็นใช้ไม่ได้ */
.filters-toggle { display: none; }
@media (min-width: 861px) {
  .filters.collapse { display: flex !important; height: auto !important; visibility: visible; }
}
/* V4 (W8): ช่วงเดียวกับแถวกระชับ — แท็บเล็ตสัมผัสถึง 1024 ด้วย (แนวนอน 1024×768 เดิมตัวกรองกางสองแถว
   ~190px ดันแถวข้อมูลแรกลงไปที่ y=917 ซึ่งเลยจอ 768 ไปแล้ว) */
@media (min-width: 861px) and (max-width: 1024px) and (any-pointer: coarse) {
  .filters.collapse:not(.show):not(.collapsing) { display: none !important; }
}
@media (max-width: 860px), (max-width: 1024px) and (any-pointer: coarse) {
  .filters-toggle { display: inline-flex; margin-bottom: var(--s-3); }
}
/*
 * ใบจาก POS รอตรวจ (sales/index) — จอที่ตัวกรองพับได้ใช้ปุ่มเล็กในแถวเดียวกับปุ่มตัวกรอง แทนแถบเต็ม (27 ก.ย. 2569)
 *   แถบเต็มบนจอ 390 สูง ~110px ดันแถวข้อมูลแรกลงใต้จอแรก (design:audit C3 898 > 788) และปุ่มในแถบล้นขอบขวา
 *   ⚠️ ช่วงจอเดียวกับ .filters-toggle ทุกตัวอักษร — ไม่งั้นมีช่วงที่ไม่เห็นทั้งสองอย่าง หรือเห็นทั้งสองอย่างพร้อมกัน
 *   จอกว้าง: แถบเต็มตัดบรรทัดได้ ปุ่มขึ้นบรรทัดใหม่ชิดขวาเมื่อข้อความยาว (ไม่ล้นแถบ)
 */
.pos-drafts-chip { display: none; }
/* จอกว้าง: ลิงก์สีเตือนต่อท้ายบรรทัดคำอธิบายใต้ชื่อหน้า แทนแถบเต็ม (28 ก.ย. 2569 · design:audit C2 เดสก์ท็อป)
   แถบเต็มดันแถวข้อมูลแรกลง 60px · บรรทัดคำอธิบายมีอยู่แล้ว จึงไม่เพิ่มความสูง (เหตุผลเต็มใน sales/index.php) */
.pos-drafts-inline { color: var(--warn); font-weight: 600; margin-left: var(--s-2); white-space: nowrap; }
.pos-drafts-inline svg { vertical-align: -2px; }
@media (max-width: 860px), (max-width: 1024px) and (any-pointer: coarse) {
  .pos-drafts-inline { display: none; }
  .pos-drafts-chip {
    display: inline-flex; align-items: center; gap: var(--s-1); margin: 0 0 var(--s-3) var(--s-2);
    color: var(--warn); background: var(--warn-bg); border-color: var(--warn-line); max-width: 100%;
  }
}
/* [UAT3 · design:audit C3 30 ก.ย. 2569] บรรทัดคำอธิบายใต้ชื่อหน้าขายเหลือแต่รายชื่อชนิดเอกสารที่แท็บข้างล่างพูดซ้ำ (ลิงก์ POS ถอยข้างบนแล้ว)
   — สองบรรทัด 52px บนจอ 390 ดันแถวแรกลงใต้จอแรกเมื่อแถวแรกเป็นร่างที่ชื่อลูกค้ายาว (แถว 96px · ขอบล่าง 805 > 788)
   ⚠️ ช่วงจอเดียวกับบล็อกข้างบน แต่ต้องมี screen — max-width ล้วนตรงกับกระดาษ A4 (~794px) แล้วคำอธิบายหายจากหน้าพิมพ์ (บทเรียนข้อ 343 · P2 จับได้) */
@media screen and (max-width: 860px), screen and (max-width: 1024px) and (any-pointer: coarse) {
  .page-head p.sub[data-sub-restates-tabs] { display: none; }
}
/* สรุปค่าที่ใช้อยู่บนปุ่มตัวกรอง (แผน V4 W8 "ตัวกรอง · [สรุปค่าที่เลือก]") — ยาวเกินตัดด้วย … ค่าเต็มอยู่ใน title/aria-label
   ⚠️ ปุ่มต้องหดได้ (min-width: 0) ไม่งั้นสรุปยาวดันปุ่มเกินจอ แล้วหน้าเลื่อนแนวนอนบนมือถือ */
.filters-toggle { max-width: 100%; min-width: 0; }
.filters-summary {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--brand-ink); font-weight: 600;
}
.filters-summary::before { content: '·'; margin: 0 var(--s-1); color: var(--muted); font-weight: 400; }

/* ปุ่มค้นหาบนแถบบน — เฉพาะจอที่เมนูข้างถูกซ่อน
   ⚠️ ใช้ 992px ให้ตรงกับจุดที่ sidebar ถูกเลื่อนออกนอกจอ ไม่ใช่ 860px
   ถ้าใช้ 860 ช่วง 861-992px จะไม่มีทั้งเมนูที่เห็นและปุ่มค้นหา
   ซึ่งเป็นช่วงเดียวกับที่เคยเปิดเมนูไม่ได้เลย */
.topbar-search { display: none; }
@media (max-width: 992px) { .topbar-search { display: inline-grid; } }

/* สีของตัวเลขกำไร/ขาดทุน
   ⚠️ .text-ok ถูกใช้ที่ accounting/fx_revaluation.php แต่ไม่เคยถูกนิยาม
   คลาสที่ไม่มีนิยามไม่ทำให้อะไรพัง มันแค่ "ไม่มีสี" ซึ่งบนคอลัมน์
   กำไร/ขาดทุนแปลว่ากำไรกับขาดทุนหน้าตาเหมือนกันทุกประการ
   ส่วน .text-danger เดิมมาจาก Bootstrap ซึ่งใช้ --bs-danger ที่ bridge
   ผูกไว้แล้ว แต่ตั้งชื่อคู่กันเองจะชัดกว่าและไม่ขึ้นกับไลบรารีภายนอก */
.text-ok { color: var(--ok); }
.text-danger { color: var(--danger); }

/* ⚠️ ตารางบรรทัดสินค้าต้องไม่กลายเป็นกล่องเลื่อน
   เพดานความสูงของ .table-wrap มีไว้เพื่อให้หัวตารางลอยได้ในตารางรายงานที่ยาว
   แต่ตารางบรรทัดสินค้าในฟอร์มเอกสารเป็น "ที่กรอก" ไม่ใช่ "ที่อ่าน"
   กด "เพิ่มบรรทัด" แล้วแถวใหม่จะไปโผล่นอกกรอบที่มองเห็น และหัวของมันก็ไม่ sticky
   คนจึงเห็นตารางนิ่งอยู่เฉย ๆ แล้วกดเพิ่มซ้ำอีกหลายครั้งโดยไม่รู้ว่าแถวถูกเพิ่มไปแล้ว

   ⚠️ ผูกกับ "ตารางที่อยู่ข้างใน" ไม่ใช่รายชื่อไฟล์ที่ต้องมีคนจำมาเติม
   ฟอร์มเอกสารใบที่เพิ่มทีหลังจะได้กฎนี้เองโดยอัตโนมัติ (บทเรียนข้อ 16) */
@media (min-width: 861px) {
  .table-wrap:has(.line-table) { max-height: none; overflow-y: visible; }
}

/* ⚠️ โหมดย่อเมนูซ่อนหัวกลุ่ม ซึ่งเป็นปุ่มเดียวที่กางกลุ่มได้
   กลุ่มที่ผู้ใช้พับไว้ก่อนเข้าโหมดย่อ จะกางไม่ได้อีกเลยจนกว่าจะออกจากโหมดย่อ
   เมนูที่มีอยู่แต่เข้าไม่ถึง แย่กว่าเมนูที่ไม่มี เพราะคนรู้ว่ามันควรอยู่ตรงนั้น
   ⚠️ ใช้ [hidden] ซึ่งเป็นสิ่งที่ shell.js ตั้ง — ไม่ใช่คลาสที่ตั้งชื่อเอง */
body.rail .nav-group[hidden] { display: block !important; }

/* ⚠️ กล่องเลื่อนที่มีหัวตารางลอย ต้องเผื่อที่ให้หัวตารางเสมอ
   ไม่งั้นการกด Tab ไปยังแถวที่อยู่นอกจอ จะเลื่อนแถวนั้นมาพอดีใต้หัวตาราง
   แล้วหัวตารางบังทับ — คนใช้คีย์บอร์ดจะเห็นโฟกัสหายไปเฉย ๆ
   48px คือความสูงของหัวตารางบวกระยะหายใจ */
.table-wrap { scroll-padding-top: 48px; }

/* ปุ่มเปิดตารางข้อมูลของกราฟ + ตารางที่ถูกเปิด
   ⚠️ ตาราง .sr-only ช่วยเฉพาะคนที่ใช้โปรแกรมอ่านหน้าจอ
   คนที่ใช้คีย์บอร์ดแต่มองเห็น เสียความสามารถอ่านตัวเลขไปตอนย้ายมา canvas
   (กราฟ CSS เดิมมี tabindex ให้ทุกแท่ง) ปุ่มนี้คืนให้ทุกคนพร้อมกัน */
.chart-wrap { padding-bottom: 0; }
.chart-table-toggle {
  position: absolute; right: 0; top: calc(var(--s-2) * -1);
  background: none; border: 0; padding: var(--s-1) var(--s-2);
  color: var(--muted); font: inherit; font-size: var(--t-micro);
  cursor: pointer; border-radius: var(--r-xs);
}
@media (hover: hover) { .chart-table-toggle:hover { color: var(--brand); background: var(--surface-2); } }

/* ตารางที่ถูกเปิดออกมา — วางทับกราฟและเลื่อนได้ในตัว */
.chart-table {
  position: absolute; inset: 0; overflow: auto;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-sm); font-size: var(--t-micro);
  width: 100%; border-collapse: collapse;
}
.chart-table caption { padding: var(--s-2); color: var(--muted); text-align: left; }
.chart-table th, .chart-table td { padding: var(--s-1) var(--s-2); border-bottom: 1px solid var(--line); text-align: right; }
.chart-table th:first-child, .chart-table td:first-child { text-align: left; }
.chart-table th { color: var(--muted); font-weight: 600; }

/* หัวคอลัมน์ที่เรียงได้
   ⚠️ ลูกศรต้องจองที่ไว้ตลอด ไม่ใช่โผล่ตอนเรียง
   ถ้าโผล่ทีหลัง ความกว้างคอลัมน์จะขยับตอนกด ซึ่งทำให้ตาหลุดแถวที่กำลังดู */
/* ⚠️⚠️ ห้ามใส่ position: relative ที่นี่เด็ดขาด

   เคยใส่ไว้เพื่อให้ ::after (ลูกศรเรียงลำดับ) มีบล็อกอ้างอิง
   แต่ position: sticky เป็นบล็อกอ้างอิงให้ลูก absolute อยู่แล้ว
   relative จึงไม่ได้เพิ่มอะไรเลย — มันแค่ "เขียนทับ" sticky ของหัวตารางทิ้ง

   ผลที่เกิดขึ้นจริง: หัวตาราง 9 ช่อง มี 6 ช่องที่เรียงลำดับได้จึงได้คลาสนี้
   แล้วเลิกตรึง ส่วนอีก 3 ช่อง (ช่องติ๊ก · วันที่ · คอลัมน์ปุ่ม) ไม่ได้คลาส
   จึงยังตรึงอยู่ พอเลื่อนตาราง สามช่องนั้นค้างที่เดิมแล้วลอยทับแถวแรก
   ผู้ใช้เห็นเป็น "แถบเทา" คร่อมข้อมูลแถวบนสุด — ทุกหน้าที่มีตารางยาว

   ⚠️ คลาสนี้ tablesort.js เป็นคนใส่ตอนรัน จึงไม่มีอยู่ใน HTML ให้ค้นเจอ
   และกฎทั้งสองข้อ "ถูก" เมื่ออ่านแยกกัน — เจอได้ทางเดียวคือวัด position
   ที่คำนวณแล้วของหัวตารางทุกช่อง ตอนที่เลื่อนกรอบอยู่กลางทางจริง

   น้ำหนัก selector: .tbl th.is-sortable = (0,2,1) ชนะ table.tbl th = (0,1,2)
   จึงชนะโดยไม่เกี่ยวกับลำดับบรรทัด ย้ายกฎไปไว้ตรงไหนก็ไม่ช่วย

   ⚠️ ลูกศรต้องจองที่ไว้ตลอด ไม่ใช่โผล่ตอนเรียง (padding-right)
   ถ้าโผล่ทีหลัง ความกว้างคอลัมน์จะขยับตอนกด ทำให้ตาหลุดแถวที่กำลังดู */
.tbl th.is-sortable { cursor: pointer; user-select: none; padding-right: var(--s-6); }

/* ⚠️ คืน relative เฉพาะช่วงจอที่หัวตาราง "ไม่ได้" sticky
   บล็อก max-width: 860px ตั้ง table.tbl th { position: static } ไว้ และ static
   ไม่เป็นบล็อกอ้างอิงให้ลูก absolute — ถ้าไม่คืนให้ ลูกศรจะไปเกาะบรรพบุรุษ
   ตัวถัดไปที่มี position แล้วลอยไปอยู่คนละที่กับคอลัมน์ของมัน */
@media (max-width: 860px) {
  .tbl th.is-sortable { position: relative; }
}
@media (hover: hover) { .tbl th.is-sortable:hover { color: var(--ink); background: var(--surface-3); } }
.tbl th.is-sortable::after {
  content: ""; position: absolute; right: var(--s-3); top: 50%;
  width: 0; height: 0; margin-top: -1px;
  border-left: 3px solid transparent; border-right: 3px solid transparent;
  border-bottom: 4px solid var(--line-strong);
  opacity: 0; transition: opacity var(--fast) var(--ease), transform var(--fast) var(--ease);
}
@media (hover: hover) { .tbl th.is-sortable:hover::after { opacity: .55; } }
.tbl th[aria-sort="ascending"]::after { opacity: 1; border-bottom-color: var(--brand); }
.tbl th[aria-sort="descending"]::after { opacity: 1; border-bottom-color: var(--brand); transform: rotate(180deg); }
.tbl th.is-sortable:focus-visible { outline: 2px solid var(--brand); outline-offset: -3px; }

/* ป้ายเทียบเดือนก่อนบนการ์ดตัวเลข
   ⚠️ สีต้องมาจากทิศทาง ไม่ใช่จากชนิดของการ์ด
   ค่าใช้จ่ายที่ "ขึ้น" ไม่ใช่เรื่องดี แต่ก็ไม่ใช่เรื่องร้ายเสมอไป
   จึงใช้สีกลางบอกทิศทาง แล้วปล่อยให้คนอ่านตัดสินเอง
   การใส่สีเขียว/แดงตามทิศทางจะทำให้ 'ค่าใช้จ่ายลดลง' กลายเป็นสีแดง */
.kpi-delta { font-variant-numeric: tabular-nums; font-weight: 600; }
.kpi-delta.up { color: var(--ink-2); }
.kpi-delta.down { color: var(--ink-2); }
.kpi-flat, .kpi-new { color: var(--muted); }

/* ปุ่มเรียกคืนใน toast
   ⚠️ ต้องเด่นพอที่จะเห็นภายในสามวินาทีที่ toast อยู่บนจอ
   ปุ่มที่กลมกลืนไปกับข้อความ = ปุ่มที่ไม่มีใครทัน */
.toast-undo {
  margin-left: var(--s-3); padding: var(--s-1) var(--s-3);
  background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.28);
  color: inherit; font: inherit; font-weight: 600;
  border-radius: var(--r-xs); cursor: pointer;
}
@media (hover: hover) { .toast-undo:hover { background: rgba(255,255,255,.28); } }

/* ป้ายบอกช่วงที่นำมาเทียบ — อยู่เหนือการ์ดสรุป */
.compare-note { margin: calc(var(--s-2) * -1) 0 var(--s-3); }

/* แถบเครื่องมือของการเลือกหลายรายการ
   ⚠️ ต้องติดขอบล่างจอ ไม่ใช่ลอยอยู่บนสุดของตาราง
   คนที่เลื่อนดูรายการยาว ๆ แล้วติ๊กไปเรื่อย ๆ ต้องเห็นปุ่มอนุมัติตลอดเวลา
   ไม่ใช่ต้องเลื่อนกลับขึ้นไปหา */
.bulk-bar {
  position: sticky; bottom: var(--s-4); z-index: 46;
  display: flex; align-items: center; gap: var(--s-3);
  margin: var(--s-3) 0; padding: var(--s-3) var(--s-4);
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r); box-shadow: var(--sh-float);
}
/* ⚠️ ดีไซน์ V4 (UAT 24 ก.ย. 2569) — แถบต้องอยู่ "ใต้" ตารางในมาร์กอัป sticky;bottom ถึงจะทำงาน
   (ของที่อยู่เหนือจอไม่ถูกดึงลงมา) · บนจอที่มีแถบเมนูล่าง ต้องหลบขึ้นมาให้พ้นด้วยกฎเดียวกับ
   .card-foot.is-sticky / .toast-host — ไม่งั้นปุ่ม "อนุมัติที่เลือก" ถูกแถบเมนูทับ
   กฎนี้ต้องอยู่หลังกฎฐาน น้ำหนักเท่ากัน ตัวล่างชนะ (บทเรียนข้อ 18 · 29) */
@media (max-width: 860px) {
  .bulk-bar { bottom: calc(var(--bottom-nav-h) + var(--s-3) + env(safe-area-inset-bottom, 0px)); }
}
/* ⚠️ .card { overflow: hidden } ทำให้การ์ดเป็น "กล่องเลื่อน" ของลูกที่ sticky (บทเรียนข้อ 12)
   แถบนี้จึงไม่เคยติดขอบจอเลยตั้งแต่วันแรก ทั้งตอนอยู่เหนือและใต้ตาราง — วัดจริงบน 390:
   แถบอยู่ที่ 1217px ขณะที่จอสูง 844 · ใช้ clip ตามแบบของ .card:has(> .table-wrap.is-fit)
   (ยังตัดมุมโค้งให้ลูก แต่ไม่สร้างกล่องเลื่อน) · เบราว์เซอร์ที่ไม่รู้จัก clip ได้แถบในลำดับปกติใต้ตาราง */
.card:has(> .bulk-bar) { overflow: clip; }
.bulk-count { font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
/* ⚠️ ช่องติ๊กต้องกดง่ายด้วยนิ้ว — คอลัมน์แคบไม่ใช่ข้ออ้างให้เป้ากดเล็ก */
/* ⚠️ 18px คือขนาดที่ตาต้องการ ไม่ใช่ขนาดที่นิ้วต้องการ
   เป้ากดต้องถึงเกณฑ์บนอุปกรณ์สัมผัส (เกต qa_touch_target)
   ขยายพื้นที่กดด้วย padding ของเซลล์ แทนการขยายตัวช่องให้ใหญ่จนดูเทอะทะ */
.w-check input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; }
@media (any-pointer: coarse) {
  .w-check input[type="checkbox"] { width: 24px; height: 24px; }
}

/* ==========================================================================
   แถบบอกว่ากำลังดูข้อมูลของกิจการไหน (อยู่ใน <main> ดูเหตุผลที่ layouts/main.php)

   ⚠️ ต้องติดไปกับกระดาษเสมอ — @media print ซ่อน .topbar ซึ่งเป็นที่เดียวที่มี
   ชื่อกิจการอยู่ก่อนหน้านี้ เอกสารที่พิมพ์ออกมาจึงไม่มีอะไรระบุกิจการเลย
   ========================================================================== */
.company-context {
  margin: 0 0 var(--s-3);
  color: var(--muted);
  font-size: var(--t-label);
  font-weight: 500;
}

.company-context a { margin-left: var(--s-2); }

/* บนจอ: ผู้เช่าที่มีกิจการเดียวและตั้งชื่อแล้ว ไม่ต้องเห็นแถบนี้ */
.company-context.print-only { display: none; }

/* ชื่อกิจการพูดครั้งเดียว (ดีไซน์ V4 W6 · UAT ตรวจความครบ 25 ก.ย. 2569)
   หน้าที่ต้องมีตัวตน (งบ · ภาษี · ตั้งค่า) เขียนชื่อกิจการในคำโปรยของตัวเองอยู่แล้ว — มุมมองติด data-company ที่คำโปรยนั้น
   เดิมผู้ใช้หลายกิจการเห็นชื่อเดียวกันสองบรรทัดติดกันบนจอ และกระดาษของ "ทุกคน" มีสองบรรทัด (แถบนี้พิมพ์เสมอ)
   ⇒ แถบนี้ถอยให้ทั้งบนจอและบนกระดาษ (น้ำหนัก id ชนะกฎ @media print ข้างล่าง) — ตัวตนบนกระดาษยังอยู่ในคำโปรย
   ⚠️ ยกเว้นกิจการที่ยังไม่มีชื่อ (แถบมีลิงก์ไปตั้งชื่อ) · เกต qa_company_label บังคับว่าคำโปรยที่เอ่ยชื่อกิจการต้องติดป้าย
   ⚠️ รับป้ายจาก "องค์ประกอบใดก็ได้ในหัวหน้า" ไม่ใช่เฉพาะ .sub (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · ข้อ 3)
      ใบสำคัญรายวันและหน้ารอบเงินเดือนพูดชื่อกิจการใน .eyebrow — กฎที่จับแค่ .sub[data-company] มองไม่เห็น
      กระดาษใบสำคัญ (ที่ยื่นผู้สอบบัญชี · บทเรียนข้อ 90) จึงพิมพ์ชื่อกิจการสองบรรทัดติดกันทุกใบ (บั๊ก U1 เดิม)
      · <main data-company> ของเลย์เอาต์เป็นบรรพบุรุษของ .page-head ไม่ใช่ลูก — ไม่ชนกฎนี้ */
#mainContent:has(> .page-head [data-company]) > .company-context:not(:has(a)) { display: none; }
/* หัวข้อเล็ก (.eyebrow) ที่พูดชื่อกิจการแทนแถบ ต้องเด่นไม่น้อยกว่าแถบที่มันมาแทน (ข้อ 3 · บทเรียนข้อ 91)
   .eyebrow ปกติเป็นสี --faint (ป้ายหมวด) — หน้ารอบเงินเดือนคือหน้าที่มีปุ่มลงบัญชี/จ่ายเงิน ชื่อกิจการต้องไม่จางกว่าแถบเดิม (--muted) */
.page-head .eyebrow[data-company] { color: var(--muted); }

@media print {
  /* บนกระดาษ: ต้องมีเสมอ ไม่ว่าจะกิจการเดียวหรือหลายกิจการ */
  .company-context,
  .company-context.print-only {
    display: block;
    margin-bottom: 6pt;
    color: #000;
    font-weight: 600;
  }

  .company-context a { display: none; }

  /* ชื่อกิจการในหัวข้อเล็ก (.eyebrow) ของใบสำคัญรายวัน/รอบเงินเดือน คือตัวตนเดียวที่เหลือบนกระดาษเมื่อแถบข้างบนถอย
     (ตรวจทานรอบสุดท้าย 25 ก.ย. 2569 · ข้อ 3) — .eyebrow บนจอเป็นตัวจิ๋วตัวพิมพ์ใหญ่สี --faint (ป้ายหมวด ไม่ใช่ตัวตน)
     บนกระดาษจึงยกให้เด่นเท่าแถบที่มันมาแทน: สีดำ ตัวหนา --t-label (ตรึงสเกลกระดาษแล้วในบล็อก print ข้างบน) */
  .page-head .eyebrow[data-company] {
    color: #000;
    font-weight: 600;
    font-size: var(--t-label);
    text-transform: none;
  }
}


/* ==========================================================================
   ขอบปลอดภัยของจอบาก (safe area) — ด้านซ้าย/ขวา
   --------------------------------------------------------------------------
   ⚠️ ระบบนี้เคยมีแต่ safe-area-inset-bottom (4 จุด) ซึ่งดูแลกรณีเดียว:
   ไอโฟนตั้งแนวตั้ง แล้วแถบขีดล่างทับปุ่ม

   พอตะแคงเป็นแนวนอน บากย้ายไปอยู่ด้านข้าง แล้วปุ่มเปิดเมนู · ปุ่มบนแถบบน ·
   ปุ่มแรกกับปุ่มสุดท้ายของแถบเมนูล่าง ตกไปอยู่ใต้บากจนแตะไม่โดน

   ⚠️ ต้องใช้ max() เสมอ ห้ามเขียนทับค่า padding เดิม
   บนเครื่องที่ไม่มีบาก env() คืน 0px — เขียนทับตรง ๆ แล้วระยะขอบที่ออกแบบไว้
   จะหายไปทั้งหมดบนเครื่องส่วนใหญ่ ซึ่งเป็นคนกลุ่มที่ใหญ่กว่ามาก

   ⚠️ อยู่ท้ายไฟล์โดยตั้งใจ — กฎเหล่านี้ "เพิ่มขึ้นจาก" กฎของแต่ละคอมโพเนนต์
   ไม่ได้แทนที่ จึงต้องมาทีหลังเสมอ (ต่างจากบทเรียนข้อ 18/29 ที่ห้ามต่อ
   *กฎฐาน* ท้ายไฟล์ — ข้อนี้ไม่ใช่กฎฐาน แต่เป็นส่วนเพิ่มที่ตั้งใจให้ชนะ)
   ========================================================================== */
.topbar {
  padding-left: max(var(--s-6), var(--edge-l));
  padding-right: max(var(--s-6), var(--edge-r));
}

.content {
  padding-left: max(var(--s-7), var(--edge-l));
  padding-right: max(var(--s-7), var(--edge-r));
}

.sidebar { padding-left: var(--edge-l); }

.toast-host {
  right: max(var(--s-5), var(--edge-r));
  bottom: max(var(--s-5), var(--edge-b));
}

/* [UAT2 F2-2] กฎขอบปลอดภัยข้างบนเป็นกฎฐานที่อยู่ "หลัง" กฎจอแคบของ toast ⇒ ทับระยะเหนือแถบเมนูล่าง (≤860)
   และขอบ 12px ของจอ ≤480 ไปเงียบ ๆ — toast ทับแถบเมนูล่างบนมือถือ (บทเรียนข้อ 18 · 29) · คืนค่าจอแคบต่อจากกฎฐานที่นี่ */
@media (max-width: 860px) {
  .toast-host { bottom: calc(var(--bottom-nav-h) + var(--s-3) + env(safe-area-inset-bottom, 0px)); }
}
@media (max-width: 480px) {
  .toast-host { left: max(var(--s-3), var(--edge-l)); right: max(var(--s-3), var(--edge-r)); }
}

.to-top { right: max(var(--s-5), var(--edge-r)); }

@media (max-width: 860px) {
  .bottom-nav {
    padding-left: max(var(--s-2), var(--edge-l));
    padding-right: max(var(--s-2), var(--edge-r));
  }

  /* แถบยอดรวมของฟอร์มเอกสารก็ชนขอบเหมือนกัน */
  .docbar {
    padding-left: max(var(--s-4), var(--edge-l));
    padding-right: max(var(--s-4), var(--edge-r));
  }
}

@media (max-width: 700px) {
  .content {
    padding-left: max(var(--s-4), var(--edge-l));
    padding-right: max(var(--s-4), var(--edge-r));
  }
}

/* ==========================================================================
   การเลื่อนต้องหยุดอยู่ในกล่องของตัวเอง
   --------------------------------------------------------------------------
   ไม่มี overscroll-behavior อยู่ที่ไหนเลยทั้งไฟล์ ผลบนมือถือคือ:

   · เลื่อนในลิ้นชักเมนูจนสุดแล้วเลื่อนต่อ → หน้าข้างหลังเลื่อนตาม
     ปิดลิ้นชักแล้วไม่รู้ว่าอ่านค้างไว้ตรงไหน
   · เลื่อนตารางไปสุดซ้ายแล้วปัดต่อ → iOS ตีความว่า "ย้อนกลับ"
     แล้วออกจากหน้าที่กำลังกรอกอยู่ทั้งที่ยังไม่บันทึก
   · ดึงลงในกล่องโมดัล → หน้าข้างหลังรีเฟรช
   ========================================================================== */
/*
 * ⚠️ ห้ามใส่คลาสที่ไม่มีอยู่จริงในระบบ
 *
 * ฉบับแรกเขียน `.line-table-wrap` ด้วย ซึ่ง grep ทั้ง app/ และ public/
 * แล้วไม่มีใครใช้เลยสักที่ — กฎนั้นจึงไม่มีผลกับอะไรทั้งสิ้น
 * และทำให้คนอ่านเข้าใจผิดว่าตารางกรอกถูกคุมแล้ว ทั้งที่ยังไม่
 *
 * ตารางกรอก (.line-table) อยู่ใน .table-wrap เหมือนตารางอื่น จึงได้กฎนี้
 * ไปด้วยอยู่แล้ว
 */
.sidebar, .modal-body, .pop, .table-wrap {
  overscroll-behavior: contain;
}

/* ==========================================================================
   ช่องกรอกบนอุปกรณ์สัมผัสต้องไม่เล็กกว่า 16px
   --------------------------------------------------------------------------
   Safari บน iOS ซูมทั้งหน้าเข้าไปทันทีที่แตะช่องที่เล็กกว่า 16px และ
   **ไม่ซูมกลับให้เมื่อออกจากช่อง** คนกรอกใบแจ้งหนี้ 10 บรรทัดจึงต้องหุบ
   สองนิ้วคืนเอง 10 ครั้ง — เป็นอาการที่คนอธิบายว่า "ใช้บนมือถือไม่ได้"
   แต่บอกไม่ได้ว่าพังตรงไหน เพราะทุกอย่างทำงานถูกหมด

   ⚠️ ต้องครอบ **ทุกอย่างที่โฟกัสแล้วพิมพ์ได้** ไม่ใช่แค่ .input
   ตัวที่หลุดตัวเดียวก็ยังซูมทั้งหน้า (Safari ดูที่ช่องที่กำลังโฟกัส)
   ========================================================================== */
@media (any-pointer: coarse) {
  .input, .select, textarea.input, select.select,
  .datefield-text, .cbx-btn, .menu-search input,
  input[type="text"], input[type="search"], input[type="number"],
  input[type="email"], input[type="tel"], input[type="password"],
  input[type="date"], textarea, select {
    font-size: var(--t-field);
  }

  /*
   * ⚠️ ช่องในบรรทัดสินค้ามีกฎของตัวเองที่น้ำหนัก selector สูงกว่า
   *
   * `.line-table .input` (สองคลาส) ชนะ `.input` (คลาสเดียว) เสมอ ไม่ว่า
   * กฎไหนจะอยู่บรรทัดล่างกว่า — วัดบนเครื่องจริงแล้วพบว่าช่องทั้ง 8 ช่อง
   * ของบรรทัดสินค้ายังเป็น 13-14px อยู่ ทั้งที่กฎข้างบนถูกทุกบรรทัด
   *
   * และนี่คือช่องที่ถูกกรอกบ่อยที่สุดในทั้งระบบ — ถ้ามีที่เดียวที่ต้องถูก
   * ก็คือที่นี่ · ต้องตั้งชื่อให้ครบทุกตัว ไม่ใช่พึ่งการสืบทอด เพราะ
   * `<input>`/`<select>` ไม่สืบทอดขนาดอักษรจากพ่อ (บทเรียนข้อ 15)
   */
  .line-table .input,
  .line-table .select,
  .line-table .cbx-native,
  .line-table .r-name,
  .line-table .input-narrow {
    font-size: var(--t-field);
  }
}

/* ---- Single Point C3: แบนเนอร์เซสชันช่วยเหลือของทีมกลาง (ค้างทุกหน้า) ---- */
.sp-support-bar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s-2) var(--s-4);
  padding: var(--s-2) var(--s-4);
  background: var(--surface-2); color: var(--ink);
  border-bottom: 1px solid var(--line); border-left: 4px solid var(--warn);
  font-size: var(--t-body);
}
.sp-support-bar.is-write { border-left-color: var(--danger); }
.sp-support-text { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-1) var(--s-3); min-width: 0; }
.sp-support-mode { font-weight: 600; color: var(--warn); }
.sp-support-bar.is-write .sp-support-mode { color: var(--danger); }
.sp-support-exp { color: var(--muted); font-size: var(--t-label); }
.sp-support-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
.sp-support-exit { display: inline; margin: 0; }
.sp-support-write { display: contents; }
.sp-support-write[open] { display: block; flex: 1 1 100%; }
.sp-support-write summary { list-style: none; cursor: pointer; display: inline-block; }
.sp-support-write summary::-webkit-details-marker { display: none; }
.sp-support-form { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-top: var(--s-2); }
.sp-support-form .input { min-width: 260px; }
.sp-support-label { font-size: var(--t-label); color: var(--muted); }
@media print { .sp-support-bar { display: none; } }

/* ---- การ์ดนำส่งเงิน: ประวัติการนำส่งหลายครั้ง · ยืนยันรายการที่คีย์มือ · ยกเลิกครั้งล่าสุด (24 ก.ย. 2569) ---- */
.remit-history { margin: 0; padding-left: var(--s-5); display: grid; gap: var(--s-1); line-height: 1.6; }
.remit-manual {
  display: grid; gap: var(--s-2);
  margin: 0 0 var(--s-3); padding: var(--s-3);
  background: var(--warn-bg); border: 1px solid var(--warn-line); border-radius: var(--r-sm);
}
.remit-cancel > summary {
  display: inline-flex; align-items: center; min-height: 32px;
  cursor: pointer; color: var(--muted); font-size: var(--t-label);
}
@media (any-pointer: coarse) {
  .remit-cancel > summary { min-height: 44px; }
}
/* หัวทางเลือกของแผนซ่อม (accounting/convert-audit · <details class="ca-alt">) — กติกาเดียวกับ summary ข้างบน:
   บนจอสัมผัสต้องสูงเต็มนิ้ว (device:sweep วัดได้ 24px บนแท็บเล็ต 768/1024 · [RV4-Q3-S2])
   ⚠️ คงหน้าตาเดิมบนเดสก์ท็อป (ลูกศรเปิด/ปิดของเบราว์เซอร์ยังอยู่ — display ยังเป็น list-item)
      align-content จัดข้อความกลางกล่องที่สูงขึ้น (เบราว์เซอร์เก่าที่ไม่รู้จัก = ข้อความชิดบน กล่องยังสูงพอ) */
.ca-alt > summary { cursor: pointer; }
@media (any-pointer: coarse) {
  .ca-alt > summary { min-height: 44px; align-content: center; }
}
/* ช่องที่ใช้เฉพาะบางชนิดของการเคลียร์ 2015 — `.field` ตั้ง display เองจึงชนะ [hidden] ของเบราว์เซอร์ */
[data-clear-for][hidden] { display: none; }

/* ---- หน้าช่องทางรับเงินจาก POS (finance/tenders · ใบสั่งงาน 4 ต.ค. 2569 ข้อ 2): ส่วนตั้งแยกรายสาขาที่พับได้ ----
   หัวของส่วนพับต้องสูงเต็มนิ้วบนจอสัมผัส (กติกาเดียวกับ .ca-alt / .remit-cancel) · บล็อกของคอมโพเนนต์อยู่รวมกันพร้อม media query ของมันเอง (บทเรียนข้อ 18) */
.tender-branches > summary { cursor: pointer; font-weight: 600; padding: var(--s-2) 0; }
@media (any-pointer: coarse) {
  .tender-branches > summary { min-height: 44px; align-content: center; }
}

/* หน้าเชื่อมต่อระบบ — สถานะกับวันหมดอายุของคีย์ API อยู่ช่องเดียว ซ้อนกันแนวตั้ง (ใบสั่งงาน 4 ต.ค. 2569 ข้อ 4) */
.key-state { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-1); }

/* หน้าเชื่อมต่อระบบ — ลิงก์ไปหน้าตั้งค่าของส่วนกลาง และหัวพับ "ตัวอย่างการตรวจลายเซ็น" เคยสูง 24px บนจอสัมผัส
   (หน้านี้ไม่เคยอยู่ในตัวกวาดอุปกรณ์จนรอบใบสั่งงาน 4 ต.ค. 2569 ข้อ 4) */
.int-sample > summary { cursor: pointer; }
@media (any-pointer: coarse) {
  .int-touch-link { display: inline-flex; align-items: center; min-height: 44px; }
  .int-sample > summary { min-height: 44px; align-content: center; }
}
