/* ============================================================
   ui-v2.css — Design System V2 (Theme registry)
   ridproject — ใช้เฉพาะกับหน้า *-v2.html เท่านั้น (admin-only, คู่ขนานของเดิม)
   อ้างอิงแผน: purring-watching-quilt.md §3.1.1/§3.2

   สถาปัตยกรรมธีม: เดิมรองรับหลายธีมแบบมีชื่อสลับได้ ผู้ใช้สั่งตัดเหลือธีมเดียว (18 ส.ค. 2026) —
   ลบ aurora-nordic / corporate-radial / neon-cyber / ethereal ทิ้งทั้งหมดแล้ว เหลือแค่
   "ios-liquid-glass" ผ่าน [data-v2-theme="ios-liquid-glass"] บน <html>
   ============================================================ *//* ---------- 0. FONT FACES (ios-liquid-glass เท่านั้น — ดู --v2-font ของธีมนั้น) ---------- */@font-face { font-family:"Sukhumvit Set"; src:url("/fonts/SukhumvitSet-Thin.woff2") format("woff2"); font-weight:200; font-style:normal; font-display:block; }
@font-face { font-family:"Sukhumvit Set"; src:url("/fonts/SukhumvitSet-Light.woff2") format("woff2"); font-weight:300; font-style:normal; font-display:block; }
@font-face { font-family:"Sukhumvit Set"; src:url("/fonts/SukhumvitSet-Text.woff2") format("woff2"); font-weight:400; font-style:normal; font-display:block; }
@font-face { font-family:"Sukhumvit Set"; src:url("/fonts/SukhumvitSet-Medium.woff2") format("woff2"); font-weight:500; font-style:normal; font-display:block; }
@font-face { font-family:"Sukhumvit Set"; src:url("/fonts/SukhumvitSet-SemiBold.woff2") format("woff2"); font-weight:600; font-style:normal; font-display:block; }
@font-face { font-family:"Sukhumvit Set"; src:url("/fonts/SukhumvitSet-Bold.woff2") format("woff2"); font-weight:700; font-style:normal; font-display:block; }
/* ---------- 1. TOKENS ---------- *//* ธีม: Aurora Boreal Nórdica — dark-only */
/* ธีม: Corporate Radial Process — light-only *//* ธีม: Neon Cyber — dark-only *//* ธีม: Ethereal — light (เต็ม) + dark (partial, โทนสลัวนุ่ม ไม่ใช่ธีมมืดจัด) *//* ธีม: iOS 27 · Liquid Glass — light เต็ม + dark เต็ม (ตาม Apple iOS 27 UI Kit tokens)
   ที่มา: C:\Users\Thurn67\Documents\Codex\designmd\ (foundations/colors.md, tokens/css-variables.md)
   หลักการสำคัญ: glass ใช้เฉพาะพื้นผิวลอย (navbar/card/sheet/HUD) ไม่ใช้กับ control ทั่วไป (input/chip/table) */:root[data-v2-theme="ios-liquid-glass"] {
  --v2-theme-label: "iOS 27 · Liquid Glass";
  --v2-bg: #F2F2F7;                                  /* Grouped Light Primary — แบน ไม่มี gradient */
  --v2-surface-solid: #FFFFFF;                       /* Grouped Light Secondary */
  --v2-surface-2: rgb(118 118 128 / 12%);            /* Fill Tertiary */
  --v2-hair: rgb(0 0 0 / 12%);                       /* Separator Non-Opaque */
  --v2-hair-strong: #C6C6C8;                         /* Separator Opaque */
  --v2-text: rgb(0 0 0);                             /* Label Primary */
  --v2-text-dim: rgb(60 60 67 / 60%);                /* Label Secondary */
  --v2-text-mute: rgb(60 60 67 / 30%);                /* Label Tertiary */
  --v2-accent-1: rgb(0 136 255);                     /* System Blue */
  --v2-accent-2: rgb(0 136 255);
  --v2-accent-fg: #FFFFFF;
  --v2-accent: var(--v2-accent-1);                   /* ปุ่มหลัก = สีทึบเดี่ยว ไม่มี gradient */
  --v2-ok: rgb(52 199 89); --v2-warn: rgb(255 141 40); --v2-danger: rgb(255 56 60); --v2-info: rgb(0 192 232);
  --v2-ok-bg: rgb(52 199 89 / 14%); --v2-warn-bg: rgb(255 141 40 / 14%);
  --v2-danger-bg: rgb(255 56 60 / 12%); --v2-info-bg: rgb(0 192 232 / 14%);
  --v2-idle-bg: rgb(116 116 128 / 8%);               /* Fill Quinary */
  --v2-material-border: rgb(255 255 255 / 40%);
  --v2-shadow-1: 0 1px 3px rgb(0 0 0 / 8%);
  --v2-shadow-2: 0 12px 40px rgb(0 0 0 / 16%);       /* --ios-shadow-elevated */
  --v2-shadow-3: 0 20px 56px rgb(0 0 0 / 22%);
  --v2-sp-1: 4px; --v2-sp-2: 8px; --v2-sp-3: 12px; --v2-sp-4: 16px; --v2-sp-5: 20px; --v2-sp-6: 28px;
  --v2-r-control: .875rem; --v2-r-card: 1.25rem; --v2-r-sheet: 1.75rem;
  --v2-r-panel: 1.75rem; /* visionOS-style — panel ลอย (card/modal/sheet/HUD) โค้งมนกว่า iPhone card ทั่วไป */
  --v2-ease: cubic-bezier(.25,.1,.25,1);
  --v2-nav-h: calc(52px + env(safe-area-inset-top));
  --v2-font: "Sukhumvit Set","SF Pro Text","SF Pro",-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans Thai",sans-serif;
  /* มาตราฐานขนาด "หัวข้อ + ข้อความรอง" ของทั้งธีม (ยึดจาก .v2-brand ตามที่ผู้ใช้สั่ง 2 ส.ค. 2026) — ใช้กับคู่
     หัวข้อ/ข้อความรองใหม่ทุกจุดจากนี้ไป ไม่ใช่แค่ .v2-brand */
  --v2-heading-size: 1.375rem; --v2-heading-size-sm: 1.125rem;
  --v2-subtext-size: 1rem; --v2-subtext-size-sm: .875rem;
  /* ปุ่มลอย light/dark (v2-ios-switcher) — ตัวแปรกระจกหักเหแสงจริงตามโค้ดอ้างอิง */
  --v2-glass-tint: #bbbbbc; --v2-glass-saturation: 150%;
  --v2-glass-reflex-light: 1; --v2-glass-reflex-dark: 1;
}
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] {
  --v2-bg: #000000;                                  /* Grouped Dark Primary — true black ตาม token จริง */
  --v2-glass-reflex-light: 0.3; --v2-glass-reflex-dark: 2;
  --v2-surface-solid: #1C1C1E;                       /* Grouped Dark Secondary */
  --v2-surface-2: rgb(118 118 128 / 24%);
  --v2-hair: rgb(255 255 255 / 12%);
  --v2-hair-strong: #38383A;
  --v2-text: #FFFFFF;
  --v2-text-dim: rgb(235 235 245 / 60%);
  --v2-text-mute: rgb(235 235 245 / 30%);
  --v2-accent-1: rgb(0 145 255); --v2-accent-2: rgb(0 145 255); --v2-accent: var(--v2-accent-1);
  --v2-ok: rgb(48 209 88); --v2-warn: rgb(255 146 48); --v2-danger: rgb(255 66 69); --v2-info: rgb(60 211 254);
  --v2-ok-bg: rgb(48 209 88 / 16%); --v2-warn-bg: rgb(255 146 48 / 16%);
  --v2-danger-bg: rgb(255 66 69 / 14%); --v2-info-bg: rgb(60 211 254 / 16%);
  --v2-idle-bg: rgb(118 118 128 / 18%);
  --v2-material-border: rgb(255 255 255 / 12%);
  --v2-shadow-1: 0 1px 3px rgb(0 0 0 / 30%);
  --v2-shadow-2: 0 14px 44px rgb(0 0 0 / 45%);
  --v2-shadow-3: 0 24px 60px rgb(0 0 0 / 55%);
}
/* ---------- 2. BASE (เฉพาะโครงกลาง ไม่มีสไตล์เฉพาะธีม) ---------- */* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
/* 12 ส.ค. 2026: หน้า v2 ไม่โหลด Tailwind (ไม่มี preflight reset) เลยไม่เคยมีใครล้าง margin เริ่มต้น 8px ของ
   <body> ที่ browser ใส่ให้เอง (UA stylesheet) มาก่อน — เนื้อหาทั้งหน้าเลยเยื้องขวา/ล่างเกินไป 8px ทับซ้อนกับ
   gutter ของ .v2-content ทุกจุด (เพิ่งเจอตอนวัด offset จริงเทียบกับ app.html เดิมที่ได้ reset ฟรีจาก Tailwind) */
html, body.v2-body {
  margin: 0;
  background: var(--v2-bg); color: var(--v2-text); font-family: var(--v2-font);
  min-height: 100vh; min-height: 100svh; overscroll-behavior: none;
}
/* 19 ส.ค. 2026 (แก้รอบ 3): 100svh ทำให้ .v2-body (เนื้อหา UI) ปลอดภัยจากแถบเบราว์เซอร์บังแล้ว แต่ผู้ใช้ทักต่อว่า
   background เว้นเป็นสีขาว (ของ browser เอง) ไม่เต็มจอตอนแถบเบราว์เซอร์ยุบ/ซ่อนไปแล้ว (พื้นที่มองเห็นจริงมากกว่า
   svh ที่กันไว้แบบระมัดระวังเกินไป) — สาเหตุ: กฎด้านบนให้ "html" (ผืนพื้นหลังจริงของทั้งหน้า อยู่หลัง body)
   ก็โดน min-height:100svh (อนุรักษ์นิยม/เตี้ยสุด) ไปด้วยจากการรวม selector เดียวกับ body — html เตี้ยเท่า body
   เป๊ะ พอพื้นที่มองเห็นจริงมากกว่า svh (แถบยุบ) พื้นที่ส่วนเกินนั้นไม่มีกล่องไหนคลุมถึงเลย โชว์สีขาวเปล่าของ
   browser canvas เอง แก้โดยแยก html ออกมาให้ใช้ 100lvh (large viewport height — ค่าใหญ่สุดที่เป็นไปได้ตอนแถบ
   เบราว์เซอร์ยุบหมด) แทน — html เป็น "ผืนหลัง" ที่ต้องกว้าง/สูงคลุมทุกสถานะแถบเสมอไม่ว่าจะยุบหรือโชว์ ส่วน body/
   .v2-body (ที่ใส่ UI จริง อาจโดนแถบบังถ้าสูงเกิน) ยังคง 100svh (อนุรักษ์นิยม) เหมือนเดิมไม่เปลี่ยน — ผลคือพื้นหลัง
   สี var(--v2-bg) ของ html โชว์ต่อเนื่องไม่มีรอยขาวในพื้นที่ส่วนเกินใต้ .v2-body เสมอ ไม่ว่าแถบเบราว์เซอร์จะอยู่
   สถานะไหน */
html { min-height: 100vh; min-height: 100lvh; }
/* 19 ส.ค. 2026: ผู้ใช้ทักว่าตารางที่ยืดเต็มจอ (flex:1 chain จาก .v2-body→.v2-content→.app-tablewrap ดู
   app-v2.html) ดูเหมือนบั๊กทั้ง PC/iPad/มือถือ — root cause คือ height:100vh ตรงนี้ "หน่วยคงที่" ของ vh ในมือถือ
   (iOS Safari/Android Chrome) ไม่ตรงกับพื้นที่จอที่มองเห็นจริงตอนแถบ address bar ยุบ/ขยายเอง (แถบ URL บาร์เลื่อน
   ซ่อน/โผล่ระหว่างสกอลล์) ทำให้ความสูงจริงของ .v2-body คลาดจากพื้นที่มองเห็นจริง เนื้อหาด้านล่าง (ตาราง+
   app-bottombar) โดนแถบเบราว์เซอร์บังบางส่วนหรือมีที่ว่างเกินโผล่ตามจังหวะสกอลล์ — บน PC ไม่มีปัญหานี้เพราะไม่มี
   address bar แบบไดนามิก (100vh คงที่เท่า innerHeight เป๊ะ) ผู้ใช้เลยเห็นว่า "3 อุปกรณ์" มีอาการต่างกันเล็กน้อย
   19 ส.ค. 2026 (แก้รอบ 1 — ไม่พอ): ลองใช้ 100dvh (dynamic viewport height คำนวณจากพื้นที่มองเห็นจริง ณ ขณะนั้น)
   ก่อน ผู้ใช้ทดสอบจริงยืนยันว่ายังตกจอเหมือนเดิมทุกอย่าง โดยเฉพาะ "ตอนรีเฟรช" — ต้นเหตุคือ dvh เป็นค่าที่ "ไล่ตาม"
   สถานะแถบ address bar ของเบราว์เซอร์แบบเรียลไทม์ ซึ่งมี timing bug จริงบนหลายเบราว์เซอร์/บิลด์: ทันทีที่รีเฟรช
   หน้า (ก่อนผู้ใช้ทำ scroll gesture ใดๆ) เบราว์เซอร์ยังไม่ "commit" ค่า dvh ให้ตรงกับสถานะแถบที่อยู่ที่โชว์อยู่จริง
   (มักค้างอยู่ที่ค่าตอนแถบยุบ/ใหญ่สุด ทั้งที่จอจริงตอนนั้นแถบยังโชว์เต็ม) จึงได้ค่าความสูงที่ใหญ่กว่าพื้นที่มองเห็น
   จริงชั่วขณะ — ซ้ำกับปัญหา 100vh เดิมทุกประการ แค่ไม่บ่อยเท่า
   19 ส.ค. 2026 (แก้รอบ 2 — เปลี่ยนแนวทาง): เปลี่ยนไปใช้ 100svh (small viewport height) แทน — เป็นค่า "แน่นอน
   คงที่" เท่ากับพื้นที่มองเห็นตอนแถบเบราว์เซอร์โชว์เต็มที่สุดเสมอ ไม่ไล่ตามสถานะแถบแบบ dvh จึงไม่มี timing bug
   ประเภทนี้อีก (ไม่ต้องรอ "commit" อะไรทั้งสิ้น ค่าคงที่ตั้งแต่ paint แรก) แลกมาด้วยพื้นที่หน้าจอน้อยกว่าค่าที่เป็น
   ไปได้จริงเล็กน้อยตอนแถบเบราว์เซอร์ยุบไปแล้ว (เผื่อพื้นที่ไว้ล่วงหน้าตลอด) แต่รับประกันว่าเนื้อหาไม่มีทางถูกแถบ
   เบราว์เซอร์บังตั้งแต่แรกโหลด — ผสานกับ .app-bottombar ที่ปักหมุด position:sticky ไว้แล้ว (app-v2.html) เผื่อ
   double-safety อีกชั้น ใส่ vh เป็น fallback แรกสุดเหมือนเดิม (เบราว์เซอร์เก่าไม่รู้จัก svh จะข้ามบรรทัดนี้ไปเงียบๆ
   ใช้ vh เดิมแทน) */
.v2-body { display:flex; flex-direction:column; height:100vh; height:100svh; overflow:hidden; position:relative; }
/* 12 ส.ค. 2026: ขอบซ้าย-ขวาลดเหลือ 12px คงที่ทุกขนาดจอ (เดิม 20px/28px ตาม --v2-sp-5/6 กว้างกว่า app.html เดิม
   ที่ใช้ 12px ตรงๆ ทุกที่ — ผู้ใช้ขอให้แก้เป็นค่ากลางเลยแทนที่จะ override ทีละหน้า) เฉพาะแนวนอนเท่านั้น
   แนวตั้ง (bottom) ยังใช้ --v2-sp-5/6 เหมือนเดิม ไม่เกี่ยวกับ "ขอบ" ที่ผู้ใช้ทัก */
/* 21 ส.ค. 2026: ผู้ใช้ทักว่ามี scrollbar แนวนอนโผล่ล่างสุดของหน้า (ปกติไม่มี) — ต้นเหตุคือ .v2-content ตั้ง
   overflow-y:auto อย่างเดียวไม่ตั้ง overflow-x เลย ตามสเปก CSS แกนที่ไม่ได้ตั้งไว้ถูกดันเป็น auto ไปด้วยเสมอ
   เมื่อแกนใดแกนหนึ่งเป็นค่าเลื่อนได้ (ดู CLAUDE.md กับดักเรื่องนี้) — พอ .v2-kpi-grid ขยาย negative margin
   ซ้าย-ขวาจาก -4px เป็น -24px (แก้เงาการ์ดโดนตัดรอบก่อน) กว้างเกิน padding แนวนอนของ .v2-content เอง (12px)
   .v2-content ที่ overflow-x กลายเป็น auto เลยโชว์ scrollbar แนวนอนของตัวเองที่ก้นหน้า — ตั้ง overflow-x:hidden
   ตรงๆ (ตั้งใจให้เลื่อนได้แค่แนวตั้งเท่านั้นอยู่แล้ว) ไม่กระทบการเลื่อนแนวนอนของ .v2-kpi-grid เอง (คนละ
   overflow context กัน) */
.v2-content { flex:1; overflow-y:auto; overflow-x:hidden; padding: 0 12px var(--v2-sp-5) 12px; -webkit-overflow-scrolling: touch; position:relative; z-index:1; }
::selection { background: var(--v2-accent-1); color: var(--v2-accent-fg); }
@media (min-width: 641px) and (max-width: 1024px) {
.v2-card-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1025px) {
.v2-content { padding: 0 12px var(--v2-sp-6) 12px; }
}
/* ============================================================
   ธีม: iOS 27 · LIQUID GLASS — glass เฉพาะพื้นผิวลอย, flat fill บน control ทั่วไป (ธีมเดียวที่เหลือในระบบ)
   ============================================================ *//* wallpaper = จำเป็นสำหรับ Liquid Glass — backdrop-filter บนพื้นสีล้วนไม่มีอะไรให้กระจกหักเห
   (เบลอสีเดียว = สีเดียว) ต้องมีสีสัน/รายละเอียดข้างหลังก่อนถึงจะเห็นเอฟเฟกต์กระจกจริง */[data-v2-theme="ios-liquid-glass"] .v2-wallpaper {
  position: fixed; inset:0; z-index:0; pointer-events:none;
  background:
    radial-gradient(760px 760px at 12% 8%, rgb(0 136 255 / 40%), transparent 60%),
    radial-gradient(700px 700px at 88% 22%, rgb(203 48 224 / 32%), transparent 60%),
    radial-gradient(780px 780px at 28% 92%, rgb(255 141 40 / 26%), transparent 60%),
    radial-gradient(720px 720px at 92% 88%, rgb(52 199 89 / 24%), transparent 60%);
}
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-wallpaper {
  background:
    radial-gradient(760px 760px at 12% 8%, rgb(0 145 255 / 30%), transparent 60%),
    radial-gradient(700px 700px at 88% 22%, rgb(219 52 242 / 24%), transparent 60%),
    radial-gradient(780px 780px at 28% 92%, rgb(255 146 48 / 20%), transparent 60%),
    radial-gradient(720px 720px at 92% 88%, rgb(48 209 88 / 18%), transparent 60%);
}
/* เลิก navbar แบบคาดหัวเดิมทั้งหมด (ผู้ใช้สั่ง 18 ส.ค. 2026) — hero-band เหลือหน้าที่แค่กันพื้นที่ด้านบน
   ให้ไม่โดนปุ่มลอย .v2-navpill (มุมบนขวา) บัง — ตัวเนื้อหา navbar จริงไม่มีแถบเต็มความกว้างอีกต่อไป */
[data-v2-theme="ios-liquid-glass"] .v2-hero-band { position:relative; z-index:1; padding-top: calc(env(safe-area-inset-top) + 70px); box-sizing:border-box; }

/* v2-navback = ปุ่มย้อนกลับลอยมุมบนซ้าย (เฉพาะหน้าที่ส่ง opts.back) — แยกจาก .v2-navpill โดยสิ้นเชิง */
[data-v2-theme="ios-liquid-glass"] .v2-navback {
  position:fixed; z-index:60; top: calc(env(safe-area-inset-top) + 12px); left: calc(12px + env(safe-area-inset-left));
  width:44px; height:44px; border-radius:50%; display:flex; align-items:center; justify-content:center;
  color: var(--v2-text); text-decoration:none;
  /* สูตรตรงจาก codefronts.com liquid-glass-card-with-hover-refraction (ที่ผู้ใช้ยืนยันแล้วว่าใช้งานได้จริง
     ในเบราว์เซอร์เดียวกัน) ไม่ใช่สูตรกล่องเงาหลายชั้นที่ประดิษฐ์เอง — เรียบง่ายกว่าและพิสูจน์แล้วว่าไม่มีขอบ/เงา
     ล้นออกนอกกรอบแบบที่เจอตอนขยาย box-shadow เอง 10 เท่า */
  background-color: color-mix(in oklab, white 12%, transparent);
  border: 1px solid rgba(255,255,255,.35);
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  box-shadow: 0 30px 60px -30px rgba(0,0,0,.65), inset 0 1px 0 rgba(255,255,255,.5);
}
[data-v2-theme="ios-liquid-glass"] .v2-navback svg { width:17px; height:17px; }

/* v2-brand = โลโก้+ชื่อหน่วยงานมุมบนซ้าย (โผล่แทน .v2-navback ในหน้าที่ไม่มี opts.back)
   3 ส.ค. 2026: ข้อความ "ส่วนแผนงาน กองแผนงาน" เจอบั๊กร้ายแรงจาก CSS `background-clip:text` (ตัวการเดียวกับที่ใช้
   ทำ gradient text ทุกสูตรที่เคยลอง — tg-11 static §4.8, ta-02 animated §4.13, hv-04 reveal §4.15): **ไม้เอก
   ของ "ส่วน" หายไปเป็นระยะ ไม่คงที่** ขึ้นกับบริบท DOM/animation แม้จะลอง font-weight ทุกค่า, font-kerning:none,
   ปิด ligatures, เปลี่ยน line-height, ปิด transition ก็ยังหลุดเป็นบางครั้ง — เป็นบั๊ก Chromium ในขั้นตอน
   compositing mask ของ background-clip:text ชนกับ mark-to-base ของสระ/วรรณยุกต์ลอย ไม่ใช่บั๊กจากฝั่งเราแก้
   ด้วย CSS property เดียวได้จริง (ดู git history ของไฟล์นี้สำหรับ log การไล่บั๊กแบบละเอียด ถ้าต้องสืบต่อ)
   **ทางแก้ที่ยืนยันเสถียรจริง (ทดสอบซ้ำแล้ว):** เลิกใช้ background-clip:text ทั้งหมด เปลี่ยนไปวาดข้อความ gradient
   ด้วย **inline SVG `<text fill="url(#...)">`** แทน (สร้างใน ui-v2.js `buildBrandGradSvg()`) — SVG text ใช้
   pipeline paint ปกติ (fill color ธรรมดา ไม่ใช่ clip-mask compositing) จึงไม่มีทางเจอบั๊กกลุ่มนี้เลย
   .v2-brand__title ตอนนี้เป็นแค่กล่องห่อ <svg> ไม่ใช่ข้อความ DOM ธรรมดาอีกต่อไป (ไม่มี .v2-brand__grad/font-size/
   line-height ที่นี่แล้ว — ขนาดตัวอักษรคุมผ่าน JS ที่ set font-size บน <text> ตรงๆ ตาม breakpoint 480px) */
.v2-brand {
  position:fixed; z-index:60; top: calc(env(safe-area-inset-top) + 12px); left: calc(12px + env(safe-area-inset-left));
  height:54px; display:flex; align-items:center; gap:10px; max-width: calc(100vw - 220px);
}
.v2-brand__logo { height:64px; width:64px; object-fit:contain; flex-shrink:0; }
.v2-brand__text { min-width:0; overflow:visible; }
/* 7 ส.ค. 2026: .v2-brand__title เดิม overflow:hidden (จำเป็นตอนยังเป็น <svg> ที่คุมความกว้างเอง) — ตอนนี้เป็น
   เอฟเฟกต์ vanish/reveal ที่ตัวอักษรลอย/หมุน/ขยายออกนอกกรอบระหว่างอนิเมชัน (translate ไกลสุด -74px, scale 1.35)
   overflow:hidden ของ ancestor ตัดขอบเขตนี้ทิ้งหมดไม่ว่า .v2-brand__vanish-wrap เองจะตั้ง overflow:visible
   ไว้แค่ไหน (ancestor's overflow:hidden ชนะเสมอ) ทำให้ตัวอักษรที่เริ่มกระเจิงออกไปไกลอยู่นอกกรอบที่มองไม่เห็น
   เลยทั้งชุด (ผู้ใช้ทัก "สีหาย/มีขอบตัด") — เปลี่ยนเป็น overflow:visible ให้แสดงเต็มระยะจริง */
.v2-brand__title { display:flex; align-items:center; overflow:visible; }

/* 7 ส.ค. 2026: เลิกใช้ SVG gradient-sweep (buildBrandGradSvg, §4.16/§4.17) เปลี่ยนป้ายชื่อเป็นเอฟเฟกต์
   "vanish/reveal" ต่อกลุ่มอักขระแทน (ผู้ใช้เตรียม CSS มาเอง — ดู liquid-glass-v2.md §5.35) เล่น reveal-in
   ครั้งเดียวตอนโหลด (ไม่วนลูป) ตัด <i> ต่อ "กลุ่มอักขระที่แสดงผลจริง" (grapheme cluster ผ่าน Intl.Segmenter
   ใน ui-v2.js buildBrandVanishWord()) ไม่ใช่ตัดทีละ Unicode code point ดิบ — ทดสอบยืนยันแล้วว่าไม้เอกไม่หาย
   7 ส.ค. 2026 (ต่อ, กลับคำวินิจฉัยเดิม): เคยเอา gradient/background-clip:text ออกเพราะทดสอบตอนนั้นแล้วข้อความ
   หายทั้งก้อน สรุปว่าเป็น compositor bug — **ผิด** ตอนทดสอบรอบนั้น `.v2-brand__text`/`.v2-brand__title` ยัง
   เป็น overflow:hidden อยู่ (ยังไม่ได้แก้เป็น visible) สาเหตุจริงคือ overflow:hidden ตัดขอบเขต ไม่ใช่ background-
   clip:text พัง — ผู้ใช้ทำตัวอย่างพิสูจน์บนสำเนาหน้าเว็บจริงทั้งหน้า (มี wallpaper/กระจกซ้อนเหมือนกันทุกอย่าง)
   ว่า gradient เห็นปกติเมื่อ overflow เป็น visible ครบทุก ancestor — คืน gradient กลับมาตามเดิม */
.v2-brand__vanish-wrap { display:inline-flex; align-items:center; gap:7px; min-width:0; overflow:visible!important; }
.v2-brand__vanishword { display:inline-flex; align-items:center; white-space:pre; cursor:pointer; overflow:visible; }
/* 10 ส.ค. 2026: ลดรัศมี blur จาก 14px → 9px แก้อาการกระตุกช่วง reveal (ยังเห็นเป็น "เบลอ" ชัดเจน แค่ลด cost
   การ composite filter ที่แพงกว่า transform/opacity เยอะ โดยเฉพาะเพราะตัวอักษรพวกนี้บางกลุ่ม (การ์ด KPI) อยู่ใต้
   ancestor ที่มี backdrop-filter อยู่แล้ว (.v2-kpi-card) — คอนเซปต์เบลอ+กระเจิงยังเหมือนเดิม ไม่ได้ตัดเอฟเฟกต์ออก
   10 ส.ค. 2026 (ต่อ): เคยลองเพิ่ม will-change:filter,transform,opacity ถาวรคู่กันด้วย — ผู้ใช้ทดสอบจริงบน iPad
   แล้วพบว่าทำให้**ทั้งหน้าค้าง เลื่อนจอไม่ได้เลย** (compositor layer ~180 ตัวพร้อมกัน (title+subtitle+6การ์ด×
   label/unit/value) ชนกับ backdrop-filter ที่มีอยู่แล้วบน .v2-kpi-card 6 ใบ เกินขีด GPU/memory ของ iOS Safari)
   ถอด will-change ออกทั้งหมดแล้ว (ดู liquid-glass-v2.md §5.30/§3.9 — เครื่องมือ browser ของเซสชันเป็น
   Chromium เท่านั้น ตรวจบั๊ก Safari เองไม่ได้ ต้องเชื่อผลทดสอบจริงจากผู้ใช้เสมอ ไม่ใช่เดาแก้ต่อ) */
.v2-brand__vanishword i {
  display:inline-block; min-width:.08em; font-style:normal; font-size:22px; font-weight:700; line-height:1;
  background: linear-gradient(90deg, #2563eb 0%, #ff1f3d 100%); background-size:220px 100%;
  background-position: calc(var(--i) * -10px) 50%; -webkit-background-clip:text; background-clip:text; color:transparent;
  transition: transform 1.1s cubic-bezier(.3,.6,.4,1), opacity 1s ease, filter 1s ease;
  transition-delay: calc(var(--i) * 55ms);
}
.v2-brand__vanish-wrap.is-vanished .v2-brand__vanishword i {
  opacity:0; filter:blur(9px); transform:translate(var(--x),var(--y)) rotate(var(--r)) scale(1.35);
}
/* border-top:none กันไว้ตาม local-assets.css ต้นฉบับ — ตรวจแล้วหน้าจริงไม่มี border-top ติดมาอยู่แล้ว (0px
   none ทุกตัว) เป็นการป้องกันไว้เฉยๆ ให้ตรงกับต้นฉบับ 100% ไม่ได้แก้บั๊กอะไรเพิ่ม */
.v2-brand__vanish-wrap, .v2-brand__vanishword, .v2-brand, .v2-brand__text, .v2-brand__title { border-top:none!important; }
/* is-auto (วนลูปตลอด) เก็บโค้ดไว้เผื่อใช้ในอนาคต — ปัจจุบันไม่มีจุดไหนใส่คลาสนี้จริง (ผู้ใช้สั่ง "ไม่ต้องลูป") */
.v2-brand__vanish-wrap.is-auto .v2-brand__vanishword i { animation: v2-vanish-auto 7s ease-in-out infinite; animation-delay: calc(var(--i) * 55ms); }
@keyframes v2-vanish-auto {
  0%, 30% { opacity:1; filter:blur(0); transform:none; }
  55% { opacity:0; filter:blur(14px); transform:translate(var(--x),var(--y)) rotate(var(--r)) scale(1.35); }
  80%, 100% { opacity:1; filter:blur(0); transform:none; }
}
/* 7 ส.ค. 2026: ปุ่มสวิตช์ "dissolve" (.v2-brand__vanish-toggle/.v2-brand__vanish-knob) ผู้ใช้สั่งเอาออก —
   ลบ CSS ของปุ่มทิ้ง (ui-v2.js ก็เลิกสร้าง element นี้แล้ว) กลไก is-vanished/is-auto ด้านบนยังอยู่ ไม่มี UI
   trigger ให้กดแล้วเฉยๆ */
@media (prefers-reduced-motion: reduce) {
  .v2-brand__vanishword i { transition: opacity .4s ease; transition-delay:0s; }
  .v2-brand__vanish-wrap.is-vanished .v2-brand__vanishword i { transform:none; filter:none; }
  .v2-brand__vanish-wrap.is-auto .v2-brand__vanishword i { animation:none; }
}
/* 3 ส.ค. 2026: กลับไปใช้ margin-top:1px + line-height:1.2 เหมือน**ก่อน**แก้บั๊กไม้เอก (§4.16) เป๊ะๆ ตามที่ผู้ใช้
   ยืนยันว่าต้องชิดแบบเดิม ไม่ใช่ห่างขึ้น — ค่า 4px/8px ที่เคยลองก่อนหน้านี้ผิดทิศทาง ถอนออกทั้งหมด */
[data-v2-theme="ios-liquid-glass"] .v2-brand__sub { font-size: var(--v2-subtext-size); color: var(--v2-text-dim); line-height:1.2; margin-top:-3px; white-space:nowrap; overflow:visible; }
/* 8 ส.ค. 2026: "กรมชลประทาน" ใช้เอฟเฟกต์ vanish/reveal ต่อกลุ่มอักขระเดียวกับป้าย KPI (splitKpiText ใน ui-v2.js
   เรียกตรงกับ .v2-brand__sub) — เอา overflow:hidden/text-overflow:ellipsis เดิมออก (ตัดขอบตัวอักษรที่กระเจิงออก
   นอกกรอบระหว่างอนิเมชัน เหมือนบั๊กเดิมของ .v2-brand__title ดู §5.35) ข้อความนี้สั้นคงที่ ไม่ต้อง ellipsis อยู่แล้ว
   scope ด้วย .v2-brand__sub เอง (ไม่ใช้ .v2-kpi-scene--vanish-once เพราะไม่ได้อยู่ในโครง KPI) */
.v2-brand__sub .v2-kpi-letter {
  display:inline-block; white-space:pre; transform-origin:center;
  transition: transform 1.1s cubic-bezier(.3,.6,.4,1), opacity 1s ease, filter 1s ease;
  transition-delay: calc(var(--i) * 55ms);
}
.v2-brand__sub.v2-kpi-text-reveal .v2-kpi-letter { opacity:0; filter:blur(9px); transform:translate(var(--x),var(--y)) rotate(var(--r)) scale(1.35); }
.v2-brand__sub.v2-kpi-text-reveal.is-ready .v2-kpi-letter { opacity:1; filter:blur(0); transform:none; }
@media (prefers-reduced-motion: reduce) {
  .v2-brand__sub .v2-kpi-letter { transition: opacity .4s ease; transition-delay:0s; }
  .v2-brand__sub.v2-kpi-text-reveal .v2-kpi-letter { transform:none; filter:none; }
}
@media (max-width: 480px) {
  .v2-brand { max-width: calc(100vw - 84px); gap:8px; }
  .v2-brand__logo { height:50px; width:50px; }
  [data-v2-theme="ios-liquid-glass"] .v2-brand__sub { font-size: var(--v2-subtext-size-sm); }
}

/* v2-navpill = ปุ่มนำทางหลักลอยมุมบนขวา ประยุกต์สูตรกระจกจากปุ่ม light/dark ตรงๆ (ผู้ใช้สั่งให้คุมโทนเดียวกัน) —
   PC โชว์ไอคอนลิงก์หน้าหลัก 5 อัน + ☰; หน้าปัจจุบันไม่มีกล่อง highlight แล้ว แค่ไอคอนเข้ม (var(--v2-text)) +
   ขยายโชว์ชื่อหน้ากำกับ ไอคอนอื่นสีเทาจาง; จอ ≤640px (มือถือ) ไอคอนหลักหายหมดเหลือแต่ ☰ (มติ "แบบ B") */
[data-v2-theme="ios-liquid-glass"] .v2-navpill {
  position:fixed; z-index:60; top: calc(env(safe-area-inset-top) + 12px); right: calc(12px + env(safe-area-inset-right));
  height:54px; box-sizing:border-box; padding:4px; border-radius:999px; display:flex; align-items:center; gap:2px;
  background-color: color-mix(in oklab, white 12%, transparent);
  border: 1px solid rgba(255,255,255,.35);
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  box-shadow: 0 30px 60px -30px rgba(0,0,0,.65), inset 0 1px 0 rgba(255,255,255,.5);
  transition: background-color .4s cubic-bezier(1,0,.4,1), box-shadow .4s cubic-bezier(1,0,.4,1);
}
/* 2 ก.ย. 2026 (สถาปัตยกรรมรอบที่ 5 — ก็อป codepen.io/Anna_Batura/pen/qBbaXBd ตรงๆ ไม่ปรับแต่ง): ผู้ใช้ทักว่ารอบ
   ที่ 4 (ย้ายพื้นหลังมาไว้บนปุ่มเองแต่ยังคงลูกบอลลอยเฉพาะเฟสเลื่อน + ลด duration — ดู liquid-glass-v2.md
   §5.89-§5.90) "ยังไม่ลื่นเหมือนต้นฉบับ" — เปิดโค้ดจริงต้นฉบับมาดูอีกรอบ (ผ่าน iframe จริง) พบว่าไม่มี JS เลย ใช้
   `.menu_label { transition: 0.3s; }` transition เดียวครอบ width+background-color พร้อมกันทีเดียว ไม่มีลูกบอล
   ลอยเลื่อนข้ามปุ่มเลย (ความรู้สึก "เลื่อน" เป็นผลข้างเคียงของ flexbox reflow ล้วนๆ)
   ผู้ใช้ยืนยันให้ตัดเอฟเฟกต์ลูกบอลทิ้ง เอาตามต้นฉบับเป๊ะ — ลบ `.v2-navpill__activepill` (element+CSS) ทิ้งทั้งหมด
   (ดู wireNavPillActive ใน ui-v2.js) เหลือ transition เดียวบน `.v2-navpill__link` ครอบทั้งรูปทรง+สี+พื้นหลัง+ป้าย
   พร้อมกันทีเดียว ไม่มีการหยุดรอเป็นเฟสๆ อีกต่อไป (`.is-label-out`/`.is-wide`/`.is-tinted` เดิมที่เคยแยกจังหวะ
   สำหรับ JS คุมทีละเฟส ตอนนี้เหลือแค่ `.is-active` ตัวเดียวพอ — ลบคลาสที่ไม่ได้ใช้แล้วทิ้ง)
   border-radius คง 999px (จากรอบที่ 4 — เบราว์เซอร์ cap ที่ min(width,height)/2 อัตโนมัติ ได้ทรงแคปซูล/สเตเดียม
   ถูกต้องตอนกว้างโดยไม่ต้องพึ่ง 50% ที่จะกลายเป็นวงรีผิดรูปเมื่อ width≠height) */
[data-v2-theme="ios-liquid-glass"] .v2-navpill__link {
  position:relative; z-index:1; height:46px; width:auto; max-width:46px; padding:0 13.5px; border-radius:999px;
  display:flex; align-items:center; justify-content:flex-start; gap:0; overflow:hidden;
  color: var(--v2-text); text-decoration:none; background-color:transparent; box-shadow:none;
  transition: max-width .3s ease, padding .3s ease, gap .3s ease, color .2s ease, background-color .2s ease, box-shadow .2s ease, transform .15s cubic-bezier(.3,1,.4,1);
  /* 2 ก.ย. 2026: ผู้ใช้ทัก "ตอนขยายปกติ ตอนหดกลายเป็นวงกลมทันที" — ต้นตอ: base rule เดิมใช้ `width:46px`
     (ค่าตายตัว) ต่างจาก `.is-wide` ที่ใช้ `width:auto` — `width` ไม่ได้อยู่ใน transition list เลย (มีแค่
     max-width) เพราะงั้นตอนถอด `.is-wide` ค่า `width` จะ**สแนปจาก auto→46px ทันที**ไม่รอ max-width ที่กำลัง
     หดอยู่แบบค่อยเป็นค่อยไปเลย (46px เป็นค่าที่แคบกว่า max-width ปัจจุบันเสมอ บังคับกล่องเล็กทันที) ตอน "ขยาย"
     ไม่เป็นแบบนี้เพราะ `width:auto` ไม่เคยเปลี่ยนค่าเลยทั้งสองสถานะ (ปัญหาซ้ำกับที่เจอใน §5.91 แต่กลับทิศ — คราวนี้
     ฝั่ง "หด" แทนที่จะเป็นฝั่ง "ขยาย") — แก้โดยให้ `width:auto` เหมือนกันทั้งสองสถานะ (ไม่เปลี่ยนค่าเลยไม่ว่าสลับ
     .is-wide ทิศไหน) เหลือแค่ `max-width` (46↔190px ตัวเลขคงที่ทั้งคู่) เป็นตัวควบคุมขนาดที่มองเห็นแต่ผู้เดียว
     ทั้งสองทิศทางเท่ากัน */
  /* 2 ก.ย. 2026 (สถาปัตยกรรมรอบที่ 6): ผู้ใช้ขอเพิ่มรายละเอียดกลับเข้าไป — "ย่อปุ่มดำเหลือแค่ไอคอนแล้วหายไปก่อน
     แล้วปรากฏที่ปุ่มต่อไปแล้วขยาย" (เหมือน reveal animation ที่ทำอยู่แล้ว) แทนที่จะสลับพร้อมกันทีเดียวแบบ codepen
     เป๊ะๆ (รอบที่ 5, §5.91) — ต้องแยก "รูปทรง" (`.is-wide`: max-width/padding/gap) กับ "สี/พื้นหลัง" (`.is-tinted`:
     color/background-color/box-shadow) กลับเป็นคนละคลาสอีกครั้ง (คล้าย §5.86 เดิม) เพื่อให้ JS (wireNavPillActive)
     สั่งเป็นจังหวะได้: ปุ่มเก่าหดก่อน (ถอด is-wide) → ค่อยจางหาย (ถอด is-tinted) → ปุ่มใหม่ปรากฏเป็นวงกลมดำ (เติม
     is-tinted) → ค่อยขยาย (เติม is-wide) — .is-active เป็น alias คุมทั้งคู่พร้อมกัน (ให้ปุ่ม "หน้าปัจจุบัน" นิ่งๆ
     ตอนโหลดหน้าเหมือนเดิมทุกจุด ไม่กระทบ markup เดิมที่มีแค่ is-active) — ต่างจาก §5.86 ตรงที่**ไม่มีแคปซูลลอย
     ให้ต้อง sync ขนาดกับปุ่มอีกแล้ว** (โครงสร้างรอบที่ 5 กำจัดปัญหานั้นไปแล้วถาวร) จึงปลอดภัยที่จะแยกจังหวะได้โดย
     ไม่กลับไปเจอบั๊กเดิม — แยก transition duration ตามจังหวะจริงด้วย: รูปทรง .3s (T_SHAPE), สี .2s (T_TINT) ต้อง
     ตรงกับ T_SHAPE/T_TINT ใน wireNavPillActive (ui-v2.js) เป๊ะเสมอ แก้ที่นี่ต้องแก้ที่นั่นด้วย */
}
/* :active ให้ความรู้สึก "กดปุ่ม" แทนวงกลมเทาที่ตัดออกจากจอสัมผัสแล้ว (ดู wireNavPillHover ใน ui-v2.js) */
[data-v2-theme="ios-liquid-glass"] .v2-navpill__link:active { transform: scale(.9); }
[data-v2-theme="ios-liquid-glass"] .v2-navpill__link svg { width:19px; height:19px; flex-shrink:0; }
[data-v2-theme="ios-liquid-glass"] .v2-navpill__link .lbl {
  display:inline-block; font-size:.8125rem; font-weight:600; white-space:nowrap; opacity:0; max-width:0;
  transition: opacity .15s ease, max-width .3s ease;
  /* 2 ก.ย. 2026: ผู้ใช้ทัก "หลังกดปุ่มจะกลายเป็นไอคอนไม่ควรมีตัวหนังสือ" — เดิม opacity ของป้ายใช้ transition
     เดียวกับรูปทรงทั้งไปและกลับ ทำให้ตอน "หด" ป้ายยังจางอยู่ตลอดครึ่งแรกของการหด เห็นเป็นตัวอักษรเบลอๆ ค้างอยู่
     ในวงกลมที่กำลังเล็กลง — แยก opacity เป็นคนละความเร็วกันสองทิศทาง: **ทิศ "หด" (base rule นี้) เร็ว .15s**
     ป้ายหายเกือบทันทีตั้งแต่เริ่มกด ก่อนรูปทรงจะเริ่มหดเห็นชัดด้วยซ้ำ — ทิศ "ขยาย" (.is-wide .lbl ด้านล่าง) ยังคง
     ตรงกับรูปทรง .3s (จางเข้าไปพร้อมรูปทรงขยาย) — CSS ใช้ transition ของ**ปลายทางหลังเปลี่ยน class เสมอ**
     จึงประกาศคนละค่าที่ base/.is-wide ได้ */
  will-change: opacity;
}
[data-v2-theme="ios-liquid-glass"] .v2-navpill__link.is-wide { gap:8px; width:auto; max-width:190px; padding:0 16px 0 13px; }
[data-v2-theme="ios-liquid-glass"] .v2-navpill__link.is-wide .lbl { opacity:1; max-width:120px; transition: opacity .3s ease, max-width .3s ease; }
/* 10 ส.ค. 2026: light = วงกลมดำ/ไอคอนขาว, dark = วงกลมขาว/ไอคอนดำ (เด่นทั้งคู่ทาง) — สีไอคอนต้องสลับตรงข้ามกับ
   พื้นหลังวงกลมเสมอ ไม่ใช่ผูกกับ var(--v2-text)
   2 ก.ย. 2026: `.is-active` ตัดออกจากทั้งสองกฎนี้แล้ว (เดิมเป็น alias คู่กับ is-wide/is-tinted) — ตอนนี้
   `.is-active` เป็นแค่ bookkeeping marker ล้วนๆ ไม่มีผลภาพเลย เพราะถ้ายังทำ CSS ผิดคู่กับ is-wide/is-tinted
   จะชนกัน: JS ต้องคง is-active ติดกับปุ่มระหว่างเฟส 1 (หด) เพื่อบอกว่า "ยังเป็นปุ่มเดิม" แต่ถ้า is-active ยังกระตุ้น
   max-width:190 อยู่ด้วย จะหักล้างการถอด is-wide ในเฟส 1 ทันที (วัดจริงเจอ: max-width ค้าง 190px ไม่ยอมหด) —
   ต้องแยก is-wide/is-tinted เป็นเจ้าของ CSS แต่ผู้เดียวเท่านั้น (ดู wireNavPillActive ที่ sync is-wide+is-tinted
   ให้ปุ่มเริ่มต้นตอนโหลดหน้าแทน เพราะ markup เดิมมีแค่ is-active) */
[data-v2-theme="ios-liquid-glass"] .v2-navpill__link.is-tinted {
  color: #fff; background-color: rgba(28,28,30,.92); box-shadow: 0 2px 8px rgba(0,0,0,.18);
}
[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-navpill__link.is-tinted {
  color: #1c1c1e; background-color: rgba(255,255,255,.92);
}
[data-v2-theme="ios-liquid-glass"] .v2-navpill__burger {
  position:relative; z-index:1; width:46px; height:46px; border-radius:50%; border:none; background:transparent; color: var(--v2-text); cursor:pointer;
  display:flex; align-items:center; justify-content:center; flex-shrink:0;
  transition: transform .15s cubic-bezier(.3,1,.4,1);
}
[data-v2-theme="ios-liquid-glass"] .v2-navpill__burger:active { transform: scale(.9); }
[data-v2-theme="ios-liquid-glass"] .v2-navpill__burger svg { width:18px; height:18px; }

/* v2-navpill__hoverpill = แคปซูลทึบเลื่อนตามไอคอนที่ hover/focus — ต้นแบบจาก simeydotme "Frosted Glass Gooey
   Gravity Nav" (codepen.io/simeydotme/pen/qEBmggz) เฉพาะส่วน "สถานะนิ่ง" (nav ul li:after: พื้นหลังทึบ+opacity/
   scale transition ธรรมดา) — ไม่ใช้ mix-blend-mode/metaball ที่เจอบั๊กไปแล้วรอบก่อน (ดู §5.34) จึงไม่มี ancestor
   backdrop-filter ตัดขอบเขต ปลอดภัยที่จะเป็นลูกของ .v2-navpill ตรงๆ กลไก JS: wireNavPillHover() ใน ui-v2.js
   14 ส.ค. 2026: ผู้ใช้ขอลองเทียบเวอร์ชันจาง (12%, ไม่กลับสีไอคอน) กับเวอร์ชันทึบ 90%+กลับสีไอคอน — ตอนนี้คือ
   เวอร์ชันจาง ให้ตรงกับ .v2-navdd__hoverpill */
[data-v2-theme="ios-liquid-glass"] .v2-navpill__hoverpill {
  position:absolute; left:0; top:0; width:0; height:0; z-index:0; border-radius:999px;
  background-color: color-mix(in oklab, var(--v2-text) 12%, transparent);
  opacity:0; pointer-events:none; will-change: transform;
  /* transform แทน left/top (GPU-composited ไม่ชน layout) — .v2-navpill เองมี backdrop-filter:blur() หนักอยู่แล้ว
     ถ้า animate left/top (layout ทุกเฟรม) ซ้อนทับอีกชั้นเสี่ยงตกเฟรมบนเครื่องที่ไม่แรงมาก ดูเหมือน "อนิเมชั่นไม่จบ" */
  transition: transform .32s cubic-bezier(.3,1,.4,1), width .32s cubic-bezier(.3,1,.4,1), height .32s cubic-bezier(.3,1,.4,1), opacity .2s;
}
[data-v2-theme="ios-liquid-glass"] .v2-navpill__hoverpill.is-visible { opacity:1; }

/* 2 ก.ย. 2026: .v2-navpill__activepill (ลูกบอลลอยเลื่อนข้ามปุ่ม, ต้นแบบเดิม codepen Italo-Almeida) ถูกลบทิ้ง
   ทั้งหมดแล้ว — สถาปัตยกรรมรอบที่ 5 เอาตาม codepen.io/Anna_Batura/pen/qBbaXBd เป๊ะ: พื้นหลัง/สี "หน้าปัจจุบัน"
   อยู่บน .v2-navpill__link เอง (.is-active ด้านบน) ไม่มี element ลอยแยกอีกต่อไป ดู wireNavPillActive (ui-v2.js)
   + liquid-glass-v2.md §5.91 สำหรับที่มาเต็ม */
@media (prefers-reduced-motion: reduce) {
  [data-v2-theme="ios-liquid-glass"] .v2-navpill__hoverpill { transition: opacity .2s; }
}

/* dropdown ของ ☰ — เดิมเคยเป็น native <details>/<summary> ซ้อนอยู่ใน .v2-navpill แต่พบบั๊กจริง (1 ส.ค. 2026):
   .v2-navpill เองมี backdrop-filter อยู่แล้ว → backdrop-filter ของ ancestor "ตัดขอบเขต" แหล่งภาพที่ backdrop-filter
   ของลูกจะเบลอ (backdrop root) ทำให้ .v2-navdd__panel เบลอแค่พื้นผิวโปร่งใสของ .v2-navpill เอง ไม่ใช่ภาพจริงที่อยู่
   หลังทั้งหน้า (สังเกตได้เมื่อสลับพื้นหลังเป็นภาพถ่ายจริงแล้วไม่เบลอเลย ทั้งที่ getComputedStyle รายงานค่าถูกต้อง —
   ปิด backdrop-filter ของ .v2-navpill ชั่วคราวแล้ว panel เบลอถูกทันที ยืนยัน root cause) — ดู liquid-glass-v2.md §3.9
   แก้โดยย้าย .v2-navdd__panel ออกมาเป็นลูกตรงของ document.body (พี่น้องกับ .v2-navpill ไม่ใช่ลูก) แล้วคุม
   เปิด/ปิด/ตำแหน่งด้วย JS แทน native <details> (ui-v2.js: openNavDD/closeNavDD) */
[data-v2-theme="ios-liquid-glass"] .v2-navdd__panel {
  position:fixed; z-index:70;
  width:260px; max-height:70vh; overflow-y:auto; padding:8px; border-radius: var(--v2-r-panel);
  transform-origin: top right;
  display:none;
  /* 25 ส.ค. 2026: ผู้ใช้ขอซ่อน scroll bar (ยังเลื่อนได้ปกติ แค่ไม่โชว์แถบ) */
  scrollbar-width: none; -ms-overflow-style: none;
  /* สูตรตรงจาก codefronts.com liquid-glass-card-with-hover-refraction — เรียบง่ายกว่า box-shadow หลายชั้นที่
     ประดิษฐ์เอง และผู้ใช้ยืนยันแล้วว่าเบราว์เซอร์เดียวกันเรนเดอร์ได้ถูกต้อง ไม่มีขอบ/เงาล้นแบบที่เจอตอนขยายเอง 10 เท่า */
  background-color: color-mix(in oklab, white 12%, transparent);
  border: 1px solid rgba(255,255,255,.35);
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  box-shadow: 0 30px 60px -30px rgba(0,0,0,.65), inset 0 1px 0 rgba(255,255,255,.5);
}
[data-v2-theme="ios-liquid-glass"] .v2-navdd__panel::-webkit-scrollbar { display: none; }
[data-v2-theme="ios-liquid-glass"] .v2-navdd__panel.is-open { display:block; animation: v2-navdd-drop .4s cubic-bezier(.3,1.3,.5,1); }
@supports not (backdrop-filter: blur(1px)) {
  [data-v2-theme="ios-liquid-glass"] .v2-navpill, [data-v2-theme="ios-liquid-glass"] .v2-navback, [data-v2-theme="ios-liquid-glass"] .v2-navdd__panel { background: var(--v2-surface-solid); }
}
/* กลุ่มเมนูใน dropdown = accordion กด-ขยายเอง (native details/summary, grid-template-rows 0fr→1fr เหมือน
   .v2-accordion ด้านล่าง) — เจตนา "ไม่มี backdrop-filter ของตัวเอง" เพราะ .v2-navdd__panel เป็นกระจกอยู่แล้ว
   ซ้อนกระจกอีกชั้นจะเจอบั๊ก backdrop-filter ของ ancestor ตัดขอบเขตกันเอง (ดู liquid-glass-v2.md §3.9) */
[data-v2-theme="ios-liquid-glass"] .v2-navdd__acc-q {
  list-style:none; cursor:pointer; display:flex; align-items:center; justify-content:space-between; gap:8px;
  padding:10px 12px 4px; font-size:.6875rem; font-weight:700; text-transform:uppercase; letter-spacing:.05em; color: var(--v2-text-mute);
}
[data-v2-theme="ios-liquid-glass"] .v2-navdd__acc-q::-webkit-details-marker { display:none; }
[data-v2-theme="ios-liquid-glass"] .v2-navdd__acc-mk { position:relative; flex:none; width:12px; height:12px; }
[data-v2-theme="ios-liquid-glass"] .v2-navdd__acc-mk::before, [data-v2-theme="ios-liquid-glass"] .v2-navdd__acc-mk::after {
  content:''; position:absolute; top:50%; left:50%; width:9px; height:1.5px; background: var(--v2-text-mute);
  transform: translate(-50%,-50%); transition: transform .3s;
}
[data-v2-theme="ios-liquid-glass"] .v2-navdd__acc-mk::after { transform: translate(-50%,-50%) rotate(90deg); }
[data-v2-theme="ios-liquid-glass"] .v2-navdd__acc[open] .v2-navdd__acc-mk::after { transform: translate(-50%,-50%) rotate(0); }
.v2-navdd__acc-wrap { display:grid; grid-template-rows:0fr; transition: grid-template-rows .3s cubic-bezier(.4,0,.2,1); }
.v2-navdd__acc[open] .v2-navdd__acc-wrap { grid-template-rows:1fr; }
.v2-navdd__acc-body { overflow:hidden; }
@media (prefers-reduced-motion: reduce) {
  .v2-navdd__acc-wrap { transition:none; }
  [data-v2-theme="ios-liquid-glass"] .v2-navdd__acc-mk::before, [data-v2-theme="ios-liquid-glass"] .v2-navdd__acc-mk::after { transition:none; }
}
/* 25 ส.ค. 2026: ผู้ใช้ทักว่าความโค้งของ "สีขาว" (hoverpill/is-active) กับกรอบ .v2-navdd__panel ไม่เท่ากัน — panel
   ใช้ var(--v2-r-panel)=28px, item เดิม 10px ต่างกันมาก ทำให้มุมของแถบสีขาว (โดยเฉพาะรายการแรก/สุดท้ายที่ชิดขอบ
   panel) ดูเหลี่ยมกว่าโค้งของกรอบนอกอย่างเห็นได้ชัด — ใช้สูตร concentric corner (โค้งนอก − padding = โค้งใน):
   panel padding:8px → 28−8=20px ให้แถบสีขาวโค้งเข้าชุดกับกรอบ panel จริง */
[data-v2-theme="ios-liquid-glass"] .v2-navdd__item { position:relative; z-index:1; display:flex; align-items:center; gap:12px; padding:10px 12px; border-radius:20px; text-decoration:none; color: var(--v2-text); font-size:.9375rem; font-weight:700; transition: color .22s, background-color .22s; }
/* 3 ก.ย. 2026: ผู้ใช้ระบุจังหวะเวลาที่ต้องการเป๊ะ — สีเปลี่ยนจบที่ 220ms, เผื่อ buffer 30ms, ค่อย navigate ที่
   250ms รวม — ต้องตรงกับ T_COLOR/T_BUF ใน wireNavDDActive (ui-v2.js) เป๊ะเสมอ แก้ที่นี่ต้องแก้ที่นั่นด้วย */
[data-v2-theme="ios-liquid-glass"] .v2-navdd__item svg { width:18px; height:18px; flex-shrink:0; color: var(--v2-text); transition: color .2s; }
/* 10 ส.ค. 2026: ผู้ใช้ทักว่า is-active เดิม (แค่เปลี่ยนสีตัวอักษรเป็น accent) ดูเหมือน hover ไม่ใช่สถานะ "หน้า
   ปัจจุบัน" — ขอให้เป็นแคปซูลทึบดำ/ขาวสลับตามธีมแบบเดียวกับ .v2-navpill__activepill ในแถบหลัก (ดำ+ไอคอนขาวใน
   light, ขาว+ไอคอนดำใน dark) ไม่ใช่แค่สีตัวหนังสือ */
[data-v2-theme="ios-liquid-glass"] .v2-navdd__item.is-active { background-color: rgba(28,28,30,.92); color:#fff; }
[data-v2-theme="ios-liquid-glass"] .v2-navdd__item.is-active svg { color:#fff; }
[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-navdd__item.is-active { background-color: rgba(255,255,255,.92); color:#1c1c1e; }
[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-navdd__item.is-active svg { color:#1c1c1e; }

/* v2-navdd__hoverpill = แคปซูลเลื่อนตามรายการ dropdown ที่ hover/focus — กลไกเดียวกับ .v2-navpill__hoverpill
   (พื้นหลังทึบธรรมดา ไม่มี mix-blend-mode ดู §5.34) แค่เลื่อนแนวตั้งแทนแนวนอน
   14 ส.ค. 2026: ผู้ใช้ขอลองเทียบเวอร์ชันจาง (12%, ไม่กลับสีตัวอักษร) กับเวอร์ชันทึบ 90%+กลับสีตัวอักษร — ตอนนี้คือ
   เวอร์ชันจาง ให้ตรงกับ .v2-navpill__hoverpill */
[data-v2-theme="ios-liquid-glass"] .v2-navdd__hoverpill {
  position:absolute; left:8px; top:0; width:0; height:0; z-index:0; border-radius:20px;
  background-color: color-mix(in oklab, var(--v2-text) 12%, transparent);
  opacity:0; pointer-events:none;
  transition: top .26s cubic-bezier(.3,1,.4,1), height .26s cubic-bezier(.3,1,.4,1), width .26s cubic-bezier(.3,1,.4,1), opacity .18s;
}
[data-v2-theme="ios-liquid-glass"] .v2-navdd__hoverpill.is-visible { opacity:1; }

/* 3 ก.ย. 2026: v2-navdd__activepill = แคปซูลทึบ "หน้าปัจจุบัน" ของรายการ ☰ dropdown — ผู้ใช้ทัก มือถือส่วนใหญ่
   นำทางผ่านตรงนี้ (ดู liquid-glass-v2.md §5.96 ที่ทำแถวไอคอนหลักไปก่อนแล้วพบว่าไม่ engage บนมือถือจริง) แค่
   "เลื่อน" ระหว่างสองตำแหน่งครั้งเดียวก็พอ ไม่ต้องมี is-wide/is-tinted แยกแบบแถวไอคอนหลัก เพราะ dropdown item
   ไม่เปลี่ยนขนาด/รูปทรงเลย (สีเดียวกับ .v2-navdd__item.is-active ด้านบนเป๊ะ ย้ายมาไว้ที่แคปซูลแทนตอนกำลังเลื่อน) */
[data-v2-theme="ios-liquid-glass"] .v2-navdd__activepill {
  position:absolute; left:8px; top:0; width:0; height:0; z-index:0; border-radius:20px;
  background-color: rgba(28,28,30,.92);
  opacity:0; pointer-events:none;
  transition: top .3s cubic-bezier(.3,1,.4,1), height .3s cubic-bezier(.3,1,.4,1), width .3s cubic-bezier(.3,1,.4,1), opacity .2s;
}
[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-navdd__activepill { background-color: rgba(255,255,255,.92); }
[data-v2-theme="ios-liquid-glass"] .v2-navdd__activepill.is-visible { opacity:1; }
@media (prefers-reduced-motion: reduce) {
  [data-v2-theme="ios-liquid-glass"] .v2-navdd__hoverpill { transition: opacity .18s; }
  [data-v2-theme="ios-liquid-glass"] .v2-navdd__activepill { transition: opacity .2s; }
}
[data-v2-theme="ios-liquid-glass"] .v2-navdd__sep { border:none; border-top:.5px solid var(--v2-hair); margin:6px 8px; }
/* จอใหญ่ปกติไอคอนหลักอยู่ใน pill อยู่แล้ว ไม่ต้องซ้ำใน dropdown — แต่ "ใหญ่แค่ไหนถึงพอ" ไม่ใช่ breakpoint ตายตัว
   อีกต่อไป (เดิม max-width:640px ตัดหมดทีเดียวทั้ง 5 อัน ทั้งที่บางจอ ≤640px ยังมีที่ว่างพอใส่ได้ 2-3 อัน —
   ผู้ใช้ทักเป็นบั๊ก "พื้นที่ยังเหลือ") ตอนนี้ wireNavPillOverflow() ใน ui-v2.js วัดพื้นที่จริงระหว่างขอบ brand/
   navback กับ pill แล้วซ่อนทีละไอคอนจากท้ายแถวเมื่อไม่พอ ไม่ใช่ all-or-nothing — ไอคอนที่ถูกซ่อนไปโผล่ในนี้แทน
   (JS สลับ style.display ของ .v2-navdd__primary + ลูก a ตรงๆ ไม่ใช้ media query แล้ว) */
[data-v2-theme="ios-liquid-glass"] .v2-navdd__primary { display:none; }
@keyframes v2-navdd-drop {
  0% { opacity:0; transform: translateY(-8px) scaleY(.7); }
  60% { opacity:1; transform: translateY(2px) scaleY(1.04); }
  100% { opacity:1; transform: translateY(0) scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
  [data-v2-theme="ios-liquid-glass"] .v2-navdd__panel.is-open { animation:none; }
}

/* v2-ios-switcher = ปุ่มลอย (FAB) light/dark มุมล่างขวา — พอร์ตจากโค้ดอ้างอิง (fieldset+radio+:has())
   ใช้ backdrop-filter: blur() url(#v2-ios-glass) saturate() จริง (SVG feDisplacementMap หักเหแสงขอบแคปซูล)
   ไม่ใช่แค่ blur+saturate เฉยๆ แบบที่การ์ด/nav อื่นในธีมนี้ใช้
   ย่อสัดส่วนเดิม 244×70 ลงเหลือ 188×54 (s = 54/70 = .7714) ให้สูงเท่า navbar มาตรฐาน (54px) — คงอัตราส่วน
   เดิมทุกมิติ (safe เพราะ feDisplacementMap ใช้ objectBoundingBox = สัดส่วนสัมพัทธ์ ไม่ใช่ pixel ตายตัว)
   position:fixed มุมล่างขวาเหมือนเดิม (ไม่ชนหัว navbar บนมือถือแบบตอนวางบนขวา) — เดิมโชว์ตลอดแล้วหายตอนเลื่อน
   เนื้อหาลง (.is-scrolled ฟัง scroll ของ .v2-content) แต่ผู้ใช้ขอเปลี่ยนเป็น "ซ่อนไว้ก่อนเสมอ โผล่เฉพาะตอนกด
   เมนู ☰" (19 ส.ค. 2026) ผูกกับ openDD/closeDD ใน ui-v2.js ตรงๆ แทน scroll listener เดิม — ใช้แอนิเมชันหยด
   ลงตัวเดียวกับ .v2-navdd__panel.is-open (v2-navdd-drop) ให้เข้าชุดกับเมนูที่โผล่พร้อมกัน */
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher {
  position:fixed; z-index:60; bottom: calc(20px + env(safe-area-inset-bottom)); right: calc(16px + env(safe-area-inset-right));
  display:none; align-items:center; gap:6px; width:188px; height:54px; box-sizing:border-box;
  padding:6px 9px 8px; margin:0; border:none; border-radius:99em;
  background-color: color-mix(in srgb, var(--v2-glass-tint) 12%, transparent);
  backdrop-filter: blur(6px) saturate(var(--v2-glass-saturation));
  -webkit-backdrop-filter: blur(6px) saturate(var(--v2-glass-saturation));
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, #fff calc(var(--v2-glass-reflex-light) * 10%), transparent),
    inset 1.4px 2.3px 0px -1.5px color-mix(in srgb, #fff calc(var(--v2-glass-reflex-light) * 90%), transparent),
    inset -1.5px -1.5px 0px -1.5px color-mix(in srgb, #fff calc(var(--v2-glass-reflex-light) * 80%), transparent),
    inset -2.3px -6.2px 0.8px -4.6px color-mix(in srgb, #fff calc(var(--v2-glass-reflex-light) * 60%), transparent),
    inset -0.2px -0.8px 3px 0px color-mix(in srgb, #000 calc(var(--v2-glass-reflex-dark) * 12%), transparent),
    inset -1.2px 1.9px 0px -1.5px color-mix(in srgb, #000 calc(var(--v2-glass-reflex-dark) * 20%), transparent),
    inset 0px 2.3px 3px -1.5px color-mix(in srgb, #000 calc(var(--v2-glass-reflex-dark) * 20%), transparent),
    inset 1.5px -5px 0.8px -3.1px color-mix(in srgb, #000 calc(var(--v2-glass-reflex-dark) * 10%), transparent),
    0px 0.8px 3.9px 0px color-mix(in srgb, #000 calc(var(--v2-glass-reflex-dark) * 10%), transparent),
    0px 4.6px 12.3px 0px color-mix(in srgb, #000 calc(var(--v2-glass-reflex-dark) * 8%), transparent);
  transition: background-color .4s cubic-bezier(1,0,.4,1), box-shadow .4s cubic-bezier(1,0,.4,1);
}
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher.is-open { display:flex; animation: v2-navdd-drop .4s cubic-bezier(.3,1.3,.5,1); }
@media (prefers-reduced-motion: reduce) {
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher.is-open { animation:none; }
}
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher__legend { position:absolute; width:1px; height:1px; margin:-1px; border:0; padding:0; white-space:nowrap; clip-path:inset(100%); overflow:hidden; }
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher__input { clip-path:inset(100%); height:1px; width:1px; overflow:hidden; position:absolute; white-space:nowrap; }
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher__filter { position:absolute; width:0; height:0; z-index:-1; }
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher__option {
  --c: var(--v2-text); position:relative; z-index:1; display:flex; justify-content:center; align-items:center;
  width:82px; height:100%; box-sizing:border-box; border-radius:99em; cursor:pointer; transition: all .16s;
}
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher__option:hover { --c: var(--v2-accent-1); }
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher__option:hover .v2-ios-switcher__icon { scale:1.2; }
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher__icon { display:block; width:18px; height:18px; transition: scale .2s cubic-bezier(.5,0,0,1); }
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher::after {
  content:''; position:absolute; left:3px; top:3px; z-index:0; display:block; width:94px; height:calc(100% - 8px);
  border-radius:99em; background-color: color-mix(in srgb, var(--v2-glass-tint) 36%, transparent);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, #fff calc(var(--v2-glass-reflex-light) * 10%), transparent),
    inset 1.5px 0.8px 0px -0.8px color-mix(in srgb, #fff calc(var(--v2-glass-reflex-light) * 90%), transparent),
    inset -1.2px -0.8px 0px -0.8px color-mix(in srgb, #fff calc(var(--v2-glass-reflex-light) * 80%), transparent),
    inset -1.5px -4.6px 0.8px -3.9px color-mix(in srgb, #fff calc(var(--v2-glass-reflex-light) * 60%), transparent),
    inset -0.8px 1.5px 2.3px -0.8px color-mix(in srgb, #000 calc(var(--v2-glass-reflex-dark) * 20%), transparent),
    inset 0px -3.1px 0.8px -1.5px color-mix(in srgb, #000 calc(var(--v2-glass-reflex-dark) * 10%), transparent),
    0px 2.3px 4.6px 0px color-mix(in srgb, #000 calc(var(--v2-glass-reflex-dark) * 8%), transparent);
}
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher:has(input[value="light"]:checked)::after {
  translate: 0 0; transform-origin: right; animation: v2SwitcherBounceLight .44s ease;
  transition: background-color .4s cubic-bezier(1,0,.4,1), box-shadow .4s cubic-bezier(1,0,.4,1), translate .4s cubic-bezier(1,0,.4,1);
}
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher:has(input[value="dark"]:checked)::after {
  translate: 88px 0; transform-origin: left; animation: v2SwitcherBounceDark .44s ease;
  transition: background-color .4s cubic-bezier(1,0,.4,1), box-shadow .4s cubic-bezier(1,0,.4,1), translate .4s cubic-bezier(1,0,.4,1);
}
/* ต้องเป็นคนละชื่อ keyframe แม้เนื้อหาเหมือนกันเป๊ะ — ไม่งั้น browser มองว่า animation ไม่เปลี่ยนค่า
   (animation-name เดิม) ตอนสลับ :has() rule เลยไม่ restart แอนิเมชัน (ผู้ใช้ตรวจพบ: "ปุ่มไม่ยืดก่อนขยับ")
   เทคนิคเดียวกับต้นฉบับที่ตั้งชื่อ scaleToggle/scaleToggle2/scaleToggle3 แยกกันทั้งที่เนื้อหาเหมือนกัน */@keyframes v2SwitcherBounceLight { 0% { scale:1 1; } 50% { scale:1.15 1; } 100% { scale:1 1; } }
@keyframes v2SwitcherBounceDark { 0% { scale:1 1; } 50% { scale:1.15 1; } 100% { scale:1 1; } }
@media (prefers-reduced-motion: reduce) {
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher::after { animation:none !important; }
}
/* v2-water-orb = ปุ่มลอย "ปุ่มน้ำ" (เฉพาะ app-v2.html ก่อน — ui-v2.js: buildWaterOrb)
   append เข้า document.body ตรงๆ เหมือน FAB อื่นทุกตัว (ไม่มี ancestor กระจกอื่นซ้อน จึงไม่ชน §3.9)
   23 ส.ค. 2026: เคยลองเอากระจก/ขอบวงแหวนออกให้เหมือนต้นฉบับ originkit.dev/sections/hero-19 เป๊ะ (ลูกบอลใส ไม่มี
   กระจก/ขอบ) → 26 ส.ค. 2026 ผู้ใช้ลองใส่กระจกพื้นหลัง+ขอบวงแหวนสีรุ้งแบบปุ่ม "ล้างทั้งหมด" (--ring) อยู่พักหนึ่ง
   (glass tint → ring เปล่า → พื้นทึบดำ → ขาว → ปรับความหนาขอบ 2→5→3px) แล้วขอย้อนกลับมาเป็นแบบเดิมทั้งหมดไปครั้งหนึ่ง
   → 26 ส.ค. 2026 (รอบ 2) ผู้ใช้ขอกลับมาใช้ขอบวงแหวนสีรุ้งอีกครั้ง คราวนี้พื้นหลังเป็นสีขาว (ไม่ใช่ดำแบบรอบแรก) —
   ใช้เทคนิค "double background-clip" เดียวกับ .v2-btn-pill--ring ทุกประการ (ไม่ใช้ mask-composite เด็ดขาด ดู
   คอมเมนต์อธิบายเหตุผลที่หัวข้อนั้น) แค่เปลี่ยนชั้นในจาก #1c1c1e เป็น #fff และไม่มี ::after bloom (ผู้ใช้ไม่ได้ขอ
   บวกกับลูกบอลมีอนุภาคเคลื่อนไหวอยู่แล้ว บวก bloom เพิ่มเสี่ยงรก) — border 3px ทำให้ canvas ต้องหักลบ border-width
   ออกจากสูตร inset ด้วย (ดู .v2-water-orb__canvas ด้านล่าง)
   26 ส.ค. 2026 (รอบ 3): ผู้ใช้ขอเลิกลากย้ายตำแหน่งทั้งหมด — fix ตำแหน่งตายตัวจุดเดียวตรงกับตำแหน่ง "dock" เดิม
   (กึ่งกลางแนวนอน + ใต้ตาราง ใกล้ขอบล่างจอ) ตลอดเวลา ไม่ว่าเปิด/ปิดแชท ต่างกันแค่ "ขนาด": ปกติย่อเหลือเท่าความสูง
   ปุ่ม "ขยายตาราง" (ตั้งค่าจาก JS ผ่าน --v2-orb-mini-scale, ดู buildWaterOrb ใน ui-v2.js + app-v2.html ที่วัดจริง)
   ขยายกลับเป็นเต็ม 72px เฉพาะตอนเปิดแชท (.is-open) — ใช้ transform:scale() ล้วน ไม่ต้องคำนวณเรขาคณิตอนุภาคใหม่เลย
   transform-origin:50% 100% (กึ่งกลางล่าง) ให้ขยาย/หดจากขอบล่างขึ้นบนเท่านั้น ขอบล่างไม่ขยับ ดูเป็นจุดเดียวที่โต/
   หดในที่ ไม่ใช่ลอยขึ้นลง — position:fixed ทุกด้าน (left/bottom) กำหนดตายตัวใน rule นี้เลย ไม่มี JS ตั้ง left/top
   อีกต่อไป (ต่างจาก 2 รอบก่อนที่ปุ่มลากได้จึงต้องให้ JS คุม left/top เอง) */
[data-v2-theme="ios-liquid-glass"] .v2-water-orb {
  position:fixed; z-index:65; width:72px; height:72px; padding:0; margin:0; box-sizing:border-box;
  /* 26 ส.ค. 2026: ผู้ใช้ขอให้ขอบล่างลูกแก้วตรงกับขอบล่างปุ่ม "ขยายตาราง" เป๊ะ — วัดระยะจริงจาก DOM
     (app-v2.html: measureMiniBottom) แล้วส่งมาเป็น --v2-orb-bottom แทนเลขคงที่เดา ๆ เดิม เหลือ 14px+safe-area
     ไว้เป็นแค่ fallback เผื่อหาปุ่มไม่เจอ (เพราะ transform-origin:50% 100% ทำให้ไม่ว่าจะ scale เท่าไหร่ ขอบล่างของ
     รูปที่เห็นจริงจะตรงกับค่า bottom นี้เป๊ะเสมอ ไม่ต้องคิดเผื่อ scale) */
  left:50%; right:auto; top:auto; bottom: var(--v2-orb-bottom, calc(14px + env(safe-area-inset-bottom)));
  cursor:pointer; border-radius:50%;
  -webkit-tap-highlight-color: transparent;
  /* 27 ส.ค. 2026: ผู้ใช้ขอเอากรอบ (ขอบวงแหวนสีรุ้ง + พื้นขาว) ออก แล้วแทนที่ด้วยลูกบอลกลุ่มจุด thinking-orbs
     ล้วน ๆ ให้เหมือนต้นฉบับ — ตัด border/background/animation:v2-ring-spin ทั้งหมด เหลือกล่องโปร่งใสสำหรับ
     วาง canvas จุด (buildDotsOrbContent) เท่านั้น (ตำแหน่ง fix + scale มินิ↔เต็ม ยังเหมือนเดิม) */
  border: none; background: transparent;
  transform-origin: 50% 100%;
  transform: translateX(-50%) scale(var(--v2-orb-mini-scale, .56));
  transition: transform 1s cubic-bezier(.4,0,.2,1);
}
@media (prefers-reduced-motion: reduce) {
  [data-v2-theme="ios-liquid-glass"] .v2-water-orb { transition:none; }
}
[data-v2-theme="ios-liquid-glass"] .v2-water-orb.is-open {
  transform: translateX(-50%) scale(1);
}
/* 26 ส.ค. 2026 (รอบ 3, หลังเพิ่มขอบวงแหวนสีรุ้ง): ผู้ใช้ทักว่า transition ที่เพิ่งยืนยันว่าใช้ได้จริงหายไป
   (กลับไปวาปทันที) — จุดที่เปลี่ยนไปตรงกับตอนที่เพิ่ม animation:v2-ring-spin ต่อเนื่องบน .v2-water-orb เอง เข้าเค้า
   บั๊กที่เคยเจอมาแล้ว (liquid-glass-v2.md §5.61): "CSS transition ที่ trigger พร้อมกับ animation ต่อเนื่องบนตัว
   เดียวกัน มีสิทธิ์ถูกเบราว์เซอร์รวบเฟรมข้าม transition ไปเลย (สแนป)" — is-docking เหลือหน้าที่เดียวตอนนี้: สั่ง
   หยุดหมุนชั่วคราว (animation-play-state:paused) ระหว่าง transition 1s (ui-v2.js orbDock ใส่/ถอดเอง) กันการ
   recalc ต่อเนื่องชนกับช่วงเริ่ม transition พอดี แล้วกลับไปหมุนต่อเองหลังถอด class */
[data-v2-theme="ios-liquid-glass"] .v2-water-orb.is-docking {
  animation-play-state: paused;
}
/* 26 ส.ค. 2026: ลองให้ลูกบอลเข้าฉากด้วย CSS class .v2-orb-reveal/.is-visible + keyframe ผูกกับ transform มาก่อน
   (2 รอบ) — debug จริงบนมือถือเจอว่า animation ไม่เล่นเลย (animationName ยังเป็น v2-ring-spin ตลอด) ต้นตอไม่ชัด
   พอจะไล่ต่อได้ปลอดภัย (อาจเป็น transition/animation ชนกันอีกแบบที่ยังไม่เจอจุด หรือ browser edge case อื่น) —
   เปลี่ยนไปใช้ WAAPI (el.animate() บน opacity ล้วนๆ ใน app-v2.html) แทนทั้งหมด ไม่แตะ transform/CSS class เลย
   กันชนกับ transition:transform ถาวร + is-open/--v2-orb-mini-scale ที่คุม transform อยู่แล้วโดยตรง — เหลือแค่
   opacity ให้ WAAPI คุมชั่วคราว ปลอดภัยกว่าเยอะ (ดูเหตุผลเต็มในคอมเมนต์ app-v2.html จุด mount orb) */
/* is-notify = แจ้งเตือน "Plan Agent ตอบแล้ว" ระหว่างที่ popup ปิดอยู่ (ปิดผ่าน V2.orbNotify — app-v2.html) —
   จุดแดงเด้ง + pulse วงแหวนซ้ำ ตรงมุมขวาบนของลูกบอล เคลียร์ทิ้งตอนกดเปิด popup
   26 ส.ค. 2026: ลองเปลี่ยนเป็นแนวคิด "Ring Pulse" (3 วงแหวน+จุดกลางเต้น เป็น element จริง) มาแล้ว 2 รอบ —
   ตรวจ live จริงเจอปัญหาหลายจุด (เห็นวงเดียว/ขยายใหญ่เกิน/ไม่ smooth) แก้บอดหลายรอบไม่ตรงจุด ผู้ใช้ขอเอาออก
   กลับมาใช้แบบเดิมที่พึ่ง ::before (จุด)/::after (วงแหวนเดียว) ล้วนๆ ไม่ต้องสร้าง element เพิ่มเลย (เรียบง่าย
   กว่า พิสูจน์แล้วว่าใช้งานได้จริงมาก่อน) */
/* 26 ส.ค. 2026 (แก้บั๊กที่ผู้ใช้ทัก "มีสีแดงอีกอันโผล่ข้างเส้นขาว"): border ขาวรอบจุด (เดิมตั้งใจทำ "ขอบตัดขาด"
   แยกจุดแดงจากพื้นหลัง) ตัดกับพื้นหลัง/อนุภาคของลูกบอลที่ไม่ใช่สีเดียวคงที่ กลายเป็นเส้นขาวลอยแปลกปลอม+ทำให้จุดแดง
   ดูเหมือนแยกเป็น 2 ชิ้น (เจอบั๊กแบบเดียวกันมาแล้วรอบ Ring Pulse ก่อนหน้า) ตัดออก + ลดขนาดลงจาก 16px เหลือ 11px
   ตามที่ผู้ใช้ขอ (จุดเล็กลง ไม่แย่งความสนใจจากลูกบอลมากเกินไป) */
[data-v2-theme="ios-liquid-glass"] .v2-water-orb.is-notify::before {
  content:''; position:absolute; top:3px; right:3px; width:11px; height:11px; border-radius:50%;
  background: var(--v2-danger); z-index:2;
  box-shadow: 0 2px 6px rgb(0 0 0 / 35%);
  animation: v2-orb-notify-pop .3s cubic-bezier(.3,1.4,.4,1);
}
/* box-shadow แผ่ขยาย+จางหาย (ผู้ใช้ขอโดยตรง — เทคนิคทั่วไป กล่องเดียวมี box-shadow ขยายรัศมี+ลด alpha ไม่มีชื่อ/
   สี/comment เฉพาะจากที่ใดเลย) ข้อดีคือวงแหวนเบลอนุ่มธรรมชาติจาก box-shadow เอง ไม่ต้องพึ่งการเปลี่ยนขนาดจริงของ
   element เลย */
[data-v2-theme="ios-liquid-glass"] .v2-water-orb.is-notify::after {
  content:''; position:absolute; top:3px; right:3px; width:11px; height:11px; border-radius:50%;
  background: var(--v2-danger); z-index:1;
  box-shadow: 0 0 0 0 color-mix(in oklab, var(--v2-danger) 40%, transparent);
  animation: v2-orb-notify-pulse 1.6s infinite;
}
@keyframes v2-orb-notify-pop { 0% { transform:scale(0); } 70% { transform:scale(1.25); } 100% { transform:scale(1); } }
@keyframes v2-orb-notify-pulse { 100% { box-shadow: 0 0 0 11px color-mix(in oklab, var(--v2-danger) 0%, transparent); } }
@media (prefers-reduced-motion: reduce) {
  [data-v2-theme="ios-liquid-glass"] .v2-water-orb.is-notify::after { animation:none; display:none; }
}
[data-v2-theme="ios-liquid-glass"] .v2-water-orb__sphere {
  position:absolute; inset:0; border-radius:50%; overflow:hidden; transform-style:preserve-3d;
}
[data-v2-theme="ios-liquid-glass"] .v2-water-orb__c {
  /* inset:.5px (ผู้ใช้ขอละเอียดกว่า 1px อีกขั้น 19 ส.ค. 2026) — เล็กกว่า sphere แค่นิดเดียวพอกัน "ขอบขาวหนา"
     จากเส้นทุกวงบรรจบขอบวงกลมเดียวกันพอดี แต่ห้ามเผื่อเยอะแบบ 3px ที่เคยลอง (กลายเป็นแถบกระจกใสว่างเปล่าหนา
     กลายเป็น "ขอบ" อีกแบบที่ผู้ใช้ยังเห็นว่าใหญ่อยู่) — ต้องให้ลายเส้นชิดขอบจริงของปุ่มเกือบสุด ไม่เหลือช่องว่าง
     กระจกเปล่าก่อนถึงวงแหวนสีรุ้ง */
  position:absolute; inset:.5px; border-radius:50%; transform-style:preserve-3d; backface-visibility:hidden;
  background:transparent; border:1px solid transparent; box-sizing:border-box;
}
/* v2-water-orb__canvas = สไตล์ 'particle' (ค่าเริ่มต้นใหม่ 23 ส.ค. 2026) — canvas 2D วาดจุดเองด้วย JS
   (buildParticleContent ใน ui-v2.js) width/height attribute ตั้งเป็น px*DPR ไว้แล้วในโค้ด (CANVAS = SIZE*2.7 ≈
   194px ตอนนี้ SIZE=72 — ต้องตรงกับค่าคงที่ CANVAS ใน buildParticleContent เป๊ะ ไม่งั้นภาพเบี้ยว) บังคับขนาด
   แสดงผลจริงด้วย CSS ทับกลับมาเป็น px นี้เสมอไม่ให้ยึดตาม attribute (มิฉะนั้นจะโตตาม DPR) — ไม่มี border-radius/
   overflow ตัดขอบอีกต่อไป (ของเดิมตัดขอบพอดีตัวปุ่ม ทำให้อนุภาคที่กระเจิงออกจาก cursor-repulsion/click โดนตัดคา
   ไม่เห็นบินหลุดออกไปนอกลูกแก้วเลย — ผู้ใช้ทักหลังทดสอบจริง) canvas ใหญ่กว่าปุ่ม วางกึ่งกลางทับ + pointer-events:
   26 ส.ค. 2026 (รอบ 2): กลับมามีขอบวงแหวน 3px แล้ว (.v2-water-orb ด้านบน) — inset ของ absolute-positioned child
   อ้างอิงจาก padding-edge ของ parent (ไม่รวม border) ถ้ายังใช้สูตรเปล่า -(CANVAS-SIZE)/2 เฉยๆ (ไม่หักลบ border)
   ศูนย์กลาง canvas จะเยื้องจากศูนย์กลางวงแหวนจริงไปทาง bottom-right เท่ากับ border-width ทุกด้าน (ผู้ใช้เจอจริง:
   "ฉากหลังลูกแก้วไม่อยู่ตรงกลางกับขอบและพื้นสีขาว") ต้องลบ border-width (3px) ออกจากค่า inset เดิมด้วยเสมอ:
   none กันพื้นที่ที่ล้นออกไปนอกปุ่มไปขยาย hit-area ของการลาก/คลิกโดยไม่ตั้งใจ
   26 ส.ค. 2026: เคยมีช่วงที่ต้องหักลบ border-width ออกจากสูตรนี้ (ตอน .v2-water-orb มีขอบวงแหวน --ring)
   ตอนนี้ย้อนกลับไม่มี border แล้ว (border:none) สูตรเลยกลับมาเป็นแบบง่ายเดิม: inset = -(CANVAS-SIZE)/2 ตรงๆ */
[data-v2-theme="ios-liquid-glass"] .v2-water-orb__canvas {
  position:absolute; inset:-64px; width:194px; height:194px; display:block; pointer-events:none;
}
/* v2-water-orb__dots = สไตล์ 'dots' (27 ส.ค. 2026, buildDotsOrbContent ใน ui-v2.js) — canvas อยู่ในวงกลมพอดี
   (ไม่ล้นออกนอกปุ่มแบบ 'particle' เพราะจุดอยู่ในวงแหวนไม่กระเจิง) canvas.width/height ตั้งเป็น SIZE*DPR ในโค้ด
   แล้วบังคับแสดง 100% ทับตัวปุ่ม (พื้นที่ padding-box ในขอบวงแหวน 3px) */
[data-v2-theme="ios-liquid-glass"] .v2-water-orb__dots {
  position:absolute; inset:0; width:100%; height:100%; display:block; pointer-events:none; border-radius:50%;
}
/* v2-water-orb__swirl = วงหางดาววนรอบลูกบอล (31 ส.ค. 2026, buildOrbSwirl ใน ui-v2.js — เปิดด้วย opts.swirl)
   canvas ใหญ่กว่าปุ่ม (BOX=194 vs SIZE=72) วางกึ่งกลางทับ ให้หางวนอยู่รอบนอกตัวลูกบอล — inset = -(194-72)/2
   = -61px (สูตรเดียวกับ .v2-water-orb__canvas แต่ที่นั่นเป็น -64 เพราะยุคนั้นปุ่มมีขอบวงแหวน 3px ต้องหักเพิ่ม
   ⚠ ถ้าวันหน้าใส่ border กลับมาที่ .v2-water-orb ต้องหักลบ border-width ตรงนี้ด้วย ไม่งั้นวงจะเยื้อง)
   31 ส.ค. 2026: ตัดขอบเป็น "วงกลม" (border-radius:50%) — ไม่ใช่การตกแต่ง มีเหตุผล 2 ข้อ:
   1. ผู้ใช้รายงานเห็น "สี่เหลี่ยมจาง ๆ" รอบลูกบอลในโหมดมืด — canvas นี้เป็นเลเยอร์สี่เหลี่ยมที่วาดใหม่ทุกเฟรม
      และวางทับพื้นผิวกระจก (.app-tablewrap/.app-bottombar ที่มี backdrop-filter) พอดี ซึ่งเป็นสูตรของ artifact
      ที่บันทึกไว้แล้วใน liquid-glass-v2.md §5.60/§5.61 — ตัดขอบเป็นวงกลมทำให้ไม่มีรูปทรงสี่เหลี่ยมให้โผล่อีก
   2. ตอน burst ดาวพุ่งออกไกลถึงรัศมี ~111px ซึ่งเกินครึ่งความกว้าง canvas (97px) — ขอบสี่เหลี่ยมทำให้หาง
      "ไปได้ไกลกว่าตรงมุมทแยง" ดูไม่สมมาตร ตัดเป็นวงกลมแล้วจางหายเท่ากันทุกทิศ
   ⚠ ปลอดภัยกับวงปกติแน่นอน: วัดจริงแล้วหางตอนพักไปไกลสุดแค่รัศมี 88px < 97px จึงไม่โดนตัดเลย */
[data-v2-theme="ios-liquid-glass"] .v2-water-orb__swirl {
  position:absolute; inset:-61px; width:194px; height:194px; display:block; pointer-events:none;
  border-radius:50%;
}
/* v2-water-orb__phaser = สไตล์ 'phaser' (26 ส.ค. 2026, buildPhaserOrbContent ใน ui-v2.js) — กล่องห่อ
   <canvas> ที่ Phaser.Game สร้างเอง (parent:wrap) ไม่ใช่ canvas ที่เราคุม attribute ตรงๆ แบบ 'particle' —
   ขนาด/ตำแหน่งคำนวณสูตรเดียวกับ .v2-water-orb__canvas เป๊ะ (CANVAS = SIZE*2.2 ≈ 158px ที่ SIZE=72,
   inset = -(CANVAS-SIZE)/2 = -43px) */
[data-v2-theme="ios-liquid-glass"] .v2-water-orb__phaser {
  position:absolute; inset:-43px; width:158px; height:158px; display:block; pointer-events:none;
}
[data-v2-theme="ios-liquid-glass"] .v2-water-orb__phaser canvas { display:block; width:100%; height:100%; }
/* v2-water-orb__ribboncanvas = สไตล์ 'ribbons' (26 ส.ค. 2026, buildRibbonContent ใน ui-v2.js — ตีความ "Siri
   Orb" ใหม่ด้วย canvas 2D) — class แยกจาก .v2-water-orb__canvas ของ 'particle' เพราะ CANVAS เล็กกว่า
   (SIZE*1.6 ≈ 115px แทน SIZE*2.7 ≈ 194px — ไม่มี click-scatter ให้อนุภาคกระเด็นออกนอกกรอบ เลยไม่ต้องเผื่อ
   overflow เยอะเท่า 'particle') inset = -(CANVAS-SIZE)/2 = -21.5px */
[data-v2-theme="ios-liquid-glass"] .v2-water-orb__ribboncanvas {
  position:absolute; inset:-21.5px; width:115px; height:115px; display:block; pointer-events:none;
}
/* KPI = การ์ดสรุปแบน เรียบ ไม่มีวงกลม/แคปซูล/เส้นเชื่อม — วางตัวเลขตรงไปตรงมาแบบ Widget
   ขนาด+สี ลอกมาจาก .sum-card/.sum-label/.sum-value/.sum-unit ต้นฉบับ app.html ตรงๆ (3 breakpoint มือถือ/
   ไอแพด/PC เดียวกับต้นฉบับ) ตามที่ผู้ใช้สั่ง (3 ส.ค. 2026) — พื้นหลัง/shadow ยังอิง token ธีม (--v2-surface-solid/
   --v2-shadow-1) ให้ตอบสนอง dark mode ได้ ส่วนสีข้อความ/กรอบต่อการ์ดใช้สี Tailwind ต้นฉบับตรงๆ ผ่าน
   .v2-kpi--indigo/--blue/--emerald (ดู §4.14) */
[data-v2-theme="ios-liquid-glass"] .v2-kpi-row { position:relative; z-index:2; display:flex; gap:8px; padding: var(--v2-sp-4) var(--v2-sp-4) 0; margin:0; }
/* เข้าจอ = "Skew Sweep Fade" verbatim จาก codefronts.com (fi-11__block, 3 ส.ค. 2026) — skewX(15deg)+
   translateX(-40px)+opacity:0 → skewX(0)+translateX(0)+opacity:1 ทีละใบไล่ delay ตาม nth-child (ค่า delay
   ต้นฉบับมีแค่ 3 ใบ .05/.2/.35s เพิ่มลำดับ 4-6 ต่อ pattern +.15s เผื่อ KPI แถวไหนมีเกิน 3 ใบในอนาคต) */
[data-v2-theme="ios-liquid-glass"] .v2-kpi { flex:0 0 auto; min-width:128px; text-align:left; padding:7px 9px; border-radius:12px; display:flex; flex-direction:column; gap:1px; background: var(--v2-surface-solid); box-shadow: var(--v2-shadow-1); border:1px solid var(--v2-hair);
  opacity:0; transform: skewX(15deg) translateX(-40px); animation: v2-kpi-skew-in .7s cubic-bezier(.16,1,.3,1) forwards;
}
[data-v2-theme="ios-liquid-glass"] .v2-kpi-row .v2-kpi:nth-child(1) { animation-delay: .05s; }
[data-v2-theme="ios-liquid-glass"] .v2-kpi-row .v2-kpi:nth-child(2) { animation-delay: .2s; }
[data-v2-theme="ios-liquid-glass"] .v2-kpi-row .v2-kpi:nth-child(3) { animation-delay: .35s; }
[data-v2-theme="ios-liquid-glass"] .v2-kpi-row .v2-kpi:nth-child(4) { animation-delay: .5s; }
[data-v2-theme="ios-liquid-glass"] .v2-kpi-row .v2-kpi:nth-child(5) { animation-delay: .65s; }
[data-v2-theme="ios-liquid-glass"] .v2-kpi-row .v2-kpi:nth-child(6) { animation-delay: .8s; }
@keyframes v2-kpi-skew-in { to { opacity:1; transform: skewX(0deg) translateX(0); } }
@media (prefers-reduced-motion: reduce) {
  [data-v2-theme="ios-liquid-glass"] .v2-kpi { animation:none !important; opacity:1 !important; transform:none !important; }
}
[data-v2-theme="ios-liquid-glass"] .v2-kpi-val { font-size:14px; font-weight:700; color: var(--v2-text); font-variant-numeric: tabular-nums; line-height:1.2; white-space:nowrap; }
/* 3 ส.ค. 2026: lbl/unit เปลี่ยนเป็นตัวหนา (700) ตามที่ผู้ใช้สั่ง — เดิม lbl น้ำหนัก 400, unit ไม่ได้ตั้งไว้เลย
   (inherit 400 จาก body) ตอนนี้หนาเท่า .v2-kpi-val ทั้ง 3 บรรทัด */
/* Title2 */[data-v2-theme="ios-liquid-glass"] .v2-kpi-lbl { font-size:9px; line-height:1.3; color: var(--v2-text-dim); font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* Caption1 */[data-v2-theme="ios-liquid-glass"] .v2-kpi-unit { font-size:9px; color: var(--v2-text-dim); font-weight:700; white-space:nowrap; }
@media (min-width:640px) {
  [data-v2-theme="ios-liquid-glass"] .v2-kpi { min-width:145px; padding:8px 11px; }
  [data-v2-theme="ios-liquid-glass"] .v2-kpi-lbl { font-size:10px; }
  [data-v2-theme="ios-liquid-glass"] .v2-kpi-val { font-size:16px; }
  [data-v2-theme="ios-liquid-glass"] .v2-kpi-unit { font-size:10px; }
}
@media (min-width:1024px) {
  [data-v2-theme="ios-liquid-glass"] .v2-kpi { flex:1 1 0; min-width:0; padding:10px 14px; }
  [data-v2-theme="ios-liquid-glass"] .v2-kpi-lbl { font-size:12px; }
  [data-v2-theme="ios-liquid-glass"] .v2-kpi-val { font-size:22px; }
  [data-v2-theme="ios-liquid-glass"] .v2-kpi-unit { font-size:11px; }
}
/* สีต่อการ์ด — ค่า hex ตรงจาก Tailwind indigo/blue/emerald ที่ app.html ใช้จริง (label=-500, value=-700,
   unit=-400, border=-100) โทน dark mode ใช้เฉดเดียวกันแต่ขยับให้อ่านง่ายบนพื้นเข้ม (-400/-300/-200/กรอบโปร่ง)
   3 ส.ค. 2026: เพิ่ม background gradient wash + border โปร่งแสง ตามสูตร "Skew Sweep Fade" (fi-11__block,
   §4.18) verbatim — ต้นฉบับใช้ `linear-gradient(135deg, สีหลัก/.18, สีถัดไป/.08)` + `border:1px solid สีหลัก/.25`
   ต่อการ์ด ที่นี่ปรับสีเป็น indigo/blue/emerald ของเราแทนสี rose/pink/purple ต้นฉบับ (สีการ์ดต้องตรง app.html
   ตาม §4.14 อยู่ ไม่เปลี่ยนสีหลัก เปลี่ยนแค่ "ลักษณะพื้นผิว" ตามสูตรใหม่) — เป็น rgba โปร่งแสงเลยใช้ค่าเดียวกันได้
   ทั้ง light/dark mode ไม่ต้องแยก override สีกรอบอีกต่อไป (ลบของเดิมทิ้ง เหลือแค่ override สีตัวหนังสือ)
   3 ส.ค. 2026 (ต่อ): unit สี light mode เปลี่ยนจากเฉด -400 เป็นสีเดียวกับ lbl (-500) ตามที่ผู้ใช้สั่ง "หน่วยสี
   เท่ากัน" — dark mode เดิมใช้สีเดียวกันระหว่าง lbl/unit อยู่แล้ว (ไม่ต้องแก้) */
[data-v2-theme="ios-liquid-glass"] .v2-kpi--indigo { background: linear-gradient(135deg, rgba(99,102,241,.18), rgba(139,92,246,.08)); border-color: rgba(99,102,241,.25); }
[data-v2-theme="ios-liquid-glass"] .v2-kpi--indigo .v2-kpi-lbl { color:#6366f1; }
[data-v2-theme="ios-liquid-glass"] .v2-kpi--indigo .v2-kpi-val { color:#4338ca; }
[data-v2-theme="ios-liquid-glass"] .v2-kpi--indigo .v2-kpi-unit { color:#6366f1; }
[data-v2-theme="ios-liquid-glass"] .v2-kpi--blue { background: linear-gradient(135deg, rgba(59,130,246,.18), rgba(99,102,241,.08)); border-color: rgba(59,130,246,.25); }
[data-v2-theme="ios-liquid-glass"] .v2-kpi--blue .v2-kpi-lbl { color:#3b82f6; }
[data-v2-theme="ios-liquid-glass"] .v2-kpi--blue .v2-kpi-val { color:#1d4ed8; }
[data-v2-theme="ios-liquid-glass"] .v2-kpi--blue .v2-kpi-unit { color:#3b82f6; }
[data-v2-theme="ios-liquid-glass"] .v2-kpi--emerald { background: linear-gradient(135deg, rgba(16,185,129,.18), rgba(6,182,212,.08)); border-color: rgba(16,185,129,.25); }
[data-v2-theme="ios-liquid-glass"] .v2-kpi--emerald .v2-kpi-lbl { color:#10b981; }
[data-v2-theme="ios-liquid-glass"] .v2-kpi--emerald .v2-kpi-val { color:#047857; }
[data-v2-theme="ios-liquid-glass"] .v2-kpi--emerald .v2-kpi-unit { color:#10b981; }
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-kpi--indigo .v2-kpi-lbl { color:#a5b4fc; }
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-kpi--indigo .v2-kpi-val { color:#c7d2fe; }
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-kpi--indigo .v2-kpi-unit { color:#a5b4fc; }
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-kpi--blue .v2-kpi-lbl { color:#93c5fd; }
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-kpi--blue .v2-kpi-val { color:#bfdbfe; }
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-kpi--blue .v2-kpi-unit { color:#93c5fd; }
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-kpi--emerald .v2-kpi-lbl { color:#6ee7b7; }
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-kpi--emerald .v2-kpi-val { color:#a7f3d0; }
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-kpi--emerald .v2-kpi-unit { color:#6ee7b7; }
/* Caption2 *//* section label = Settings.app group header: uppercase footnote เทา — ตรวจกับ Sketch จริงแล้ว
   (component thumbnail หน้า Menus ไม่ใช่แค่หน้า Colors) "Section Title" ในเมนูจริงเป็นสีเทา(muted) ไม่ใช่ดำ
   ตัวหนาแบบ item ปกติ — เคยเปลี่ยนเป็นดำมาก่อนโดยอิงแค่หลัก Primary/Secondary ทั่วไปจากหน้า Colors ผิด ดู
   liquid-glass-v2.md §4.4 (แก้กลับ) */[data-v2-theme="ios-liquid-glass"] .v2-sec-lbl, [data-v2-theme="ios-liquid-glass"] .sec-lbl, [data-v2-theme="ios-liquid-glass"] .v2-eyebrow {
  font-size:.8125rem; font-weight:590; letter-spacing:.02em; text-transform:uppercase; color: var(--v2-text-mute);
  display:block; margin: 22px 4px 6px;
}
[data-v2-theme="ios-liquid-glass"] .v2-sec-lbl::after, [data-v2-theme="ios-liquid-glass"] .sec-lbl::after, [data-v2-theme="ios-liquid-glass"] .v2-eyebrow::after { content:none; }
/* ปุ่ม = ลำดับชั้น iOS: prominent(--primary) / tinted(--outline) / plain(--ghost) / destructive(--danger) */[data-v2-theme="ios-liquid-glass"] .v2-btn {
  font-family: var(--v2-font); font-size:1.0625rem; font-weight:600; cursor:pointer;
  padding:10px 20px; border:none; background: var(--v2-surface-2); color: var(--v2-text);
  border-radius: var(--v2-r-control); display:inline-flex; align-items:center; gap:6px; white-space:nowrap;
  transition: opacity .15s var(--v2-ease), transform .1s var(--v2-ease);
}
[data-v2-theme="ios-liquid-glass"] .v2-btn:active { opacity:.7; transform: scale(.98); }
[data-v2-theme="ios-liquid-glass"] .v2-btn--sm { padding:6px 14px; font-size:.9375rem; border-radius:9px; }
[data-v2-theme="ios-liquid-glass"] .v2-btn--lg { padding:13px 26px; font-size:1.0625rem; }
[data-v2-theme="ios-liquid-glass"] .v2-btn--icon { padding:8px; width:36px; height:36px; }
[data-v2-theme="ios-liquid-glass"] .v2-btn--primary { background: var(--v2-accent-1); color:#fff; }
/* prominent */[data-v2-theme="ios-liquid-glass"] .v2-btn--outline { background: rgb(0 136 255 / 14%); color: var(--v2-accent-1); }
/* tinted */[data-v2-theme="ios-liquid-glass"] .v2-btn--ghost { background:transparent; color: var(--v2-accent-1); }
/* plain */[data-v2-theme="ios-liquid-glass"] .v2-btn--danger { background: var(--v2-danger-bg); color: var(--v2-danger); }
[data-v2-theme="ios-liquid-glass"] .v2-btn--warn { background: var(--v2-warn-bg); color: var(--v2-warn); }
[data-v2-theme="ios-liquid-glass"] .v2-btn--ok { background: var(--v2-ok-bg); color: var(--v2-ok); }

/* 9 ส.ค. 2026: v2-btn-liquid (พอร์ตจาก codepen.io/cyrus-chau/pen/MYyNEea, ปุ่ม "Contact") ถูกถอดออกจาก
   design-system-v2.html แล้ว — ผู้ใช้ขอรวมทุกปุ่มใต้หัวข้อ "ปุ่ม" ให้เป็นสไตล์แคปซูลกระจก v2-btn-pill ชุดเดียว
   แทน grep ยืนยันแล้วว่า class นี้ไม่ถูกใช้ที่อื่นในโปรเจกต์เลย (มีแค่ในไฟล์นี้กับ demo html ที่เพิ่งลบ) จึงลบ CSS
   ทิ้งทั้งบล็อกได้ปลอดภัย ไม่ใช่ dead code ที่แขวนไว้เฉยๆ */

/* v2-btn-pill = ปุ่มแคปซูลกระจกเรืองสี พอร์ตจากภาพอ้างอิงปุ่ม "Add to Cart" ที่ผู้ใช้ส่งมา (liquidglassdesign.com
   gallery, 8-9 ส.ค. 2026) — ทำ mockup 3 น้ำหนักให้เลือกก่อน (Standing Rule 4) ผู้ใช้เลือกแบบ A "ตรงต้นแบบ เรือง
   แสงจัดเต็ม" คู่กัน 2 ทรง: --tag = แคปซูลใสกลาง (เทียบ "Size: 12" ในภาพ), --action = แคปซูลเรืองสี inner-glow
   ก้นแคปซูลใต้ + ลูกศร 3 ตัวไล่ opacity สั่นตอน hover (เทียบลูกศร ">>>" ในภาพ) สีปรับได้อิสระต่อปุ่มผ่าน --v2-pill-tint
   (ผูก semantic accent/ok/danger/warn ของธีมได้ตรงๆ) ไม่ต้องมี isolation/effect-layer แยกเหมือน v2-btn-liquid
   เพราะพื้นหลังไม่ใช่กระจกซ้อนกระจก (ดู §3.9) ปุ่มเดี่ยววางบนพื้นหลังปกติได้เลย */
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill {
  position:relative; border:none; cursor:pointer; font-family: var(--v2-font);
  display:inline-flex; align-items:center; gap:10px; white-space:nowrap;
  border-radius:999px; padding:13px 24px; font-size:.9375rem; font-weight:700;
  -webkit-tap-highlight-color:transparent;
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill svg { width:15px; height:15px; flex-shrink:0; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--tag {
  color: var(--v2-text); background: color-mix(in oklab, white 12%, transparent);
  backdrop-filter: blur(18px) saturate(160%); -webkit-backdrop-filter: blur(18px) saturate(160%);
  border:1px solid rgba(255,255,255,.35);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.5), inset 0 -6px 10px rgb(0 0 0 / 12%), 0 8px 20px rgb(0 0 0 / 18%);
  transition: background-color .2s, transform .15s;
}
/* 12 ส.ค. 2026: --tag ไม่เคยมี state ตอนกดเลย (บั๊กเดียวกับ --outline/--ghost ที่เคยเจอ) เพิ่มไว้ให้ครบทุกปุ่ม
   ในหัวข้อ "ปุ่ม" ตามที่ผู้ใช้ทัก */
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--tag:active,
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--tag.is-pressed {
  background: color-mix(in oklab, white 24%, transparent);
  transform: scale(.95);
}
[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-btn-pill--tag { color:#fff; }
/* 9 ส.ค. 2026: ผู้ใช้ทักว่าภาพต้นแบบ "Add to Cart" เป็นกระจกโปร่งแสงจริง (มองเห็นพื้นหลังเบลอผ่านสีทิ้นท์) ไม่ใช่
   สีทึบ — เดิม background ผสม tint ที่ 88-100% แทบทึบ ทำให้ backdrop-filter blur ด้านล่างไม่มีผลอะไรให้เห็นเลย
   (blur ของอะไรก็ตามที่ทึบคลุมทับอยู่ ไม่มีความหมาย) แก้โดยผสมโค้ดเดิมที่มีอยู่แล้ว: ใช้สูตร color-mix(...,
   transparent) แบบเดียวกับ .v2-btn-pill--tag/.v2-btn-liquid__tint (ผสมสีจาง 45-70% แทนขาว) ให้ backdrop-filter
   ที่มีอยู่แล้วเห็นผลจริง มองทะลุเห็นพื้นหลังเบลอ+ทิ้นท์สีตามปุ่ม เหมือนภาพต้นแบบ
   9 ส.ค. 2026 (ต่อ): ผู้ใช้ขอ "ใสอีก" — ลดสัดส่วน tint ลงอีกรอบ (45/72% → 22/42%) ให้โปร่งใสมากขึ้น */
/* 10 ส.ค. 2026: ตัวหนังสือปุ่ม --action เดิม color:#fff คงที่ทุกธีม — ผู้ใช้ขอให้ light mode เป็นดำ, dark mode
   เหมือนเดิม (ขาว) เปลี่ยนเป็น var(--v2-text) เฉยๆ ก็ได้ผลตรงตามนั้นแล้ว เพราะ token นี้เป็นดำในธีม light (rgb(0 0 0))
   และขาวใน [data-v2-mode="dark"] (#FFFFFF) อยู่แล้ว (ดู tokens บรรทัด 25/61) ไม่ต้องเพิ่ม override แยก */
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action {
  color: var(--v2-text); --v2-pill-tint: var(--v2-accent-1);
  background: linear-gradient(180deg, color-mix(in srgb, var(--v2-pill-tint) 22%, transparent), color-mix(in srgb, var(--v2-pill-tint) 42%, transparent));
  backdrop-filter: blur(14px) saturate(180%); -webkit-backdrop-filter: blur(14px) saturate(180%);
  border:1px solid color-mix(in srgb, var(--v2-pill-tint) 30%, rgba(255,255,255,.6));
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.65),
    inset 0 -8px 14px color-mix(in srgb, var(--v2-pill-tint) 28%, transparent),
    0 10px 24px -4px color-mix(in srgb, var(--v2-pill-tint) 42%, transparent),
    0 2px 8px rgb(0 0 0 / 16%);
  transition: transform 300ms cubic-bezier(.3,1,.4,1), border-color .25s; /* 300ms ตรงกับ .is-confirming duration */
}
/* 9 ส.ค. 2026 (ต่อ): hover กลับไปใช้เลขทิ้นท์/เงาก่อนลดสัดส่วน (45/72%) เพื่อให้เห็นสีปุ่มชัดขึ้นตอนโฮเวอร์ ส่วน
   state ปกติยังใสที่ 22/42% ตามเดิม — ผู้ใช้ทักว่าสลับ background/box-shadow ตรงๆ แบบ transition ทำให้ "ตัดทันที
   ไม่ค่อยๆ สว่าง" เพราะ CSS ไม่ animate gradient/box-shadow หลายชั้นแบบ crossfade จริง (เปลี่ยนแบบ step เดียวจบ
   ไม่ว่าจะตั้ง transition-duration เท่าไหร่) แก้ด้วยเทคนิค "ซ้อนชั้นแล้ว fade opacity" แทน: ทำ ::after เป็นชั้นทิ้นท์/
   เงาเข้ม (เลขก่อนปรับ) วางซ้อนทับพื้นหลังเดิม เริ่มที่ opacity:0 แล้ว fade เป็น 1 ตอน hover — opacity เป็นค่าเดียวที่
   เบราว์เซอร์ interpolate ทีละเฟรมได้จริงเสมอ ไม่ว่าเนื้อหาข้างในจะเป็น gradient/shadow ซับซ้อนแค่ไหน จึงได้ค่อยๆ
   สว่างขึ้น/หรี่ลงจริงทั้งสองทิศทาง (border-color ยังปล่อยให้ transition ตรงๆ บน element จริงได้ปกติ เพราะสีเดี่ยว
   interpolate ได้อยู่แล้วไม่มีปัญหานี้) z-index:-1 ให้แทรกอยู่เหนือพื้นหลัง/กรอบเดิมแต่ใต้ตัวอักษร/ไอคอนข้างใน
   ปุ่ม (ต้องมี position:relative บน parent ซึ่ง .v2-btn-pill มีอยู่แล้ว) */
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action::after {
  content:''; position:absolute; inset:0; border-radius:inherit; z-index:-1;
  opacity:0; transition: opacity .25s;
  background: linear-gradient(180deg, color-mix(in srgb, var(--v2-pill-tint) 45%, transparent), color-mix(in srgb, var(--v2-pill-tint) 72%, transparent));
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.65),
    inset 0 -8px 14px color-mix(in srgb, var(--v2-pill-tint) 45%, transparent),
    0 10px 24px -4px color-mix(in srgb, var(--v2-pill-tint) 60%, transparent),
    0 2px 8px rgb(0 0 0 / 20%);
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:hover {
  transform:translateY(-2px);
  border-color: color-mix(in srgb, var(--v2-pill-tint) 35%, rgba(255,255,255,.55));
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:hover::after { opacity:1; }
/* 11 ส.ค. 2026: ลบแนวทาง JS จำลอง :hover ด้วย touchstart/touchend ทิ้งไปก่อน แล้วลองผูกกับ `:active` ตรงๆ ก็ยัง
   "ไม่มีผล" อีก — สืบจนเจอสาเหตุจริง: ปุ่ม "ลบ" ใน confirmDanger() กด onclick แล้ว `m.close()` ทันที (ทั้ง popup
   fade-out ใน .22s) เท่ากับตัด :active ทิ้งเกือบจะพร้อมกับที่มันเริ่ม ไม่มีช่วงเวลาให้อนิเมชันเล่นให้เห็นเลยไม่ว่า
   จะตั้ง duration เท่าไหร่ (ปัญหาไม่ได้อยู่ที่ animation/specificity เลยสักครั้ง — อยู่ที่ popup ปิดเร็วกว่า
   อนิเมชันจะได้เล่นเสมอ) — เปลี่ยนมาใช้ class `.is-confirming` ที่ควบคุมจาก JS ตรงๆ แทน `:active` (ดู confirmDanger
   ใน ui-v2.js: ใส่ class ตอนคลิก แล้วหน่วง resolve()+m.close() ให้รอ animation เล่นจบก่อนค่อยปิดจริง) */
/* 12 ส.ค. 2026: ทดลองบนปุ่ม "อนุมัติ" ตัวเดียวก่อน (ตัดจังหวะเด้งขึ้น-ยุบลงของ keyframe v2-pill-press ออก
   เปลี่ยนไปใช้ CSS transition ตรงไปตรงมาแบบเดียวกับ --tag) ผู้ใช้ทดสอบแล้วชอบกว่า สั่งให้ใช้คอนเซปนี้กับทุกปุ่ม
   --action เลย (รวมปุ่ม "ลบ" ใน confirmDanger ด้วยเพราะใช้ class เดียวกัน) — ย้ายจาก modifier ทดลองเข้า base
   rule ตรงๆ, ลบ @keyframes v2-pill-press ทิ้ง (ไม่มีใครเรียกใช้แล้ว) ส่วนแสงเรือง (::after ด้านบน) ไม่แตะเลย
   ยังเป็น v2-pill-press-glow keyframe เดิม — transform ใช้ transition บนสถานะปกติด้านบน (300ms ปรับให้ตรงกับ
   .is-confirming duration แล้ว) ไม่ต้องประกาศซ้ำที่นี่ */
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:active,
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action.is-confirming {
  transform: translateY(0) scale(.97);
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:active::after,
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action.is-confirming::after {
  animation: v2-pill-press-glow 300ms ease forwards;
}
@keyframes v2-pill-press-glow {
  0% { opacity:0; }
  40% { opacity:1; }
  100% { opacity:1; }
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill__chev { display:inline-flex; align-items:center; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill__chev svg { width:9px; height:9px; margin-left:-4px; opacity:.9; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill__chev svg:nth-child(1) { opacity:1; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill__chev svg:nth-child(2) { opacity:.6; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill__chev svg:nth-child(3) { opacity:.3; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:hover .v2-btn-pill__chev svg,
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:active .v2-btn-pill__chev svg,
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action.is-confirming .v2-btn-pill__chev svg { animation: v2-pill-chev .7s ease-in-out infinite; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:hover .v2-btn-pill__chev svg:nth-child(2),
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:active .v2-btn-pill__chev svg:nth-child(2),
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action.is-confirming .v2-btn-pill__chev svg:nth-child(2) { animation-delay:.08s; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:hover .v2-btn-pill__chev svg:nth-child(3),
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:active .v2-btn-pill__chev svg:nth-child(3),
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action.is-confirming .v2-btn-pill__chev svg:nth-child(3) { animation-delay:.16s; }
@keyframes v2-pill-chev { 0%,100% { transform:translateX(0); } 50% { transform:translateX(3px); } }
@media (prefers-reduced-motion: reduce) {
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:active,
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action.is-confirming,
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:active::after,
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action.is-confirming::after { animation:none; }
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action { transition:none; }
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action::after { transition:none; }
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:hover .v2-btn-pill__chev svg,
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action:active .v2-btn-pill__chev svg,
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--action.is-confirming .v2-btn-pill__chev svg { animation:none; }
}
/* 9 ส.ค. 2026: ผู้ใช้ขอรวมทุกปุ่มใต้หัวข้อ "ปุ่ม" (เดิม primary/outline/danger/warn/ok/ghost/sm/lg/disabled ของ
   .v2-btn) ให้เป็นสไตล์แคปซูลกระจกชุดนี้ทั้งหมด — เพิ่ม modifier ให้ครบลำดับชั้นเดียวกับ .v2-btn (prominent/
   tinted/plain) โดย --outline/--ghost ใช้สี --v2-pill-tint เป็นสีตัวอักษร/กรอบแทนพื้นเรืองแสง ⚠ นี่คือชุดปุ่ม
   สำหรับ "โชว์ในหน้า design-system-v2.html" เท่านั้น ไม่ได้แตะ .v2-btn เดิมที่ ui-v2.js ใช้จริงใน V2.confirm()
   (ปุ่ม ตกลง/ยกเลิก ของ modal ยืนยัน) — เปลี่ยน .v2-btn เองจะกระทบทุกจุดที่เรียก confirm ทั้งธีม ไม่ใช่ scope ที่ขอ */
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--outline {
  --v2-pill-tint: var(--v2-accent-1); color: var(--v2-pill-tint);
  background: color-mix(in srgb, var(--v2-pill-tint) 14%, transparent);
  backdrop-filter: blur(14px) saturate(180%); -webkit-backdrop-filter: blur(14px) saturate(180%);
  border:1.5px solid color-mix(in srgb, var(--v2-pill-tint) 55%, transparent);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.4);
  transition: background-color .2s, transform .15s;
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--outline:hover { background: color-mix(in srgb, var(--v2-pill-tint) 22%, transparent); }
/* 11 ส.ค. 2026: ปุ่ม outline ไม่เคยมี state ตอนกดเลย (บั๊กเดียวกับ --ghost ด้านล่าง) — เพิ่มไว้ให้ทุกปุ่ม outline
   ในระบบ (ไม่ใช่แค่ confirmDanger — ปุ่ม "ยกเลิก" ของ confirmDanger ย้ายไปใช้ --ghost แทนแล้ว ดูด้านล่าง) */
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--outline:active,
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--outline.is-pressed {
  background: color-mix(in srgb, var(--v2-pill-tint) 32%, transparent);
  transform: scale(.96);
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ghost {
  --v2-pill-tint: var(--v2-accent-1); color: var(--v2-pill-tint);
  background:transparent; border:1px solid transparent; box-shadow:none;
  backdrop-filter:none; -webkit-backdrop-filter:none;
  transition: background-color .2s, transform .15s;
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ghost:hover { background: color-mix(in srgb, var(--v2-pill-tint) 10%, transparent); }
/* 11 ส.ค. 2026: --ghost ไม่เคยมี state ตอนกดเช่นกัน (บั๊กคลาสเดียวกับ --outline) เพิ่มไว้ให้ทุกปุ่ม ghost ในระบบ
   เผื่อประโยชน์ ไม่ใช่แค่ confirmDanger */
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ghost:active,
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ghost.is-pressed {
  background: color-mix(in srgb, var(--v2-pill-tint) 30%, transparent); /* 18%→30% — ผู้ใช้บอกว่ากดแล้วไม่เห็นอะไรเลยบนมือถือจริง (11 ส.ค. 2026) */
  transform: scale(.94);
}
/* confirmDanger ปุ่ม "ยกเลิก": ผู้ใช้ขอให้เหมือนปุ่ม "ดูเพิ่มเติม" (ghost — โปร่งใสสนิท ไม่มีพื้นหลังเลย) แต่ใส่
   กรอบด้วย (ghost ปกติ border:1px solid transparent คือมองไม่เห็น) — modifier นี้ทำแค่เติม border-color กลับมา */
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ghost.v2-btn-pill--ghost-bordered {
  border-color: color-mix(in srgb, var(--v2-pill-tint) 55%, transparent);
}
/* v2-btn-pill--ring = "True Ring" — conic-gradient หมุนรอบขอบ (เทคนิค+สีตรงจาก codefronts.com "Animated
   Gradient Border Card, Conic Rotate" 11 ส.ค. 2026, ผู้ใช้ยืนยันสีให้ตรงต้นฉบับเป๊ะ แล้วเลือกแบบ B "เส้น+bloom"
   จาก mockup 3 แบบ) — สีคงที่ 3 ตัวไม่ผูก semantic accent/danger/ok เพราะเป็นชุดสีเฉพาะของเอฟเฟกต์นี้ ไม่ใช่ tint
   ที่ปรับต่อปุ่มแบบ --action/--outline/--ghost — ใช้แทนที่ --action/--outline/--ghost ไม่ใช่ต่อยอด
   ⚠ ไม่ใช้ mask-composite: ผู้ใช้ทดสอบจริงบน Firefox แล้วขอบหายสนิท ทั้งที่ตัวตรวจ CSS.supports() และ
   getComputedStyle บอกว่า mask-composite:exclude รองรับและคำนวณ background/mask ถูกต้องครบทุกอย่าง — เข้าเค้า
   บั๊ก compositing เฉพาะ Firefox ที่เคยเจอมาแล้วกับ backdrop-filter/blend-mode ในธีมนี้ (ดู §5.30/§5.34) คือ
   "รายงานว่ารองรับแต่ไม่วาดจริง" เบราว์เซอร์ทดสอบของทุกเซสชันเป็น Chromium ล้วนตรวจบั๊กแบบนี้เองไม่ได้เลย — แก้
   โดยเลี่ยง mask ทั้งกลไก เปลี่ยนไปใช้เทคนิคเก่ากว่าที่เสถียรข้ามเบราว์เซอร์กว่ามาก: "double background-clip"
   (border จริง 2px โปร่งใส + วาง background 2 ชั้น ชั้นในตัน clip แค่ padding-box ชั้นนอกไล่สี clip ถึง
   border-box) ไม่พึ่ง mask/mask-composite เลยแม้แต่นิดเดียว รองรับมาตั้งแต่ก่อนยุค mask-composite ทั้งหมด */
@property --v2-ring-angle { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ring {
  /* --v2-ring-angle:0deg ประกาศตรงนี้ซ้ำกับ @property initial-value ด้านบนโดยตั้งใจ กันเบราว์เซอร์ที่ไม่รองรับ
     @property (ข้าม at-rule เงียบๆ) ทำให้ var() ไม่มีค่าจน background ทั้งเส้น invalid ไปด้วย */
  --v2-ring-angle: 0deg;
  --v2-ring-1: oklch(0.78 0.19 300); --v2-ring-2: oklch(0.82 0.16 200); --v2-ring-3: oklch(0.85 0.17 88);
  color:#fff;
  /* 11 ส.ค. 2026: เคยลอง isolation:isolate ถาวร ตั้งใจแก้อาการ bloom "ไปอยู่บนปุ่ม" ตอนกดค้าง — deploy จริงกลับ
     แย่กว่าเดิม (ไม่กดก็เห็นแสงบนปุ่มตลอดเวลาไปเลย) แปลว่าทฤษฎี stacking-context ไม่ตรงจุด หรือ isolate ทำให้ bloom
     ที่ z-index:-2 compositing ตรงกับปุ่มแน่นขึ้นแทนที่จะจางลงแบบที่ตอน rest เคยเป็น (ตอนไม่มี isolate อาจมีการ
     เจือจางกับ ancestor context อื่นอยู่ตอนพัก) ย้อนกลับทันทีที่ผู้ใช้รายงานว่าแย่ลง — ห้ามใส่ isolation กลับมาอีก
     โดยไม่ตรวจสอบทฤษฎีให้แน่นกว่านี้ก่อน (บทเรียน: ทฤษฎีที่ "ฟังดูมีเหตุผลทาง spec" ไม่ได้แปลว่าตรงจุดจริงเสมอไป
     ถ้ายืนยันด้วยสายตาจริงไม่ได้ ต้องระวังการเปลี่ยนอะไรที่กระทบทั้ง 2 สถานะพร้อมกัน) */
  border: 2px solid transparent; padding:11px 22px; /* ลด padding ฐาน 2px/ด้าน ชดเชย border ใหม่ ให้ขนาดรวมเท่าปุ่มอื่น */
  background:
    linear-gradient(#1c1c1e, #1c1c1e) padding-box,
    conic-gradient(from var(--v2-ring-angle), var(--v2-ring-1), var(--v2-ring-2), var(--v2-ring-3), var(--v2-ring-1)) border-box;
  box-shadow: 0 10px 24px -8px rgb(0 0 0 / 45%);
  animation: v2-ring-spin 6.5s linear infinite reverse;
  transition: transform .15s ease, box-shadow .15s ease;
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ring::after {
  content:''; position:absolute; inset:-6px; z-index:-2; border-radius:999px;
  background: conic-gradient(from var(--v2-ring-angle), var(--v2-ring-1), var(--v2-ring-2), var(--v2-ring-3), var(--v2-ring-1));
  filter: blur(16px); opacity:.55;
  /* transition อยู่บน "สถานะปกติ" นี้เท่านั้น (ไม่ใช่บน :active::after) — ตอนปล่อยนิ้ว (กลับมาสถานะนี้) opacity
     ไล่กลับขึ้นมา ส่วนตอนกด (:active::after ด้านล่างมี transition:none ทับ) ยังคงตัดฮวบทันทีเหมือนเดิม — ผู้ใช้เจอ
     ว่าซิงก์ระยะเวลาเท่ากับ transform เฉยๆ (.15s ทั้งคู่) ยังไม่พอ ยังเห็น bloom โผล่มาก่อนปุ่มคืนรูปเสร็จอยู่ดี
     (สองอย่างเริ่มพร้อมกันจริง แต่ curve/ความไวสายตาต่อสีกับรูปทรงไม่เท่ากัน สีจะ "รู้สึกไว" กว่าการเปลี่ยนขนาด) —
     เปลี่ยนกลยุทธ์เป็น "หน่วงให้ opacity เริ่มไล่ทีหลัง transform" แทนการซิงก์เวลาเริ่มพร้อมกัน: delay .12s (ปุ่ม
     คืนรูปไปเกือบหมดจาก transform .15s ก่อน) แล้วค่อยไล่ opacity ขึ้นอีก .2s ให้ bloom โผล่มาทีหลังตอนปุ่มคืนรูป
     เกือบเต็มแล้วเท่านั้น ไม่มีช่วงที่ปุ่มยังเล็ก/ต่ำอยู่พร้อมกับ bloom สว่างเห็นชัดอีก */
  transition: opacity .2s ease .12s;
  animation: v2-ring-spin 5s linear infinite;
}
/* 11 ส.ค. 2026: ผู้ใช้ทักว่ากดปุ่มไฮไลต์แล้ว "ไม่รู้ว่ากด" — ปุ่มนี้ไม่มี onclick/JS logic ใดๆ (เดโมเฉยๆ) เดิมมีแค่
   :hover เร่งความเร็วหมุน (6.5s/5s → 1.6s) ซึ่งทัชสกรีนไม่ trigger hover ก่อน tap แน่นอน (ต่างจาก mouse) — เพิ่ม
   feedback ที่เห็นทันทีตอนนิ้วแตะลง ลองหลายแบบ: scale+glow สี (ผู้ใช้ไม่เอา), หักล้าง translateY บน ::after ให้
   bloom นิ่ง (subtle เกินไปไม่เห็นผล), ทำพื้นผิวปุ่มโปร่งแสงให้ bloom ทะลุออกมาทาบหน้าปุ่ม (ผู้ใช้บอกไม่ต้องเห็น
   ทะลุ) — ทุกรอบที่แก้ ผู้ใช้ยังเห็น "สีลอยอยู่บนปุ่มตอนกด" อยู่ดี ตรวจ deploy จริงด้วย curl ยืนยันว่าไม่ใช่ cache
   เก่า — จุดที่ลืมตลอดมา: ทั้ง `:hover` (บรรทัดข้างบนเดิม) และ `:active` ยังคง `animation-duration:1.6s` เร่งความ
   เร็วหมุนอยู่ (มือถือหลายรุ่น/เบราว์เซอร์ trigger `:hover` แบบ sticky ตอนแตะหน้าจอด้วย ไม่ใช่แค่ mouse) นี่คือ
   "color effect" อีกแบบที่ผู้ใช้ไม่ได้ขอ (แค่เร่งหมุนก็ทำให้สี conic-gradient/bloom หมุนไล่เร็วขึ้น 4 เท่าในจังหวะ
   เดียวกับที่ปุ่มขยับลง มองเห็นเป็น "สีแล่นวูบขึ้นมาตอนกด" ได้ง่าย) ลบทั้ง :hover และ :active speedup ออกหมด
   เหลือแค่ translateY กดจมลง + scale เข้าเล็กน้อยเฉยๆ ความเร็วหมุนคงที่ 6.5s/5s ตลอด ไม่มีสถานะไหนเปลี่ยนเลย */
/* ผู้ใช้ทักเพิ่ม: scale(.97) เดิมหด 3% น้อยเกินจะรู้สึกว่า "ขนาดลดลงจริง" ตอนกด — เพิ่มเป็น .92 ให้เห็นชัดขึ้น
   สุดท้าย: แทนที่จะไล่หาสาเหตุที่แท้จริงของ "แสง bloom ไปอยู่บนปุ่ม" ต่อ (ลองมาหลายทฤษฎีแล้วยังไม่ตรงจุด) ผู้ใช้เสนอ
   ทางลัดที่ตรงและชัวร์กว่า: ปิด bloom (::after) ไปเลยตอนกด ไม่ว่าจะเกิดจากกลไกอะไรก็ตามจะไม่มีทางเห็นได้อีกเพราะ
   ตัวมันหายไปเลย — ลอง fade ด้วย transition .15s ก่อน แต่ผู้ใช้เจอว่ายังมีสีวูบขึ้นมาแวบนึงตอนเริ่มกด (คือช่วง
   ระหว่างเฟดที่ opacity ยังไม่ถึง 0 นั่นเอง) ตัด transition ออก (:active::after ทับ transition:none) ให้ตัดฮวบ
   ทันทีตอนกดแทน — แต่พอปล่อยนิ้ว (:active หลุด กลับไปใช้ transition บนสถานะปกติ) ผู้ใช้เจอปัญหาใหม่: opacity
   กระโดดกลับเต็ม .55 ทันทีขณะปุ่มยังหดตัว/อยู่ต่ำกว่าปกติ (transform ของปุ่มยังไล่กลับรูปอยู่ 150ms) เห็นเป็น
   bloom เต็มพลังลอยอยู่บนปุ่มเล็กๆ ช่วงสั้นๆ ตอนเด้งกลับ — เลยแยกทิศทางให้ไม่เท่ากัน: ตอนกด (:active::after)
   transition:none ตัดฮวบทันที, ตอนปล่อย (กลับไปใช้ transition .15s บนสถานะปกติของ ::after ด้านบน) ไล่ opacity
   ขึ้นพร้อมจังหวะเดียวกับ transform คืนรูป (.15s เท่ากัน) เห็นโตพร้อมกันเป็นธรรมชาติ ไม่มีช่วงที่ไม่สมส่วนกันเลย */
/* 12 ส.ค. 2026: เพิ่ม .is-pressed คู่กับ :active (เหมือน --outline/--ghost) ให้ JS demo ในหน้า design-system-v2
   ถือ class นี้ไว้ครบตามเวลาที่ตั้งได้ แทนที่จะพึ่ง :active เพียวๆ ซึ่งหลุดทันทีที่ปล่อยนิ้ว/เมาส์ (แตะเร็วๆ
   อาจไม่ทันเห็นเลย) — ดู applyDemoPressFeedback ใน design-system-v2.html */
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ring:active,
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ring.is-pressed {
  transform: translateY(2px) scale(.92);
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ring:active::after,
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ring.is-pressed::after {
  opacity: 0;
  transition: none;
}
@keyframes v2-ring-spin { to { --v2-ring-angle: 360deg; } }
@media (prefers-reduced-motion: reduce) {
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ring,
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ring::after { animation:none; }
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ring { transition:none; }
}
/* v2-btn-pill--glow = "Rotating Rainbow Glowing Border" — คัดลอกจาก codefronts.com "CSS Glowing Border
   Buttons" component "Rotating Rainbow Gradient Glowing Border Button" ตรงๆ (12 ส.ค. 2026) สีไล่รุ้งเต็มวง
   (oklch hue 20/90/160/230/300) ตรงต้นฉบับเป๊ะ — เทคนิคต่างจาก --ring (True Ring, double background-clip):
   ที่นี่ ::before/::after เป็น element แยกซ้อน inset:-2px หลังปุ่มทึบ ไม่ใช้ mask/mask-composite เลยแม้แต่นิดเดียว
   (ไม่มีความเสี่ยง Firefox paint bug ตระกูล mask ที่เจอกับ --ring ก่อนหน้า ดู liquid-glass-v2.md §5.50) ปุ่มทึบ
   เองบังกึ่งกลางไว้ เหลือแค่ 2px ริมขอบให้ ::before/::after โผล่เป็นวงแหวน/แสงฟุ้ง */
@property --v2-glow-a { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--glow {
  --v2-glow-a: 0deg; /* fallback ตรงกับ @property initial-value เผื่อเบราว์เซอร์ไม่รองรับ @property (เหตุผลเดียวกับ --ring) */
  position: relative; isolation: isolate;
  color: #fff; background: #1c1c1e;
  transition: transform .25s ease;
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--glow::before,
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--glow::after {
  content: ''; position: absolute; inset: -2px; z-index: -1; border-radius: inherit;
  background: conic-gradient(from var(--v2-glow-a),
    oklch(0.72 0.2 20), oklch(0.8 0.17 90), oklch(0.75 0.16 160),
    oklch(0.7 0.15 230), oklch(0.65 0.22 300), oklch(0.72 0.2 20));
  animation: v2-glow-spin 3.5s linear infinite;
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--glow::after {
  filter: blur(18px); opacity: .55; transition: opacity .35s ease;
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--glow:hover { transform: translateY(-2px); }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--glow:hover::before,
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--glow:hover::after { animation-duration: 1.4s; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--glow:hover::after { opacity: .95; }
/* .is-pressed คู่กับ :active — ให้ JS demo (applyDemoPressFeedback ใน design-system-v2.html) ถือ class ไว้ครบ
   ตามเวลาที่ตั้ง แทนที่จะพึ่ง :active เพียวๆ ที่หลุดทันทีตอนปล่อยนิ้ว/เมาส์ (เหตุผลเดียวกับปุ่มอื่นทั้งหมดในหัวข้อ
   "ปุ่ม") */
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--glow:active,
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--glow.is-pressed {
  transform: translateY(0) scale(.98);
}
@keyframes v2-glow-spin { to { --v2-glow-a: 360deg; } }
@media (prefers-reduced-motion: reduce) {
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--glow,
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--glow::before,
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--glow::after { animation:none; transition:none; }
}
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--sm { padding:9px 18px; font-size:.8125rem; gap:6px; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--sm svg { width:12px; height:12px; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill--lg { padding:17px 32px; font-size:1.0625rem; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill:disabled, [data-v2-theme="ios-liquid-glass"] .v2-btn-pill[disabled] {
  opacity:.45; cursor:not-allowed; pointer-events:none;
}
/* confirmDanger ปิดปุ่มกันกดซ้ำระหว่างรอ .is-confirming เล่นอนิเมชัน — ไม่ต้องการให้ opacity:.45 ด้านบนทับจนแสง
   เรืองมองไม่เห็น (specificity สูงกว่าเพราะมี 2 class) */
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill.is-confirming:disabled { opacity:1; }
[data-v2-theme="ios-liquid-glass"] .v2-btn-pill.is-pressed:disabled { opacity:1; }
@media (prefers-reduced-motion: reduce) {
  [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--outline, [data-v2-theme="ios-liquid-glass"] .v2-btn-pill--ghost { transition:none; }
}

[data-v2-theme="ios-liquid-glass"] .v2-field label { font-size:.9375rem; font-weight:400; color: var(--v2-text); }
[data-v2-theme="ios-liquid-glass"] .v2-inp, [data-v2-theme="ios-liquid-glass"] .v2-sel, [data-v2-theme="ios-liquid-glass"] .v2-ta {
  font-family: var(--v2-font); font-size:1.0625rem; color: var(--v2-text);
  background: var(--v2-surface-2); border:none; border-radius: var(--v2-r-control);
  padding:10px 13px; outline:none; width:100%; transition: box-shadow .15s;
}
[data-v2-theme="ios-liquid-glass"] .v2-inp:focus, [data-v2-theme="ios-liquid-glass"] .v2-sel:focus, [data-v2-theme="ios-liquid-glass"] .v2-ta:focus { box-shadow: 0 0 0 3px rgb(0 136 255 / 30%); }
[data-v2-theme="ios-liquid-glass"] .v2-ta { resize:vertical; min-height:80px; }
/* toggle = "liquid glass" knob: ขนาดสัดส่วนจริงของ iOS (51x31), เงากลอสสามมิติบนลูกบิด,
   สปริงตอนสลับ + บีบยืดตอนกด (squish) ทั้งหมดเป็น CSS ล้วน — ไม่ใช้ SVG goo-merge/drag
   (goo filter ต้องจูน stdDeviation ให้พอดีกับขนาดจริง เสี่ยงเบลอเลอะถ้าจูนผิดแบบไม่เห็นผลจริง
   และ drag-to-toggle ไม่จำเป็นกับฟอร์มแอดมิน — ปุ่มกด/แตะยังคงพฤติกรรมเดิมทั้งหมด) */[data-v2-theme="ios-liquid-glass"] .v2-switch { width:51px; height:31px; }
[data-v2-theme="ios-liquid-glass"] .v2-switch .slider {
  position:absolute; inset:0; background: var(--v2-hair-strong); border-radius:999px;
  transition: background .25s var(--v2-ease); cursor:pointer;
}
[data-v2-theme="ios-liquid-glass"] .v2-switch input:checked + .slider { background: var(--v2-ok); }
[data-v2-theme="ios-liquid-glass"] .v2-switch .slider::before {
  content:''; position:absolute; width:27px; height:27px; left:2px; top:2px; border-radius:50%; background:#fff;
  box-shadow:
    1px -1px 2px hsl(0 0% 100% / 50%) inset,
    0 -1px 2px hsl(0 0% 100% / 50%) inset,
    -1px -1px 2px hsl(0 0% 100% / 50%) inset,
    1px 1px 2px hsl(0 0% 30% / 45%) inset,
    -6px 3px 8px -5px hsl(0 0% 30% / 25%) inset,
    -1px 1px 5px hsl(0 0% 30% / 25%) inset,
    0 1px 3px hsl(0 0% 30% / 35%),
    0 2px 5px hsl(0 0% 30% / 20%);
  transition: transform .32s cubic-bezier(.34,1.8,.64,1), width .18s ease-out, left .18s ease-out;
}
[data-v2-theme="ios-liquid-glass"] .v2-switch input:checked + .slider::before { transform: translateX(20px); }
/* squish ตอนกดค้าง — ลูกบิดยืดออกด้านที่กำลังจะเคลื่อนที่ไป แล้วสปริงกลับตอนปล่อย */[data-v2-theme="ios-liquid-glass"] .v2-switch:active .slider::before { width:33px; }
[data-v2-theme="ios-liquid-glass"] .v2-switch:active input:checked + .slider::before { transform: translateX(14px); }
/* card = Liquid Glass material แท้ — สูตรเดียวกับ .v2-navback/.v2-navpill/.v2-navdd__panel เป๊ะ (verbatim
   codefronts.com, ดู liquid-glass-v2.md §2) ผู้ใช้สั่งให้ทั้งหน้าใช้สูตรเดียวกันหมด ไม่แยกสองสูตรอีก */
[data-v2-theme="ios-liquid-glass"] .v2-card {
  position: relative; background-color: color-mix(in oklab, white 12%, transparent);
  border: 1px solid rgba(255,255,255,.35); border-radius: var(--v2-r-panel);
  padding: var(--v2-sp-5); box-shadow: 0 30px 60px -30px rgba(0,0,0,.65), inset 0 1px 0 rgba(255,255,255,.5);
  backdrop-filter: blur(18px) saturate(180%); -webkit-backdrop-filter: blur(18px) saturate(180%);
}
[data-v2-theme="ios-liquid-glass"] .v2-card-foot { display:flex; align-items:center; justify-content:space-between; margin-top:12px; padding-top:12px; border-top: .5px solid var(--v2-hair); }
@supports not (backdrop-filter: blur(1px)) {
[data-v2-theme="ios-liquid-glass"] .v2-card { background: var(--v2-surface-solid); }
}
/* รายการ = แถบ Label แบบ visionOS: ไอคอนเหลี่ยมมนขอบเส้น (checkbox-style) + chevron ท้ายแถว */[data-v2-theme="ios-liquid-glass"] .v2-rail-card { display:flex; align-items:center; gap:12px; padding: 11px 4px; border-bottom: .5px solid var(--v2-hair); }
[data-v2-theme="ios-liquid-glass"] .v2-rail { width:22px; height:22px; margin-top:0; border-radius:6px; flex-shrink:0; background: transparent; border:1.5px solid var(--v2-accent-1); position:relative; }
[data-v2-theme="ios-liquid-glass"] .v2-rail--warn { background: transparent; border-color: var(--v2-warn); }
[data-v2-theme="ios-liquid-glass"] .v2-rail--idle { background: transparent; border-color: var(--v2-hair-strong); }
[data-v2-theme="ios-liquid-glass"] .v2-rail-title { font-size:1.0625rem; font-weight:400; color: var(--v2-text); }
/* Body */[data-v2-theme="ios-liquid-glass"] .v2-rail-meta { font-size:.8125rem; color: var(--v2-text-dim); margin-top:2px; }
/* Footnote */[data-v2-theme="ios-liquid-glass"] .v2-rail-foot { display:flex; align-items:center; justify-content:space-between; margin-top:8px; }
[data-v2-theme="ios-liquid-glass"] .v2-link-btn { font-family: var(--v2-font); font-size:.9375rem; font-weight:400; color: var(--v2-accent-1); background:none; border:none; cursor:pointer; }
[data-v2-theme="ios-liquid-glass"] .v2-rail-card::after {
  content:''; width:7px; height:7px; flex-shrink:0; border-top:2px solid var(--v2-text-mute); border-right:2px solid var(--v2-text-mute);
  transform: rotate(45deg); margin-left:auto; align-self:center; order:99;
}
/* ตาราง = verbatim จาก codefronts.com "data-table-with-glass-header" (ct-13, 2 ส.ค. 2026) — sticky thead
   จริง (ไม่ใช่แค่ position:sticky บนพื้นทึบแบบเดิม) มี backdrop-filter ทำให้แถวที่เลื่อนลอดใต้หัวตารางดูเบลอฝ้า
   จริง — ห้ามวาง .v2-table-wrap นี้เป็นลูกหลานของพื้นผิวกระจกอื่น (เช่น .v2-card) เด็ดขาด เพราะ backdrop-filter
   ของ thead จะโดน ancestor ตัดขอบเขตแบบเดียวกับ §3.9 (ดู liquid-glass-v2.md) — ถ้าจำเป็นต้องอยู่ใน .v2-card
   ให้ตัด backdrop-filter/-webkit-backdrop-filter ของ thead ออกเหลือแค่ background ทึบแทน */
[data-v2-theme="ios-liquid-glass"] .v2-table-wrap {
  max-height:300px; border-radius: var(--v2-r-panel);
  border: 1px solid rgba(255,255,255,.3);
  background-color: color-mix(in oklab, white 8%, transparent);
  /* 20 ส.ค. 2026: เคยเพิ่ม backdrop-filter:blur(18px) ตรงนี้เพื่อแก้เส้นขอบสีไม่สม่ำเสมอใต้ปุ่ม toolbar — กลายเป็น
     บั๊กใหม่ที่ร้ายแรงกว่าเดิมมาก: thead th (ลูกของ .v2-table-wrap นี้เอง บรรทัดถัดไป) มี backdrop-filter ของ
     ตัวเองอยู่แล้ว — เพิ่ม backdrop-filter ให้ parent ทำให้เกิด "กระจกซ้อนกระจก" (ตรงกับกฎ §3.9 ที่คอมเมนต์ด้านบน
     เตือนไว้ตั้งแต่ต้น แต่ตอนนั้นเช็คผิดทิศ — เช็คแค่ว่า .v2-table-wrap เป็นลูกของ .v2-card หรือไม่ ลืมเช็คว่า
     .v2-table-wrap เองมีลูกที่เป็นกระจกอยู่แล้ว) ผลคือ backdrop-filter ของ thead ถูกตัดขอบเขตภาพที่จะเบลอ เหลือแค่
     พื้นผิวโปร่งใสของ .v2-table-wrap เอง (ไม่ใช่ภาพแถวข้อมูลจริงด้านหลัง) กลายเป็นไม่เบลออะไรเลย ตัวหนังสือแถว
     ข้อมูลเลยทะลุซ้อนหัวตารางแบบคมชัดเต็มๆ (ผู้ใช้ยืนยันด้วยภาพจริง + พิสูจน์แล้วด้วยการปิด backdrop-filter ตรงนี้
     ผ่าน devtools สดๆ บนเว็บจริง หัวตารางสะอาดทันที) — เอา backdrop-filter ออกจากที่นี่ถาวร ห้ามใส่กลับมาอีก
     ปัญหาขอบไม่สม่ำเสมอ (ปัญหาเดิมที่ backdrop-filter นี้พยายามแก้) ยังไม่ได้แก้จริง รอแนวทางอื่นที่ไม่ใช่
     backdrop-filter ซ้อน เช่น ขยับสูตร bleed แบบเดียวกับ §5.56 แทน */
  box-shadow: 0 26px 56px -28px rgba(0,0,0,.65);
}
[data-v2-theme="ios-liquid-glass"] .v2-table thead th {
  position: sticky; top:0; z-index:2; text-align:left; padding:13px 16px;
  font-size:.75rem; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color: var(--v2-text);
  /* 20 ส.ค. 2026: ผู้ใช้ขอย้อนค่าตรงนี้กลับเป็นค่าเดิมก่อนแก้ (20%+blur10px) — เคยลองขยับเป็น 38%/18px แล้ว 85%/18px
     ทั้งสองรอบยังไม่ตรงกับที่ผู้ใช้ต้องการ + ผู้ใช้บอกชัดว่าไม่ต้องการให้แก้ค่าที่นี่อีกจนกว่าจะยืนยันแนวทางให้ก่อน
     (กำลังไล่เทียบผ่าน .claude/scratch/table-header-fix-compare.html แทน) ห้ามแก้ค่าพวกนี้อีกจนกว่าผู้ใช้จะสั่ง */
  background-color: color-mix(in oklab, white 20%, transparent);
  border-bottom: 1px solid rgba(255,255,255,.4);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.55);
  /* 22 ส.ค. 2026: ถอด saturate(180%) ออกจาก backdrop-filter ของหัวตาราง (เหลือ blur ล้วน) — แก้อาการ "หัวตาราง
     กระพริบตอนอนิเมชันตารางเล่นจบ" ที่มากับวิธี A (WAAPI + commitStyles+cancel บน .app-tablebleed ดูคอมเมนต์เต็ม
     ที่จุดเรียกใน app-v2.html): ระหว่างอนิเมชันเล่น backdrop-filter ของ thead ถูก ancestor ตัดขอบเขต (ไม่ทำงาน)
     พอ cancel() ปุ๊บมันกลับมาทำงานในเฟรมเดียว สีจึงกระโดด — ตัวที่ทำให้ "สีกระโดด" ชัดคือ saturate(180%) ไม่ใช่
     blur เพราะพื้นหลังหลังหัวตารางเป็นเกรเดียนต์เรียบ (เบลอเกรเดียนต์เรียบแทบไม่เห็นความต่าง แต่เร่งอิ่มสี 180%
     เห็นชัดมาก) เอา saturate ออกแล้วสองสถานะก่อน/หลัง cancel จึงหน้าตาเกือบเหมือนกัน กระพริบหายไปเอง
     ⚠ นี่เป็นข้อยกเว้นเฉพาะ thead ที่มีเหตุผลรองรับ ไม่ใช่การเบี่ยงสูตรกระจกมาตรฐาน (§2) ตามอำเภอใจ — thead
     เบี่ยงจากสูตรอยู่แล้ว (blur 10px ไม่ใช่ 18px) เพราะเป็นพื้นผิวบางที่ซ้อนในกล่องกระจกอีกที ไม่ใช่การ์ดลอย */
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
}
[data-v2-theme="ios-liquid-glass"] .v2-table td { padding:12px 16px; border-bottom: 1px solid color-mix(in oklab, white 14%, transparent); color: var(--v2-text); font-size:.9375rem; }
[data-v2-theme="ios-liquid-glass"] .v2-table tbody tr:nth-child(odd) { background-color: color-mix(in oklab, white 6%, transparent); }
[data-v2-theme="ios-liquid-glass"] .v2-table tbody tr:hover { background-color: color-mix(in oklab, white 12%, transparent); }
@supports not (backdrop-filter: blur(1px)) {
  [data-v2-theme="ios-liquid-glass"] .v2-table-wrap, [data-v2-theme="ios-liquid-glass"] .v2-table thead th { background: var(--v2-surface-solid); }
}
/* 12 ส.ค. 2026: ผู้ใช้ทักว่าคอลัมน์ "งบ (ลบ.)" สีต่างจากคอลัมน์อื่นในตาราง — เดิม color:var(--v2-text-dim)
   (จางกว่า .v2-table td ปกติที่ใช้ var(--v2-text)) ตัด color override ออก ให้สืบทอดสีเดียวกับคอลัมน์อื่นเสมอ
   เหลือแค่ font-weight:600 ไว้เน้นตัวเลข (จัดชิดขวา+tabular-nums อยู่แล้วจาก .v2-table td.is-num อีกกฎที่บรรทัด
   1164) ไม่ใช่คุณสมบัติที่ทำให้สีต่างกัน */
[data-v2-theme="ios-liquid-glass"] .v2-table td.is-num { font-weight:600; }
[data-v2-theme="ios-liquid-glass"] .v2-chip { display:inline-flex; align-items:center; gap:6px; background: var(--v2-surface-2); border:none; color: var(--v2-text); font-size:.875rem; font-weight:400; padding:6px 8px 6px 12px; border-radius:999px; }
[data-v2-theme="ios-liquid-glass"] .v2-chip b { color: var(--v2-accent-1); font-weight:600; }
[data-v2-theme="ios-liquid-glass"] .v2-badge { border-radius:6px; font-weight:600; text-transform:none; letter-spacing:0; }
/* สถานะ = วงกลมระบบสี + เครื่องหมายถูกเมื่อเสร็จ (คล้าย SF Symbol checkmark.circle.fill) */[data-v2-theme="ios-liquid-glass"] .v2-status { font-size:.8125rem; font-weight:400; }
[data-v2-theme="ios-liquid-glass"] .v2-status .diamond { width:16px; height:16px; border-radius:50%; transform:none; flex-shrink:0; background: transparent; position:relative; }
[data-v2-theme="ios-liquid-glass"] .v2-status--done .diamond { background: var(--v2-ok); }
[data-v2-theme="ios-liquid-glass"] .v2-status--done .diamond::after { content:''; position:absolute; left:4px; top:7px; width:6px; height:3px; border-left:1.5px solid #fff; border-bottom:1.5px solid #fff; transform: rotate(-45deg); }
[data-v2-theme="ios-liquid-glass"] .v2-status--pending .diamond { background: var(--v2-warn-bg); border:1.5px solid var(--v2-warn); }
[data-v2-theme="ios-liquid-glass"] .v2-status--empty .diamond { border:1.5px solid var(--v2-hair-strong); }
/* tab = Segmented Control จริง: แทร็กเทา + segment ที่เลือกเป็นแผ่นขาว/เข้มลอย ไม่ใช่สีทึบ *//* 12 ส.ค. 2026: ผู้ใช้ทักว่ากรอบเทาของแท็บยาวเลย "สมาชิก" (แท็บสุดท้าย) ไปจนสุดจอ — display:flex บน div (block-level)
   ค่าเริ่มต้น width:auto กว้างเท่า containing block เสมอ (ไม่ shrink-to-fit ตามเนื้อหาแบบ inline) ทั้งที่ปุ่ม
   ข้างในรวมกันแคบกว่ามาก เพิ่ม width:fit-content ให้กรอบแคบพอดีเนื้อหา + max-width:100% กัน overflow ล้นขวา
   บนจอแคบ (ยังต้องพึ่ง overflow-x:auto เดิมเลื่อนได้ปกติถ้าเนื้อหากว้างกว่าจอจริง) */
[data-v2-theme="ios-liquid-glass"] .v2-tabs { position:relative; display:flex; width:fit-content; max-width:100%; overflow-x:auto; background: var(--v2-surface-2); border-radius:9px; padding:2px; border-bottom:none; gap:2px; }
[data-v2-theme="ios-liquid-glass"] .v2-tab { position:relative; z-index:1; font-family: var(--v2-font); font-size:.875rem; font-weight:600; cursor:pointer; padding:6px 14px; border:none; background:transparent; color: var(--v2-text); border-radius:7px; margin-bottom:0; border-bottom:none; }
/* 12 ส.ค. 2026: ผู้ใช้ขอเอาโมชั่นแคปซูลลอยของเมนู (.v2-navpill__hoverpill/__activepill + wireNavPillHover/
   wireNavPillActive ใน ui-v2.js) มาประยุกต์กับแท็บ — เดิม .v2-tab.is-active สลับพื้นหลังแบบ instant (แค่ transition
   background/box-shadow ซึ่งไม่เนียนเท่าเลื่อนจริง) และไม่มี hover เลย เปลี่ยนเป็นแคปซูลลอย 2 ชั้นแบบเดียวกับ
   nav ทุกอย่าง (JS วัดตำแหน่งจริงด้วย getBoundingClientRect() แล้ว transform ให้เลื่อนตาม ไม่ใช้ left/top กัน
   layout thrash) ต่างแค่สีให้ตรงกับดีไซน์ segmented control เดิมของแท็บเอง (การ์ดขาว/เข้มลอย ไม่ใช่วงกลมดำ/ขาว
   แบบ nav) — ดู wireTabsHover/wireTabsActive ใน ui-v2.js */
[data-v2-theme="ios-liquid-glass"] .v2-tab__hoverpill {
  position:absolute; left:0; top:0; width:0; height:0; z-index:0; border-radius:7px;
  background-color: color-mix(in oklab, var(--v2-text) 10%, transparent);
  opacity:0; pointer-events:none; will-change: transform;
  transition: transform .32s cubic-bezier(.3,1,.4,1), width .32s cubic-bezier(.3,1,.4,1), height .32s cubic-bezier(.3,1,.4,1), opacity .2s;
}
[data-v2-theme="ios-liquid-glass"] .v2-tab__hoverpill.is-visible { opacity:1; }
[data-v2-theme="ios-liquid-glass"] .v2-tab__activepill {
  position:absolute; left:0; top:0; width:0; height:0; z-index:0; border-radius:7px;
  background: var(--v2-surface-solid); box-shadow: var(--v2-shadow-1);
  opacity:0; pointer-events:none; will-change: transform;
  transition: transform .32s cubic-bezier(.3,1,.4,1), width .32s cubic-bezier(.3,1,.4,1), height .32s cubic-bezier(.3,1,.4,1), opacity .2s;
}
[data-v2-theme="ios-liquid-glass"] .v2-tab__activepill.is-visible { opacity:1; }
@media (prefers-reduced-motion: reduce) {
  [data-v2-theme="ios-liquid-glass"] .v2-tab__hoverpill { transition: opacity .2s; }
  [data-v2-theme="ios-liquid-glass"] .v2-tab__activepill { transition: opacity .2s; }
}
/* modal = visionOS/iOS Alert แท้: ไอคอนวงกลมบนหัว, เนื้อหากึ่งกลาง, ปุ่มเรียงซ้อนแนวตั้งคั่นเส้นแนวนอน ไม่มีปุ่มปิดมุม */[data-v2-theme="ios-liquid-glass"] .v2-modal {
  background-color: color-mix(in oklab, white 12%, transparent);
  border: 1px solid rgba(255,255,255,.35); border-radius: var(--v2-r-panel);
  box-shadow: 0 30px 60px -30px rgba(0,0,0,.65), inset 0 1px 0 rgba(255,255,255,.5);
  backdrop-filter: blur(18px) saturate(180%); -webkit-backdrop-filter: blur(18px) saturate(180%);
  text-align:center; overflow:hidden;
}
/* 11 ส.ค. 2026: white 12% ข้างบนถูก .v2-backdrop (ม่านมืด rgba(2,4,10,.55)) เบลอทับจนดรอปสี — เฉพาะ**โหมดสว่าง**
   เท่านั้นที่มีปัญหา (โหมดมืดผู้ใช้ยืนยันว่า ok อยู่แล้ว ไม่แตะ) เพิ่ม tint ขาวเป็น 55% ชดเชยความมืดจากม่าน ให้การ์ด
   อ่านชัดโดยไม่ขึ้นกับว่าข้างหลังมืดแค่ไหน (mockup ตัวเลือก D ที่ผู้ใช้เลือก) */
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="light"] .v2-modal {
  background-color: color-mix(in oklab, white 55%, transparent);
  border-color: rgba(255,255,255,.7);
}
/* 25 ส.ค. 2026: ผู้ใช้ขอให้กระจกของ popup เป็น "แบบเดียวกับลิ้นชักตัวกรอง ที่มีทั้งสว่างและมืด" — โหมดสว่างตรงกัน
   อยู่แล้ว (white 55% ทั้งคู่) แต่โหมดมืดยังค้างที่ white 12% ของสูตรกลาง ทำให้ popup ในโหมดมืดสว่างกว่าพื้นหลัง
   มากและคอนทราสต์ตัวหนังสือต่ำ — ลอกวิธีเดียวกับ .app-filter-drawer-glass-test: **สลับทิศทางของ tint เป็นผสมดำ
   ไม่ใช่ลดเปอร์เซ็นต์ทึบ** (55% ขาวเหมาะกับพื้น pastel ของ light mode อยู่แล้ว ปัญหาคือทิศทางไม่ใช่ตัวเลข) +
   ขอบ/เงาปรับให้เข้ากับพื้นเข้มชุดเดียวกัน */
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-modal {
  background-color: color-mix(in oklab, black 55%, transparent);
  border-color: rgba(255,255,255,.18);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.12), 0 30px 60px -30px rgba(0,0,0,.7);
}
[data-v2-theme="ios-liquid-glass"] .v2-modal-head { border-bottom:none; padding:22px 22px 4px; flex-direction:column; align-items:center; justify-content:center; position:relative; }
[data-v2-theme="ios-liquid-glass"] .v2-modal-head::before {
  content:''; display:block; width:36px; height:36px; margin:0 auto 12px; border-radius:50%; background: var(--v2-accent-1);
}
[data-v2-theme="ios-liquid-glass"] .v2-modal-head h3 { font-size:1.0625rem; font-weight:700; color: var(--v2-text); text-transform:none; letter-spacing:0; }
[data-v2-theme="ios-liquid-glass"] .v2-modal-close { display:none; }
/* Alert ไม่มีปุ่มปิดมุม — ปิดผ่าน action เท่านั้น ตามแบบอ้างอิง */[data-v2-theme="ios-liquid-glass"] .v2-modal-body { padding:2px 22px 18px; font-size:.8125rem; color: var(--v2-text-dim); border-bottom: .5px solid var(--v2-hair); padding-bottom:16px; }
/* 25 ส.ค. 2026 (แก้บั๊ก — popup รายละเอียดโครงการใน app-v2.html เลื่อนไม่ได้เลย):
   base `.v2-modal` (ท้ายไฟล์) ตั้ง `max-height:88vh; overflow:auto` ไว้ถูกแล้ว แต่กฎธีมด้านบนตั้ง `overflow:hidden`
   ทับ (specificity สูงกว่า: attribute+class ชนะ class เปล่า) เพื่อให้มุมโค้งตัดปุ่ม foot ที่ชิดขอบได้ — ผลคือเนื้อหา
   ที่ยาวเกิน 88vh **ถูกตัดทิ้งเฉยๆ ไม่มีทางเลื่อนถึง** เดิมไม่มีใครเจอเพราะ modal ธีมนี้ออกแบบเป็น iOS Alert สั้นๆ
   จนกระทั่งพอร์ต popup รายละเอียด 5 หมวด + เอกสารแนบเข้ามา
   วิธีแก้ (ไม่ทิ้ง overflow:hidden ของกล่อง เพราะยังต้องใช้ตัดมุมโค้ง): ทำกล่องเป็น flex column แล้วย้าย
   scroll ไปไว้ที่ `.v2-modal-body` แทน — head/foot ตรึงอยู่กับที่ (ปุ่ม "ปิด" ไม่หนีไปกับ scroll ด้วย)
   ⚠ §5.51: แกนใดไม่ visible อีกแกนถูกดันเป็น auto เสมอ จึงตั้ง overflow-x:hidden ชัดๆ กันแถบเลื่อนแนวนอนโผล่
     (เนื้อหาใน popup ใช้ word-break อยู่แล้ว ไม่มีอะไรควรล้นแนวนอน) */
[data-v2-theme="ios-liquid-glass"] .v2-modal { display:flex; flex-direction:column; }
[data-v2-theme="ios-liquid-glass"] .v2-modal-head,
[data-v2-theme="ios-liquid-glass"] .v2-modal-foot { flex:0 0 auto; }
[data-v2-theme="ios-liquid-glass"] .v2-modal-body {
  flex:1 1 auto; min-height:0; overflow-y:auto; overflow-x:hidden;
  -webkit-overflow-scrolling:touch;
  /* ซ่อนแถบเลื่อน (ยังเลื่อนได้ปกติ) ให้เข้าชุดกับเมนู ☰ + ลิ้นชักตัวกรอง */
  scrollbar-width:none; -ms-overflow-style:none;
}
[data-v2-theme="ios-liquid-glass"] .v2-modal-body::-webkit-scrollbar { display:none; }
[data-v2-theme="ios-liquid-glass"] .v2-modal-foot { border-top:none; padding:0; display:flex; flex-direction:column; }
[data-v2-theme="ios-liquid-glass"] .v2-modal-foot .v2-btn { flex:none; width:100%; border-radius:0; background:transparent; color: var(--v2-accent-1); font-weight:400; padding:13px; border-right:none; border-top: .5px solid var(--v2-hair); }
[data-v2-theme="ios-liquid-glass"] .v2-modal-foot .v2-btn:first-child { border-top:none; }
[data-v2-theme="ios-liquid-glass"] .v2-modal-foot .v2-btn.v2-btn--primary { color: var(--v2-accent-1); font-weight:600; background:transparent; }
[data-v2-theme="ios-liquid-glass"] .v2-modal-foot .v2-btn.v2-btn--outline { color: var(--v2-text-dim); background:transparent; }
[data-v2-theme="ios-liquid-glass"] .v2-confirm-actions { flex-direction:column; gap:0; }
/* ---------- Danger confirm (V2.confirmDanger) — mockup แบบ A ที่ผู้ใช้เลือก (delete-confirm-mockup),
   ไอคอนวงกลมแดงทึบเหนือหัวข้อ, ปุ่มเรียงแนวนอนแทนแนวตั้งแบบ alert ปกติ — ปุ่ม "ลบ" ใช้ .v2-btn-pill--action
   ที่ออกแบบไว้แล้ว (--v2-pill-tint:var(--v2-danger), เดียวกับตัวอย่างใน design-system-v2.html) ไม่ทำปุ่มใหม่ */
[data-v2-theme="ios-liquid-glass"] .v2-modal--danger .v2-modal-head { padding:28px 24px 0; }
[data-v2-theme="ios-liquid-glass"] .v2-modal--danger .v2-modal-head::before { display:none; }
[data-v2-theme="ios-liquid-glass"] .v2-modal--danger .v2-modal-icon {
  width:52px; height:52px; border-radius:50%; margin:0 auto 16px;
  display:flex; align-items:center; justify-content:center;
  background: var(--v2-danger);
  box-shadow: 0 8px 20px -4px color-mix(in srgb, var(--v2-danger) 55%, transparent), inset 0 1px 0 rgba(255,255,255,.4);
}
[data-v2-theme="ios-liquid-glass"] .v2-modal--danger .v2-modal-icon svg { width:24px; height:24px; }
[data-v2-theme="ios-liquid-glass"] .v2-modal--danger .v2-modal-body { border-bottom:none; padding:2px 24px 22px; }
[data-v2-theme="ios-liquid-glass"] .v2-modal--danger .v2-modal-foot { flex-direction:row; gap:10px; padding:0 24px 24px; }
/* .v2-confirm-actions (wrap div ข้างในจริงที่มีปุ่ม) มี flex-direction:column ตายตัวจากกฎ alert ปกติ (บรรทัดบน) —
   ต้อง override ที่ตัวมันเองด้วย ไม่ใช่แค่ .v2-modal-foot (ซึ่งมีแค่ลูกเดียวคือ wrap div นี้ เลยไม่มีผลอะไร) */
[data-v2-theme="ios-liquid-glass"] .v2-modal--danger .v2-confirm-actions { flex-direction:row; gap:10px; width:100%; }
[data-v2-theme="ios-liquid-glass"] .v2-modal--danger .v2-modal-foot .v2-btn-pill { flex:1; justify-content:center; }
/* drawer--bottom = Sheet: มือจับ (grabber) + มุมบนโค้งมนแบบ panel ลอย */[data-v2-theme="ios-liquid-glass"] .v2-drawer { background: var(--v2-surface-solid); border-left: .5px solid var(--v2-hair); }
[data-v2-theme="ios-liquid-glass"] .v2-drawer--bottom { border-radius: var(--v2-r-panel) var(--v2-r-panel) 0 0; border-top:none; padding-top:10px; }
[data-v2-theme="ios-liquid-glass"] .v2-drawer--bottom::before { content:''; display:block; width:36px; height:5px; border-radius:999px; background: var(--v2-hair-strong); margin:0 auto 8px; }

/* ============================================================
   v2-agentchat — Plan Agent chat popup (app-v2.html เท่านั้น)
   26 ส.ค. 2026: เดิมสร้างบน V2.drawer({side:'bottom'}) (แผงเลื่อนขึ้นจากล่าง) → ผู้ใช้ขอเปลี่ยนเป็น V2.modal
   กลางจอแบบเดียวกับ popup รายละเอียดโครงการ (.app-detail-modal) แทน — โครง head/body/foot ของเราเอง
   (buildPopupBody ใน app-v2.html) ยังเหมือนเดิมทุกจุด แค่เปลี่ยน "กล่องนอก" จาก .v2-drawer เป็น .v2-modal
   (border-radius ทุกมุม + backdrop-filter ของ .v2-modal เองอยู่แล้วจากธีม ไม่ต้องประกาศซ้ำ) สูตรกระจก tint/
   border/shadow ด้านล่างยัง verbatim จาก .v2-navdd__panel เหมือนเดิม (white 12% + blur18 + border .35 —
   สูตรเดียวที่ยืนยันแล้วจาก codefronts.com ดู liquid-glass-v2.md) */
[data-v2-theme="ios-liquid-glass"] .v2-agentchat {
  /* 26 ส.ค. 2026: ผู้ใช้ทักว่าขอบนอก (ระยะห่างจากขอบจอ) ชิดเกินไปเทียบ popup รายละเอียดโครงการ — เดิม
     height:min(76vh,680px) ปล่อยให้ vh ล้วนคุมระยะขอบบน/ล่าง ซึ่งบนอุปกรณ์ที่มี dynamic toolbar/safe-area
     (เช่น iPad) เปอร์เซ็นต์ vh ไม่ได้แปลว่าเผื่อระยะขอบจริงเสมอไป — เพิ่ม max-height เป็น calc(100vh-64px)
     บังคับระยะขอบขั้นต่ำจริง 32px ต่อด้าน (บน+ล่างรวม 64px) เสมอไม่ว่า vh จะคำนวณเท่าไหร่
     27 ส.ค. 2026 (แก้บั๊กจริง — 2 รอบก่อนแก้ผิดจุด): ผู้ใช้ทักซ้ำว่า "ยังเหลือพื้นที่อยู่ เทียบ popup
     รายละเอียด" แม้ตอน**ไม่มีคีย์บอร์ดเปิดเลย** — 2 รอบที่แล้วแก้แค่ syncAgentChatViewport (JS, ทำงานเฉพาะตอน
     คีย์บอร์ดเปิดจริง เกิน 80px) กับตำแหน่งปุ่มน้ำ ไม่ได้แตะ "ขนาดปกติ" ของกล่องนี้เลยสักจุด (ยัง
     height:min(70vh,620px) เดิม) — ภาพที่ผู้ใช้ส่งมาเทียบทั้ง 2 รอบเป็นภาพ**ไม่มีคีย์บอร์ด**เหมือนกันทั้งคู่ จึงไม่
     เห็นความต่างเลยเป็นเรื่องคาดได้ ต้นตอจริงคือ 70vh/620px เตี้ยกว่า popup รายละเอียด (โตได้ถึง max-height:88vh
     ตามเนื้อหา) มาก — เปลี่ยนเป็นเติมเต็มระยะขอบ calc(100vh-64px) ที่ตั้งไว้เป็นแค่เพดานอยู่แล้วให้เป็น "ความสูงจริง"
     ปกติเลย (ไม่ใช่แค่เพดานที่ height เดิมไปไม่ถึง)
     27 ส.ค. 2026 (รอบ 3 — แก้ด้วยป้ายดีบักจริงจาก iPad): เพดาน 900px ที่เพิ่งใส่ไว้ "กันจอใหญ่เกินจำเป็น" กลับ
     เตี้ยกว่าพื้นที่จริงบน iPad เอง (avail จริง 1069px วัดจากอุปกรณ์จริง) ทำให้ยังเหลือพื้นที่ว่างเหมือนเดิม —
     ตัดเพดาน px ทิ้ง ให้ตรงกับ syncAgentChatViewport (JS, app-v2.html) ที่ก็ไม่มีเพดาน px ตายตัวเหมือนกันแล้ว
     ค่านี้ใช้แค่ตอน~250ms แรกก่อน JS sync ทำงาน (กันโผล่มาแล้วเปลี่ยนขนาดวาบ) ไม่ใช่ค่าที่ใช้งานจริงถาวร */
  height: calc(100vh - 64px);
  max-height: calc(100vh - 64px);
  background-color: color-mix(in oklab, white 12%, transparent);
  border: 1px solid rgba(255,255,255,.35);
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  box-shadow: 0 30px 60px -30px rgba(0,0,0,.65), inset 0 1px 0 rgba(255,255,255,.5);
  display:flex; flex-direction:column;
  /* 26 ส.ค. 2026 (แก้บั๊ก): .v2-modal ธีมตั้ง text-align:center ไว้ (ออกแบบสำหรับ iOS Alert) — ไม่ได้ override
     ตอนแรกที่แปลง .v2-agentchat จาก .v2-drawer มาเป็น .v2-modal ทำให้ข้อความในบับเบิลแชท (inherit มาจากกล่องนี้)
     กลายเป็นกึ่งกลางไปด้วยทั้งที่แชทต้องชิดซ้าย */
  text-align:left;
}
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-agentchat {
  /* สลับทิศทาง tint เป็นผสมดำใน dark mode — เหตุผลเดียวกับ .app-filter-drawer-glass-test/.v2-modal
     (white tint บนพื้นมืดคอนทราสต์ต่ำ) */
  background-color: color-mix(in oklab, black 55%, transparent);
  border-color: rgba(255,255,255,.14);
}
/* .v2-modal ปกติมี head (h3+ไอคอนวงกลม default) กับ body ที่มี padding/border-bottom ของตัวเอง (ออกแบบไว้
   สำหรับ iOS Alert ทั่วไป) — เราไม่ใช้ทั้งคู่เลยเพราะหัวข้อของเราเอง (.v2-agentchat__head: agent selector +
   ปุ่มเริ่มเซสชันใหม่/ประวัติ/ปิด) อยู่ใน body wrap ของ buildPopupBody() อยู่แล้ว ซ่อน head เริ่มต้นทิ้ง แล้วรีเซ็ต
   .v2-modal-body ให้ไม่มี padding/border-bottom/scroll ของตัวเอง (ให้ .v2-agentchat__body ด้านในเป็นตัวเลื่อน
   แทนแต่ผู้เดียว เหมือนตอนเป็น .v2-drawer) */
[data-v2-theme="ios-liquid-glass"] .v2-agentchat .v2-modal-head { display:none; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat .v2-modal-body {
  padding:0; border-bottom:none; overflow:hidden; display:flex; flex-direction:column;
}
@supports not (backdrop-filter: blur(1px)) {
  [data-v2-theme="ios-liquid-glass"] .v2-agentchat { background: var(--v2-surface-solid); }
}
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__head {
  /* 26 ส.ค. 2026 (แก้บั๊ก): padding เดิม 2px บน / 10px ล่าง ไม่เท่ากัน (ตกทอดจากตอนยังมีปุ่ม agentbtn แบบ pill
     ที่มี padding ในตัวเองชดเชยอยู่ — ตอนนี้เหลือแค่ป้ายชื่อเปล่าไม่มี padding ในตัวชดเชยแล้ว) ทำให้ปุ่ม/ตัวหนังสือ
     ดูเยื้องขึ้นไม่อยู่กึ่งกลางแนวตั้งของแถบหัวข้อ — เปลี่ยนเป็นสมมาตรบน-ล่างเท่ากัน */
  flex:0 0 auto; display:flex; align-items:center; gap:8px; padding:10px 14px; border-bottom: .5px solid var(--v2-hair);
}
/* 26 ส.ค. 2026: .v2-agentchat__agentbtn (ปุ่มเลือก agent ที่หัวข้อ แบบ pill กดได้) ถูกเอาออกตามที่ผู้ใช้ขอ —
   เหลือ .v2-agentchat__agentname เป็นป้ายชื่อเฉยๆ ไม่ใช่ปุ่ม (เลือก agent ย้ายไปทำที่ปุ่มไอคอนข้างปุ่มส่งแทน
   ดู .v2-agentchat__agenticonbtn ด้านล่าง) — flex:1 1 auto ยังเก็บไว้เพื่อดันปุ่มไอคอน new/hist/close ใน
   หัวข้อไปชิดขวาเหมือนเดิม */
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__agentname {
  flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  text-align:left; font-size:.9375rem; font-weight:700; color: var(--v2-text); padding-left:4px;
}
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__agenticon {
  flex:none; width:26px; height:26px; border-radius:50%; display:flex; align-items:center; justify-content:center;
  font-size:15px; background: color-mix(in oklab, var(--v2-accent-1) 20%, transparent);
}
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__iconbtn {
  flex:none; width:34px; height:34px; border-radius:50%; border:none; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  background: #1c1c1e; color:#fff; box-shadow: 0 6px 16px -6px rgb(0 0 0 / 45%);
  transition: transform .15s, filter .15s;
}
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-agentchat__iconbtn { background:#fff; color:#1c1c1e; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__iconbtn:active { transform: scale(.9); filter: brightness(1.3); }
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-agentchat__iconbtn:active { filter: brightness(.8); }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__iconbtn svg { width:16px; height:16px; }
/* 27 ส.ค. 2026: ปุ่มปรับขนาดตัวหนังสือ (.app-fontsize-group ต้นแบบ) มีเงาหลายชั้น (inset highlight + drop
   shadow 8px/20px) หนักกว่าปุ่มไอคอนอื่นในหัวข้อ popup นี้มาก (.v2-agentchat__iconbtn เงาชั้นเดียว 6px/16px) —
   ผู้ใช้ขอให้เท่ากับปุ่มปิด ovveride เฉพาะ instance นี้ (.app-agentchat-fontsize ไม่ใช่ .app-fontsize-group เปล่าๆ)
   ไม่กระทบปุ่มปรับฟอนต์ตาราง/popup รายละเอียดที่ยังใช้เงาเดิม */
[data-v2-theme="ios-liquid-glass"] .app-agentchat-fontsize { box-shadow: 0 6px 16px -6px rgb(0 0 0 / 45%); }
@media (prefers-reduced-motion: reduce) { [data-v2-theme="ios-liquid-glass"] .v2-agentchat__iconbtn:active { transform:none; } }
/* 26 ส.ค. 2026: ปุ่มไอคอน agent ข้างปุ่มส่ง (ย้ายมาจากหัวข้อตามที่ผู้ใช้ขอ) — วงกลม 38px เท่าปุ่มส่ง (.v2-agentchat__send)
   ข้างๆ กัน ไอคอนจริงข้างในยังใช้ .v2-agentchat__agenticon เดิม (26px, พื้นทินท์ accent) แค่ย้ายที่อยู่ */
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__agenticonbtn {
  flex:none; width:38px; height:38px; border-radius:50%; border:none; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  background: color-mix(in oklab, var(--v2-text) 7%, transparent); color: var(--v2-text);
  transition: background-color .15s, transform .15s;
}
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__agenticonbtn:active { background: color-mix(in oklab, var(--v2-text) 14%, transparent); transform: scale(.92); }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__agenticonbtn.is-open { background: color-mix(in oklab, var(--v2-accent-1) 25%, transparent); }
@media (prefers-reduced-motion: reduce) { [data-v2-theme="ios-liquid-glass"] .v2-agentchat__agenticonbtn:active { transform:none; } }

/* กระเป๋า dropdown เลือก agent / ประวัติเซสชัน — ต้อง append เข้า document.body ตรงๆ เสมอ (JS) ไม่ใช่ลูกของ
   .v2-agentchat (มี backdrop-filter อยู่แล้ว — ซ้อนกระจกจะเบลอผิด ดู §3.9) ตำแหน่งคำนวณจาก getBoundingClientRect
   ของปุ่มที่กด คนละก้อนกับ .v2-navdd__panel แต่สูตรกระจก/แอนิเมชันเดียวกันเป๊ะ */
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__menu {
  position:fixed; z-index:96; width:min(300px,86vw); max-height:60vh; overflow-y:auto; padding:6px; border-radius: var(--v2-r-panel);
  scrollbar-width:none; -ms-overflow-style:none;
  background-color: color-mix(in oklab, white 12%, transparent);
  border: 1px solid rgba(255,255,255,.35);
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  box-shadow: 0 30px 60px -30px rgba(0,0,0,.65), inset 0 1px 0 rgba(255,255,255,.5);
  display:none;
}
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__menu::-webkit-scrollbar { display:none; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__menu.is-open { display:block; animation: v2-navdd-drop .3s cubic-bezier(.3,1.3,.5,1); }
@media (prefers-reduced-motion: reduce) { [data-v2-theme="ios-liquid-glass"] .v2-agentchat__menu.is-open { animation:none; } }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__menuitem {
  display:flex; align-items:center; gap:10px; width:100%; padding:9px 10px; border:none; background:transparent; cursor:pointer;
  border-radius:16px; color: var(--v2-text); font-size:.8125rem; font-weight:600; text-align:left;
  transition: background-color .15s;
}
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__menuitem:active { background: color-mix(in oklab, var(--v2-text) 12%, transparent); }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__menuitem.is-active { background: rgba(28,28,30,.92); color:#fff; }
:root[data-v2-theme="ios-liquid-glass"][data-v2-mode="dark"] .v2-agentchat__menuitem.is-active { background: rgba(255,255,255,.92); color:#1c1c1e; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__menuitem .ico { flex:none; width:22px; height:22px; border-radius:50%; display:flex; align-items:center; justify-content:center; background: color-mix(in oklab, var(--v2-accent-1) 20%, transparent); font-size:12px; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__menuitem small { display:block; font-weight:400; opacity:.65; font-size:.6875rem; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__menusep { border:none; border-top:.5px solid var(--v2-hair); margin:4px 6px; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__menuempty { padding:14px 10px; text-align:center; font-size:.75rem; color: var(--v2-text-mute); }

[data-v2-theme="ios-liquid-glass"] .v2-agentchat__body {
  flex:1 1 auto; min-height:0; overflow-y:auto; -webkit-overflow-scrolling:touch;
  scrollbar-width:none; -ms-overflow-style:none;
  padding:14px 14px 6px; display:flex; flex-direction:column; gap:10px;
}
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__body::-webkit-scrollbar { display:none; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__empty { margin:auto; text-align:center; color: var(--v2-text-mute); font-size:calc(13px * var(--v2-agentchat-fontscale,1)); padding:24px; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-msg { display:flex; max-width:88%; animation: v2-agentchat-msg-in .28s cubic-bezier(.3,1,.4,1); }
@keyframes v2-agentchat-msg-in { 0% { opacity:0; transform: translateY(8px) scale(.97); } 100% { opacity:1; transform: translateY(0) scale(1); } }
@media (prefers-reduced-motion: reduce) { [data-v2-theme="ios-liquid-glass"] .v2-agentchat-msg { animation:none; } }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-msg--user { align-self:flex-end; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-msg--ai { align-self:flex-start; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-msg__bubble {
  padding:9px 13px; border-radius:18px; font-size:calc(13px * var(--v2-agentchat-fontscale,1)); line-height:1.55; white-space:pre-wrap;
  /* 26 ส.ค. 2026 (แก้บั๊ก "AI ตอบล้นช่องพิมพ์"): word-break:break-word เดิมไม่พอสำหรับ token ยาวไม่มีช่องว่าง
     ภายใน (เช่น [[ref:kb:20:63]] ที่ Plan Agent ใส่ท้ายประโยคอ้างอิง) — เปลี่ยนเป็น overflow-wrap:anywhere
     (บังคับตัดได้ทุกจุดจริงๆ ถ้าจำเป็น) + max-width:100% กันบับเบิลกว้างเกิน .v2-agentchat-msg ที่ห่อไว้ */
  overflow-wrap:anywhere; max-width:100%;
}
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-msg--user .v2-agentchat-msg__bubble { background: var(--v2-accent-1); color:#fff; border-bottom-right-radius:5px; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-msg--ai .v2-agentchat-msg__bubble { background: color-mix(in oklab, var(--v2-text) 8%, transparent); color: var(--v2-text); border-bottom-left-radius:5px; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-msg--err .v2-agentchat-msg__bubble { background: var(--v2-danger-bg); color: var(--v2-danger); }
/* 26 ส.ค. 2026: การ์ดโครงการ ([[pid:...]]) + ชิปอ้างอิง ([[ref:...]]) ในบับเบิลแชท — พอร์ตแนวคิดจาก agent.html
   (buildProjInlineCard/buildQaRefChip) มาใช้สไตล์ v2 เอง ไม่ใช้ inline style/สีเดิมของ agent.html ตรงๆ */
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-projchip {
  display:flex; align-items:center; gap:8px; width:100%; margin-top:4px; padding:8px 10px;
  border-radius:12px; border:1px solid var(--v2-hair); background: var(--v2-surface-solid); color: var(--v2-text);
  cursor:pointer; text-align:left; font:inherit;
}
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-msg--user .v2-agentchat-projchip { background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.3); color:#fff; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-projchip:active { transform: scale(.98); }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-projchip .ico { flex:none; font-size:16px; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-projchip .txt { flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:1px; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-projchip .txt b { font-size:.75rem; font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-projchip .txt small { font-size:.6875rem; opacity:.75; }
/* 26 ส.ค. 2026: ผู้ใช้ขอเอาไอคอน 📎 ออก ให้หน้าตาเหมือนป้าย "ข้อความ" ในหัวข้อฟิลด์ลิ้นชักตัวกรอง (v2-badge
   v2-badge--idle) — สี/พื้น/ตัวอักษรมาจาก 2 คลาสนั้นแล้ว (ดู HTML: class="v2-badge v2-badge--idle
   v2-agentchat-refchip") เหลือแค่ override จุดที่ badge เฉยๆ ไม่มี (cursor/margin/active feedback + สี on
   พื้นบับเบิลผู้ใช้ที่เป็นสี accent เข้ม ซึ่ง badge--idle ปกติจางเกินไปอ่านไม่ออก) */
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-refchip { margin:0 2px; cursor:pointer; vertical-align:middle; border:none; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-msg--user .v2-agentchat-refchip { background: rgba(255,255,255,.22); color:#fff; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-refchip:active { transform: scale(.94); }
/* 26 ส.ค. 2026: ผู้ใช้ขอเปลี่ยน "AI กำลังคิด" เป็นแนวคิด "Bounce Row" (จุดเด้งขึ้นสลับจังหวะทีละจุด) — ตีความใหม่
   ด้วยค่าของธีมเราเอง (4 จุดสีเดียวกัน var(--v2-text-mute) ไม่ใช่ไล่สีรุ้งแบบต้นฉบับ เพราะเป็นแค่ตัวบอกสถานะ
   "กำลังพิมพ์" ในบับเบิลแชท ไม่ใช่ของตกแต่ง — ระยะเด้งย่อจาก 30px เหลือ 5px ให้พอดีกับจุดขนาด 6px ในบับเบิลเล็กๆ) */
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-typing { display:flex; align-items:flex-end; gap:4px; padding:12px 14px; height:32px; }
/* 26 ส.ค. 2026 (แก้ต่อ): ต้นฉบับ "Bounce Row" ที่ผู้ใช้ส่งมาแต่ละจุดคนละสี (แดง/เหลือง/เขียว/ฟ้า) — รอบแรกใช้
   สีเดียว (text-mute) ทั้งหมดเพราะคิดว่าสีรุ้งดูเล่นเกินไปสำหรับ status indicator ผู้ใช้ทักว่าไม่ตรงกับที่ส่งมา
   เปลี่ยนเป็นสีต่างกันจริงต่อจุดตามที่ขอ — ใช้ชุดสี semantic ของธีมเราเอง (--v2-danger/--v2-warn/--v2-ok/
   --v2-info) แทนค่า hex ตรงจากต้นฉบับ ให้เข้ากับโทนสีที่ใช้อยู่แล้วทั้งระบบ */
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-typing span { width:6px; height:6px; border-radius:50%; animation: v2-agentchat-typing .8s ease-in-out infinite alternate; }
/* 26 ส.ค. 2026 (แก้ต่อ): สี semantic ตรงๆ (--v2-danger/--v2-warn/--v2-ok/--v2-info) สดเกินไปสำหรับจุดเล็กๆ
   ผู้ใช้ขอให้พาสเทลลง — ผสมขาวเข้าไป (color-mix) แทนการลดแค่ opacity (ลด opacity แล้วจะจางเห็นพื้นหลังทะลุ
   ไม่ใช่ "พาสเทล" จริง — พาสเทล = สีเดียวกันแต่อ่อนลง ยังทึบแสงอยู่) */
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-typing span:nth-child(1) { background: color-mix(in oklab, var(--v2-danger) 55%, white); animation-delay:0s; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-typing span:nth-child(2) { background: color-mix(in oklab, var(--v2-warn) 55%, white); animation-delay:.15s; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-typing span:nth-child(3) { background: color-mix(in oklab, var(--v2-ok) 55%, white); animation-delay:.3s; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat-typing span:nth-child(4) { background: color-mix(in oklab, var(--v2-info) 55%, white); animation-delay:.45s; }
@keyframes v2-agentchat-typing { 0% { transform: translateY(0) scaleX(1); } 100% { transform: translateY(-5px) scaleX(.85); } }
@media (prefers-reduced-motion: reduce) { [data-v2-theme="ios-liquid-glass"] .v2-agentchat-typing span { animation:none; opacity:.7; } }

[data-v2-theme="ios-liquid-glass"] .v2-agentchat__foot {
  /* 26 ส.ค. 2026 (แก้บั๊ก "ตัวหนังสือทับช่องพิมพ์"): เดิม position:absolute ลอยทับอยู่บนสุด ไม่กินพื้นที่ใน
     flex column ของ wrap เลย — .v2-agentchat__body (flex:1 1 auto) เลยคำนวณความสูงที่ใช้ได้ผิด (นึกว่ามีที่ว่าง
     เต็มพื้นที่ที่เหลือทั้งหมด ทั้งที่จริง foot ลอยทับพื้นที่ท้ายๆ อยู่) ข้อความยาวๆ ใน body เลยเลื่อนไปโผล่ใต้/ทับ
     foot ได้ (ซ่อนอยู่หลัง glass ของ foot แต่ยังมองทะลุเห็นเงาตัวหนังสือ) เปลี่ยนเป็น flex item ปกติในลำดับ
     column (head/body/foot) แทน — body คำนวณพื้นที่เหลือถูกต้องจริง ไม่ต้องเดา padding-bottom ชดเชยอีก */
  flex:0 0 auto; display:flex; align-items:flex-end; gap:8px;
  padding:10px 14px calc(14px + env(safe-area-inset-bottom));
  border-top: .5px solid var(--v2-hair);
}
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__input {
  flex:1 1 auto; min-width:0; max-height:100px; resize:none; border:none; outline:none;
  padding:10px 14px; border-radius:20px; font:inherit; font-size:.8125rem; color: var(--v2-text);
  background: color-mix(in oklab, var(--v2-text) 7%, transparent);
}
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__input::placeholder { color: var(--v2-text-mute); }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__send {
  flex:none; width:38px; height:38px; border-radius:50%; border:none; cursor:pointer;
  display:flex; align-items:center; justify-content:center; background: var(--v2-accent-1); color:#fff;
  transition: transform .15s, opacity .15s;
}
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__send:active { transform: scale(.9); }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__send:disabled { opacity:.4; cursor:default; }
[data-v2-theme="ios-liquid-glass"] .v2-agentchat__send svg { width:16px; height:16px; }
@media (prefers-reduced-motion: reduce) { [data-v2-theme="ios-liquid-glass"] .v2-agentchat__send:active { transform:none; } }
/* ปุ่มน้ำที่ dock ลงมาอยู่ใต้แผงนี้ (ui-v2.js orbDock) มองเห็นได้เต็มลูกในพื้นที่ padding-bottom ที่เผื่อไว้ข้างบน —
   ไม่ต้องมี element พิเศษของ .v2-agentchat เอง orb เป็นปุ่มลอย fixed แยกตัวอยู่แล้ว */
[data-v2-theme="ios-liquid-glass"] .v2-search { display:flex; align-items:center; gap:8px; padding:8px 12px; background: var(--v2-surface-2); border:none; border-radius: var(--v2-r-control); }
[data-v2-theme="ios-liquid-glass"] .v2-search input { font-family: var(--v2-font); color: var(--v2-text); font-size:1.0625rem; background:transparent; border:none; outline:none; flex:1; min-width:0; }
[data-v2-theme="ios-liquid-glass"] .v2-ac-menu { background: var(--v2-surface-solid); border:1px solid var(--v2-material-border); border-radius: var(--v2-r-panel); box-shadow: var(--v2-shadow-2); }
/* toast = liquid-glass notification card (verbatim layout จาก codefronts.com "liquid-glass-toast-notification"
   lg-14 — ผู้ใช้สั่งให้การแจ้งเตือนทั้งระบบใช้แบบนี้ 1 ส.ค. 2026): ไอคอนวงกลมสี + หัวข้อ/เนื้อหา + ปุ่มปิด +
   แถบนับถอยหลังด้านล่าง เลื่อนเข้าจากขวา — ไม่ใช่ HUD pill กลางล่างแบบเดิมอีกต่อไป */
[data-v2-theme="ios-liquid-glass"] .v2-toast {
  background-color: color-mix(in oklab, white 14%, transparent);
  border: 1px solid rgba(255,255,255,.3);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.4), 0 20px 40px -20px rgba(0,0,0,.7);
  backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);
}
[data-v2-theme="ios-liquid-glass"] .v2-toast--ok .ico { background: var(--v2-ok); }
[data-v2-theme="ios-liquid-glass"] .v2-toast--err .ico { background: var(--v2-danger); }
[data-v2-theme="ios-liquid-glass"] .v2-toast--warn .ico { background: var(--v2-warn); }
[data-v2-theme="ios-liquid-glass"] .v2-toast--info .ico { background: var(--v2-info); }
[data-v2-theme="ios-liquid-glass"] .v2-toast .msg { color: var(--v2-text); }
[data-v2-theme="ios-liquid-glass"] .v2-toast .close { color: var(--v2-text); background: color-mix(in oklab, var(--v2-text) 12%, transparent); }
[data-v2-theme="ios-liquid-glass"] .v2-toast .close:hover { background: color-mix(in oklab, var(--v2-text) 20%, transparent); }
[data-v2-theme="ios-liquid-glass"] .v2-toast::after { background: color-mix(in oklab, var(--v2-text) 70%, transparent); }
[data-v2-theme="ios-liquid-glass"] .v2-progress { height:5px; background: var(--v2-surface-2); border-radius:999px; overflow:hidden; }
[data-v2-theme="ios-liquid-glass"] .v2-progress-fill { background: var(--v2-accent-1); border-radius:999px; }
[data-v2-theme="ios-liquid-glass"] .v2-chart-card { padding: var(--v2-sp-5); background-color: color-mix(in oklab, white 12%, transparent); border: 1px solid rgba(255,255,255,.35); border-radius: var(--v2-r-panel); box-shadow: 0 30px 60px -30px rgba(0,0,0,.65), inset 0 1px 0 rgba(255,255,255,.5); backdrop-filter: blur(18px) saturate(180%); -webkit-backdrop-filter: blur(18px) saturate(180%); }
[data-v2-theme="ios-liquid-glass"] .v2-chart-head h4 { font-size:.9375rem; font-weight:600; color: var(--v2-text); text-transform:none; letter-spacing:0; }
[data-v2-theme="ios-liquid-glass"] .v2-seg { border-radius:9px; border:none; background: var(--v2-surface-2); padding:2px; overflow:hidden; }
[data-v2-theme="ios-liquid-glass"] .v2-seg button.is-active { background: var(--v2-surface-solid); box-shadow: var(--v2-shadow-1); border-radius:7px; }
/* accordion = verbatim จาก codefronts.com "liquid-glass-accordion-component" (lg-16, 1 ส.ค. 2026) — ใช้ย่อ
   คำตอบ Q&A (ปิดไว้ กดขยายเอา) และย่อเนื้อหายาวใน card ต่างๆ; แต่ละ item เป็นพื้นผิวกระจกของตัวเอง (ไม่ซ้อน
   ใต้กระจกอื่น — ถ้าจะใช้ในที่ที่มีกระจกอยู่แล้วเช่น dropdown ให้ใช้ .v2-navdd__acc (flat, ไม่มี backdrop-fil
   ter) แทน ไม่ใช่ตัวนี้ — ดู §3.9) เปิด/ปิดด้วย native details/summary ล้วน ไม่มี JS */
[data-v2-theme="ios-liquid-glass"] .v2-accordion { display:flex; flex-direction:column; gap:14px; }
[data-v2-theme="ios-liquid-glass"] .v2-accordion__item {
  border-radius: var(--v2-r-card); overflow:hidden;
  background-color: color-mix(in oklab, white 10%, transparent);
  border: 1px solid rgba(255,255,255,.28);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.4), 0 16px 34px -22px rgba(0,0,0,.6);
  backdrop-filter: blur(18px) saturate(170%); -webkit-backdrop-filter: blur(18px) saturate(170%);
  transition: border-color .3s;
}
[data-v2-theme="ios-liquid-glass"] .v2-accordion__item[open] { border-color: rgba(255,255,255,.5); }
[data-v2-theme="ios-liquid-glass"] .v2-accordion__q {
  list-style:none; cursor:pointer; display:flex; align-items:center; justify-content:space-between; gap:16px;
  font-size:.9375rem; font-weight:700; color: var(--v2-text); padding:16px 18px;
}
[data-v2-theme="ios-liquid-glass"] .v2-accordion__q::-webkit-details-marker { display:none; }
[data-v2-theme="ios-liquid-glass"] .v2-accordion__mk { position:relative; flex:none; width:20px; height:20px; }
[data-v2-theme="ios-liquid-glass"] .v2-accordion__mk::before, [data-v2-theme="ios-liquid-glass"] .v2-accordion__mk::after {
  content:''; position:absolute; top:50%; left:50%; width:14px; height:2px; background: var(--v2-text);
  transform: translate(-50%,-50%); transition: transform .3s;
}
[data-v2-theme="ios-liquid-glass"] .v2-accordion__mk::after { transform: translate(-50%,-50%) rotate(90deg); }
[data-v2-theme="ios-liquid-glass"] .v2-accordion__item[open] .v2-accordion__mk::after { transform: translate(-50%,-50%) rotate(0); }
.v2-accordion__wrap { display:grid; grid-template-rows:0fr; transition: grid-template-rows .35s cubic-bezier(.4,0,.2,1); }
.v2-accordion__item[open] .v2-accordion__wrap { grid-template-rows:1fr; }
[data-v2-theme="ios-liquid-glass"] .v2-accordion__a { overflow:hidden; font-size:.875rem; line-height:1.65; color: var(--v2-text-dim); padding:0 18px; }
[data-v2-theme="ios-liquid-glass"] .v2-accordion__item[open] .v2-accordion__a { padding:0 18px 18px; }
@media (prefers-reduced-motion: reduce) {
  .v2-accordion__wrap { transition:none; }
  [data-v2-theme="ios-liquid-glass"] .v2-accordion__mk::before, [data-v2-theme="ios-liquid-glass"] .v2-accordion__mk::after { transition:none; }
}
/* ---------- Backdrop/spinner ขนาด — คงค่ากลาง ไม่ต้องแยกธีม ---------- */
/* 11 ส.ค. 2026: ผู้ใช้ลองเอาม่านมืดออกก่อนแล้วพบว่า "ไม่เด่น" เพราะไม่มีอะไรให้การ์ดตัดกับพื้นหลัง — ถามว่าทำม่าน
   บังทุกอย่างยกเว้นตัว popup ได้ไหม: **ทำไม่ได้ด้วย backdrop-filter ล้วนๆ** เพราะกระจกของ popup สุ่มพิกเซลจาก
   สิ่งที่ render อยู่ข้างใต้จริง (รวมม่านมืดที่มันซ้อนอยู่) เลี่ยงไม่ได้ — มาตรฐานที่ระบบใหญ่ๆ (iOS/Material) ใช้กัน
   คือหลักการเดิม (มืดพื้นหลังทั้งจอ) + **ชดเชยด้วยการให้พื้นผิวกระจกของ popup เองมี tint/opacity สูงพอที่จะอ่านชัด
   โดยไม่ขึ้นกับว่าข้างหลังมืดแค่ไหน** — เอาม่านกลับมาเหมือนเดิม แล้วไปเพิ่ม tint เฉพาะโหมดสว่างที่ `.v2-modal` แทน
   (ดูรายละเอียดที่บรรทัดกฎ .v2-modal ด้านล่าง) */
.v2-backdrop { position:fixed; inset:0; background: rgba(2,4,10,.55); z-index:90; opacity:0; pointer-events:none; transition: opacity .2s var(--v2-ease); }
.v2-backdrop.is-open { opacity:1; pointer-events:auto; }
.v2-modal { position:fixed; top:50%; left:50%; transform:translate(-50%,-46%); width:min(480px,92vw); max-height:88vh; overflow:auto; z-index:91; box-shadow: var(--v2-shadow-3); opacity:0; pointer-events:none; transition: opacity .2s var(--v2-ease), transform .2s var(--v2-ease); }
.v2-modal.is-open { opacity:1; pointer-events:auto; transform:translate(-50%,-50%); }
.v2-modal--lg { width:min(720px,94vw); }
.v2-modal--full { width:100vw; height:100vh; max-height:100vh; top:0; left:0; transform:none; border-radius:0 !important; }
.v2-modal--full.is-open { transform:none; }
.v2-modal-head { display:flex; align-items:center; justify-content:space-between; padding:16px 18px; border-bottom:1px solid var(--v2-hair); }
.v2-modal-body { padding:18px; }
.v2-modal-foot { display:flex; justify-content:flex-end; gap:8px; padding:14px 18px; border-top:1px solid var(--v2-hair); }
.v2-modal-close { width:30px; height:30px; border:1px solid var(--v2-hair); background:transparent; color: var(--v2-text-dim); cursor:pointer; border-radius:50%; display:flex; align-items:center; justify-content:center; }
.v2-drawer { position:fixed; top:0; bottom:0; width:min(380px,88vw); z-index:91; transition: transform .25s var(--v2-ease); overflow-y:auto; box-shadow: var(--v2-shadow-3); }
.v2-drawer--right { right:0; transform: translateX(100%); }
.v2-drawer--right.is-open { transform: translateX(0); }
.v2-drawer--left { left:0; transform: translateX(-100%); }
.v2-drawer--left.is-open { transform: translateX(0); }
.v2-drawer--bottom { top:auto; left:0; right:0; bottom:0; width:100%; height:min(70vh,600px); transform: translateY(100%); }
.v2-drawer--bottom.is-open { transform: translateY(0); }
/* top ชิดกับ .v2-hero-band (calc(...+78px)) เพื่อไม่ให้ทับ .v2-navpill ที่ลอยอยู่มุมบนขวาแล้ว */
.v2-toast-host {
  position:fixed; top: calc(env(safe-area-inset-top) + 78px); right: calc(20px + env(safe-area-inset-right));
  z-index:80; display:flex; flex-direction:column; gap:12px; width:min(320px,86vw); pointer-events:none;
}
.v2-toast {
  position:relative; isolation:isolate; overflow:hidden; pointer-events:auto;
  display:flex; gap:12px; align-items:flex-start; padding:14px 40px 14px 14px; border-radius:16px;
  animation: v2-toast-in .4s cubic-bezier(.2,.9,.2,1);
}
.v2-toast.is-out { animation: v2-toast-out .3s ease forwards; }
.v2-toast .ico { flex:none; width:26px; height:26px; border-radius:50%; display:grid; place-items:center; font-size:14px; font-weight:900; color:#fff; }
.v2-toast .msg { font-size:14px; line-height:1.4; }
.v2-toast .msg strong { display:block; font-weight:800; margin-bottom:2px; }
.v2-toast .close { position:absolute; top:8px; right:8px; width:24px; height:24px; border-radius:50%; border:none; cursor:pointer; font-size:13px; }
.v2-toast::after { content:''; position:absolute; left:0; bottom:0; height:3px; width:100%; transform-origin:left; animation: v2-toast-rim var(--life,4000ms) linear forwards; }
@keyframes v2-toast-in { from { opacity:0; transform: translateX(110%); } to { opacity:1; transform: translateX(0); } }
@keyframes v2-toast-out { to { opacity:0; transform: translateX(110%); } }
@keyframes v2-toast-rim { from { transform: scaleX(1); } to { transform: scaleX(0); } }
@media (prefers-reduced-motion: reduce) {
  .v2-toast { animation:none; }
  .v2-toast.is-out { animation:none; opacity:0; }
}
/* spinner = verbatim จาก codefronts.com "particle-scatter-burst-spinner" (sp-20, 2 ส.ค. 2026) — สีรุ้ง 4 สีวน
   ตามต้นฉบับตรงๆ (ไม่ผูกกับ --v2-accent) เจตนาให้ดูสนุก/มีชีวิตชีวาแตกต่างจากโทนกระจกเรียบของธีมที่เหลือ —
   ผู้ใช้เลือก component นี้เจาะจงสำหรับจุดโหลดข้อมูลทั้งระบบ ขนาด sm/md/lg สเกลด้วย % + CSS var เดียว
   (--v2-spin-travel) ไม่ต้องเขียนกฎซ้ำ 3 ชุด */
.v2-spinner { display:inline-block; position:relative; }
.v2-spinner--sm { width:16px; height:16px; --v2-spin-travel: 6.72px; }
.v2-spinner--md { width:28px; height:28px; --v2-spin-travel: 11.76px; }
.v2-spinner--lg { width:40px; height:40px; --v2-spin-travel: 16.8px; }
.v2-spinner__burst { position:relative; width:100%; height:100%; animation: v2-spinner-rotate 3s linear infinite; }
.v2-spinner__particle {
  position:absolute; top:50%; left:50%; width:8%; height:8%; border-radius:50%; margin:-4% 0 0 -4%;
  animation: v2-spinner-scatter 1.5s ease-out infinite;
}
.v2-spinner__particle:nth-child(1) { background:#ff6b6b; animation-delay:0s;   --r: rotate(0deg); }
.v2-spinner__particle:nth-child(2) { background:#ffd93d; animation-delay:.1s; --r: rotate(45deg); }
.v2-spinner__particle:nth-child(3) { background:#6bcb77; animation-delay:.2s; --r: rotate(90deg); }
.v2-spinner__particle:nth-child(4) { background:#4d96ff; animation-delay:.3s; --r: rotate(135deg); }
.v2-spinner__particle:nth-child(5) { background:#ff6b6b; animation-delay:.4s; --r: rotate(180deg); }
.v2-spinner__particle:nth-child(6) { background:#ffd93d; animation-delay:.5s; --r: rotate(225deg); }
.v2-spinner__particle:nth-child(7) { background:#6bcb77; animation-delay:.6s; --r: rotate(270deg); }
.v2-spinner__particle:nth-child(8) { background:#4d96ff; animation-delay:.7s; --r: rotate(315deg); }
@keyframes v2-spinner-scatter {
  0% { transform: var(--r,rotate(0deg)) translateX(0) scale(1); opacity:1; }
  60% { opacity:.8; }
  100% { transform: var(--r,rotate(0deg)) translateX(var(--v2-spin-travel)) scale(0); opacity:0; }
}
@keyframes v2-spinner-rotate { to { transform: rotate(360deg); } }
.v2-spinner__core {
  position:absolute; inset:40%; border-radius:50%; background:#fff; box-shadow:0 0 12px #fff;
  animation: v2-spinner-pulse 1.5s ease-in-out infinite;
}
@keyframes v2-spinner-pulse {
  0%, 100% { transform:scale(.8); opacity:.6; }
  50% { transform:scale(1.2); opacity:1; }
}
@media (prefers-reduced-motion: reduce) {
  .v2-spinner__particle, .v2-spinner__burst, .v2-spinner__core { animation:none; }
}
.v2-skeleton { background: linear-gradient(90deg, var(--v2-surface-2) 25%, var(--v2-hair) 37%, var(--v2-surface-2) 63%); background-size:400% 100%; animation: v2-shimmer 1.4s ease infinite; border-radius:8px; }
@keyframes v2-shimmer { 0% { background-position:100% 50%; } 100% { background-position:0 50%; } }
.v2-empty { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:48px 20px; color: var(--v2-text-mute); text-align:center; gap:10px; }
.v2-empty svg { opacity:.5; }
.v2-empty .msg { font-size:13px; }
.v2-badge { display:inline-flex; align-items:center; gap:5px; font-size:10.5px; letter-spacing:.05em; text-transform:uppercase; padding:3px 9px; }
.v2-badge--ok { background: var(--v2-ok-bg); color: var(--v2-ok); }
.v2-badge--warn { background: var(--v2-warn-bg); color: var(--v2-warn); }
.v2-badge--danger { background: var(--v2-danger-bg); color: var(--v2-danger); }
.v2-badge--info { background: var(--v2-info-bg); color: var(--v2-info); }
.v2-badge--idle { background: var(--v2-idle-bg); color: var(--v2-text-mute); }
.v2-status { display:inline-flex; align-items:center; gap:7px; font-size:11px; font-weight:600; color: var(--v2-text-dim); }
.v2-status--done { color: var(--v2-ok); }
.v2-status--pending { color: var(--v2-warn); }
.v2-status--empty { color: var(--v2-text-mute); }
.v2-card-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(260px,1fr)); gap: var(--v2-sp-3); }
.v2-chipbar { display:flex; flex-wrap:wrap; gap:8px; }
.v2-chip-x { width:15px; height:15px; background:none; border:none; display:flex; align-items:center; justify-content:center; font-size:9px; color: var(--v2-text-mute); cursor:pointer; }
.v2-ac { position:relative; }
.v2-ac-menu { position:absolute; top:calc(100% + 4px); left:0; right:0; max-height:240px; overflow-y:auto; z-index:40; display:none; }
.v2-ac-menu.is-open { display:block; }
.v2-ac-item { padding:9px 12px; font-size:13px; color: var(--v2-text); cursor:pointer; }
.v2-ac-item:hover, .v2-ac-item.is-active { background: var(--v2-surface-2); }
.v2-range { display:flex; align-items:center; gap:8px; }
.v2-range .v2-inp { text-align:center; }
.v2-field { display:flex; flex-direction:column; gap:5px; margin-bottom: var(--v2-sp-3); }
.v2-field .hint { font-size:11px; color: var(--v2-text-mute); }
.v2-field .err { font-size:11px; color: var(--v2-danger); }
@media (max-width: 640px) {
.v2-inp, .v2-sel, .v2-ta { font-size:16px; }
/* 26 ส.ค. 2026: .v2-agentchat__input (13px) ไม่ได้อยู่ในกฎกันซูมนี้มาก่อน — โฟกัสช่องพิมพ์แชทบน iOS แล้ว
   auto-zoom เข้า (เพราะ <16px) วิว viewport ที่มองเห็นเปลี่ยน ทำให้ปุ่มน้ำที่ dock ไว้กึ่งกลางล่าง (position:fixed
   left:50%) ดูไม่อยู่ตรงกลางอีกต่อไป (คำนวณ 50% จาก layout viewport เดิม ไม่ใช่ visual viewport ที่ซูมแล้ว) —
   เพิ่มเข้ากฎเดียวกันนี้แก้ที่ต้นเหตุ ไม่ต้องไล่แก้ตำแหน่ง orb เอง */
.v2-agentchat__input { font-size:16px; }
}
/* กัน iOS auto-zoom */.v2-radio { width:16px; height:16px; accent-color: var(--v2-accent-1); cursor:pointer; }
/* .v2-check = โครงสร้าง/กลไกจาก "Neon Glow Checkbox" ของ codefronts.com (cb-08, §4.20) — input ซ่อน +
   span.v2-check-box พี่น้อง ควบคุมด้วย :checked ~ ., checkmark mask ::after
   3 ส.ค. 2026 (ต่อ): ผู้ใช้สั่ง "เอา glow ออกแล้วปรับเป็นสีดำและขาว" — ลบ ::before (glow halo + pulse
   animation + keyframe) ทิ้งทั้งหมด เหลือแค่กรอบ+ติ๊กถูกธรรมดา
   3 ส.ค. 2026 (ต่ออีกที): ผู้ใช้แก้ไข "เครื่องหมายเป็นสีดำใน light แล้วสีขาวใน dark ไม่ต้องถมสีทั้งหมด" —
   รอบแรกเข้าใจผิดไปถมพื้นกล่องด้วย (background:var(--v2-text)) แก้เหลือแค่**ตัวเครื่องหมายถูกเอง** กล่องตอน
   checked ไม่ถมพื้นอีกต่อไป มีแค่กรอบเปลี่ยนสี
   3 ส.ค. 2026 (ต่ออีกที): ผู้ใช้สั่ง "ปรับไปใช้สีรองแทน" — เปลี่ยนจาก `var(--v2-text)` (Label Primary ดำ/ขาว
   เข้มสุด) เป็น **`var(--v2-text-dim)`** (Label Secondary — ธีมนี้ใช้เป็น "สีรอง" มาตลอดทั้งระบบ เช่น
   .v2-kpi-lbl/.v2-brand__sub) ให้ checkbox ดูนุ่มนวลกว่าตัวหนังสือหลัก ไม่ใช่สีเข้มสุดของธีม
   markup: <label class="v2-check-row"><input type="checkbox" class="v2-check">
           <span class="v2-check-box"></span> ข้อความ</label> */
.v2-check-row { display:inline-flex; align-items:center; gap:10px; cursor:pointer; }
.v2-check { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; }
.v2-check-box {
  position:relative; flex:none; width:24px; height:24px; border-radius:7px;
  border:2px solid var(--v2-hair-strong); background: var(--v2-surface-2);
  transition: border-color .25s, background .25s;
}
.v2-check-box::after {
  content:""; position:absolute; inset:0; opacity:0; transform:scale(.5);
  background: var(--v2-text-dim);
  transition: opacity .22s, transform .22s cubic-bezier(.2,.9,.3,1.3);
  -webkit-mask: no-repeat center/15px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
  mask: no-repeat center/15px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
}
.v2-check:checked ~ .v2-check-box { border-color: var(--v2-text-dim); }
.v2-check:checked ~ .v2-check-box::after { opacity:1; transform:scale(1); }
.v2-check:focus-visible ~ .v2-check-box { outline:3px solid var(--v2-text-dim); outline-offset:3px; }
@media (prefers-reduced-motion: reduce) {
  .v2-check-box, .v2-check-box::after { transition:none; }
}
.v2-switch { position:relative; display:inline-block; width:40px; height:22px; }
.v2-switch input { opacity:0; width:0; height:0; }
.v2-card-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; }
.v2-rail-body { flex:1; min-width:0; position:relative; z-index:1; }
.v2-rail-foot { display:flex; align-items:center; justify-content:space-between; margin-top:10px; }
.v2-rail-meta { display:flex; gap:10px; flex-wrap:wrap; }
.v2-table-wrap { overflow: auto; -webkit-overflow-scrolling: touch; }
table.v2-table { width:100%; border-collapse:collapse; font-size:13px; }
.v2-table td.is-num { text-align:right; font-variant-numeric: tabular-nums; }
.v2-table thead th { white-space:nowrap; }
.v2-table tbody tr:hover { background: var(--v2-surface-2); }
.v2-table tr:last-child td { border-bottom:none; }
.v2-chart-body { position:relative; min-height:180px; }
.v2-chart-legend { display:flex; flex-wrap:wrap; gap:10px; margin-top:10px; font-size:11px; color: var(--v2-text-mute); }
.v2-divider { height:1px; background: var(--v2-hair); margin: var(--v2-sp-3) 0; border:none; }
.v2-pager { display:flex; align-items:center; justify-content:center; gap:10px; }
.v2-loadmore { display:flex; justify-content:center; padding: var(--v2-sp-4) 0; }
.v2-breadcrumb { font-size:11px; color: var(--v2-text-mute); display:flex; align-items:center; gap:6px; }
.v2-seg { display:inline-flex; }
.v2-seg button { font-family: var(--v2-font); font-size:11px; font-weight:600; border:none; background:transparent; color: var(--v2-text-dim); padding:7px 14px; cursor:pointer; }
.v2-btn:disabled, .v2-btn.is-loading { opacity:.45; cursor:not-allowed; pointer-events:none; }
.v2-confirm-actions { display:flex; gap:8px; width:100%; }

/* ============================================================
   KPI SCENE CARDS — verbatim จาก irrigation-widgets-v2.html (Codex\designmd, 3 ส.ค. 2026)
   ผู้ใช้ส่งไฟล์ต้นแบบมาให้ใช้ตรงๆ ("การ์ด KPI ให้ใช้ตามนี้", ขอ 6 การ์ดครบ + ภาพเคลื่อนไหวเฉพาะตัวทุกใบ
   ไม่ตัดทอน) — คัดลอก CSS ทั้งบล็อกจากไฟล์ต้นฉบับ เปลี่ยน prefix class จาก lg- เป็น v2-kpi- เท่านั้น (ไม่แก้
   ค่าอื่นเลย) ตัดเฉพาะส่วน .v2-kpi-dashboard (พื้นหลังเต็มจอ + header) ออก เพราะเรามี .v2-wallpaper + navbar
   ของตัวเองอยู่แล้ว ส่วน .v2-kpi-card ทั้งหมด (รวม "Formal executive-dashboard" cascade ที่ override ซ้อนท้าย
   ไฟล์ต้นฉบับ — เป็น cascade ตั้งใจ ไม่ใช่ของซ้ำ ห้ามลบ) คงไว้ตรงทุกค่า
   การ์ดนี้เป็นธีมมืดของตัวเอง (--ink, --accent, --series) แยกจาก light/dark mode ของระบบ v2 theme — ไม่ปรับตาม
   data-v2-mode เพราะต้นฉบับตั้งใจให้เป็น "การ์ดมืดหรูเหนือพื้นหลังสว่าง" เหมือน hero widget ไม่ใช่การ์ดที่ตาม
   เนื้อธีมทั้งหน้า — asset SVG (thailand.svg, rice-asset-1/2/3.svg, grain-wheat-original.svg, benefit-*.svg,
   bang-city.svg) คัดลอกมาไว้ที่ /assets/kpi/ ของโปรเจกต์นี้ (href ในโค้ดต้นฉบับเป็น "assets/xxx.svg" สัมพัทธ์
   กับไฟล์ demo เดิม — ต้องแก้เป็น "/assets/kpi/xxx.svg" ให้ตรงตำแหน่งจริงในเว็บนี้)
   markup 6 การ์ด (วงเงินรวม/จำนวนรายการ/พื้นที่ชลประทานเพิ่มขึ้น/พื้นที่รับประโยชน์/ปริมาณน้ำเพิ่มขึ้น/ครัวเรือน)
   สร้างจาก ui-v2.js buildKpiSceneGrid() — เรียกแทน kpi-row แบบเดิมใน navbar()
   3 ส.ค. 2026 (ต่อ): ผู้ใช้ทักว่า "ระยะห่างหัวข้อกับตัวเลขมีปัญหา" — ตรวจแล้วพบว่าตอนตัดส่วน .v2-kpi-dashboard
   ออก (บรรทัดข้างบน) เผลอตัดกฎ reset `.lg-dashboard, .lg-dashboard * { margin:0; padding:0; box-sizing:
   border-box; }` ของต้นฉบับไปด้วย (มันเป็นกฎแรกสุดในไฟล์ ผูกกับ class เดียวกับพื้นหลังที่ไม่ต้องการ เลยโดนตัด
   ไปพร้อมกันโดยไม่ตั้งใจ) — ผลคือ `.v2-kpi-card__label`/`__unit` (เป็นแท็ก `<p>`) ไม่มี reset margin ของตัวเอง
   เลยโดน user-agent stylesheet ใส่ margin:1em 0 (~13px บน+ล่าง) มาแทรกเพิ่มระหว่างหัวข้อกับตัวเลข ทำให้ระยะ
   ห่างเกินกว่าที่ต้นฉบับตั้งใจ (32px จริง แทนที่จะเป็นแค่ margin-top:19px ของ `.v2-kpi-card__metric` ตามสเปก)
   แก้โดยเพิ่ม reset กลับมาเฉพาะสโคป `.v2-kpi-card` (ไม่ใช้ `.v2-kpi-dashboard` เพราะ class นั้นไม่มีในมาร์กอัปจริง
   ของเราแล้ว) */
    .v2-kpi-card, .v2-kpi-card * { margin:0; padding:0; box-sizing:border-box; }
    /* 11 ส.ค. 2026: เงาการ์ดใบแรก/สุดท้ายโดนตัดห้วนๆ ตรงขอบจอ (blur 60px spread -30px ของ .v2-kpi-card ต้องการ
       พื้นที่ราว 30px ถึงจะจางหมดพอดี) — เพิ่ม padding ให้ .v2-kpi-grid เท่ากับ .v2-content เองก่อน (20-28px)
       ผู้ใช้ทดสอบจริงพบว่าแย่กว่าเดิม (เห็นเป็นช่องว่างเปล่าก่อนถึงการ์ดแรกตอนเลื่อนสุดซ้าย เพราะแนวนอนนี้ผู้ใช้
       scroll ผ่าน padding จริง ต่างจากเงาล่างที่ไม่มีใครสังเกตเพราะ overflow-y:hidden ไม่ใช่แกนที่ scroll ผ่าน) —
       กลับไปใช้ 4px เดิม + ไล่โปร่งใสด้วย mask-image แทน (ซ่อนรอยตัดด้วยภาพลวงตา ไม่ได้แก้ที่ต้นเหตุการตัดเงาจริง)
       ปรับ 3 จุด: (1) mask-image ไล่โปร่งใสขอบซ้าย-ขวา (สมมาตร 30px) (2) scroll-snap-type:none กันการ์ดเด้งกลับ
       ตอนปล่อยลาก (3) spacer element ก่อนการ์ดแรก/หลังการ์ดสุดท้าย (ดู .v2-kpi-grid__edge-spacer ด้านล่าง +
       จุดแทรกใน design-system-v2.html)
       ⚠ บทเรียน (12 ส.ค. 2026): เคยมีไฟล์ ui-v2-fix.css (โหลดเฉพาะ design-system-v2-fix.html) เป็นสำเนา KPI ซ้ำ
       ทำให้แก้ผิดไฟล์เสียหลายรอบ (แก้ที่สำเนา หน้าจริงไม่เห็นผล) — ลบ fix page/CSS/JS ทิ้งทั้งหมดแล้ว ตอนนี้
       ui-v2.css นี้เป็นตัวเดียวที่ design-system-v2.html โหลด ห้ามสร้างสำเนา CSS ของ KPI ขึ้นมาใหม่อีก */
    .v2-kpi-grid {
      display: flex;
      flex-wrap: nowrap;
      gap: 18px;
      overflow-x: auto;
      overflow-y: hidden;
      /* 20 ส.ค. 2026: bleed เดิม 50px (ตั้งใจให้เท่ากับ "รัศมีเงา" offset-y 28 + blur 52 − spread 30 = 50px แบบ
         คำนวณตรงๆ) — แต่ box-shadow ของ browser จริงไม่ได้จางเป็น 0 พอดีที่ระยะ blur ตามตัวเลข (Gaussian falloff
         ยังเหลือความทึบอยู่บ้างเกินระยะ blur เล็กน้อย) ทำให้เงาโดน overflow-y:hidden ตัดขาดกลางคันเป็นเส้นคม
         (ไม่ใช่จางสนิทเป็นธรรมชาติ) — ผู้ใช้ทักว่าขอบตาราง/ toolbar สีไม่สม่ำเสมอ พิสูจน์แล้วว่าไม่ใช่ปุ่ม toolbar
         (ขยายเงาปุ่มไม่มีผลอะไรเลยเพราะโดน clip ที่ 50px เท่าเดิม) แต่เป็นเส้นตัดของเงาการ์ด KPI นี้เอง ขยาย bleed
         เป็น 120px ให้เงาจางสนิทจริงก่อนโดนตัด (ไม่กระทบ layout อื่นเลย เพราะ margin ติดลบเท่ากันชดเชยพอดี) */
      /* 21 ส.ค. 2026: ผู้ใช้ทักว่าเงาการ์ดใบแรก (ดึงซ้ายสุด) มีที่เว้นให้เงาแสดงผล แต่การ์ดใบสุดท้าย (ดึงขวาสุด)
         ไม่มี — bleed แนวนอนเดิม 4px น้อยกว่ารัศมีเงาจริง (blur 52 − spread 30 = 22px) มาก ทำให้ overflow-x:auto
         ตัดเงาทิ้งก่อนที่ mask-image (fade 30px ด้านล่าง) จะมีโอกาสจางให้เนียนด้วยซ้ำ (mask จางสิ่งที่ยังไม่ถูก
         clip ออกไปเท่านั้น) เพิ่มเป็น 24px (เกิน 22px ที่คำนวณได้ ตามหลัก over-provision เดียวกับ §5.56) แต่ยังน้อย
         กว่า mask 30px เพื่อให้ mask ยังคลุมโซนนี้ได้เต็มพอดี — ต้องแก้ _fitKpiScale() ใน app-v2.html คู่กัน (เลข
         -8 อิงจาก padding แนวนอนเดิม 4px×2 ตรงๆ) ไม่งั้นสเกล "พอดีเป๊ะ" บนจอกว้างจะเพี้ยน */
      padding: 20px 24px 120px;
      margin: -20px -24px -120px;
      scroll-snap-type: none;
      -webkit-overflow-scrolling: touch;
      scrollbar-width: none;
      cursor: grab;
      /* ไฟล์ต้นฉบับ Codex/textui/ui-v2.css ไม่สมมาตร (12px ซ้าย/30px ขวา) ผู้ใช้ทดสอบแล้วขอให้สมมาตร โดยยึดเลข
         ฝั่งขวา (30px) เป็นค่ามาตรฐานทั้งสองข้าง แทนที่จะยึดฝั่งซ้าย (12px) */
      -webkit-mask-image: linear-gradient(to right, transparent 0, #000 30px, #000 calc(100% - 30px), transparent 100%);
      mask-image: linear-gradient(to right, transparent 0, #000 30px, #000 calc(100% - 30px), transparent 100%);
    }
    .v2-kpi-grid::-webkit-scrollbar { display: none; }
    .v2-kpi-grid__edge-spacer { flex:0 0 0px!important; width:0px!important; min-width:0px!important; height:1px!important; padding:0!important; margin:0!important; pointer-events:none; }
    .v2-kpi-grid.is-dragging {
      cursor: grabbing;
      scroll-snap-type: none;
      user-select: none;
      -webkit-user-select: none;
    }
    .v2-kpi-grid.is-dragging .v2-kpi-card { pointer-events: none; }

    .v2-kpi-card {
      --accent: oklch(.72 .17 155);
      --series: oklch(.82 .16 150);
      position: relative;
      isolation: isolate;
      flex: 0 0 auto;
      width: min(300px, 82vw);
      scroll-snap-align: start;
      min-height: 218px;
      overflow: hidden;
      border: 1px solid rgba(255, 255, 255, .28);
      border-radius: 24px;
      background: color-mix(in oklab, white 9%, transparent);
      /* 19 ส.ค. 2026: ผู้ใช้ขอลดพื้นที่เผื่อเงาจาก 58px (28 offset-y + (60 blur − 30 spread)) เหลือ 50px ให้พอดี
         กับ bleed 50px ที่ .v2-kpi-grid เผื่อไว้อยู่แล้ว (app-v2.html) พอดีเป๊ะไม่มีส่วนเกิน — ลดเฉพาะ blur
         60→52 (28 + (52−30) = 50) คง offset-y/spread เดิมไว้ไม่ขยับตำแหน่ง/ขนาดเงาแนวนอน แค่คมขึ้นเล็กน้อย */
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, .48),
        0 28px 52px -30px rgba(0, 0, 0, .72);
      backdrop-filter: blur(14px) saturate(180%);
      -webkit-backdrop-filter: blur(14px) saturate(180%);
      will-change: backdrop-filter;
      transition: transform .25s ease, border-color .25s ease, background .25s ease;
    }

    .v2-kpi-card:hover {
      transform: translateY(-4px);
      border-color: rgba(255, 255, 255, .48);
      background: color-mix(in oklab, white 13%, transparent);
    }

    /* 9 ส.ค. 2026: การ์ดเข้าเฟรมแบบ slide-in-stagger (เทคนิคพอร์ตจาก codefronts.com "Fintech Dashboard KPI
       Card with Count-Up" — บันทึกวิธีทำเต็มไว้ที่ liquid-glass-v2.md §5.36) — เดิม .v2-kpi-card ปรากฏขึ้นทันที
       ไม่มีชั้นนี้ มีแค่ตัวอักษร/ตัวเลขข้างในที่ vanish/reveal (splitKpiText/animateKpiDigits ใน ui-v2.js อยู่แล้ว
       ไม่แตะ — นับเป็นอีกชั้นซ้อนกัน) ชั้นใหม่นี้คือ "กล่องการ์ด" เลื่อนขึ้น+จางเข้าทีละใบตามลำดับ nth-child
       (delay ไล่จากการ์ดแรกไปการ์ดสุดท้าย) ให้ scope เดียวกับ .v2-kpi-scene--vanish-once (เล่นครั้งเดียวตอน
       mount เหมือนชั้นตัวอักษร ไม่วนซ้ำ) */
    .v2-kpi-scene--vanish-once .v2-kpi-card {
      opacity: 0;
      transform: translateY(30px);
      animation: v2-kpi-card-in .55s cubic-bezier(.22,1,.36,1) forwards;
    }
    /* 12 ส.ค. 2026: ใช้ :nth-of-type ไม่ใช่ :nth-child — การ์ดเป็น <article> ส่วน edge-spacer เป็น <div>
       :nth-child นับ spacer ด้วยทำให้ index การ์ดเลื่อนไป 1 (ใบ 6 กลายเป็น nth-child(7) ไม่มีดีเลย์ เด้งขึ้นก่อน
       เพื่อน) :nth-of-type นับเฉพาะ article → ข้าม spacer เสมอ ได้ mapping 1-6 ถูกเสมอ ไม่ว่ามี spacer หรือไม่ */
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(1) { animation-delay: .1s; }
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(2) { animation-delay: .25s; }
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(3) { animation-delay: .4s; }
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(4) { animation-delay: .55s; }
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(5) { animation-delay: .7s; }
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(6) { animation-delay: .85s; }
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(7) { animation-delay: 1s; }
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(8) { animation-delay: 1.15s; }
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(9) { animation-delay: 1.3s; }
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(10) { animation-delay: 1.45s; }
    @keyframes v2-kpi-card-in {
      from { opacity: 0; transform: translateY(30px); }
      to { opacity: 1; transform: translateY(0); }
    }

    /* 9 ส.ค. 2026: ยูทิลิตี้ทั่วไปพอร์ตชั้น slide-in เดียวกับการ์ด KPI (ด้านบน) ไปใช้กับ "ทุก component"
       ในหน้า ไม่ใช่แค่การ์ด KPI — ใช้ keyframe v2-kpi-card-in ตัวเดียวกัน (ไม่แยกซ้ำ) ต่างกันแค่วิธีตั้ง
       delay: การ์ด KPI ใช้ nth-child เพราะจำนวนคงที่ (6 ใบ) ส่วนคลาสนี้ตั้ง delay ผ่าน inline style
       (--v2-reveal-i คูณ step ฝั่ง JS) เพราะจำนวน/ชนิด element ในแต่ละหน้าไม่แน่นอน — ดู liquid-glass-v2.md §5.37
       V2.applyRevealStagger(container) ใน ui-v2.js เป็นตัวเติมคลาส+ตัวแปรนี้ให้
       9 ส.ค. 2026 (ต่อ, §5.38): เปลี่ยนเป็น scroll-triggered ตามตัวอย่าง codefronts.com "Slide-In Animation
       on Scroll" — element ที่ยังไม่เข้า viewport ต้องค้าง opacity:0 เฉยๆ ไม่เล่น animation ทันทีตอน mount
       เหมือนเดิม (ของเดิมเล่นทันทีทุกใบพร้อมกันหมดตอนโหลด ทั้งที่ครึ่งหน้าล่างยังมองไม่เห็น) — แยก keyframe
       ออกจาก state เริ่มต้น: ตัวนี้กำหนดแค่ opacity:0/translateY(30px) เฉยๆ ไม่มี animation ผูกอยู่, ต้องรอ
       IntersectionObserver (ใน ui-v2.js) เติมคลาส .is-visible ก่อนถึงจะ animation:...forwards จริง */
    .v2-reveal-stagger {
      opacity: 0;
      transform: translateY(30px);
    }
    .v2-reveal-stagger.is-visible {
      animation: v2-kpi-card-in .55s cubic-bezier(.22,1,.36,1) forwards;
      animation-delay: calc(var(--v2-reveal-i, 0) * 200ms);
    }

    .v2-kpi-card--blue { --accent: oklch(.69 .17 245); --series: oklch(.80 .15 225); }
    .v2-kpi-card--gold { --accent: oklch(.77 .17 78); --series: oklch(.87 .15 85); }
    .v2-kpi-card--lime { --accent: oklch(.76 .18 125); --series: oklch(.85 .17 125); }
    .v2-kpi-card--violet { --accent: oklch(.70 .17 300); --series: oklch(.83 .13 300); }
    .v2-kpi-card--cyan { --accent: #3461c1; --series: #4579e2; }
    .v2-kpi-card--cyan .v2-kpi-card__chart {
      inset: auto 0 0;
      height: 38%;
      opacity: 1;
    }

    .v2-kpi-card__chart {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      opacity: .82;
    }

    .v2-kpi-card__area { fill: color-mix(in oklab, var(--series) 26%, transparent); }

    /* 20 ส.ค. 2026: ผู้ใช้ยืนยันเส้นนี้ขาดเป็น 2 ท่อนบนมือถือจริง (Android) ทั้งใน app-v2.html (การ์ดถูกย่อ
       scale(.5)) และ design-system-v2.html (การ์ดขนาดธรรมชาติ ไม่ย่อเลย) — ตัดทฤษฎี scalewrap/timing ของ
       getTotalLength() ออกไปแล้ว (แก้ setTimeout(0) รอบก่อนไม่ได้ผล) เหลือสิ่งที่เหมือนกันทั้ง 2 หน้าคือตัว
       markup เอง: vector-effect:non-scaling-stroke ทำงานร่วมกับ viewBox ที่ไม่ได้สัดส่วนเดียวกับกล่อง
       (viewBox 360×218 ยืดลง box 300×218 ผ่าน preserveAspectRatio="none" — แกน X บีบ .833 เท่า, แกน Y ไม่บีบ)
       เป็นคู่ผสมที่หลายเอนจิ้นมือถือมีประวัติ render dasharray/dashoffset เพี้ยนจริง — ถอด vector-effect ออกเฉพาะ
       คลาสนี้ (ไม่แตะ .v2-kpi-card__map/.v2-kpi-card__water ที่ใช้ค่านี้เหมือนกันแต่ไม่มีอาการ) ผู้ใช้ทดสอบบน
       เครื่อง Android จริงแล้วยืนยันว่าหายสนิท (20 ส.ค. 2026) — เป็นสาเหตุจริง ไม่ใช่แค่ทฤษฎี เส้นจะหนา/บางเปลี่ยน
       ไปตามสเกลการ์ดแทนคงที่เสมอ (ผลข้างเคียงเล็กน้อยที่ยอมรับได้ ไม่กระทบ layout/ตำแหน่ง) */
    .v2-kpi-card__line {
      fill: none;
      stroke: var(--series);
      stroke-width: 2.5;
    }

    /* 3 ส.ค. 2026: ลบ .v2-kpi-card__bars + .v2-kpi-card__field .plot/.canal ทิ้ง — เป็นคลาสจากสูตร variant อื่น
       ของไฟล์ต้นฉบับที่ไม่เคยมี markup คู่กันอยู่ใน 6 การ์ดที่เราคัดลอกมาเลยตั้งแต่แรก (§4.21) ไม่ใช่แค่ซ่อนด้วย
       display:none แต่ไม่มีอยู่ในหน้าเลย เจอจากการตรวจ "ขยะ" ในหน้า */
    .v2-kpi-card__map path,
    .v2-kpi-card__water path {
      vector-effect: non-scaling-stroke;
    }

    .v2-kpi-card__thailand-map .country {
      fill: color-mix(in oklab, var(--series) 18%, transparent);
      stroke: color-mix(in oklab, var(--series) 72%, white 8%);
      stroke-width: 1.4;
      vector-effect: non-scaling-stroke;
      filter: drop-shadow(0 8px 14px rgba(0, 28, 48, .22));
    }
    .v2-kpi-card__thailand-map .province-line {
      fill: none;
      stroke: rgba(255, 255, 255, .14);
      stroke-width: .75;
      vector-effect: non-scaling-stroke;
    }
    .v2-kpi-card__thailand-map .country-image {
      opacity: .58;
      filter: drop-shadow(0 8px 14px rgba(0, 28, 48, .26));
    }
    .v2-kpi-card__thailand-map .project-point {
      transform-box: fill-box;
      transform-origin: center;
      animation: v2-kpi-project-pulse 19s ease-in-out infinite;
      animation-fill-mode: backwards;
    }
    .v2-kpi-card__thailand-map .project-point .halo {
      fill: none;
      stroke: var(--series);
      stroke-width: 1.45;
      r: 9px;
      vector-effect: non-scaling-stroke;
      transform-box: fill-box;
      transform-origin: center;
      animation: v2-kpi-project-ring 1.75s ease-out infinite;
    }
    .v2-kpi-card__thailand-map .project-point .core {
      fill: #dffaff;
      stroke: var(--series);
      stroke-width: 1.4;
      r: 3.8px;
      vector-effect: non-scaling-stroke;
      filter: drop-shadow(0 0 5px var(--series));
    }
    .v2-kpi-card__thailand-map .project-point:nth-of-type(2) { animation-delay: 1.9s; }
    .v2-kpi-card__thailand-map .project-point:nth-of-type(3) { animation-delay: 3.8s; }
    .v2-kpi-card__thailand-map .project-point:nth-of-type(4) { animation-delay: 5.7s; }
    .v2-kpi-card__thailand-map .project-point:nth-of-type(5) { animation-delay: 7.6s; }
    .v2-kpi-card__thailand-map .project-point:nth-of-type(6) { animation-delay: 9.5s; }
    .v2-kpi-card__thailand-map .project-point:nth-of-type(7) { animation-delay: 11.4s; }
    .v2-kpi-card__thailand-map .project-point:nth-of-type(8) { animation-delay: 13.3s; }
    .v2-kpi-card__thailand-map .project-point:nth-of-type(9) { animation-delay: 15.2s; }
    .v2-kpi-card__thailand-map .project-point:nth-of-type(10) { animation-delay: 17.1s; }

    .v2-kpi-card__rice .stalk {
      fill: none;
      stroke: rgba(250, 204, 92, .64);
      stroke-width: 1.25;
      vector-effect: non-scaling-stroke;
    }
    .v2-kpi-card__rice .grain {
      fill: rgba(250, 204, 92, .46);
      stroke: rgba(255, 224, 138, .78);
      stroke-width: .65;
      vector-effect: non-scaling-stroke;
    }
    .v2-kpi-card__rice .rice-sway {
      transform-box: fill-box;
      transform-origin: center bottom;
      animation: v2-kpi-rice-sway 5.8s ease-in-out infinite alternate;
    }
    .v2-kpi-card__rice .rice-sway use {
      fill: none;
      stroke: rgba(250, 204, 92, .72);
      stroke-width: 1.05;
    }
    .v2-kpi-card__rice .rice-plant:nth-of-type(2n) .rice-sway { animation-delay: -1.9s; animation-duration: 6.6s; }
    .v2-kpi-card__rice .rice-plant:nth-of-type(3n) .rice-sway { animation-delay: -3.4s; animation-duration: 7.2s; }

    .v2-kpi-card__rice {
      position: absolute;
      width: 0;
      height: 0;
      opacity: 0;
      pointer-events: none;
    }
    /* layer ทุ่งข้าวไหวลม (การ์ด --gold "พื้นที่ชลประทานเพิ่มขึ้น") */
    .v2-kpi-card__rice-field { z-index: 1; }
    .v2-kpi-card__rice-field .plant {
      transform-box: fill-box;
      transform-origin: center bottom;
      animation: v2-kpi-wheat-sway 9.2s ease-in-out var(--delay, -1s) infinite alternate;
      filter: saturate(.72) brightness(.9);
    }
    .v2-kpi-card__rice-field .back { opacity: .19; --phase: -1.7s; }
    .v2-kpi-card__rice-field .mid { opacity: .4; --phase: -4.3s; }
    .v2-kpi-card__rice-field .front { opacity: .7; --phase: -7.2s; }
    .v2-kpi-card__rice-field .plant:nth-child(2n) { --delay: calc(var(--phase) - 2.1s); animation-duration: 10.4s; animation-direction: alternate-reverse; }
    .v2-kpi-card__rice-field .plant:nth-child(3n) { --delay: calc(var(--phase) - 4.8s); animation-duration: 11.1s; }
    .v2-kpi-card__rice-field .plant:nth-child(5n) { --delay: calc(var(--phase) - 7.4s); animation-duration: 9.7s; animation-direction: alternate-reverse; }
    .v2-kpi-card__rice-field .plant:not(:nth-child(2n)):not(:nth-child(3n)):not(:nth-child(5n)) { --delay: var(--phase); }

    .v2-kpi-card__map .region {
      fill: color-mix(in oklab, var(--series) 16%, transparent);
      stroke: color-mix(in oklab, var(--series) 58%, transparent);
      stroke-width: 1;
    }
    .v2-kpi-card__map .route {
      fill: none;
      stroke: rgba(255,255,255,.38);
      stroke-width: 1.2;
      stroke-dasharray: 4 5;
      animation: v2-kpi-route-flow 9s linear infinite;
    }
    .v2-kpi-card__map circle {
      fill: var(--series);
      filter: drop-shadow(0 0 5px var(--series));
      opacity: .82;
    }
    /* 13 ส.ค. 2026: เปลี่ยนจาก direct-child (>) เป็น descendant เฉยๆ — app-v2.html ห่อลูกการ์ดด้วย
       .v2-kpi-card__scalewrap (ดู app-v2.html mount script) ทำให้ .v2-kpi-card__chart ไม่ใช่ลูกตรงของ
       .v2-kpi-card--lime อีกต่อไป selector เดิมเลย match ไม่ติดเงียบๆ เหมือนบั๊กเดียวกับ households card
       (§ .v2-kpi-card--violet ด้านบน) — เลเยอร์ SVG map/route ที่ควรถูกซ่อนเลยโผล่ทับฉากโรงงานแทน (ผู้ใช้ทัก
       ว่าเห็น svg แปลกลอยอยู่ + โรงงานดูลอย) */
    .v2-kpi-card--lime .v2-kpi-card__chart:not(.v2-kpi-card__benefit-scene):not(.v2-kpi-card__benefit-smoke-layer) { display: none; }
    /* 13 ส.ค. 2026: ผ่านมาหลายรอบ (CSS y, attribute y, preserveAspectRatio meet/slice) ยืนยันด้วย display:none
       ว่า deploy/cache ไม่ใช่ปัญหา — โค้ดไปถึงเครื่องจริงแน่นอน แต่ตำแหน่งภาพไม่ขยับตามที่สั่งเลยไม่ว่าจะลองอะไร
       สรุปว่า preserveAspectRatio keyword-based fitting (meet/slice + alignment) มีพฤติกรรมไม่แน่นอนข้ามเบราว์เซอร์
       จริง — เลิกพึ่งมันทั้งหมด ย้ายไปคำนวณ x/y/width/height เองตรงๆ ล่วงหน้า (ดู ui-v2.js) แล้วใช้
       preserveAspectRatio="none" (ไม่มี fitting algorithm ให้ตีความผิดได้เลย ภาพ map 1:1 เข้ากล่องที่คำนวณมาแล้ว
       ตรงเป๊ะพอดี) เหลือแค่ opacity/filter ไว้ที่นี่ */
    .v2-kpi-card__benefit-scene .factory {
      opacity: .84;
      filter: saturate(.96) brightness(1.04) contrast(1.18)
              drop-shadow(0 -1px 0 rgba(196, 236, 232, .18));
    }
    .v2-kpi-card__benefit-scene > rect { opacity: .58; }
    .v2-kpi-card__benefit-scene .cloud {
      opacity: .34;
      animation: v2-kpi-benefit-cloud 18s ease-in-out infinite alternate;
    }
    .v2-kpi-card__benefit-scene .cloud:nth-of-type(2) { animation-delay: -6s; animation-duration: 22s; }
    .v2-kpi-card__benefit-scene .cloud:nth-of-type(3) { animation-delay: -12s; animation-duration: 20s; }
    .v2-kpi-card__benefit-scene .smoke {
      opacity: .42;
      transform-box: fill-box;
      transform-origin: left center;
      animation: v2-kpi-benefit-smoke 7s ease-in-out infinite;
    }
    .v2-kpi-card__benefit-scene .smoke--secondary { animation-delay: -3.8s; animation-duration: 8.4s; opacity: .3; }
    .v2-kpi-card__benefit-scene .smoke:not(.smoke--secondary) { y: 95px; }
    .v2-kpi-card__benefit-scene .smoke--secondary { y: 115px; }
    .v2-kpi-card__benefit-scene .smoke { display: none; }
    .v2-kpi-card__benefit-scene .benefit-water use {
      animation: v2-kpi-benefit-water-flow 9s linear infinite;
      opacity: .72;
    }
    .v2-kpi-card__benefit-scene .benefit-water .water-back { fill: #4579e2; }
    .v2-kpi-card__benefit-scene .benefit-water .water-mid {
      fill: #3461c1;
      animation-delay: -3s;
      animation-duration: 7s;
      opacity: .8;
    }
    .v2-kpi-card__benefit-scene .benefit-water .water-front {
      fill: #2d55aa;
      animation-delay: -5s;
      animation-duration: 5.5s;
      opacity: .88;
    }
    @keyframes v2-kpi-benefit-water-flow {
      from { transform: translateX(0); }
      to { transform: translateX(120px); }
    }
    /* 3 ส.ค. 2026: เคยเพิ่ม .v2-kpi-card__scrim (ลอกสูตร scrim จากการ์ด lime ไปใส่การ์ดอื่น) แล้วผู้ใช้สั่งกลับ
       "เอาเกรดดิ้งออกทั้งหมด...รวมถึงพื้นที่รับประโยชน์ด้วย" — เอา scrim layer ออกจากทุกการ์ดแล้ว (รวม
       benefit-scrim ต้นฉบับของการ์ด lime เองด้วย ดู ui-v2.js buildKpiSceneGrid()) เหลือ class นี้เป็นแค่
       คอมเมนต์ประวัติ ไม่มี markup ไหนใช้ `.v2-kpi-card__scrim` แล้ว — ลบกฎ CSS ทิ้งไปด้วย ไม่ต้องเก็บ dead rule */
    .v2-kpi-card__benefit-smoke-layer { z-index: 1; }
    .v2-kpi-card__benefit-smoke-layer .smoke-plume {
      transform-box: fill-box;
      transform-origin: right center;
      animation: v2-kpi-benefit-smoke-motion 9.6s linear infinite, v2-kpi-benefit-smoke-opacity 9.6s ease-in-out infinite;
      filter: brightness(.88);
      will-change: transform, opacity;
    }
    .v2-kpi-card__benefit-smoke-layer .smoke-plume:nth-of-type(2) { animation-delay: -4.1s; animation-duration: 12.4s; }
    .v2-kpi-card__benefit-smoke-layer .smoke-plume:nth-of-type(3) { animation-delay: -6.3s; animation-duration: 7.8s; }
    .v2-kpi-card__benefit-smoke-layer .smoke-plume:nth-of-type(1) { x: 25px; y: 120px; width: 55px; height: 23px; }
    .v2-kpi-card__benefit-smoke-layer .smoke-plume:nth-of-type(2) { x: 86px; y: 105px; width: 82px; height: 34px; }
    .v2-kpi-card__benefit-smoke-layer .smoke-plume:nth-of-type(3) { x: 184px; y: 125px; width: 46px; height: 19px; }

    .v2-kpi-card__water .wave-back { fill: #4579e2; }
    .v2-kpi-card__water .wave-mid { fill: #3461c1; }
    .v2-kpi-card__water .wave-front { fill: #2d55aa; }
    .v2-kpi-card__water use { animation: v2-kpi-wave-move 12s linear infinite; }
    .v2-kpi-card__water use:nth-of-type(1) { animation-delay: -2s; }
    .v2-kpi-card__water use:nth-of-type(2) { animation-delay: -2s; animation-duration: 5s; }
    .v2-kpi-card__water use:nth-of-type(3) { animation-delay: -4s; animation-duration: 3s; }
    .v2-kpi-card__water .level {
      stroke: rgba(255,255,255,.45);
      stroke-width: 1;
      stroke-dasharray: 5 6;
    }
    .v2-kpi-card__water circle { fill: color-mix(in oklab, white 55%, var(--series)); opacity: .45; }

    /* 12 ส.ค. 2026: ผู้ใช้ขอให้ตึกในการ์ดครัวเรือนเป็นสีเทา (สีฟอนต์รอง --v2-text-dim — ธีมนี้ใช้เป็น "สีรอง"
       มาตลอดทั้งระบบ) แทนสีม่วง --series เดิมของการ์ด */
    .v2-kpi-card__households .building {
      fill: color-mix(in oklab, var(--v2-text-dim) 16%, transparent);
      stroke: color-mix(in oklab, var(--v2-text-dim) 52%, transparent);
      stroke-width: 1;
      vector-effect: non-scaling-stroke;
    }
    /* ปรับตำแหน่ง layer อาคาร+ไฟกะพริบของการ์ดม่วง (การ์ดเดียวใน .v2-kpi-card--violet ตอนนี้)
       12 ส.ค. 2026: เปลี่ยนจาก direct-child (>) เป็น descendant เฉยๆ — app-v2.html ห่อลูกการ์ดด้วย
       .v2-kpi-card__scalewrap (ดู app-v2.html mount script) ทำให้ .v2-kpi-card__chart ไม่ใช่ลูกตรงของ
       .v2-kpi-card--violet อีกต่อไป selector เดิมเลย match ไม่ติดเงียบๆ (ตึกเลยลอยไม่ชิดพื้นการ์ดล่างสุด
       ตามที่ผู้ใช้ทัก) — descendant selector ใช้ได้ทั้งมี/ไม่มี wrapper คั่นกลาง */
    .v2-kpi-card--violet .v2-kpi-card__chart .v2-kpi-card__households {
      transform: translateY(19px);
    }
    .v2-kpi-card__households .roofline {
      fill: none;
      stroke: color-mix(in oklab, var(--v2-text-dim) 68%, transparent);
      stroke-width: 1.25;
      vector-effect: non-scaling-stroke;
    }
    .v2-kpi-card__households .ground {
      stroke: rgba(255,255,255,.24);
      stroke-width: 1;
      vector-effect: non-scaling-stroke;
    }
    .v2-kpi-card__households .light {
      fill: #ffe9a8;
      opacity: .12;
      animation: v2-kpi-office-light 9s ease-in-out infinite;
    }
    .v2-kpi-card__households .light:nth-of-type(3n) { animation-delay: -2s; }
    .v2-kpi-card__households .light:nth-of-type(4n) { animation-delay: -4s; }
    .v2-kpi-card__households .light:nth-of-type(5n) { animation-delay: -6s; }
    .v2-kpi-card__map { animation: v2-kpi-formal-drift 20s ease-in-out infinite alternate; }

    /* 4 ส.ค. 2026: ลองทำ "จุดค่าล่าสุด" วิ่งไปพร้อมปลายเส้นวาดตัวเองของการ์ดแรกหลายรอบ (CSS motion path,
       แล้วเปลี่ยนเป็น JS rAF loop คุมทั้งเส้น+จุดจากตัวแปรเดียวกัน — พิสูจน์ด้วยโค้ดแล้วว่า sync ตรงเป๊ะทุกเฟรม)
       แต่ผู้ใช้ตัดสินใจ "เอาจุดออก" ทิ้งไปเลยเพราะแยกด้วยตาจากขอบพื้นที่แรเงา (สถิต เต็มเส้นตลอด) ที่มีสีใกล้กัน
       ไม่ออก ทำให้ดูเหมือนไม่ sync อยู่ดีแม้โค้ดจะถูกต้อง — เหลือแค่เส้นวาดตัวเองอย่างเดียว (rev2) */
    @keyframes v2-kpi-wave-move {
      from { transform: translateX(-90px); }
      to { transform: translateX(85px); }
    }
    @keyframes v2-kpi-formal-drift { to { transform: translateX(8px); } }
    @keyframes v2-kpi-project-pulse {
      0% { opacity: 0; }
      1.3%, 7.6% { opacity: 1; }
      9.2%, 100% { opacity: 0; }
    }
    @keyframes v2-kpi-project-ring {
      from { opacity: .9; transform: scale(.55); }
      to { opacity: 0; transform: scale(1.55); }
    }
    @keyframes v2-kpi-benefit-cloud {
      from { transform: translateX(-8px) translateY(1px); }
      to { transform: translateX(12px) translateY(-2px); }
    }
    @keyframes v2-kpi-benefit-smoke {
      0% { opacity: .14; transform: translateX(-3px) scale(.78); }
      45% { opacity: .46; transform: translateX(4px) scale(1); }
      100% { opacity: .08; transform: translateX(13px) scale(1.16); }
    }
    @keyframes v2-kpi-benefit-smoke-motion {
      from { transform: translateX(6px) scale(.56); }
      to { transform: translateX(-15px) scale(1.18); }
    }
    @keyframes v2-kpi-benefit-smoke-opacity {
      0%, 14% { opacity: 0; }
      30% { opacity: .18; }
      46%, 61% { opacity: .42; }
      76% { opacity: .22; }
      88%, 100% { opacity: 0; }
    }
    @keyframes v2-kpi-rice-sway {
      from { transform: rotate(-1.4deg); }
      to { transform: rotate(2deg); }
    }
    @keyframes v2-kpi-wheat-sway {
      from { transform: scaleX(1.18) scaleY(1.03) rotate(-2.2deg) translateX(-2px); }
      to { transform: scaleX(1.18) scaleY(1.03) rotate(3deg) translateX(2px); }
    }
    @keyframes v2-kpi-office-light {
      0%, 18%, 76%, 100% { opacity: .1; filter: none; }
      30%, 62% { opacity: .88; filter: drop-shadow(0 0 2.5px rgba(255, 225, 142, .8)); }
    }
    @keyframes v2-kpi-route-flow { to { stroke-dashoffset: -90; } }

    .v2-kpi-card__panel {
      position: relative;
      z-index: 1;
      display: flex;
      flex-direction: column;
      min-height: 218px;
      padding: 22px 23px 20px;
    }

    .v2-kpi-card__top {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 12px;
    }

    /* 3 ส.ค. 2026: การ์ดนี้ตั้งใจให้ "มืดเสมอ" ไม่ปรับตาม data-v2-mode (§4.21) — แต่พื้นการ์ดจริงเป็นกระจกโปร่ง
       10% (`.v2-kpi-card` background) ที่ต้องพึ่ง backdrop-filter ดูดสีพื้นหลังมาผสมถึงจะดูมืดสมจริง พอพื้นหลัง
       หน้าเป็น light mode (สีอ่อน) การ์ดเลยไม่มืดจริงอย่างที่ต้นฉบับตั้งใจ (ต้นฉบับสมมติพื้นหลังเข้มเสมอ) ตัวหนังสือ
       ขาวล้วนบนพื้นอ่อนๆ เลยอ่านยาก — ผู้ใช้เลือกให้สลับสีตัวหนังสือตามโหมด (ตัวเลือกที่ 2 จาก 2 ทางที่เสนอ: อีก
       ทางคือทำพื้นหลังการ์ดให้มืดตายตัวเสมอ) ค่า default ไม่ scope ตาม mode (เดิม) = ใช้เป็นค่าของ dark mode
       ต่อไป, เพิ่ม override เฉพาะ light mode ให้เป็นดำ+เงาขาวแทนขาว+เงาดำ */
    .v2-kpi-card__label {
      color: rgba(255, 255, 255, .78);
      font-size: 15px;
      font-weight: 700;
      letter-spacing: .015em;
      line-height: 1.45;
    }
    :root[data-v2-theme="ios-liquid-glass"][data-v2-mode="light"] .v2-kpi-card__label { color: rgba(20, 20, 30, .78); }

    .v2-kpi-card__icon {
      display: grid;
      flex: 0 0 auto;
      place-items: center;
      width: 38px;
      height: 38px;
      border: 1px solid rgba(255, 255, 255, .25);
      border-radius: 13px;
      background: color-mix(in oklab, var(--accent) 30%, transparent);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25);
      font-size: 19px;
    }

    /* 12 ส.ค. 2026: margin-top เดิม 19px ต่างจาก .v2-kpi-card__unit margin-top:7px มาก ทำให้ระยะห่าง
       ป้าย→ตัวเลข ห่างกว่า ตัวเลข→หน่วย เห็นชัด ผู้ใช้ขอให้เท่ากันโดยยึดค่าน้อยกว่า (7px) เป็นฐาน */
    .v2-kpi-card__metric {
      display: flex;
      flex-wrap: wrap;
      align-items: baseline;
      gap: 8px 10px;
      margin-top: 7px;
    }

    /* 3 ส.ค. 2026: ต้นฉบับใช้ clamp(...,Nvw,...) เพราะการ์ดอยู่ใน grid ที่กว้างตาม viewport จริง —
       ตอนนี้ .v2-kpi-card กว้างคงที่ (min(300px,82vw) ซึ่ง ≥366px จอจะได้ 300px เป๊ะทุกจอ) การอิง vw
       จึงทำให้ตัวเลขมีขนาดต่างกันระหว่างอุปกรณ์ (PC จอกว้าง vw เยอะ = ตัวหนังสือใหญ่/ขึ้นบรรทัดใหม่ ผลักการ์ด
       ให้สูงกว่าการ์ดอื่นในแถว ดันภาพฉาก (โรงงาน/ตึก) ที่ scale เต็มความสูงการ์ดให้ "ลอย" ต่างจาก iPad) เปลี่ยนเป็น
       ค่าคงที่แทน อิงตามความกว้างการ์ดจริง ไม่ใช่ viewport */
    .v2-kpi-card__value {
      color: #fff;
      font-size: 36px;
      font-weight: 800;
      letter-spacing: -.035em;
      line-height: 1.05;
      text-shadow: 0 2px 10px rgba(0, 0, 0, .35);
    }
    :root[data-v2-theme="ios-liquid-glass"][data-v2-mode="light"] .v2-kpi-card__value {
      color: #14141e;
      text-shadow: 0 2px 10px rgba(255, 255, 255, .55);
    }

    .v2-kpi-card__value--long { font-size: 30px; }

    .v2-kpi-card__delta {
      padding: 3px 8px;
      border: 1px solid color-mix(in oklab, var(--accent) 52%, transparent);
      border-radius: 999px;
      color: #f4fff9;
      background: color-mix(in oklab, var(--accent) 42%, transparent);
      font-size: 12px;
      font-weight: 700;
      white-space: nowrap;
    }

    .v2-kpi-card__delta--down {
      border-color: rgba(251, 113, 133, .42);
      background: rgba(190, 24, 93, .35);
      color: #fff1f4;
    }

    .v2-kpi-card__unit {
      margin-top: 7px;
      padding-top: 0;
      color: rgba(255, 255, 255, .68);
      font-size: 15px;
      font-weight: 500;
      line-height: 1.35;
    }
    :root[data-v2-theme="ios-liquid-glass"][data-v2-mode="light"] .v2-kpi-card__unit { color: rgba(20, 20, 30, .62); }

    /* 7 ส.ค. 2026: ป้าย/หน่วย/ตัวเลขในการ์ด KPI — เอฟเฟกต์ vanish/reveal ต่อกลุ่มอักขระ(หรือหลักตัวเลข)เดียวกับ
       ป้ายชื่อ (§5.35) เล่นครั้งเดียวตอนโหลด หน่วงตามลำดับการ์ด — scope ด้วย .v2-kpi-scene--vanish-once กันชน
       กับ .v2-kpi-letter ที่อาจถูกใช้ที่อื่นในอนาคต ระวัง: .v2-kpi-card มี overflow:hidden (จำเป็นสำหรับตัดขอบ
       ภาพประกอบในการ์ด ห้ามเปลี่ยน) ตัวอักษร/หลักกระเจิงแค่ -22px ขึ้นบน (เท่ากับ padding-top ของ
       .v2-kpi-card__panel พอดีสำหรับป้าย, ยิ่งเผื่อเยอะกว่าสำหรับตัวเลขที่อยู่ต่ำกว่าป้ายอีกที) ไม่ใช่ -74px
       แบบป้ายชื่อ — ทดสอบแล้วไม่โดนตัดขอบทั้งป้ายและตัวเลข (ดู §5.35 บั๊กที่ 2 เป็นบทเรียน) */
    .v2-kpi-digit-wrap { display: inline-flex; white-space: pre; align-items: baseline; }
    .v2-kpi-scene--vanish-once .v2-kpi-letter {
      display: inline-block; white-space: pre; transform-origin: center;
      transition: transform 1.1s cubic-bezier(.3,.6,.4,1), opacity 1s ease, filter 1s ease;
      transition-delay: calc(var(--i) * 55ms);
    }
    .v2-kpi-scene--vanish-once .v2-kpi-text-reveal .v2-kpi-letter {
      opacity: 0; filter: blur(9px); transform: translate(var(--x), var(--y)) rotate(var(--r)) scale(1.35);
    }
    .v2-kpi-scene--vanish-once .v2-kpi-text-reveal.is-ready .v2-kpi-letter { opacity: 1; filter: blur(0); transform: none; }
    /* 12 ส.ค. 2026: :nth-of-type เหตุผลเดียวกับชุดกล่องการ์ดด้านบน (spacer เป็น div ทำให้ nth-child เลื่อน) */
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(2) .v2-kpi-letter { transition-delay: calc(90ms + var(--i) * 55ms); }
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(3) .v2-kpi-letter { transition-delay: calc(180ms + var(--i) * 55ms); }
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(4) .v2-kpi-letter { transition-delay: calc(270ms + var(--i) * 55ms); }
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(5) .v2-kpi-letter { transition-delay: calc(360ms + var(--i) * 55ms); }
    .v2-kpi-scene--vanish-once .v2-kpi-card:nth-of-type(6) .v2-kpi-letter { transition-delay: calc(450ms + var(--i) * 55ms); }

    @media (max-width: 580px) {
      .v2-kpi-card { width: min(260px, 78vw); }
      .v2-kpi-card, .v2-kpi-card__panel { min-height: 205px; }
      .v2-kpi-card__value { font-size: 30px; }
      .v2-kpi-card__value--long { font-size: 26px; }
    }
    @media (prefers-reduced-motion: reduce) {
      .v2-kpi-card { transition: none; animation: none !important; opacity: 1 !important; transform: none !important; }
      .v2-reveal-stagger { animation: none !important; opacity: 1 !important; transform: none !important; }
      .v2-kpi-card__thailand-map .project-point, .v2-kpi-card__rice .rice-sway, .v2-kpi-card__rice-field .plant, .v2-kpi-card__map,
      .v2-kpi-card__map .route, .v2-kpi-card__benefit-scene .cloud, .v2-kpi-card__benefit-scene .smoke, .v2-kpi-card__benefit-smoke-layer .smoke-plume, .v2-kpi-card__water use, .v2-kpi-card__households .light { animation: none; }
      .v2-kpi-scene--vanish-once .v2-kpi-letter { transition: opacity .4s ease; transition-delay: 0s; }
      .v2-kpi-scene--vanish-once .v2-kpi-text-reveal .v2-kpi-letter { transform: none; filter: none; }
    }
  
/* ============================================================
   Cross-document View Transitions (เฟส 2 — 31 ส.ค. 2026)
   ============================================================
   เป้าหมาย: เปลี่ยนหน้าระหว่างหน้าธีม v2 ให้ "ต่อเนื่องไม่เหมือนกดแล้วรีเฟรช"
   โดยไม่ต้องทำ SPA/keep-alive (ซึ่งชนกับ global + inline onclick ของ app-v2/dashboard-v2 — ดู liquid-glass-v2.md)
   กลไก: same-origin cross-document view transition (navigation:auto) — ทำงานอัตโนมัติทุกครั้งที่ navigate
   ด้วย location.href/<a href> ไปหน้าที่ opt-in เหมือนกัน (ทั้งสองหน้าต้องมี @view-transition นี้ = ทุกหน้าที่
   โหลด ui-v2.css) — หน้าธีมเก่า (map/info/docs, dashboard.html เดิม) ไม่โหลดไฟล์นี้ จึงไม่ opt-in → เปลี่ยนหน้า
   ปกติ (ไม่มี VT) โดยไม่มี error รองรับ Chrome/Edge/Safari 18+; Firefox ยังไม่รองรับ → fallback เปลี่ยนหน้าปกติ

   การตั้ง view-transition-name ให้ปุ่มลอย navbar (navpill/ปุ่ม light-dark) + wallpaper = ยก element เหล่านี้
   ออกจาก snapshot ของ root ให้ morph old→new เอง (ตำแหน่ง/ขนาดเท่ากันทั้งสองหน้า = ค้างนิ่งไม่กระพริบ) ขณะที่
   เนื้อหา (root) cross-fade — ได้ความรู้สึก "navbar อยู่กับที่ เนื้อหาเปลี่ยน" ตามที่ผู้ใช้ขอ
   ⚠ ไม่ตั้งชื่อให้ .v2-brand (มี reveal animation เข้าฉากทุกครั้งที่โหลด — ถ้า morph จะซ้อนกับ animation ที่เพิ่ง
     เริ่ม ดูกระพริบ) ปล่อยให้ cross-fade กับ root แล้วเล่น reveal ของมันเองตามปกติ ดูตั้งใจกว่า
   ⚠ view-transition-name ต้อง unique ต่อ document — element เหล่านี้มีอย่างละ 1 ตัวต่อหน้าเสมอ (สร้างโดย
     navbar()/mountAmbient ของ ui-v2.js) จึงปลอดภัย */
@view-transition { navigation: auto; }

[data-v2-theme="ios-liquid-glass"] .v2-navpill      { view-transition-name: v2-navpill; }
[data-v2-theme="ios-liquid-glass"] .v2-ios-switcher { view-transition-name: v2-switch; }
[data-v2-theme="ios-liquid-glass"] .v2-wallpaper    { view-transition-name: v2-wallpaper; }
/* brand (logo + ป้ายหน่วยงาน) = ต้อง "ค้างอยู่กับที่" ตอนเปลี่ยนหน้า ไม่กระพริบ/เล่น reveal ซ้ำ (ผู้ใช้ขอ ไม่งั้น
   ดูเหมือนรีเฟรช) — ตั้ง view-transition-name ให้ morph old→new อยู่กับที่ + ข้าม reveal ตอน seamless nav
   (buildBrand อ่าน flag v2_seamless_nav) ปลอดภัยเพราะ reveal จริงเล่นเฉพาะตอนโหลดตรง/รีเฟรช (ไม่มี VT) เท่านั้น */
[data-v2-theme="ios-liquid-glass"] .v2-brand        { view-transition-name: v2-brand; }
/* .v2-navback (ปุ่มย้อนกลับ) ใช้ชื่อเดียวกับ brand ไม่ได้ (ต้อง unique ต่อ document) — คนละหน้าใช้คนละตัว
   (หน้าหลักมี brand, หน้าย่อยมี navback) ตั้งชื่อแยกให้ตัวที่มีจริงในแต่ละหน้า morph ข้ามกันได้ */
[data-v2-theme="ios-liquid-glass"] .v2-navback      { view-transition-name: v2-brand; }

/* เนื้อหา (root) cross-fade เร็ว/นุ่ม — ค่า default ของ UA ก็ใช้ได้ แต่ปรับให้กระชับขึ้นเล็กน้อยให้ลื่น */
::view-transition-old(root) { animation-duration: .22s; }
::view-transition-new(root) { animation-duration: .30s; }

/* เคารพ prefers-reduced-motion — ตัดอนิเมชัน VT ทั้งหมด (เปลี่ยนหน้าทันทีไม่มีเอฟเฟกต์) */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
}
