:root {
    /* ผิวและตัวหนังสือ - ชุดเดียวกับที่วาดไว้ในแบบร่าง แผงสีขาวจริงๆ ไม่ใช่โหมดมืดกลับสี

       ก่อนหน้านี้ในไฟล์นี้มีสีขาวอมฟ้าอยู่แปดเฉดที่ทำหน้าที่เดียวกัน (#f6faff #f7faff
       #f4f8ff #f2f7ff #eef5ff #eef2f7 #eef2f8 #eaf1ff) ต่างกันไม่ถึงจะสังเกตเห็น
       แต่มากพอที่ขอบการ์ดสองใบข้างกันจะดูไม่เท่ากัน ยุบเหลือสามชั้น: การ์ด / พื้นรอง / พื้นจม

       สามชั้นนี้คือสิ่งที่โหมดมืดต้องสลับ ส่วนสีที่มีความหมาย (เตือน อันตราย สำเร็จ)
       ไม่สลับ แค่เปลี่ยนพื้นหลังให้เข้ากับผิวมืด */
    /* หมายเลขชุดไฟล์ - ต้องตรงกับ BUILD ใน script.js และกับเลข ?v= ใน index.html

       25 ส.ค. 2569 อัป style.css ขึ้นไปแต่ลืม script.js สองไฟล์นี้ต้องไปด้วยกันเพราะ
       CSS รู้จักแต่คลาสที่ JS รุ่นใหม่สร้าง ผลคือสวิตช์กลายเป็นกล่องว่างที่หน้าตา
       เหมือนกันทั้งเปิดและปิด ซึ่งอ่านได้ว่า "กดแล้วไม่มีสี" และ "ไม่จำค่า" พร้อมกัน

       ?v= กันแคชได้ แต่กันการอัปไม่ครบไม่ได้ - เบราว์เซอร์ขอ URL ใหม่ทั้งสองไฟล์
       แล้วได้ CSS ใหม่กับ JS เก่ากลับมา โดยไม่มีอะไรผิดพอจะฟ้อง

       script.js อ่านค่านี้ตอนเปิดหน้า ถ้าไม่ตรงกับของตัวเองจะขึ้นแถบเตือน
       tools/build-stamp-test.js กันไม่ให้สามเลขนี้หลุดจากกันตั้งแต่ในเครื่อง */
    --build: 98;

    --bg: #eaeff5;
    --card: #ffffff;
    --soft: #f4f8fd;
    --well: #e4ebf4;
    --ledoff: #cbd6e2;
    --border: #d3dde9;
    --text: #12202e;
    --text2: #3c5266;
    --muted: #64798f;

    /* ความลึกมาจากขอบกับเงา ไม่ได้มาจากความจาง จึงใช้ได้ทั้งสองโหมด */
    --hi: rgba(255, 255, 255, 0.95);
    --sh: rgba(16, 32, 48, 0.10);
    --shadow: 0 1px 2px rgba(16, 32, 48, 0.05), 0 2px 6px rgba(16, 32, 48, 0.07);
    --shadow-lg: 0 4px 14px rgba(16, 32, 48, 0.12);
    /* กล่องที่ลอยเหนือทุกอย่าง (หน้าต่างเลือกเวลา การ์ดเข้าสู่ระบบ)
       โหมดมืดเพิ่มวงแหวนขอบเข้าไปด้วย เพราะเงาดำบนพื้นมืดแยกกล่องออกจากพื้นไม่ได้เลย */
    --shadow-float: 0 20px 60px rgba(15, 39, 64, 0.24);

    --primary: #2563eb;
    --primary-dark: #1d4ed8;
    --primary-soft: #e8f0ff;
    --accent: #7c3aed;
    --success: #f97316;   /* ส้ม: ใช้เป็นสี "ทำงานอยู่" และปุ่มบันทึกมาแต่เดิม */
    --ok: #16a34a;
    --warn: #f59e0b;
    --danger: #dc2626;
    --info: #0891b2;

    /* พื้นของป้ายที่มีความหมาย แยกโทเคนไว้เพราะโหมดมืดต้องใช้โทนเข้ม
       พาสเทลสว่างจ้าบนแผงมืดอ่านยากกว่าไม่มีสีเลย */
    --ok-bg: #dcfce7;
    --warn-bg: #fff7ed;
    --danger-bg: #fdecea;
    --info-bg: #dbeafe;
    --accent-bg: #ede9fe;

    /* ตัวหนังสือบนพื้นที่มีความหมาย - ต้องสลับพร้อมพื้นของมัน
       ไม่งั้นโหมดมืดจะได้น้ำตาลเข้มบนน้ำตาลเข้ม ซึ่งอ่านไม่ออกเลย */
    --ok-ink: #15803d;
    --warn-ink: #92400e;
    --danger-ink: #b91c1c;
    --info-ink: #1d4ed8;
    --accent-ink: #6d28d9;

    /* สองวงแสงจางๆ บนพื้นหลังหน้า ให้พื้นไม่เรียบตายจนดูเป็นกระดาษ */
    --glow1: #dbe8ff;
    --glow2: #e7dcff;
    /* สีประจำช่อง ใช้ชุดเดียวกันทั้งแอป ช่องเดิมจะเป็นสีเดิมเสมอ */
    --ch1: #2563eb; --ch2: #16a34a; --ch3: #f59e0b; --ch4: #dc2626;
    --ch5: #7c3aed; --ch6: #0891b2; --ch7: #db2777; --ch8: #65a30d;

    /* สีของค่าในเมนูสถิติ - หนึ่งสีต่อหนึ่งชนิดค่า (น้ำ ไฟ ความชื้น เวลาเปิด อุณหภูมิ)
       ตามชุดอ้างอิงของ dataviz และตรวจด้วย validate_palette.js กับพื้นการ์ดจริงทั้งสองโหมด
       (#ffffff / #232a33) ผ่านทุกข้อ - สีเขียวถูกตัดออกเพราะชิดสีเขียวน้ำเงินเกินไปบนพื้นมืด
       --st-hum บนพื้นขาวต่ำกว่า 3:1 จึงมีตัวเลขปลายเส้นและตารางข้อมูลรองรับเสมอ
       สลับค่าในโหมดมืดเป็นขั้นของเฉดเดียวกัน ไม่ใช่สีใหม่ */
    --st-water: #2a78d6;
    --st-energy: #eb6834;
    --st-hum: #1baf7a;
    --st-run: #6250d6;
    --st-temp: #e34948;

    /* ภาพระบบ 2.5D - วัสดุแต่ละชนิดมีสามเฉด (หน้าบน / ซ้าย / ขวา) แสงมาจากซ้ายบน
       ความลึกมาจากเฉดของหน้า ไม่ได้มาจากเงา จึงต้องสลับทั้งสามเฉดพร้อมกันในโหมดมืด */
    --iso-ground-t: #dde8d3; --iso-ground-l: #c6d4ba; --iso-ground-r: #b3c4a6;
    --iso-soil-t: #8b5d3e;   --iso-soil-l: #6f462d;   --iso-soil-r: #593823;
    --iso-metal-t: #e4e9ef;  --iso-metal-l: #bac4cf;  --iso-metal-r: #97a3b1;
    --iso-pipe: #9aa7b6;     --iso-pipe-hi: #e6ecf2;
    --iso-glass: #d8e8f8;    --iso-glass-edge: #86acd2;
    --iso-leaf: #3f9a49;     --iso-leaf2: #6cbf72;
}

/* โหมดมืดตามเครื่องของผู้ใช้ - เจ้าของเปิดดูกลางแดดกับกลางคืนคนละแบบ

   สลับเฉพาะโทเคน มาร์กอัปกับกฎที่เหลือไม่รู้เลยว่ามีสองโหมดอยู่ ซึ่งเป็นเหตุผล
   ที่ต้องยกสีขึ้นมาเป็นโทเคนก่อน ถ้ายังฝังสีไว้ในกฎ การทำโหมดมืดจะกลายเป็นการ
   เขียนกฎซ้ำทั้งไฟล์ แล้ววันหนึ่งสองชุดนั้นจะเพี้ยนจากกัน

   สีประจำช่อง ch1-ch8 ไม่สลับ ช่องที่ 3 ต้องเป็นสีเดิมไม่ว่าจอจะสว่างหรือมืด
   ไม่งั้นคนที่จำสีช่องได้จะจำผิดตอนสลับโหมด */
:root[data-theme="dark"] {
    --bg: #171c22;
    --card: #232a33;
    --soft: #1b2129;
    --well: #12171d;
    --ledoff: #39434f;
    --border: #39434f;
    --text: #e8eef5;
    --text2: #c6d4e2;
    --muted: #8fa3b8;
    --hi: rgba(255, 255, 255, 0.06);
    --sh: rgba(0, 0, 0, 0.42);
    --shadow: 0 1px 0 var(--hi) inset, 0 3px 8px var(--sh);
    --shadow-lg: 0 5px 14px rgba(0, 0, 0, 0.5);
    --shadow-float: 0 20px 60px rgba(0, 0, 0, 0.62), 0 0 0 1px var(--border);
    --primary-soft: #1d2b45;
    --ok-bg: #0e2a1d;
    --warn-bg: #2a1c0d;
    --danger-bg: #2a1315;
    --info-bg: #12243a;
    --accent-bg: #241d3d;
    --ok-ink: #7dfab0;
    --warn-ink: #ffc48a;
    --danger-ink: #ff9ba0;
    --info-ink: #9dc0ff;
    --accent-ink: #c9b6fd;
    --glow1: #17263c;
    --glow2: #221b33;
    --st-water: #3987e5;
    --st-energy: #d95926;
    --st-hum: #199e70;
    --st-run: #9085e9;
    --st-temp: #e66767;
    --iso-ground-t: #24302a; --iso-ground-l: #1c2621; --iso-ground-r: #161e1a;
    --iso-soil-t: #6e4a31;   --iso-soil-l: #573924;   --iso-soil-r: #452c1b;
    --iso-metal-t: #5d6875;  --iso-metal-l: #48525d;  --iso-metal-r: #3a424b;
    --iso-pipe: #5f6b79;     --iso-pipe-hi: #8e9aa8;
    --iso-glass: #2d3d4e;    --iso-glass-edge: #6283a6;
    --iso-leaf: #3d8c46;     --iso-leaf2: #5aa962;
}

/* ไอคอนบนปุ่มสลับธีม - CSS ตัดสินเอง ไม่มี JS คอยเปลี่ยนรูป
   ถ้าให้ JS เปลี่ยน จะมีสองที่ที่ต้องรู้ว่าธีมปัจจุบันคืออะไร แล้ววันหนึ่งจะไม่ตรงกัน */
:root[data-theme="dark"] .th-lt { display: none; }
:root:not([data-theme="dark"]) .th-dk { display: none; }

body {
    /* ฟอนต์ชุดเดียวกับ home.html และ soil.html - ถ้าแก้ที่นี่ต้องแก้ทั้งสามที่

       ตัวอักษรละตินมาจากฟอนต์ของระบบเอง ซึ่งไม่มีตัวไหนมีอักษรไทยเลย เบราว์เซอร์
       จึงไล่หาต่อทีละอักขระ และนั่นคือจุดที่ชื่อฟอนต์ไทยต้องอยู่ ปล่อยให้ระบบเลือก
       เองก็อ่านออก แต่จะได้คนละฟอนต์ในแต่ละหน้า ระยะสระกับความสูงบรรทัดจึงขยับ
       ตอนกดข้ามหน้า ซึ่งเคยเป็นอยู่จริง - soil.html ไม่มีชื่อฟอนต์ไทยเลยสักตัว

       ไม่มีอะไรบังคับให้สามที่นี้ตรงกันได้ นอกจาก tools/font-stack-test.js */
    font-family: "Segoe UI", system-ui, -apple-system, Roboto, "Noto Sans Thai", "Sarabun", sans-serif;
    background: var(--bg);
    background-image:
        radial-gradient(1100px 520px at 8% -10%, var(--glow1) 0%, transparent 60%),
        radial-gradient(900px 460px at 100% 0%, var(--glow2) 0%, transparent 55%);
    background-attachment: fixed;
    color: var(--text); padding: 20px; margin: 0;
}
.dashboard { max-width: 1000px; margin: auto; }

header {
    display: flex; justify-content: space-between; align-items: center;
    margin-bottom: 20px; gap: 12px; flex-wrap: wrap;
    background: linear-gradient(135deg, var(--primary), var(--accent));
    color: #fff; padding: 14px 20px; border-radius: 14px;
    box-shadow: 0 8px 22px rgba(37, 99, 235, 0.28);
}
header .logo { font-size: 1.1rem; font-weight: 800; display: flex; align-items: center; gap: 9px; }

/* ชื่อเครื่องที่กำลังเปิดดูอยู่ ต่อท้ายชื่อแบรนด์บนหัวจอ

   ก่อน 24 ส.ค. 2569 คำตอบของ "ตอนนี้ดูเครื่องไหนอยู่" อยู่ที่ dropdown เลือกเครื่อง
   พอ dropdown ย้ายไปอยู่หน้ารวมอุปกรณ์ หัวจอก็เหลือแต่ชื่อแบรนด์ ซึ่งเหมือนกันทุกเครื่อง
   คนที่ดูแลสี่แปลงจึงไม่มีอะไรให้ยืนยันว่าเปิดถูกแปลง ก่อนจะกดสั่งวาล์ว

   จุดคั่นอยู่ใน ::before ของตัวชื่อเอง และผูกกับ :not(:empty) ไว้ ตอนยังไม่รู้ว่า
   เครื่องไหน จะไม่มีจุดลอยๆ ค้างอยู่หลังชื่อแบรนด์ */
.header-dev { font-weight: 700; font-size: 0.95rem; opacity: 0.95; }
.header-dev:not(:empty)::before {
    content: "·"; margin: 0 8px; opacity: 0.55; font-weight: 400;
}
.status-badge { padding: 5px 15px; border-radius: 20px; font-size: 0.8rem; color: #fff; font-weight: 700; }
.online { background: rgba(255,255,255,0.22); border: 1px solid rgba(255,255,255,0.45); }
/* --success คือสีส้ม "กำลังทำงาน" ซึ่งเคยถูกใช้กับ OFFLINE ด้วย ส้มกระพริบอ่านว่า
   "กำลังติดต่ออยู่" ไม่ใช่ "กล่องดับ" - คนละความหมายกันคนละขั้ว จึงแยกออกจากกัน */
.offline { background: var(--danger); }
.badge-connecting { background: var(--success); animation: blink 2s infinite; }
.badge-unknown {
    background: rgba(255,255,255,0.12); border: 1px dashed rgba(255,255,255,0.55);
}

/* ค่าบนจอตอนกล่องไม่อยู่

   ทุกหัวข้อเป็น retained ค่าที่เห็นจึงยังอยู่ครบหลังกล่องดับ และมันคือค่าล่าสุดจริงๆ
   ไม่ใช่ขยะ - จะลบทิ้งก็ผิด เพราะบางทีนั่นแหละคือสิ่งที่คนอยากรู้ว่าดับตอนไหน
   สิ่งที่ต้องกันคือมันดูเหมือนค่าสดเท่านั้น */
body.link-down .status-grid,
body.link-down .rs485-group,
/* กล่องเงียบไป = สิ่งที่เห็นอาจไม่ใช่ของตอนนี้ จึงหรี่ลงทั้งแผง
   ชี้ที่ตัวกรอบ ไม่ใช่ที่คลาสของแถว เพราะแถวถูกสร้างใหม่ทุกครั้งที่วาด
   ส่วนกรอบอยู่ในมาร์กอัปตายตัว ไม่มีวันหายไประหว่างวาด */
body.link-down #valve-grid-container,
body.link-down #house-grid { opacity: 0.55; }

.status-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 20px; }
.status-card {
    background: linear-gradient(180deg, var(--card), var(--soft));
    border: 1px solid var(--border); border-top: 3px solid var(--primary);
    padding: 18px 20px; border-radius: 12px; text-align: center;
    box-shadow: var(--shadow);
}
.status-card small { color: var(--muted); font-weight: 600; }
.status-card .value { font-size: 2rem; font-weight: bold; color: var(--primary); }
.status-grid .status-card:nth-child(2) { border-top-color: var(--info); }
.status-grid .status-card:nth-child(2) .value { color: var(--info); }
.status-grid .status-card:nth-child(3) { border-top-color: var(--accent); }
.status-grid .status-card:nth-child(3) .value { color: var(--accent); }

/* สองคอลัมน์ กว้างสองต่อหนึ่ง - ใช้จริงในแท็บตั้งค่า ซึ่งมีการ์ดหกใบวางคู่กันอยู่

   คลาสนี้เคยถูกใช้ห่อการ์ดใบเดียวในแท็บแดชบอร์ดด้วย ซึ่งทำให้การ์ดนั้นได้แค่
   สองในสามของหน้า อีกหนึ่งในสามว่างเปล่า แล้วแถวควบคุมช่องข้างในถูกบีบจนชื่อช่อง
   เหลือ "แ.." (เจอจริง 25 ส.ค. 2569)

   ทางแก้คือเอาตัวห่อออกจากที่ที่มีการ์ดใบเดียว ไม่ใช่แก้กฎนี้ - เพราะที่ที่ใช้มันจริง
   ยังต้องการสองคอลัมน์อยู่ tools/tap-target-test.js กันไม่ให้มีใครเอาไปห่อของ
   ที่มีลูกไม่ครบสองใบอีก */
.main-layout { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 20px; }
.card {
    background: var(--card); border: 1px solid var(--border);
    padding: 20px; border-radius: 12px;
    box-shadow: var(--shadow);
}
.card h3 {
    margin: 0 0 4px; display: flex; align-items: center; gap: 9px;
    font-size: 1.05rem; color: var(--text);
}
.card h3 i { color: var(--primary); }
.card-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
.card-actions { display: flex; gap: 10px; flex-wrap: wrap; }

/* Valve Grid: 4 คอลัมน์สำหรับ 8 ปุ่ม */

.table-responsive { overflow-x: auto; margin-top: 10px; border-radius: 10px; border: 1px solid var(--border); }
table { width: 100%; border-collapse: collapse; min-width: 600px; }
thead th {
    background: linear-gradient(180deg, var(--soft), var(--well));
    color: var(--text); font-size: 0.82rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.03em;
    border-bottom: 2px solid var(--border);
}
th, td { padding: 11px 12px; border-bottom: 1px solid var(--border); text-align: center; font-size: 0.9rem; }
tbody tr:nth-child(even) { background: var(--soft); }
tbody tr:hover { background: var(--primary-soft); }
tbody tr:last-child td { border-bottom: none; }
.dur-text { font-family: "Consolas", monospace; color: var(--text); font-weight: 700; }
.on { color: var(--success); font-weight: bold; }

.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); }
.modal-content { max-width: 560px; margin: 80px auto; }
.form-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.form-row label { font-weight: 600; }
#timer-start { width: 100%; padding: 12px; background: var(--bg); border: 2px solid var(--primary); color: var(--text); border-radius: 6px; }
.timer-input-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; }
.timer-input { display: flex; flex-direction: column; gap: 5px; }
.timer-input label { font-size: 0.85rem; color: var(--ch, var(--primary)); font-weight: 700; display: flex; align-items: center; gap: 6px; }
.ch-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--ch, var(--primary)); flex-shrink: 0; }
.timer-input input { width: 100%; padding: 10px; background: var(--card); border: 1px solid var(--border); border-left: 3px solid var(--ch, var(--primary)); color: var(--text); border-radius: 6px; }
.timer-input input:focus { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--ch, var(--primary)) 22%, transparent); }
.hint {
    color: var(--muted); font-size: 0.88rem; margin-bottom: 14px;
    background: var(--soft); border-left: 3px solid var(--primary);
    padding: 9px 12px; border-radius: 0 8px 8px 0; line-height: 1.6;
}
/* แถบเตือนใช้โครงเดียวกับ .hint ต่างแค่สี เพื่อให้ "เรื่องที่ต้องลงมือทำ" ต่างจาก
   "เรื่องที่อ่านไว้เฉยๆ" ได้ด้วยการชำเลืองมอง ไม่ต้องอ่านข้อความก่อนถึงจะรู้ */
.hint-warn { background: var(--warn-bg); border-left-color: var(--warn); color: var(--warn-ink); }

/* ===== การ์ดอุปกรณ์ในโรงเรือน =====
   ใหญ่กว่าปุ่มวาล์วเพราะไอคอนต้องอ่านออกจากระยะที่คนยืนดูแท็บเล็ตบนโต๊ะ ไม่ใช่
   จ้องจอมือถือ และเพราะช่องพวกนี้มีแค่แปดช่อง ไม่ต้องอัดให้แน่นเหมือนวาล์วสิบหกตัว */
/* เป็นรายการแถวแล้ว ไม่ใช่ตารางสี่คอลัมน์ - แถวเรียงลงมาตรงๆ ไม่ต้องมีกฎอะไร
   เก็บชื่อคลาสไว้เพราะมาร์กอัปกับสคริปต์อ้างถึงมันอยู่ และการเปลี่ยนชื่อคลาส
   ก็ไม่ได้ทำให้อะไรดีขึ้นนอกจากทำให้ diff ยาวขึ้น */
.house-grid { display: block; }


/* ตัวเลือกไอคอนในหน้าตั้งค่า */
/* ===== ระดับปุ๋ยในถัง =====
   สีบอกสถานะได้เองโดยไม่ต้องอ่านตัวหนังสือ เขียว=ปกติ เหลือง=เหลือน้อย
   แดง=หมด ม่วง=สวิตช์ผิดปกติ ตัวสุดท้ายต้องไม่ใช่สีแดง เพราะมันไม่ใช่ "ปุ๋ยหมด"
   แต่เป็น "อ่านค่าไม่ได้" ซึ่งช่างต้องไปแก้คนละทางกับการเติมปุ๋ย */
.house-sub-h { margin: 22px 0 10px; font-size: .95rem; color: var(--muted); }
.tank-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
@media (max-width: 900px) { .tank-grid { grid-template-columns: repeat(2, 1fr); } }

.tank-card {
    border: 1px solid var(--border); border-radius: 12px; padding: 12px 10px;
    text-align: center; background: var(--card); border-left: 4px solid var(--tank, var(--muted));
}
.tank-name { font-size: .82rem; color: var(--muted); }
.tank-state { font-size: .95rem; font-weight: bold; color: var(--tank, var(--text)); margin-top: 4px; }
/* บรรทัดบอกว่าถังใบนี้คุมช่องไหน - เล็กและจาง เพราะเป็นข้อมูลตอนติดตั้ง
   ไม่ใช่สิ่งที่ต้องอ่านทุกวัน แต่ต้องอยู่ตรงนั้นให้เทียบกับสายได้ */
.tank-guard {
    margin-top: 6px; font-size: .72rem; color: var(--muted);
    line-height: 1.35; word-break: break-word;
}

/* ===== รูปถังพร้อมลูกลอย ===== */
.tank-head { display: flex; align-items: center; gap: 10px; }
.tank-meta { min-width: 0; }
.tanksvg { flex: none; display: block; }
.water-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.water-tank-gfx { flex: none; }
/* บนจอแคบให้รูปกับตัวเลขเรียงลงมา ไม่ใช่บีบตัวเลขจนอ่านไม่ออก */
@media (max-width: 560px) {
    .water-row { gap: 10px; }
    .water-row .status-grid { flex: 1 1 100%; }
}

.tank-bar { height: 8px; border-radius: 999px; background: var(--well); margin-top: 9px; overflow: hidden; }
.tank-fill { height: 100%; background: var(--tank, var(--muted)); transition: width .3s; }

/* กล่องที่ใส่แถวของวาล์ว - ต้องเป็นบล็อกธรรมดา ไม่ใช่กริด

   ของเดิมใช้ .timer-input-grid ซึ่งเป็นกริดสี่คอลัมน์ พอเปลี่ยนลูกให้เป็น "แถวแนวนอน"
   แต่ลืมเปลี่ยนแม่ ทุกแถวเลยถูกยัดเป็นเซลล์กว้างหนึ่งในสี่ แล้วกริดข้างในแถว
   (ต้องการอย่างน้อย 520px) ก็ล้นออกนอกเซลล์ ตัวหนังสือทับกันและได้แถบเลื่อนแนวนอน

   จัดสไตล์ให้ลูกแล้วต้องดูแม่ด้วยเสมอ - ลูกที่ถูกต้องในกล่องที่ผิดยังผิดอยู่ดี */
.dur-list { display: block; margin-bottom: 14px; }

/* หน้าต่างตั้งเวลาเป็นตาราง ไม่ใช่ฟอร์ม จึงต้องกว้างกว่าหน้าต่างอื่น */
.modal-content.modal-wide { max-width: 760px; }

/* ===== หน้าต่างตั้งเวลา: หนึ่งวาล์ว = หนึ่งแถวแนวนอน =====
   [ช่อง] [เวลารดน้ำ] [ถังปุ๋ย] [เวลาฉีด] อ่านจากซ้ายไปขวาได้ในบรรทัดเดียว
   ของเดิมเป็นกริดสี่คอลัมน์ ช่องเวลากับตัวเลือกปุ๋ยของวาล์วเดียวกันจึงอยู่คนละแถว */
.dur-head, .dur-row {
    display: grid; grid-template-columns: minmax(130px,1.4fr) 110px minmax(140px,1.3fr) 110px;
    gap: 10px; align-items: center;
}
.dur-head.no-fert, .dur-row.no-fert { grid-template-columns: minmax(130px,1.4fr) 110px; }
.dur-head {
    font-size: .78rem; color: var(--muted); font-weight: 700;
    padding: 6px 6px 6px 9px; border-bottom: 2px solid var(--border); margin-bottom: 2px;
    /* เหนียวติดด้านบนตอนเลื่อน แปดแถวยาวพอที่จะลืมว่าคอลัมน์ไหนคืออะไร
       หน้าต่างนี้เลื่อนได้เอง (overflow-y ที่ .modal-content) sticky จึงเกาะกับมัน */
    position: sticky; top: 0; background: var(--card); z-index: 2;
}
.dur-row {
    padding: 6px 6px 6px 9px; border-bottom: 1px solid var(--soft);
    border-left: 3px solid var(--ch, var(--primary)); border-radius: 3px;
}
/* สลับสีพื้นทีละแถว ตาไล่ตามแนวนอนได้โดยไม่หลุดไปแถวอื่น ซึ่งเป็นปัญหาหลัก
   ของตารางที่มีแปดแถวและสี่คอลัมน์ */
.dur-row:nth-child(even) { background: var(--soft); }
.dur-row:last-child { border-bottom: none; }
/* ตัวเลขชิดขวา เทียบค่ากันในแนวตั้งได้ทันทีว่าช่องไหนตั้งนานกว่ากัน
   หัวคอลัมน์ต้องชิดขวาตามด้วย ไม่งั้นป้ายจะลอยอยู่คนละข้างกับเลขที่มันอธิบาย */
.dur-row input[type="number"] { text-align: right; font-family: "Consolas", monospace; }
.dur-head .num { text-align: right; }
.dur-row > label { font-size: .85rem; font-weight: 700; display: flex; align-items: center; gap: 6px; }
.dur-row input, .dur-row select {
    width: 100%; padding: 8px; font-size: .85rem; color: var(--text);
    background: var(--card); border: 1px solid var(--border); border-radius: 6px;
    /* สไตล์ชีตนี้ไม่ได้ตั้ง border-box ไว้ทั้งไฟล์ - มีแค่ในการ์ดล็อกอินสองที่
       width:100% แบบ content-box จึงบวก padding กับ border เพิ่มอีก 18px
       ทุกช่องล้นรางของตัวเอง กริดกว้างเกินกล่อง ได้แถบเลื่อนแนวนอนมา */
    box-sizing: border-box;
}
.dur-row input:disabled { background: var(--well); color: var(--muted); }

/* จอแคบ: ยุบเป็นสองคอลัมน์ ป้ายกินเต็มแถว ยังจับคู่กันได้เพราะอยู่ในกล่องเดียวกัน */
@media (max-width: 720px) {
    .modal-content.modal-wide { max-width: 100%; }
    .dur-head { display: none; }
    .dur-row, .dur-row.no-fert { grid-template-columns: 1fr 1fr; }
    .dur-row > label { grid-column: 1 / -1; }
}

/* ===== ตารางเปิด-ปิดของไฟและพัดลม ===== */
/* คอลัมน์แรกเป็น auto ไม่ใช่ตัวเลขตายตัว

   เดิมเป็น 54px ซึ่งพอดีกับสวิตช์ 48px ของเดิม พอสวิตช์โตเป็น 68 มันก็ล้นทับ
   ชื่ออุปกรณ์ในคอลัมน์ถัดไปทันที และไม่มีอะไรฟ้อง เพราะกริดไม่ได้ผิดกฎอะไร
   แค่ของข้างในกว้างกว่าที่จองไว้

   auto วัดจากของจริงที่อยู่ข้างใน ปรับขนาดสวิตช์อีกกี่ครั้งก็ไม่ต้องกลับมาแก้ที่นี่ */
.auxsch-head, .auxsch-row {
    display: grid; grid-template-columns: auto minmax(120px,1.6fr) minmax(0,1fr) minmax(0,1fr);
    gap: 10px; align-items: center;
}
.auxsch-head {
    font-size: .78rem; color: var(--muted); font-weight: 700;
    padding: 0 2px 6px; border-bottom: 1px solid var(--border);
}
.auxsch-row {
    padding: 8px 2px 8px 9px; border-bottom: 1px solid var(--soft);
    border-left: 3px solid var(--ch, var(--primary)); border-radius: 3px;
}
/* สวิตช์ในตารางต้องไม่ยืดเต็มคอลัมน์เหมือน input ตัวอื่นในกริดเดียวกัน */
.auxsch-sw { justify-self: start; }
.auxsch-name { font-size: .87rem; font-weight: 700; display: flex; align-items: center; gap: 7px; }
.auxsch-name i { color: var(--ch, var(--primary)); }
/* ช่องเวลาแบบพิมพ์เอง - ใช้ฟอนต์ความกว้างเท่ากันเพื่อให้ HH:MM อยู่ตรงกันทุกแถว
   และชิดกลางเพราะข้อความสั้นและมีความยาวคงที่เสมอ */
.t24 { font-family: "Consolas", monospace; text-align: center; letter-spacing: .5px; }

/* ข้อความใต้ช่องที่เขียนได้อย่างเดียว บอกว่าเคยตั้งไว้แล้วหรือยัง */
/* แถวช่องติ๊กของอุปกรณ์ RS485 - กดที่ข้อความก็ติ๊กได้ ไม่ต้องเล็งช่องเล็กๆ */
.rs-dev { display: flex; align-items: center; gap: 8px; padding: 5px 0; font-size: .88rem; cursor: pointer; }
.rs-dev input { width: auto; margin: 0; }
.rs-dev input:disabled + * , .rs-dev:has(input:disabled) { color: var(--muted); cursor: default; }

.secret-hint { font-size: .76rem; margin: 4px 0 0; color: var(--muted); }
.secret-hint.ok   { color: #2ecc71; }
.secret-hint.warn { color: #e67e22; }

/* ปุ่มนาฬิกาซ้อนอยู่ในช่อง ไม่ได้กินคอลัมน์เพิ่ม

   ช่องเวลาสามในสี่ที่อยู่ในกริดที่คำนวณความกว้างไว้แล้ว การเพิ่มปุ่มเป็นอีกคอลัมน์
   จะทำให้ทุกกริดนั้นต้องคิดใหม่หมด ซ้อนทับแล้วเว้น padding ขวาให้ตัวหนังสือแทน */
.t24wrap { position: relative; display: block; min-width: 0; }
.t24wrap .t24 { padding-right: 30px; }
.t24btn {
    position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
    width: 24px; height: 24px; padding: 0; border: none; border-radius: 6px;
    background: transparent; color: var(--muted); cursor: pointer; font-size: .85rem;
}
.t24btn:hover { background: var(--soft); color: var(--primary); }

/* ===== แผงเลือกเวลาแบบวงล้อ ===== */
.tp-overlay {
    position: fixed; inset: 0; z-index: 1200;
    background: rgba(15, 23, 42, .45);
    display: flex; align-items: center; justify-content: center; padding: 16px;
}
.tp-card {
    background: var(--card); border-radius: 18px; padding: 18px 16px 14px;
    width: min(340px, 100%); box-shadow: var(--shadow-float);
}
.tp-title { font-weight: 700; text-align: center; }
.tp-sub   { font-size: .76rem; color: var(--muted); text-align: center; margin: 2px 0 12px; }
.tp-heads { display: flex; gap: 6px; }
.tp-heads span { flex: 1; text-align: center; font-size: .72rem; color: var(--muted); }

.tp-wheels { position: relative; display: flex; gap: 6px; margin-top: 4px; }

/* แถบกรอบบอกว่าแถวไหนคือแถวที่ถูกเลือก อยู่กึ่งกลางของ .tp-wheels พอดี
   จึงต้องไม่มีหัวคอลัมน์อยู่ในกล่องเดียวกัน ไม่งั้นกึ่งกลางจะเลื่อน */
.tp-band {
    position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%);
    height: 44px; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
    background: var(--soft); border-radius: 8px; pointer-events: none;
}
.tp-scroll {
    flex: 1; height: 132px; overflow-y: auto; position: relative;
    scroll-snap-type: y mandatory; scrollbar-width: none; -ms-overflow-style: none;
    /* จางที่ขอบบนล่าง ทำให้แถวที่ยังไม่ได้เลือกดูถอยหลังไป โดยไม่ต้องคำนวณทีละแถว */
    -webkit-mask-image: linear-gradient(to bottom, transparent, #000 32%, #000 68%, transparent);
            mask-image: linear-gradient(to bottom, transparent, #000 32%, #000 68%, transparent);
}
.tp-scroll::-webkit-scrollbar { display: none; }
.tp-pad  { height: 44px; }
.tp-item {
    height: 44px; line-height: 44px; text-align: center;
    scroll-snap-align: center; font-family: "Consolas", monospace;
    font-size: 1.15rem; color: var(--muted);
    transition: color .12s, font-size .12s;
}
.tp-item.on { color: var(--primary); font-weight: 700; font-size: 1.55rem; }

.tp-actions { display: flex; gap: 8px; margin-top: 14px; }
.tp-actions button { flex: 1; padding: 10px; border-radius: 8px; font-size: .9rem; cursor: pointer; }

@media (max-width: 400px) { .tp-item { font-size: 1.05rem; } .tp-item.on { font-size: 1.4rem; } }

/* ===== ตารางที่ยุบเป็นการ์ดบนจอแคบ =====

   table มี min-width: 600px อยู่ในกล่องที่ overflow-x: auto บนมือถือกว้าง 400px
   จึงต้องเลื่อนซ้ายขวา และพอเลื่อนไปดูลำดับการทำงาน คอลัมน์ #, ใช้งาน, เวลาเริ่ม
   ก็หลุดออกนอกจอไปหมด - เห็นทีละครึ่งเดียวเสมอ ไม่ว่าจะเลื่อนไปทางไหน

   ยุบเป็นการ์ดแล้วทุกอย่างของโปรแกรมหนึ่งอยู่ในกรอบเดียวกัน อ่านจากบนลงล่าง
   ไม่มีอะไรหลุดขอบ และไม่ต้องเลื่อนแนวนอนอีกเลย

   ป้ายกำกับมาจาก data-label ของแต่ละ td เพราะหัวตารางถูกซ่อนไปแล้ว
   ถ้าลืมใส่ data-label ช่องนั้นจะกลายเป็นตัวเลขลอยๆ ที่ไม่รู้ว่าคืออะไร */
@media (max-width: 720px) {
    table.stacky { min-width: 0; }
    table.stacky thead { display: none; }
    table.stacky tbody, table.stacky tr, table.stacky td { display: block; width: auto; }
    table.stacky tr {
        border: 1px solid var(--border); border-radius: 10px;
        margin: 0 0 10px; padding: 6px 10px; background: var(--card);
    }
    table.stacky tr.timer-on { border-left: 3px solid var(--primary); }
    table.stacky td {
        display: flex; align-items: center; justify-content: space-between; gap: 10px;
        border: none; padding: 6px 0; text-align: right;
    }
    table.stacky td + td { border-top: 1px dashed var(--soft); }
    table.stacky td::before {
        content: attr(data-label);
        font-size: .74rem; color: var(--muted); font-weight: 700;
        text-align: left; flex: 0 0 auto;
    }
    /* ลำดับการทำงานยาวกว่าช่องอื่นมาก วางป้ายไว้บรรทัดบนแล้วให้ชิปกินเต็มความกว้าง */
    table.stacky td.step-cell { display: block; text-align: left; }
    table.stacky td.step-cell::before { display: block; margin-bottom: 6px; }
    /* ปลดรางเลื่อนแนวนอนทิ้ง เพราะการ์ดไม่ต้องเลื่อนแล้ว

       กฎนี้ไม่ได้เจาะจงว่าข้างในต้องเป็นตาราง stacky และเจาะจงไม่ได้ง่ายๆ ด้วย
       ผลคือตารางที่ยังไม่ได้ยุบจะเสียรางเลื่อนไปด้วย แล้วดัน min-width: 600px
       ออกไปทั้งหน้า - ทั้งหน้าเลยกว้างเกินจอ ไม่ใช่แค่ในกรอบ ซึ่งแย่กว่าเดิม
       หน้าผู้ดูแลเป็นแบบนั้นอยู่จนถึง 24 ส.ค. 2569

       ทางกันคือ tools/stacky-table-test.js บังคับว่าทุก <table> ที่อยู่ใน
       .table-responsive ต้องเป็น stacky ไม่มีข้อยกเว้น */
    .table-responsive { border: none; overflow-x: visible; }
}

.auxsch-row input[type="time"], .auxsch-row input.t24 {
    /* min-width:0 ทับค่าเริ่มต้นของ input ที่กว้างตาม size ~20 ตัวอักษร
       ถ้าไม่มีบรรทัดนี้ width:100% ก็ยังหดไม่ลงเพราะขั้นต่ำมาจากตัว input เอง */
    width: 100%; min-width: 0; padding: 7px; font-size: .85rem; color: var(--text);
    background: var(--card); border: 1px solid var(--border); border-radius: 6px;
    box-sizing: border-box;   /* บั๊กตัวเดียวกับ .dur-row ยังไม่ล้นเพราะรางกว้างกว่า */
}
@media (max-width: 720px) {
    .auxsch-head { display: none; }
    /* minmax(0,1fr) ไม่ใช่ 1fr เฉยๆ

       1fr = minmax(auto,1fr) และ auto แปลว่า "หดต่ำกว่าเนื้อหาไม่ได้" ซึ่งกับ <input>
       แปลว่าหดต่ำกว่าความกว้างในตัวของมัน (size เริ่มต้น ~20 ตัวอักษร) ไม่ได้เลย
       ต่อให้สั่ง width:100% ไปแล้วก็ตาม

       ผลบนจอ 360px: สวิตช์ + ช่องเวลาสองช่องรวมกันกว้างเกินจอ ช่องที่สองถูกตัดหายไป
       ครึ่งหนึ่งพร้อมปุ่มนาฬิกาของมัน (รายงานมาจากมือถือจริง 25 ส.ค. 2569) */
    .auxsch-row { grid-template-columns: auto minmax(0,1fr) minmax(0,1fr); }
    .auxsch-name { grid-column: 2 / -1; min-width: 0; }
}

.icon-pick { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.icon-pick button {
    width: 38px; height: 34px; padding: 0; border-radius: 8px; font-size: 1rem;
    background: var(--card); border: 1px solid var(--border); color: var(--muted);
}
.icon-pick button.sel { background: var(--primary-soft); border-color: var(--primary); color: var(--primary); }

.modal-buttons { display: flex; gap: 10px; }

button { border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: bold; transition: filter .15s, transform .05s; }
button:hover { filter: brightness(1.07); }
button:active { transform: translateY(1px); }
.btn-primary { background: linear-gradient(180deg, #3b82f6, var(--primary)); color: #fff; box-shadow: 0 2px 6px rgba(37,99,235,.3); }
.btn-success { background: linear-gradient(180deg, #fb923c, var(--success)); color: #fff; flex: 1; box-shadow: 0 2px 6px rgba(249,115,22,.3); }
.btn-danger  { background: linear-gradient(180deg, #ef4444, var(--danger)); color: #fff; box-shadow: 0 2px 6px rgba(220,38,38,.3); }

.tabs { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
.tab-button {
    background: var(--card); border: 1px solid var(--border); padding: 10px 18px;
    border-radius: 999px; cursor: pointer; color: var(--muted); font-weight: 700;
    display: inline-flex; align-items: center; gap: 7px;
}
.tab-button:hover { color: var(--primary); border-color: color-mix(in srgb, var(--primary) 45%, var(--border)); }
.tab-button.active {
    background: linear-gradient(135deg, var(--primary), var(--accent));
    color: #fff; border-color: transparent;
    box-shadow: 0 4px 12px rgba(37, 99, 235, 0.32);
}
.tab-content { display: none; }
.tab-content.active { display: block; }

/* .hide เคยไม่มีในไฟล์นี้เลย มีแต่ใน <style> ของ soil.html - index.html จึงซ่อนอะไรไม่ได้

   เขียน .tab-button.hide ไว้ด้วยโดยตั้งใจ ไม่ใช่พึ่ง .hide ตัวเดียว: .tab-button ตั้ง
   display: inline-flex เอาไว้ และทั้งคู่เป็นเซเลกเตอร์คลาสเดียวน้ำหนักเท่ากัน ผู้ชนะ
   จึงขึ้นกับลำดับในไฟล์ล้วนๆ ซึ่งเป็นสิ่งที่ขยับได้ทุกครั้งที่มีคนจัดระเบียบ CSS ใหม่
   บั๊ก #advbar เมื่อ 16 ส.ค. คือเรื่องเดียวกันนี้ทุกประการ */
.hide { display: none; }
.tab-button.hide { display: none; }

@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
@media (max-width: 768px) { 
    .main-layout, .status-grid { grid-template-columns: 1fr; } 
    .timer-input-grid { grid-template-columns: 1fr; }
    .tabs { flex-direction: column; }
}
/* ====== Valve button countdown ====== */

.btn-secondary { background: #6c757d; color: white; }

/* ====== Login Page ====== */
.login-page {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background: linear-gradient(135deg, #1d4ed8 0%, #4c1d95 55%, #0f2035 100%);
    margin: -20px;
    padding: 20px;
    box-sizing: border-box;
}
.login-card {
    background: var(--card);
    border-radius: 16px;
    padding: 40px 36px;
    width: 100%;
    max-width: 360px;
    text-align: center;
    box-shadow: var(--shadow-float);
}
.login-logo {
    font-size: 3rem;
    color: var(--primary);
    margin-bottom: 10px;
    display: flex;
    justify-content: center;
}
#login-profile-img {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    object-fit: cover;
    border: 3px solid var(--primary);
    box-shadow: 0 4px 16px rgba(37,99,235,0.25);
}
.login-title {
    margin: 0 0 4px;
    font-size: 1.4rem;
    color: var(--text);
    letter-spacing: 0.05em;
}
.login-subtitle {
    color: var(--muted);
    font-size: 0.9rem;
    margin: 0 0 24px;
}
.login-card .form-row {
    text-align: left;
    margin-bottom: 14px;
}
.login-card .form-row input {
    width: 100%;
    padding: 13px 14px;
    border: 2px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
    color: var(--text);
    font-size: 1rem;
    box-sizing: border-box;
    transition: border-color 0.2s;
}
.login-card .form-row input:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(37,99,235,0.12);
}
.login-btn { width: 100%; padding: 13px; font-size: 1rem; border-radius: 8px; }
.password-error { color: var(--danger); font-size: 0.88rem; margin-top: 10px; min-height: 1.2em; }
/* ---- อุปกรณ์ในบัญชี ---- */
.device-list { display: flex; flex-direction: column; gap: 10px; }
.device-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-left: 4px solid var(--primary);
    border-radius: 10px;
    background: var(--card);
}

.device-status {
    margin-top: 4px;
    font-size: 0.82rem;
    font-weight: 600;
}
.dev-on      { color: #16a34a; }
.dev-busy    { color: #2563eb; }
.dev-off     { color: #dc2626; }
.dev-unknown { color: var(--muted, #94a3b8); }

/* บนจอแคบปุ่มสองตัวต้องยอมตกบรรทัด ไม่งั้นมันจะบีบชื่อเครื่องจนอ่านไม่ออก */
.device-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    justify-content: flex-end;
}

/* ปุ่มในรายการอุปกรณ์ต้องมีสไตล์ของตัวเอง ห้ามใช้ .btn-logout ซ้ำ
   ปุ่มนั้นเป็นตัวอักษรขาวบนพื้นโปร่งใส ออกแบบไว้สำหรับแถบหัวสีเข้ม
   พอมาวางบนการ์ดสีขาวจะกลายเป็นขาวบนขาว มองไม่เห็นจนกว่าจะเอาเมาส์ไปวาง */
.btn-row {
    background: var(--card);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 6px 12px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.88rem;
    font-weight: 600;
    white-space: nowrap;
}
.btn-row:hover { background: var(--primary); color: #fff; border-color: var(--primary); }

/* ปลดออกเป็นสิ่งที่กดผิดแล้วเสียเวลาไปผูกใหม่ ให้หน้าตาบอกไว้ก่อนว่าเป็นคนละพวก */
.btn-row-danger { color: var(--danger); border-color: var(--danger); }
.btn-row-danger:hover { background: var(--danger); border-color: var(--danger); }
.device-name   { font-weight: 700; }
.device-serial { font-family: ui-monospace, "Consolas", monospace; font-size: 0.82rem; color: var(--muted); }

.pair-prompt { margin: 4px 0 12px; line-height: 1.7; }
.pair-countdown {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    background: rgba(245, 158, 11, 0.15);
    color: var(--warn-ink);
    font-size: 0.8rem;
    font-weight: 600;
}
/* รหัสจับคู่อ่านจากจอ 20x4 ตัวเล็กๆ แล้วพิมพ์ตาม ช่องกรอกจึงต้องใหญ่และห่าง
   จะได้เห็นชัดว่าพิมพ์ครบหรือยังโดยไม่ต้องนับ */
.pair-code-input {
    font-family: ui-monospace, "Consolas", monospace;
    font-size: 1.6rem !important;
    letter-spacing: 0.5em;
    text-align: center;
    font-weight: 700;
}
.pair-msg { margin: 12px 0 0; font-size: 0.9rem; color: var(--success); min-height: 1.2em; }
.pair-msg-error { color: var(--danger); }

.auth-switch {
    margin: 12px 0 0;
    font-size: 0.9rem;
    color: var(--muted);
}
.auth-switch a { color: var(--primary); font-weight: 600; text-decoration: none; }
.auth-switch a:hover { text-decoration: underline; }
/* คำเตือนเรื่องไม่มี HTTPS ต้องเห็นตอนตั้งรหัสผ่าน ไม่ใช่ไปซ่อนในหน้าอื่น */
.auth-note {
    margin: 14px 0 0;
    padding: 8px 10px;
    border-radius: 8px;
    background: rgba(245, 158, 11, 0.12);
    border: 1px solid rgba(245, 158, 11, 0.35);
    color: var(--warn-ink);
    font-size: 0.78rem;
    line-height: 1.5;
    text-align: left;
}
/* ปุ่มสำหรับแถบหัวสีเข้มเท่านั้น ตัวอักษรขาวบนพื้นโปร่งใส
   เอาไปวางบนพื้นสีอ่อนแล้วจะกลายเป็นขาวบนขาว มองไม่เห็นจนกว่าจะเอาเมาส์ไปวาง
   บนการ์ดให้ใช้ .btn-row แทน
   (เดิมประกาศ .btn-logout ซ้ำสองก้อน ก้อนหลังทับก้อนแรกเงียบๆ รวมเป็นก้อนเดียวแล้ว) */
.btn-logout {
    background: rgba(255,255,255,0.18);
    border: 1px solid rgba(255,255,255,0.4);
    color: #fff;
    padding: 6px 10px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.9rem;
}
.btn-logout:hover { background: var(--danger); color: #fff; border-color: var(--danger); }

/* ====== Profile Image ====== */
.profile-preview-wrap { display: flex; justify-content: center; }
.profile-preview {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    object-fit: cover;
    border: 3px solid var(--primary);
    box-shadow: 0 4px 12px rgba(37,99,235,0.2);
}
.save-toast {
    position: fixed;
    bottom: 30px;
    left: 50%;
    transform: translateX(-50%) translateY(20px);
    background: #1d4ed8;
    color: #fff;
    padding: 12px 28px;
    border-radius: 24px;
    font-weight: bold;
    font-size: 0.95rem;
    opacity: 0;
    transition: opacity 0.3s, transform 0.3s;
    pointer-events: none;
    z-index: 9999;
    box-shadow: 0 4px 16px rgba(0,0,0,0.25);
}
.save-toast.show {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

input[type="file"] {
    width: 100%;
    padding: 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    cursor: pointer;
}

/* ====== Toggle Switch ======
   68x44 ไม่ใช่ 48x26

   26px คือความสูงที่ยังกดโดนได้ ไม่ใช่ความสูงที่กดสบาย ซึ่งเป็นคนละเลขกัน
   และช่องว่างระหว่างสองเลขนี้สำคัญ เพราะคนที่กดยืนอยู่กลางแปลง มือเปียก
   และสามสวิตช์ที่ใช้คลาสนี้คือ เปิดชุดเซนเซอร์ ตัดอัตโนมัติ และเปิดโปรแกรมรดน้ำ
   กดพลาดแล้วไม่ใช่แค่รำคาญ

   .toggle-row ตั้ง min-height ไว้ด้วย เพราะทั้งแถวเป็น label ที่กดได้
   ไม่ใช่แค่ตัวสวิตช์ ถ้าแถวเตี้ยกว่าสวิตช์ พื้นที่กดจริงจะเล็กกว่าที่ตาเห็น */
.toggle-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    min-height: 48px;
    font-weight: 600;
    cursor: pointer;
}
.switch { position: relative; display: inline-block; width: 68px; height: 44px; flex-shrink: 0; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider {
    position: absolute; inset: 0;
    background: var(--well); border-radius: 44px;
    transition: 0.3s; cursor: pointer;
}
.slider:before {
    content: ""; position: absolute;
    width: 36px; height: 36px; left: 4px; bottom: 4px;
    background: white; border-radius: 50%; transition: 0.3s;
    box-shadow: 0 1px 3px rgba(15, 39, 64, 0.28);
}
.switch input:checked + .slider { background: var(--success); }
.switch input:checked + .slider:before { transform: translateX(24px); }
/* วงแหวนตอนโฟกัสด้วยคีย์บอร์ด - สวิตช์จริงคือ input ที่มองไม่เห็น
   ถ้าไม่วาดวงแหวนให้ตัว slider คนที่ไล่ด้วย Tab จะไม่รู้เลยว่าตอนนี้อยู่ตรงไหน */
.switch input:focus-visible + .slider { box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35); }

/* ====== IO Extender Board Sections ====== */
.board-section { margin-bottom: 16px; }
/* ====== Board RTC Clock Bar ====== */
.rtc-clock-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 10px 18px;
    margin-bottom: 16px;
    font-size: 1.05rem;
    color: var(--text);
    box-shadow: var(--shadow);
}
.rtc-clock-bar i { color: var(--primary); font-size: 1rem; }
.rtc-clock-bar #val-datetime {
    font-family: monospace;
    font-size: 1.15rem;
    font-weight: bold;
    color: var(--primary);
    letter-spacing: 0.04em;
}
.rtc-clock-bar small { color: var(--muted); font-size: 0.78rem; margin-left: 4px; }
.rtc-clock-bar .btn-ntp {
    margin-left: auto;
    background: transparent;
    border: 1px solid var(--primary);
    color: var(--primary);
    padding: 5px 12px;
    border-radius: 20px;
    font-size: 0.8rem;
    cursor: pointer;
    transition: background 0.2s, color 0.2s;
    white-space: nowrap;
}
.rtc-clock-bar .btn-ntp:hover:not(:disabled) { background: var(--primary); color: #fff; }
.rtc-clock-bar .btn-ntp:disabled { opacity: 0.5; cursor: not-allowed; }

/* NTP toast */
.ntp-toast {
    position: fixed; bottom: 80px; left: 50%;
    transform: translateX(-50%) translateY(20px);
    padding: 10px 24px; border-radius: 20px;
    font-size: 0.9rem; font-weight: bold;
    opacity: 0; transition: opacity 0.3s, transform 0.3s;
    pointer-events: none; z-index: 9999;
    box-shadow: 0 4px 16px rgba(0,0,0,0.2);
}
.ntp-toast.show  { opacity: 1; transform: translateX(-50%) translateY(0); }
.ntp-toast.success { background: #16a34a; color: #fff; }
.ntp-toast.error   { background: #dc2626; color: #fff; }
.ntp-toast.info    { background: #2563eb; color: #fff; }

/* ===== Timer sequence chips =====
   แทนข้อความยาวติดกัน "V1 00:10 → V2 00:10 → ..." ที่อ่านยาก
   ให้แต่ละช่องเป็นชิปสีประจำตัว มองปราดเดียวรู้ว่ามีช่องไหนบ้าง นานเท่าไร */
.step-list { display: flex; flex-wrap: wrap; gap: 6px; }
.step-chip {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 3px 9px 3px 3px; border-radius: 999px;
    background: var(--soft);                                      /* fallback */
    border: 1px solid var(--border);                          /* fallback */
    background: color-mix(in srgb, var(--ch) 12%, var(--card));
    border: 1px solid color-mix(in srgb, var(--ch) 38%, var(--card));
    font-size: 0.8rem; color: var(--text); white-space: nowrap;
}

/* ชิปปุ๋ย - ทึบและมีลูกศรนำหน้า บอกว่ามันเกาะอยู่กับวาล์วที่อยู่ข้างหน้ามัน
   ไม่ใช่สเต็ปของตัวเอง คนอ่านต้องเห็นความต่างก่อนอ่านชื่อ */
/* หนึ่งสเต็ป = วาล์วกับปุ๋ยของมันในกล่องเดียว inline-flex ทำให้มันขึ้นบรรทัดใหม่
   ทั้งคู่พร้อมกัน ไม่ถูกตัดแยกจากกันกลางคู่ */
.step-pair { display: inline-flex; align-items: center; gap: 4px; margin: 0 4px 4px 0; }

.step-chip.step-fert { border-style: solid; opacity: .92; }
.step-chip.step-fert::before { content: "↳"; color: var(--ch); font-weight: 700; margin-left: 2px; }
.step-chip b {
    display: inline-flex; align-items: center; justify-content: center;
    width: 19px; height: 19px; border-radius: 50%;
    background: var(--ch); color: #fff; font-size: 0.7rem;
}
.step-chip i { font-style: normal; color: var(--ch); }
/* เฉพาะตัวเลขเวลา ห้ามแตะไอคอน - ไอคอนก็เป็น <i> และ .step-chip i หนักกว่า .fas
   ถ้าไม่กัน monospace จะไปทับฟอนต์ไอคอน แล้วกลืมฟ์จะกลายเป็นกล่องเปล่า */
.step-chip i:not(.fas) { font-family: "Consolas", monospace; font-weight: 700; }
.step-empty { color: var(--muted); font-style: italic; }

.timer-total { font-family: "Consolas", monospace; font-weight: 700; color: var(--accent); }
tbody tr.timer-on { box-shadow: inset 3px 0 0 var(--success); }
tbody tr.timer-on td:first-child strong { color: var(--success); }
.btn-icon { padding: 7px 11px; border-radius: 8px; }

/* ===== ไอคอนไม่มา ปุ่มต้องไม่กลายเป็นสี่เหลี่ยมว่าง =====

   Font Awesome โหลดจาก cdnjs ซึ่งเป็นคนละเครื่องกับที่เสิร์ฟหน้านี้ ในไร่ที่เน็ต
   มือถืออ่อน "เซิร์ฟเวอร์เราตอบแต่ cdnjs ไม่ตอบ" เกิดขึ้นได้จริง และเกิดเฉพาะที่นั่น
   ที่โต๊ะทำงานจะไม่มีวันเห็น

   ไอคอนที่อยู่ข้างข้อความหายไปเฉยๆ ก็ยังอ่านออก แต่ปุ่มที่มีแต่ไอคอนจะเหลือ <i>
   ว่างเปล่ากว้างศูนย์ กลายเป็นปุ่มที่กดได้แต่ไม่รู้ว่ากดอะไร - และ title= ช่วยไม่ได้
   เพราะบนมือถือไม่มี hover

   ทุกปุ่มที่ติด .icon-only มี title อยู่แล้ว จึงยกข้อความนั้นมาแสดงแทนตัวไอคอน
   ธง no-fa มาจาก onerror ของ <link> ปกติจึงไม่มีกฎกลุ่มนี้ทำงานเลย */
html.no-fa .icon-only::after {
    content: attr(title);
    font-size: 0.78rem;
    font-weight: 600;
    white-space: nowrap;
}
/* ปุ่มนาฬิกาเป็นข้อยกเว้น มันวางทับขอบขวาของช่องกรอกเวลาอยู่ ปล่อยให้ยืดตามคำว่า
   "เลือกเวลา" มันจะกินพื้นที่ช่องกรอกจนพิมพ์ไม่ได้ ซึ่งแย่กว่าไอคอนหายเสียอีก

   จึงคงขนาดเดิมไว้ทุกอย่างแล้วใส่จุดสามจุดแทน เป็นธรรมเนียมของปุ่มที่เปิดหน้าต่าง
   ให้เลือก และเป็นอักขระที่มีอยู่ในทุกฟอนต์ ไม่ต้องพึ่งใคร

   ปุ่มนี้เป็นทางลัดไม่ใช่ทางเดียว ถึงไม่กดก็พิมพ์เวลาลงช่องเองได้ตามปกติ
   สิ่งที่ต้องกันคือปุ่มใสๆ ที่มองไม่เห็นแต่ยังกินการกดที่ตั้งใจจะกดช่องกรอก */
html.no-fa .t24btn.icon-only::after {
    content: "...";
    font-size: 0.8rem;
    letter-spacing: 1px;
}

/* ===== LCD 20x4 mirror ===== */
.lcd-badge {
    font-size: 0.72rem; font-weight: 700; padding: 3px 10px; border-radius: 20px;
    background: var(--soft); color: var(--primary); border: 1px solid var(--border);
    white-space: nowrap;
}
.lcd-badge.simulated { background: var(--warn-bg); color: var(--warn-ink); border-color: #fed7aa; }

.lcd-bezel {
    margin-top: 14px; padding: 18px 20px; border-radius: 10px;
    background: linear-gradient(#3a3f46, #22262b);
    box-shadow: inset 0 1px 0 rgba(255,255,255,0.18), 0 6px 16px rgba(0,0,0,0.28);
    display: flex; justify-content: center;
}
.lcd-screen {
    background: #1631c8;
    padding: 12px 14px;
    border-radius: 4px;
    box-shadow: inset 0 0 22px rgba(0,0,0,0.45), 0 0 18px rgba(60,110,255,0.5);
    /* เส้นแบ่งช่องตัวอักษรจางๆ ให้เห็นเป็นดอตเมทริกซ์เหมือนจอจริง */
    background-image: repeating-linear-gradient(90deg, rgba(255,255,255,0.05) 0 1px, transparent 1px 100%);
    background-size: 1ch 100%;
    background-position: 14px 0;
    overflow-x: auto;
}
.lcd-line {
    font-family: "Consolas", "DejaVu Sans Mono", "Courier New", monospace;
    font-size: clamp(0.78rem, 2.9vw, 1.15rem);
    line-height: 1.55;
    letter-spacing: 0.06em;
    color: #f2f6ff;
    text-shadow: 0 0 6px rgba(210,228,255,0.85);
    white-space: pre;
    min-height: 1.55em;
}
.lcd-screen.stale .lcd-line { opacity: 0.42; }

.board-name-header {
    grid-column: 1 / -1;
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--primary);
    padding: 6px 4px 2px;
    border-bottom: 1px solid var(--border);
    margin-bottom: 4px;
}

/* ====== ควบคุมตามอุณหภูมิ ====== */
.temp-now {
    align-self: center; font-weight: 700; font-size: 1.05rem;
    color: var(--primary); font-family: monospace;
}
/* คำอธิบายสี่ข้อ อ่านเป็นรายการเร็วกว่าอ่านเป็นย่อหน้าติดกันสามย่อหน้า */
.temp-help { margin: 8px 0 0; padding-left: 20px; }
.temp-help li { margin-bottom: 4px; }
.temp-warn {
    margin: 12px 0; padding: 10px 14px; border-radius: 8px;
    background: var(--danger-bg); border: 1px solid #f5c6c2; color: var(--danger-ink);
    font-size: 0.85rem; font-weight: 600;
}
/* หนึ่งกฎ = หนึ่งการ์ด สามส่วนเรียงลงมา หัว / ช่องกรอก / สิ่งที่คำนวณให้
   ของเดิมยัดทุกอย่างลงตารางเดียว ปุ่มกับตัวเลขจึงไปกองปนกันจนอ่านไม่ออก */
.temp-rule {
    display: flex; flex-direction: column; gap: 12px;
    padding: 14px 16px; margin-top: 14px;
    background: var(--bg); border: 1px solid var(--border);
    border-left: 3px solid var(--border); border-radius: 10px;
    transition: border-color 0.25s;
}
/* กฎที่เปิดใช้มีขีดสีด้านซ้าย กวาดตาทีเดียวรู้ว่ามีกี่กฎที่ทำงานอยู่ */
.temp-rule:not(.temp-rule-off) { border-left-color: var(--success); }
.temp-rule-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px; flex-wrap: wrap;
    padding-bottom: 10px; border-bottom: 1px solid var(--border);
}
.temp-rule-en {
    display: flex; align-items: center; gap: 10px;
    white-space: nowrap; flex-wrap: wrap;
}
/* ช่องกรอกทั้งเจ็ดอยู่ระดับเดียวกันหมด ไม่มีอันไหนสำคัญกว่าอันไหนในเชิงการวาง
   auto-fit ทำให้ยุบเป็นสองสามคอลัมน์เองบนจอแคบ โดยไม่ต้องเขียน media query ไล่ทีละขนาด */
.temp-rule-fields {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
    gap: 10px 12px; align-items: end;
}
.temp-field-wide { grid-column: span 2; }
.temp-rule-name { font-weight: 700; }
.temp-onoff { font-size: 0.7rem; font-weight: 700; color: var(--muted); }
.temp-onoff.on { color: var(--success); }
/* กฎที่ปิดอยู่ยังแก้ค่าได้ แค่จางลงเพื่อให้กวาดตาแล้วรู้ว่าอันไหนกำลังทำงานจริง
   ไม่จางกว่านี้ เพราะยังต้องอ่านตัวเลขออกตอนกำลังร่างกฎไว้ก่อนเปิดใช้ */
.temp-rule-off { opacity: 0.72; }
.temp-field { display: flex; flex-direction: column; gap: 5px; }
.temp-field label { font-size: 0.72rem; color: var(--muted); font-weight: 600; }
.temp-field input, .temp-field select {
    width: 100%; padding: 9px; border-radius: 6px; color: var(--text);
    background: var(--card); border: 1px solid var(--border);
}
/* ท้ายการ์ด: เฉพาะสิ่งที่ระบบคำนวณให้ ไม่มีอะไรให้กดหรือกรอกตรงนี้ */
.temp-rule-info {
    display: flex; gap: 16px; flex-wrap: wrap;
    align-items: center; font-size: 0.75rem;
    padding-top: 10px; border-top: 1px dashed var(--border);
}
.btn-test {
    background: var(--card); border: 1px solid var(--primary); color: var(--primary);
    padding: 6px 12px; border-radius: 6px; cursor: pointer;
    font-size: 0.75rem; font-weight: 600; white-space: nowrap;
}
.btn-test:hover { background: var(--primary); color: #fff; }
.temp-win { color: var(--muted); }
/* ประโยคที่บอกว่ากฎนี้จะทำงานตอนไหน อ่านจากลำดับของเกณฑ์เหมือนที่บอร์ดอ่าน
   เข้มกว่าบรรทัดอื่นในแถวนี้เพราะเป็นสิ่งเดียวที่บอกว่ากฎทำงานกลับทางหรือเปล่า */
.temp-dir { color: var(--text); font-weight: 600; flex-basis: 100%; }
.temp-dir.warn { color: var(--danger-ink); }
.temp-act { color: var(--muted); font-weight: 700; font-size: 0.75rem; }
.temp-act.on { color: var(--success); }
.temp-cyc { color: var(--muted); }
/* จังหวะที่ถี่เกินไปกินอายุรีเลย์ ต้องเห็นตั้งแต่ก่อนกดบันทึก ไม่ใช่ตอนรีเลย์พัง
   ใช้สีแดง ไม่ใช่ส้ม เพราะส้มในหน้านี้แปลว่า "กำลังทำงาน" อยู่แล้ว */
.temp-cyc.warn {
    color: var(--danger-ink); font-weight: 700;
    background: var(--danger-bg); border-radius: 5px; padding: 3px 8px;
}

/* ช่องที่กฎอุณหภูมิจองไว้ ต้องดูออกว่ากดไม่ได้ ไม่ใช่กดแล้วเงียบ */
.valve-reserved { cursor: not-allowed; border-style: dashed; opacity: 0.92; }

@media (max-width: 480px) {
    /* จอแคบมาก ช่องเลือกวาล์วกินสองคอลัมน์แล้วจะเหลือที่ให้ช่องอื่นไม่พอ */
    .temp-field-wide { grid-column: span 1; }
}

/* ================= หน้าผู้ดูแลระบบ =================
   ใช้การ์ด ปุ่ม ตาราง และสวิตช์ชุดเดียวกับแดชบอร์ด หน้านี้กว้างกว่าเพราะเป็นตาราง
   ที่มีหลายคอลัมน์ ไม่ใช่แผงควบคุมที่ต้องกดด้วยนิ้วโป้งข้างเดียว */
.admin-wrap { max-width: 1180px; }

.admin-head-right { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.admin-who {
    font-size: 0.82rem; font-weight: 600; color: #fff;
    background: rgba(255,255,255,0.18); border: 1px solid rgba(255,255,255,0.35);
    padding: 5px 12px; border-radius: 20px;
}
.btn-head {
    background: rgba(255,255,255,0.18); border: 1px solid rgba(255,255,255,0.4);
    color: #fff; padding: 7px 13px; border-radius: 8px; font-size: 0.85rem;
    font-weight: 600; text-decoration: none; display: inline-flex; align-items: center; gap: 6px;
}
.btn-head:hover { background: rgba(255,255,255,0.3); }

.admin-gate { text-align: center; }
.admin-gate .btn-head { background: var(--primary); border-color: var(--primary); margin-top: 6px; }

.admin-updated { font-size: 0.8rem; color: var(--muted); }

.admin-stats {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(126px, 1fr));
    gap: 12px; margin-top: 14px;
}
.admin-stat {
    background: var(--primary-soft); border: 1px solid var(--border);
    border-radius: 12px; padding: 14px 12px; text-align: center;
}
.admin-stat small { color: var(--muted); font-weight: 600; font-size: 0.76rem; }
.admin-stat .value { font-size: 1.6rem; font-weight: 800; color: var(--text); margin-top: 4px; }
.admin-stat .value.ok     { color: var(--ok); }
.admin-stat .value.danger { color: var(--danger); }
.admin-stat .value.info   { color: var(--info); }

.admin-live { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 12px; }
.admin-live-card {
    border: 1px solid var(--border); border-left: 4px solid var(--success);
    border-radius: 10px; padding: 12px 14px; background: var(--card);
}
.admin-live-name { font-weight: 700; }
.admin-live-sub  { font-size: 0.76rem; color: var(--muted); }
.admin-live-ch   { margin-top: 6px; font-family: ui-monospace, "Consolas", monospace; color: var(--success); font-weight: 700; }

.admin-search {
    padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px;
    background: var(--card); color: var(--text); min-width: 220px;
}
.admin-row-main { font-weight: 700; }
.admin-sub  { font-size: 0.78rem; color: var(--muted); }
.admin-empty { color: var(--muted); padding: 22px 12px; }
.mono { font-family: ui-monospace, "Consolas", monospace; }

/* ป้ายสถานะ สีอย่างเดียวไม่พอ ทุกป้ายมีตัวหนังสือกำกับเสมอ */
.badge {
    display: inline-block; font-size: 0.72rem; font-weight: 700; padding: 3px 9px;
    border-radius: 20px; background: var(--soft); color: var(--muted); white-space: nowrap;
}
.badge-ok    { background: var(--ok-bg); color: var(--ok-ink); }
.badge-admin { background: var(--accent-bg); color: var(--accent-ink); }
.badge-root  { background: var(--info-bg); color: var(--info-ink); }
/* ฟีเจอร์ที่ขายแยก ต้องแยกออกจากป้ายอื่นด้วยสายตาทันที ไม่ใช่ต้องอ่านข้อความก่อน */
.badge-premium { background: var(--warn-bg); color: var(--warn-ink); border: 1px solid #fcd34d; }
.badge-susp  { background: var(--danger-bg); color: var(--danger-ink); }
.badge-lock  { background: var(--warn-bg); color: var(--warn-ink); }

.perm-off-list { color: var(--danger); font-size: 0.82rem; font-weight: 600; }
.perm-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 8px; }
.perm-item {
    display: flex; align-items: center; gap: 8px; padding: 9px 11px;
    border: 1px solid var(--border); border-radius: 8px; background: var(--card);
    font-size: 0.88rem; cursor: pointer;
}
.perm-item input { width: 17px; height: 17px; accent-color: var(--primary); }
/* ช่องของฟีเจอร์ที่ขายแยก ต้องดูออกว่าไม่ใช่สิทธิ์พื้นฐาน เพราะการติ๊กมันคือการส่งมอบสินค้า
   ไม่ใช่การผ่อนปรนข้อจำกัด และการไม่ติ๊กคือค่าปกติ ไม่ใช่การลงโทษใคร */
.perm-item-premium { border-color: #fcd34d; background: var(--warn-bg); }

/* หัวข้อกลุ่มสิทธิ์ — สินค้าสองตระกูลใช้หน้าผู้ดูแลร่วมกัน ผู้ดูแลต้องอ่านออกทันที
   ว่าช่องไหนใช้กับเครื่องแบบไหน ไม่ใช่ต้องจำเอง แล้ววันหนึ่งจะติ๊กผิดตระกูล */
.perm-group + .perm-group { margin-top: 14px; }
.perm-group-title {
    font-size: 0.86rem; font-weight: 700; color: var(--text);
    padding-bottom: 5px; margin-bottom: 7px; border-bottom: 1px solid var(--border);
}
.perm-group-note { font-size: 0.78rem; color: var(--muted); margin-bottom: 7px; }

.log-act { font-size: 0.8rem; font-weight: 700; color: var(--accent); }

/* แถบเตือนตอนผู้ดูแลกำลังดูเครื่องของคนอื่นแทนเจ้าของ ต้องเห็นตลอดเวลาที่อยู่ในโหมดนี้
   ไม่งั้นจะเผลอคิดว่าเป็นเครื่องของตัวเองแล้วสั่งงานผิดเครื่องของผิดคน */
.admin-view-bar {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    background: var(--warn-bg); border: 1px solid #f59e0b; border-left: 5px solid var(--warn-ink);
    color: var(--warn-ink); padding: 11px 15px; border-radius: 10px; margin-bottom: 16px;
    font-size: 0.9rem; font-weight: 600;
}
.admin-view-bar a { color: var(--warn-ink); font-weight: 800; }

.btn-admin {
    background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.45);
    color: #fff; padding: 8px 12px; border-radius: 8px; cursor: pointer; font-size: 0.9rem;
}
.btn-admin:hover { background: rgba(255,255,255,0.34); }

/* ปุ่มที่ผู้ดูแลปิดสิทธิ์ไว้ ยังเห็นอยู่แต่กดไม่ได้ ซ่อนทิ้งเลยจะกลายเป็นหน้าเว็บ
   ที่หน้าตาต่างกันไปในแต่ละบัญชีจนอธิบายกันทางโทรศัพท์ไม่ได้ */
.perm-denied { opacity: 0.45; cursor: not-allowed; }

@media (max-width: 640px) {
    .admin-wrap { max-width: 100%; }
    .admin-search { min-width: 130px; }
}

/* ===== ชุดเซนเซอร์ RS485 =====

   สามกลุ่มนี้ไม่ใช่การจัดหมวดให้ดูเป็นระเบียบ มันคืออุปกรณ์คนละตัวบนบัสเดียวกัน
   ที่เสียแยกกันได้ ดึงสายเซนเซอร์ดินออก เจ็ดค่าในกลุ่มดินจะเงียบพร้อมกันทั้งกลุ่ม
   ส่วนอากาศกับมิเตอร์ไฟยังตอบตามปกติ

   เดิมทั้งสามกลุ่มลอยอยู่บนพื้นเดียวกัน ต่างกันแค่หัวข้อตัวหนา ซึ่งอ่านได้ว่า
   "มีค่าหลายค่าที่ขึ้น --" แต่พอมีกรอบและหัวกลุ่มของตัวเอง มันอ่านได้ว่า
   "เซนเซอร์ดินไม่ตอบ" ซึ่งเป็นคนละข้อสรุป และเป็นข้อที่บอกได้ว่าต้องไปแก้ตรงไหน

   หมายเลขบัสอยู่บนหัวกลุ่มด้วย เพราะเวลาโทรถามกันว่าตัวไหนเงียบ คำตอบที่ใช้งานได้
   คือหมายเลข ไม่ใช่ชื่อกลุ่ม */
.rs485-group {
    margin-top: 14px; border: 1px solid var(--border); border-radius: 12px;
    overflow: hidden; background: var(--card);
}
.rs485-group:first-child { margin-top: 4px; }
.rs485-head {
    font-weight: 700; font-size: 0.92rem; color: var(--text);
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 11px 14px; background: var(--soft); border-bottom: 1px solid var(--border);
}
/* หมายเลขบัสเป็นข้อมูลรอง จึงจางและเป็นตัวพิมพ์เดียว ไม่ใช่แข่งเด่นกับชื่อกลุ่ม */
.rs485-id {
    font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
    font-size: 0.74rem; font-weight: 600; color: var(--muted);
    padding: 2px 8px; border-radius: 999px; background: rgba(15, 39, 64, 0.06);
}
/* สถานะไปชิดขวาเสมอ ตำแหน่งคงที่ทำให้กวาดตาสามกลุ่มลงมาแล้วเทียบได้ทันที
   ว่ากลุ่มไหนเงียบ โดยไม่ต้องอ่านทีละบรรทัด */
.rs485-head > [id$="-state"] { margin-left: auto; }
/* เว้นขอบให้ลูกทุกตัวที่ไม่ใช่หัวกลุ่ม แทนที่จะห่อ div ชั้นใหม่ในมาร์กอัป
   หัวกลุ่มต้องกินเต็มความกว้างเพราะมีพื้นหลังของตัวเอง ส่วนที่เหลือต้องร่นเข้า
   ทำที่ CSS ชั้นเดียวจบ ไม่ต้องแตะโครง HTML ของทั้งสี่กลุ่ม */
.rs485-group { padding-bottom: 2px; }
.rs485-group > :not(.rs485-head) { margin-left: 14px; margin-right: 14px; }
.rs485-group > .status-grid { margin-top: 12px; margin-bottom: 12px; }
/* สถานะต้องอ่านออกจากสีก่อนอ่านตัวหนังสือ ตัวเลขที่นิ่งอยู่โดยไม่รู้ว่าเซนเซอร์เงียบไปแล้ว
   คือข้อมูลที่หลอกคนดู ซึ่งอันตรายกว่าการไม่มีข้อมูลเลย */
/* --ok ไม่ใช่ --success ในไฟล์นี้ --success คือสีส้มที่แปลว่า "กำลังทำงาน" ของปุ่มวาล์ว
   ส่วนสีเขียวที่แปลว่า "ปกติ" คือ --ok เลือกผิดแล้วสถานะเซนเซอร์จะกลายเป็นสีเดียวกับวาล์วเปิด */
.rs485-ok  { color: var(--ok);     font-weight: 600; font-size: 0.82rem; }
.rs485-bad { color: var(--danger); font-weight: 600; font-size: 0.82rem; }

/* ===== หน้าปัดค่าเซนเซอร์ =====

   มีเฉพาะค่าที่มีเกณฑ์ให้เทียบจริง ค่าที่ไม่มีเกณฑ์ (N-P-K, มิเตอร์ไฟ, มาตรวัดน้ำ)
   ยังเป็นตัวเลขล้วนเหมือนเดิม หน้าปัดที่ไม่มีขีดเกณฑ์คือวงกลมที่ไม่ได้บอกอะไร
   นอกจากทำให้การ์ดสูงขึ้น

   ช่วงของหน้าปัดไม่ใช่ lo-hi ใน RULE_SRC - นั่นคือช่วงที่ระบบ "ยอมรับค่าได้"
   เช่นอุณหภูมิ -50 ถึง 100 องศา ซึ่งถ้าเอามาเป็นสเกลหน้าปัด เข็มจะขยับอยู่แถวกลาง
   ตลอดปีในโรงเรือนไทย เสียพื้นที่หน้าปัดไปเปล่าๆ กว่าครึ่ง ช่วงที่ใช้จริงอยู่ใน
   GAUGE_RANGE ของ script.js พร้อมเหตุผลของแต่ละตัว

   ตัวเลขยังเป็น .value ตัวเดิมและ id เดิม put() ไม่ต้องรู้เลยว่ามีหน้าปัดอยู่ข้างหลัง
   ถ้าวันหนึ่งเอาหน้าปัดออก ตัวเลขจะยังทำงานเหมือนเดิมทุกอย่าง */
.status-card.gauge { padding: 14px 10px 12px; }
.gwrap { position: relative; margin-top: 6px; }
.gwrap svg { display: block; width: 100%; max-width: 116px; height: auto;
    aspect-ratio: 1; margin: 0 auto; }
.gwrap .value { position: absolute; inset: 0; display: flex; align-items: center;
    justify-content: center; gap: 3px; font-size: 1.25rem; line-height: 1;
    /* เว้นที่ให้ช่องว่างล่างของวง ตัวเลขจึงอยู่กลางวงจริง ไม่ใช่กลางกล่อง */
    padding-bottom: 12px;
    /* --g มาจาก paintGauges เป็นสีประจำค่านั้น ตัวเลขกับส่วนโค้งจึงเป็นสีเดียวกัน
       ถ้าสคริปต์ยังไม่ได้ทำงาน ตกกลับไปเป็นสีหลัก ไม่ใช่ไม่มีสี */
    color: var(--g, var(--primary)); }
.gwrap .value small { font-size: .72rem; font-weight: 700; color: var(--muted); }
/* ค่าที่ข้ามเกณฑ์ที่เจ้าของตั้งไว้ - แดงทั้งเข็มและตัวเลข ไม่ใช่แดงแค่ตัวเลข
   สีเดียวสองที่ทำให้เห็นจากมุมตาได้โดยไม่ต้องอ่าน */
.gwrap.past .value { color: var(--danger); }
.gwrap .garc { stroke-linecap: round; fill: none; }

/* ป้ายบอกว่ามีของที่ยังไม่ได้บันทึก - อยู่ข้างปุ่มบันทึก ไม่ใช่ลอยอยู่ที่อื่น
   เพราะมันมีไว้เพื่อชี้ไปที่ปุ่มนั้น ไม่ได้มีไว้เพื่อรายงานสถานะเฉยๆ */
.unsaved {
    display: inline-flex; align-items: center; gap: 7px; margin-left: 12px;
    font-size: 0.82rem; font-weight: 700; color: var(--warn-ink);
    padding: 7px 12px; border-radius: 999px;
    background: var(--warn-bg); border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent);
}
/* จุดวาดด้วย CSS ไม่ใช่ไอคอนจากฟอนต์ - ฟอนต์ไอคอนมาจาก CDN คนละเครื่องกับที่
   เสิร์ฟหน้านี้ ถ้ามันไม่มา ไอคอนจะกลายเป็นสี่เหลี่ยมว่าง (มีด่าน icon-fallback-test
   คุมเรื่องนี้อยู่แล้ว) และจุดกลมขนาดเจ็ดพิกเซลไม่ใช่งานที่ต้องใช้ฟอนต์ทั้งชุด */
.unsaved::before {
    content: ""; width: 7px; height: 7px; border-radius: 50%;
    background: currentColor; flex: none;
}


/* ===== แถวควบคุมช่อง =====

   ยกมาจากแบบร่างที่ตกลงกันไว้ (design-tapo) ตรงๆ ทั้งขนาดและสัดส่วน
   ที่นั่นชื่อ .row .led .rn .rt .rk ที่นี่เติมคำนำหน้า ch ให้ เพราะคำว่า row
   กว้างเกินกว่าจะปล่อยไว้ลอยๆ ในไฟล์ที่มี .form-row .dur-row .auxsch-row อยู่แล้ว

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

   สวิตช์เป็นทรงกระดกจริง ไม่ใช่ปุ่มสี่เหลี่ยม - ครึ่งที่นูนขึ้นสลับข้างซ้ายขวาตามสถานะ
   จึงบอกสถานะด้วยรูปทรงด้วย ไม่ใช่ด้วยสีอย่างเดียว คนตาบอดสีก็ยังอ่านออก */
.chrow {
    display: flex; align-items: center; gap: 13px;
    padding: 14px 15px; border-radius: 13px;
    position: relative; overflow: hidden; width: 100%; text-align: left;
    background: linear-gradient(180deg, var(--card), var(--soft));
    border: 1px solid var(--border); box-shadow: var(--shadow);
    color: var(--text); font: inherit; cursor: pointer;
    transition: border-color .16s, box-shadow .16s;
}
/* ระยะห่างระหว่างแถวมาจาก gap ของกรอบ ไม่ใช่ margin ของตัวแถว
   ถ้าแถวถือ margin ของตัวเองไว้ด้วย พอกรอบกลายเป็นตารางสองคอลัมน์ ระยะจะซ้อนกัน
   เป็นสองเท่าเฉพาะแนวตั้ง แล้วช่องไฟแนวตั้งกับแนวนอนจะไม่เท่ากันโดยไม่มีใครตั้งใจ */
.chrow:hover { border-color: var(--ch, var(--primary)); }

/* ไฟสถานะ - บนพื้นขาวแสงเรืองมองแทบไม่เห็น จึงเพิ่มวงแหวนสีรอบดวงด้วย
   รูปทรงจึงยังบอกสถานะได้ทั้งสองโหมด ไม่ได้พึ่งความสว่างอย่างเดียว */
.chled {
    width: 11px; height: 11px; flex: none; border-radius: 50%;
    background: var(--ledoff); box-shadow: 0 1px 2px var(--sh) inset;
}
.chrow.on .chled {
    background: var(--ch, var(--primary));
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--ch, var(--primary)) 26%, transparent),
                0 0 10px var(--ch, var(--primary));
}

.chico {
    width: 38px; height: 38px; flex: none; border-radius: 10px;
    display: grid; place-items: center; font-size: 1.1rem;
    color: var(--ch, var(--primary));
    background: linear-gradient(180deg, var(--card), var(--soft));
    border: 1px solid var(--border); box-shadow: 0 1px 0 var(--hi) inset;
}

.chtxt { flex: 1; min-width: 0; }
.chtxt b { display: block; font-size: 0.92rem; font-weight: 700; letter-spacing: -.1px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chtxt i { display: block; font-style: normal; font-size: 0.74rem; font-weight: 700;
    color: var(--muted); margin-top: 1px; }

/* ป้ายเวลาที่เหลือ - อยู่ซ้ายมือของสวิตช์ ไม่ใช่ทับบนตัวสวิตช์
   ตัวเลขที่วางบนของที่กดได้ ทำให้ไม่แน่ใจว่ากดตรงนั้นแล้วจะเกิดอะไร */
.chcd {
    flex: none; font-size: 0.78rem; font-weight: 800; letter-spacing: .5px;
    font-variant-numeric: tabular-nums; color: var(--ch, var(--primary));
    padding: 4px 9px; border-radius: 7px; min-width: 0;
    background: color-mix(in srgb, var(--ch, var(--primary)) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--ch, var(--primary)) 30%, transparent);
}
.chcd:empty { display: none; }

/* สวิตช์กระดก 78x54 - ขนาดเดียวกับที่ตกลงไว้ในแบบร่าง
   ครึ่งที่นูนอยู่ทางซ้ายเมื่อเปิด ทางขวาเมื่อปิด สลับข้างให้เห็นด้วยตาจากมุมไกล */
.chsw {
    width: 78px; height: 54px; flex: none; border-radius: 13px; padding: 5px;
    display: flex; gap: 0; background: var(--well);
    border: 1px solid var(--border); box-shadow: 0 2px 6px var(--sh) inset;
}
.chsw b { flex: 1; border-radius: 10px; transition: background .18s, box-shadow .18s; }

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

   เขียนแบบนี้เพราะเคยพลาดมาแล้วแบบตรงข้าม: สลับลำดับ element ตอนสร้างแถว
   แล้วไม่มีใครเรียงใหม่ตอนสถานะเปลี่ยน ครึ่งที่นูนจึงค้างอยู่ข้างเดียวตลอด
   เปิดปิดเปลี่ยนแค่สี ซึ่งทำให้สวิตช์อ่านไม่ได้เลยถ้าแยกสีไม่ออก */
.chsw .lf, .chrow.on .chsw .rt { background: var(--well); box-shadow: 0 2px 5px var(--sh) inset; }
.chsw .rt, .chrow.on .chsw .lf {
    background: linear-gradient(180deg, var(--card), var(--soft));
    box-shadow: 0 1px 0 var(--hi) inset, 0 2px 4px var(--sh);
}
.chrow.on .chsw .lf {
    background: linear-gradient(180deg, var(--ch, var(--primary)),
                color-mix(in srgb, var(--ch, var(--primary)) 74%, #000));
}

/* แถบความคืบหน้าเป็นเส้นบางที่ขอบล่างของแถว ไม่ใช่แท่งกลางแถว
   มันเป็นข้อมูลรอง ไม่ควรแย่งสายตาไปจากชื่อช่องกับสวิตช์ */
.chprog-wrap { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: var(--well); }
.chprog { height: 100%; width: 0; background: var(--ch, var(--primary)); transition: width .9s linear; }

/* ป้ายหัวข้อกลุ่ม - ตัวเล็ก ตัวหนา เว้นวรรคกว้าง ตัวพิมพ์ใหญ่
   หน้าที่ของมันคือแบ่งกลุ่ม ไม่ใช่แข่งเด่นกับเนื้อหาในกลุ่ม */
.chlbl {
    font-size: 0.72rem; font-weight: 800; color: var(--muted);
    letter-spacing: 1.4px; text-transform: uppercase; margin: 20px 2px 10px;
}

/* ช่องที่กฎอุณหภูมิจองไว้ สั่งมือไม่ได้ - ขอบประเหมือนเดิม ความหมายเดิม */
.chrow.reserved { border-style: dashed; border-color: var(--accent); cursor: not-allowed; }
.chrow.denied   { opacity: .5; cursor: not-allowed; }


/* แถวควบคุมช่องเรียงหลายคอลัมน์ตามที่ว่างจริง

   แปดช่องเรียงลงมาเป็นแถวเดียวยาวเกินหนึ่งหน้าจอ ต้องเลื่อนถึงจะเห็นครบ ทั้งที่
   คำถามแรกของคนเปิดหน้านี้คือ "ตอนนี้มีอะไรเปิดอยู่บ้าง" ซึ่งต้องเห็นพร้อมกันถึงจะตอบได้

   340px คือความกว้างขั้นต่ำของหนึ่งแถว มาจากของที่กินที่ตายตัวรวม 261
   (padding 30 · ไฟสถานะ 11 · ไอคอน 38 · ป้ายเวลา 52 · สวิตช์ 78 · gap สี่ช่วง 52)
   บวกที่ให้ชื่ออีกราว 80 ซึ่งพอดีกับชื่อไทยที่ยาวที่สุดอย่าง "สารละลาย 1"

   ใช้ auto-fit ไม่ใช่ repeat(2) กับ media query เพราะ media query วัดความกว้าง
   "หน้าจอ" แต่สิ่งที่บีบแถวคือความกว้าง "ของกรอบที่มันอยู่" ซึ่งเป็นคนละอย่าง
   บนจอคอมกว้างเต็มที่ การ์ดวาล์วเคยได้แค่สองในสามของหน้า แล้วแถวก็ถูกบีบจนชื่อ
   เหลือ "แ.." ทั้งที่ media query ไม่มีทางรู้เรื่องเลย (เจอจริง 25 ส.ค. 2569)

   auto-fit ถามที่ว่างจริงของกรอบ จึงตอบถูกทั้งบนมือถือและในกรอบแคบบนจอกว้าง */
#valve-grid-container .board-section,
#house-grid {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 11px;
}

/* แถบเตือนตอนไฟล์เว็บอัปไม่ครบ - ดังพอที่จะไม่มีใครมองข้าม

   ของแบบนี้ควรน่ารำคาญ มันขึ้นเฉพาะตอนที่มีอะไรผิดจริงๆ และสิ่งที่ผิดคือหน้าเว็บ
   กำลังแสดงผลด้วยไฟล์คนละชุด ซึ่งทุกอย่างที่เห็นหลังจากนั้นเชื่อไม่ได้เลย

   ใช้สีอันตรายชุดเดิม ไม่ได้คิดสีใหม่ และสลับตามโหมดมืดเหมือนของอื่น */
.build-warn {
    position: sticky; top: 0; z-index: 90;
    margin: -20px -20px 16px; padding: 14px 20px;
    font-size: 0.9rem; font-weight: 700; line-height: 1.6;
    color: var(--danger-ink); background: var(--danger-bg);
    border-bottom: 3px solid var(--danger);
}
.build-warn b { font-variant-numeric: tabular-nums; }

/* ===== แถบข้างซ้ายบนจอกว้าง =====

   ที่มา 25 ส.ค. 2569: จอ 1920 เนื้อหาอยู่ในคอลัมน์กลาง 1000px เหลือที่ว่างข้างละ
   ราว 450 ซึ่งไม่ได้ทำอะไรเลย ยกโครงมาจากแบบร่างจอคอมที่ตกลงกันไว้

   แถบนี้ไม่ได้มีไว้ให้ดูเต็มจอ มันทำงานจริง: รายการกล่องทั้งหมดพร้อมสถานะ
   กดสลับได้ทันที ซึ่งเดิมต้องออกไปหน้ารวมอุปกรณ์แล้วกดกลับเข้ามา

   ต่ำกว่า 1100px ซ่อนทั้งแถบ ไม่ใช่ย่อให้เล็กลง - บนมือถือมันจะกินครึ่งจอ
   โดยไม่ได้ตอบอะไรที่ทางเข้าเดิมตอบไม่ได้ */
.rail { display: none; }

/* ทุกกฎในบล็อกนี้ต้องผูกกับ .has-rail เท่านั้น

   ที่มา 25 ส.ค. 2569 - รายงานว่า "หน้า admin เข้าดูข้อมูลไม่ได้" หน้าโหลดขึ้นมา
   เห็นภาพรวมด้านบน แล้วไม่มีอะไรต่อ กดไปหน้าผู้ใช้หรืออุปกรณ์ไม่ได้เลย

   เพราะบล็อกนี้เคยเขียนว่า .dashboard กับ .tabs เฉยๆ แต่ admin.html ใช้สองคลาสนั้น
   เหมือนกัน (class="dashboard admin-wrap" และ .tabs สี่ปุ่ม) โดยไม่มีแถบข้าง
   บนจอกว้างมันจึงโดนทั้งชุด:
     .dashboard  กลายเป็นกริดสองคอลัมน์ หัวหน้าไปกองอยู่ในคอลัมน์ 252px
     .tabs       ถูกซ่อน โดยไม่มีแถบข้างมาแทน = ไม่มีทางไปหน้าอื่นอีกเลย

   คลาสถูกซ่อนไปโดยที่ไม่มีใครตั้งใจ เพราะกฎเขียนไว้กว้างกว่าเจตนา
   .has-rail คือการให้หน้าที่มีแถบข้างจริงเป็นคนยกมือเอง หน้าใหม่ที่ใช้ .dashboard
   ต่อจากนี้จึงไม่โดนอะไรเลยจนกว่าจะใส่แถบข้างและยกมือ */
@media (min-width: 1100px) {
    /* #dashboard-page ถูกซ่อน/แสดงด้วย inline style จาก JS ซึ่งเคยเป็น 'block'
       ถ้ายังเป็น block อยู่ กฎ grid ตรงนี้จะแพ้ inline เสมอ - JS จึงเปลี่ยนไปตั้ง
       เป็นค่าว่างแทน ปล่อยให้ CSS เป็นคนตัดสินว่าหน้านี้วางตัวอย่างไร */
    .has-rail {
        display: grid; grid-template-columns: 252px minmax(0, 1fr);
        gap: 22px; align-items: start; max-width: 1500px;
    }
    /* เต็มความสูงจอ ไม่ใช่การ์ดลอยเล็กๆ มุมบน

       ตอนแรกทำเป็นกล่องสูงเท่าเนื้อใน ผลคือมันดูเหมือนติ่งที่ยื่นออกมาจากมุมซ้ายบน
       ไม่ใช่แถบนำทาง และมีรางเลื่อนแนวนอนโผล่มาด้วยเพราะเนื้อในกว้างกว่าที่จองไว้

       แถบนำทางต้องยืนเต็มด้านข้าง ถึงจะอ่านว่าเป็นด้านข้างของหน้า */
    .has-rail .rail {
        display: flex; flex-direction: column; position: sticky; top: 20px;
        height: calc(100vh - 40px); padding: 16px 12px 14px; border-radius: 14px;
        background: linear-gradient(180deg, var(--card), var(--soft));
        border: 1px solid var(--border); box-shadow: var(--shadow);
        overflow-x: hidden;
    }
    /* รายการกล่องเป็นส่วนเดียวที่เลื่อนได้ แท็บกับปุ่มท้ายแถบอยู่กับที่เสมอ
       ของที่ใช้บ่อยที่สุดไม่ควรต้องเลื่อนหา */
    .has-rail .rail-list { overflow-y: auto; overflow-x: hidden; flex: 1; min-height: 0; }
    .has-rail .rail-more { margin-top: auto; }

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

       ต้องผูกกับ .has-rail เสมอ - ซ่อนแถบแท็บของหน้าที่ไม่มีแถบข้างมาแทน
       คือตัดทางเข้าหน้าอื่นทิ้งทั้งหมด ซึ่งเคยเกิดขึ้นจริงกับหน้าผู้ดูแล */
    .has-rail .tabs { display: none; }

    .has-rail .shell { min-width: 0; }   /* กันไม่ให้เนื้อหากว้างเกินจนดันคอลัมน์ */
}

.rail-brand { display: flex; align-items: center; gap: 10px; padding: 2px 6px 14px; }
.rail-mark {
    width: 34px; height: 34px; flex: none; border-radius: 10px;
    display: grid; place-items: center; color: #fff;
    background: linear-gradient(135deg, var(--primary), var(--accent));
}
.rail-who { min-width: 0; }
.rail-who b { display: block; font-size: 0.95rem; font-weight: 800; letter-spacing: -.2px; }
.rail-who i { display: block; font-style: normal; font-size: 0.72rem; font-weight: 700; color: var(--muted); }

.rail-list { display: flex; flex-direction: column; gap: 2px; }
/* ทั้งแถวกดได้ ไม่ใช่เฉพาะตัวหนังสือ - แถวสูง 44 เพราะเป็นเป้ากดเหมือนของอื่นในแอป */
.rail-item {
    display: flex; align-items: center; gap: 10px; min-height: 44px;
    padding: 8px 10px; border-radius: 10px; cursor: pointer;
    font: inherit; font-size: 0.84rem; font-weight: 700; text-align: left;
    color: var(--text2); background: none; border: 1px solid transparent;
    text-decoration: none; width: 100%;
}
.rail-item:hover { background: var(--soft); }
/* เครื่องที่กำลังเปิดดูอยู่ - ต้องบอกได้ก่อนกด ไม่ใช่รู้ตอนกดผิดไปแล้ว */
.rail-item.on {
    color: var(--text); border-color: var(--border); box-shadow: var(--shadow);
    background: linear-gradient(180deg, var(--card), var(--soft));
}
.rail-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* จุดสถานะ - สีเดียวกับที่รายการในแท็บตั้งค่าใช้ ไม่ได้คิดสีใหม่ */
.rail-dot { width: 9px; height: 9px; flex: none; border-radius: 50%; background: var(--muted); }
.rail-item .dev-on    { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 24%, transparent); }
.rail-item .dev-busy  { background: var(--primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 24%, transparent); }
.rail-item .dev-off   { background: var(--danger); }
.rail-item .dev-unknown { background: var(--muted); }

.rail-more { margin-top: 8px; color: var(--muted); }
.rail-plus {
    width: 20px; height: 20px; border-radius: 6px; display: grid; place-items: center;
    background: var(--soft); border: 1px solid var(--border); font-size: 0.72rem;
}
/* ป้ายเล็กท้ายแถว เช่น "เงียบ" - ไม่ให้ยาวจนดันชื่อกล่อง */
.rail-tag {
    flex: none; font-size: 0.7rem; font-weight: 800; padding: 2px 7px; border-radius: 999px;
    color: var(--danger-ink); background: var(--danger-bg);
}


/* หัวข้อคั่นในแถบข้าง - บอกว่าของข้างล่างคืออะไร ไม่ได้แข่งเด่นกับรายการ */
.rail-head {
    font-size: 0.72rem; font-weight: 800; color: var(--muted);
    letter-spacing: 1.2px; text-transform: uppercase; margin: 16px 8px 6px;
}
.rail-nav { display: flex; flex-direction: column; gap: 2px; }
/* ช่องไอคอนกว้างเท่ากันทุกแถว ชื่อแท็บจึงเรียงตรงกันแม้บางแท็บไม่มีไอคอน */
.rail-ico { width: 18px; flex: none; text-align: center; color: var(--muted); }
.rail-tabitem.on .rail-ico { color: var(--primary); }
.rail-tabitem.on { color: var(--primary); }
/* ===== แท็บภาพรวมทุกกล่อง =====

   การ์ดกล่องละใบ ตอบว่า "ตอนนี้ทุกกล่องกำลังทำอะไรอยู่" ในจอเดียว
   ซึ่งเป็นคำถามที่ต้องเห็นพร้อมกันถึงจะตอบได้ - เลื่อนดูทีละใบไม่ใช่คำตอบ

   ชื่อคลาส .pips .pip .aux .mini .bar ตั้งให้ตรงกับใน home.html โดยตั้งใจ
   เพราะ pipsFor() กับ miniFor() เป็นสำเนาไบต์ต่อไบต์ของกันและกัน
   ถ้าชื่อคลาสไม่ตรง สำเนาจะวาดออกมาเป็นของไม่มีหน้าตาทันที */
.ov-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px; }

.ovcard {
    display: block; width: 100%; text-align: left; font: inherit; cursor: pointer;
    padding: 15px 16px 14px; border-radius: 13px; color: var(--text);
    background: linear-gradient(180deg, var(--card), var(--soft));
    border: 1px solid var(--border); box-shadow: var(--shadow);
    transition: border-color .16s, transform .16s;
}
.ovcard:hover { border-color: var(--primary); transform: translateY(-2px); }
/* กล่องที่กำลังเปิดดูอยู่ - บอกก่อนกด ไม่ใช่รู้ตอนกดแล้วไม่มีอะไรเปลี่ยน */
.ovcard.here { border-color: var(--primary); box-shadow: var(--shadow-lg); }
/* เงียบแล้วจางทั้งใบ ตัวเลขข้างในขึ้นขีดอยู่แล้วจาก miniFor() */
.ovcard.quiet { opacity: .62; }

.ovhead { display: flex; align-items: center; gap: 9px; }
.ovhead b { flex: 1; min-width: 0; font-size: 1rem; font-weight: 800; letter-spacing: -.2px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovtag {
    flex: none; font-size: 0.7rem; font-weight: 800; padding: 3px 9px; border-radius: 999px;
    background: var(--soft); border: 1px solid var(--border); color: var(--muted); white-space: nowrap;
}
.ovtag.dev-on   { color: var(--ok-ink);     background: var(--ok-bg);     border-color: transparent; }
.ovtag.dev-busy { color: var(--info-ink);   background: var(--info-bg);   border-color: transparent; }
.ovtag.dev-off  { color: var(--danger-ink); background: var(--danger-bg); border-color: transparent; }
.ovsn {
    display: block; margin-top: 3px; font-size: 0.72rem; font-weight: 600; color: var(--muted);
    font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; letter-spacing: .3px;
}

/* แถบแปดช่อง - ช่องไหนกำลังรดน้ำสว่างขึ้นมา กวาดตาทีเดียวเห็นทั้งบัญชี */
.pips { display: flex; gap: 5px; margin: 12px 0 0; }
.pip { flex: 1; height: 8px; border-radius: 3px; background: var(--well);
    box-shadow: 0 1px 2px var(--sh) inset; }
.pip.on { background: var(--primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 22%, transparent); }
/* ช่อง 9 ขึ้นไปไม่ได้อยู่ในแถบ ถ้ามีตัวไหนเปิดค้างต้องบอก ไม่งั้นแถบมืดทั้งแถว
   จะอ่านว่าไม่มีอะไรทำงาน ทั้งที่พัดลมเปิดอยู่ */
.aux { display: inline-block; margin-top: 9px; font-size: 0.72rem; font-weight: 700;
    color: var(--info-ink); background: var(--info-bg); padding: 3px 10px; border-radius: 999px; }

.mini { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 12px; }
.mini > div { padding: 8px 10px 9px; border-radius: 10px; background: var(--soft); border: 1px solid var(--border); }
.mini i { display: block; font-style: normal; font-size: 0.7rem; font-weight: 700; color: var(--muted);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mini b { display: block; font-size: 1.2rem; font-weight: 800; letter-spacing: -.5px; line-height: 1.2;
    margin-top: 2px; font-variant-numeric: tabular-nums; }
.mini b em { font-style: normal; font-size: 0.7rem; font-weight: 700; color: var(--muted); margin-left: 2px; }
.mini .bar { display: block; position: relative; height: 5px; border-radius: 999px;
    background: var(--well); margin-top: 6px; overflow: hidden; }
.mini .bar span { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 999px; }

/* ===== ภาพระบบ 2.5D (การ์ดบนสุดของแท็บควบคุมฟาร์ม) =====
   script.js วาดเป็น SVG (isoBuild) แล้วสลับคลาส on / wet / dry / low / fault (isoUpdate)
   น้ำไหล = เส้นประวิ่งตามทิศของท่อ (stroke-dashoffset) ปั๊มหมุน แปลงที่รดน้ำมีหยดน้ำ */
.iso-card .card-header { align-items: flex-start; }
.iso-chips { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
.iso-chip { font-size: 0.8rem; font-weight: 700; padding: 4px 10px; border-radius: 999px;
    background: var(--soft); color: var(--text2); border: 1px solid var(--border); white-space: nowrap; }
.iso-chip.on { background: var(--info-bg); color: var(--info-ink); border-color: transparent; }
.iso-chip.fert { background: var(--accent-bg); color: var(--accent-ink); border-color: transparent; }
.iso-chip.bad { background: var(--danger-bg); color: var(--danger-ink); border-color: transparent; }
/* ภาพยาวเตี้ยเต็มความกว้าง (ราว 4.5:1) ใต้ภาพเป็นแถบหกช่องเรียงตามขั้นในภาพ
   จอแคบ: ภาพเลื่อนซ้ายขวาได้แทนการย่อจนป้ายอ่านไม่ออก แถบลงเป็นสองคอลัมน์ */
.iso-scroll { margin-top: 6px; overflow-x: auto; overscroll-behavior-x: contain; }
.iso-wrap { min-width: 0; }
.iso-svg { display: block; width: 100%; height: auto; }
@media (max-width: 760px) {
    .iso-svg { width: 760px; max-width: none; }
}

.iso-strip { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1.55fr) repeat(3, minmax(0, 1fr));
    gap: 10px; margin-top: 10px; }
.iso-col { background: var(--soft); border: 1px solid var(--border); border-radius: 12px; padding: 10px 12px; min-width: 0; }
.iso-col-h { color: var(--text2); font-weight: 800; font-size: 0.85rem; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.iso-col-h i { color: var(--muted); }
.iso-col:nth-child(1) .iso-col-h i { color: var(--st-energy); }
.iso-col:nth-child(2) .iso-col-h i { color: var(--st-water); }
.iso-big { color: var(--text); font-size: 1.45rem; font-weight: 800; line-height: 1.25; margin-top: 4px;
    font-variant-numeric: tabular-nums; }
.iso-big small { font-size: 0.8rem; color: var(--muted); font-weight: 700; }
.iso-sub { color: var(--muted); font-size: 0.78rem; line-height: 1.5; font-variant-numeric: tabular-nums; }
.iso-sub b { color: var(--text); }
.iso-today { margin-top: 6px; color: var(--text2); font-weight: 700; }
.iso-strong { color: var(--text); font-weight: 800; font-size: 0.95rem; margin-top: 6px; }
.iso-chiplet { margin-left: auto; color: var(--muted); font-weight: 700; font-size: 0.75rem; }
#iso-e-cost { color: var(--muted); font-weight: 700; font-size: 0.8rem; }

/* ช่องพลังงานไฟฟ้า - หน้าตาแบบการ์ดที่เจ้าของสินค้าเลือก: วัตต์ใหญ่ ค่าย่อยมีหัวข้อกำกับ
   ใช้ไปวันนี้ แท่งรายชั่วโมงพร้อมเวลา และยอดมิเตอร์สะสม */
.iso-col-energy .iso-big { font-size: 1.8rem; }
.iso-e-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px; margin-top: 2px; }
.iso-e-grid > div { min-width: 0; }
.iso-e-grid small, .iso-e-today > small { display: block; color: var(--muted); font-size: 0.75rem; font-weight: 700;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.iso-e-grid b { color: var(--text); font-size: 1rem; font-variant-numeric: tabular-nums; }
.iso-e-grid em, .iso-e-today em { font-style: normal; color: var(--muted); font-size: 0.75rem; font-weight: 700; }
.iso-e-today { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); }
.iso-e-today b { color: var(--text); font-size: 1.2rem; font-variant-numeric: tabular-nums; }
.iso-e-axis { display: flex; justify-content: space-between; color: var(--muted); font-size: 0.72rem; margin-top: 2px; }
.iso-e-meter { margin-top: 6px; color: var(--muted); font-size: 0.75rem; }
.iso-e-meter span { color: var(--text2); font-weight: 700; font-variant-numeric: tabular-nums; }
.iso-e-bars { margin-top: 4px; }
.iso-e-bars svg { display: block; width: 100%; height: 36px; }
.iso-e-bars line { stroke: var(--border); stroke-width: 1; }
.iso-e-bars path { fill: var(--st-energy); }
.iso-e-hit { fill: transparent; }

.iso-rows { margin-top: 4px; }
.iso-row { display: flex; justify-content: space-between; gap: 8px; font-size: 0.8rem; line-height: 1.7;
    border-bottom: 1px dashed var(--border); }
.iso-row:last-child { border-bottom: 0; }
.iso-row span { color: var(--text2); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.iso-row b { color: var(--text); white-space: nowrap; font-variant-numeric: tabular-nums; }
.iso-row.on b { color: var(--info-ink); }
.iso-row.fert b { color: var(--accent-ink); }
.iso-row.bad b { color: var(--danger-ink); }

/* จอ LCD ในช่องตู้ควบคุม - กรอบบางลงและตัวหนังสือเล็กลงให้ 20 ตัวอักษรพอดีช่อง */
.iso-col-lcd .lcd-bezel { margin-top: 8px; padding: 8px; }
.iso-col-lcd .lcd-screen { padding: 6px 8px; max-width: 100%; }
.iso-col-lcd .lcd-line { font-size: clamp(0.62rem, 0.95vw, 0.82rem); }
.iso-col-lcd .lcd-badge { margin-left: auto; }

@media (max-width: 1100px) {
    .iso-strip { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .iso-col-lcd { grid-column: span 3; }
    .iso-col-lcd .lcd-line { font-size: clamp(0.75rem, 2.4vw, 1rem); }
}
@media (max-width: 600px) {
    .iso-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .iso-col-lcd { grid-column: span 2; }
}
.iso-card.iso-stale .iso-svg { filter: grayscale(.75); opacity: .75; }

.iso-svg .f-t, .iso-svg .f-l, .iso-svg .f-r { stroke-linejoin: round; }
.iso-ground .f-t { fill: var(--iso-ground-t); }
.iso-ground .f-l { fill: var(--iso-ground-l); }
.iso-ground .f-r { fill: var(--iso-ground-r); }
.iso-soil .f-t { fill: var(--iso-soil-t); }
.iso-soil .f-l { fill: var(--iso-soil-l); }
.iso-soil .f-r { fill: var(--iso-soil-r); }
.iso-metal .f-t, .iso-plinth .f-t { fill: var(--iso-metal-t); }
.iso-metal .f-l, .iso-plinth .f-l { fill: var(--iso-metal-l); }
.iso-metal .f-r, .iso-plinth .f-r { fill: var(--iso-metal-r); }

.iso-pipe .p-body { fill: none; stroke: var(--iso-pipe); stroke-width: 7; stroke-linecap: round; stroke-linejoin: round; }
.iso-pipe .p-hi { fill: none; stroke: var(--iso-pipe-hi); stroke-width: 1.6; opacity: .7; transform: translateY(-1.6px); }
.iso-pipe .p-flow { fill: none; stroke: var(--st-water); stroke-width: 3.4; stroke-linecap: round;
    stroke-linejoin: round; stroke-dasharray: 1 9; opacity: 0; transition: opacity .3s; }
.iso-pipe.on .p-flow { opacity: 1; animation: iso-flow .6s linear infinite; }
.iso-fline .p-body { stroke-width: 4; }
.iso-fline .p-hi { display: none; }
.iso-fline .p-flow { stroke-width: 2.6; }
.iso-fline.f0 .p-flow { stroke: var(--st-energy); }
.iso-fline.f1 .p-flow { stroke: var(--st-hum); }
.iso-fline.f2 .p-flow { stroke: var(--st-run); }
.iso-fline.f3 .p-flow { stroke: var(--st-temp); }

.iso-glass .c-body, .iso-glass .c-top { fill: var(--iso-glass); fill-opacity: .38; stroke: var(--iso-glass-edge); stroke-width: 1.2; }
.iso-glass .c-top { fill-opacity: .55; }
.iso-liquid .c-body { fill: var(--st-water); fill-opacity: .78; }
.iso-liquid .c-top { fill: var(--st-water); fill-opacity: .95; }
.iso-ftank.f0 .iso-liquid .c-body, .iso-ftank.f0 .iso-liquid .c-top { fill: var(--st-energy); }
.iso-ftank.f1 .iso-liquid .c-body, .iso-ftank.f1 .iso-liquid .c-top { fill: var(--st-hum); }
.iso-ftank.f2 .iso-liquid .c-body, .iso-ftank.f2 .iso-liquid .c-top { fill: var(--st-run); }
.iso-ftank.f3 .iso-liquid .c-body, .iso-ftank.f3 .iso-liquid .c-top { fill: var(--st-temp); }
.iso-tank.low .iso-glass .c-body, .iso-ftank.fault .iso-glass .c-body { stroke: var(--danger); stroke-width: 2; }
.iso-ftank.fault .iso-glass .c-body { stroke-dasharray: 3 2; }

.iso-motor .c-body { fill: var(--iso-metal-l); stroke: var(--iso-metal-r); stroke-width: 1; }
.iso-motor .c-top { fill: var(--iso-metal-t); transition: fill .3s; }
.iso-pump.on .iso-motor .c-top { fill: var(--ok); }
.iso-pump.dry .iso-motor .c-top { fill: var(--danger); }
.iso-fan ellipse { fill: var(--iso-metal-r); }
.iso-fan .hub { fill: var(--iso-metal-t); }
.iso-pump.on .iso-fan { animation: iso-spin .45s linear infinite; }
.iso-inject .f-t { fill: var(--accent); }

/* ตู้มิเตอร์ไฟ: จอเล็กสว่างเมื่อมิเตอร์ตอบ · สายไฟมีไฟวิ่งเมื่อมีโหลดจริง (>3 W) */
.iso-lcd { fill: var(--iso-metal-r); transition: fill .3s; }
.iso-cab.on .iso-lcd { fill: var(--st-energy); }
.iso-cable .c-wire { fill: none; stroke: var(--iso-metal-r); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.iso-cable .c-power { fill: none; stroke: var(--st-energy); stroke-width: 2.4; stroke-linecap: round;
    stroke-dasharray: 3 7; opacity: 0; transition: opacity .3s; }
.iso-cable.on .c-power { opacity: 1; animation: iso-flow .5s linear infinite; }

.iso-valve .stem { stroke: var(--iso-metal-r); stroke-width: 2; }
.iso-valve .wheel { fill: none; stroke: var(--muted); stroke-width: 2.6; transition: stroke .3s; }
.iso-valve.on .wheel { stroke: var(--ok); }
.iso-valve.on .f-t { fill: var(--ok-bg); }
.iso-doser.on .f-t { fill: var(--accent); }

/* ดินเปียก = ดินสีเข้มขึ้น ไม่ใช่ทาฟ้าทับ (ฟ้าบนดินน้ำตาลออกมาเป็นเทาม่วง) น้ำบอกด้วยสายหยดกับหยดน้ำแทน */
.iso-wet { fill: var(--iso-soil-r); opacity: 0; transition: opacity .6s; }
.iso-bed.wet .iso-wet { opacity: .6; }
.iso-drip { fill: none; stroke: var(--iso-pipe); stroke-width: 1.6; stroke-linecap: round; }
.iso-bed.wet .iso-drip { stroke: var(--st-water); stroke-width: 2; stroke-dasharray: 2 8; animation: iso-flow .9s linear infinite; }
.iso-plant line { stroke: var(--iso-leaf); stroke-width: 1.6; }
.iso-plant ellipse { fill: var(--iso-leaf); }
.iso-plant .l2 { fill: var(--iso-leaf2); }
.iso-drops circle { fill: var(--st-water); opacity: 0; }
.iso-bed.wet .iso-drops circle { animation: iso-drop .9s ease-in infinite; }

/* ป้ายบนภาพมีขอบสีพื้นการ์ดรอบตัวอักษร อ่านออกทั้งบนพื้นหญ้า ดิน และท่อ
   ขนาดเป็นหน่วยของภาพ ไม่ใช่พิกเซลจอ - ภาพแนวนอนกว้างราว 870 หน่วย บนจอคอมถูกขยายราว 1.2 เท่า
   จอแคบภาพไม่ย่อตาม (กว้างคงที่ 760 px เลื่อนดูได้) ตัวเลขชุดนี้จึงอ่านได้ทุกขนาดจอ */
.iso-lbl { fill: var(--text2); font-size: 11.5px; font-weight: 700; paint-order: stroke;
    stroke: var(--card); stroke-width: 3.5px; stroke-linejoin: round; }
.iso-stage { fill: var(--text); font-size: 12.5px; font-weight: 800; }
.iso-num { fill: var(--text); font-size: 11px; stroke-width: 3px; }
.iso-bstat { fill: var(--info-ink); }
.iso-ew, .iso-fw { fill: var(--text); font-size: 12px; }
.iso-pole .c-body { fill: var(--iso-metal-r); }
.iso-pole .c-top { fill: var(--iso-metal-t); }
.iso-wire { fill: none; stroke: var(--iso-metal-r); stroke-width: 1.2; }
.iso-hand { fill: none; stroke: var(--iso-metal-r); stroke-width: 2.6; }
.iso-lcd-line { fill: none; stroke: var(--card); stroke-width: 1.4; opacity: 0; }
.iso-cab.on .iso-lcd-line { opacity: .85; }
.iso-ant { fill: var(--iso-metal-r); }
.iso-ctl.on .iso-ant { fill: var(--ok); }
.iso-ctl.on .iso-lcd { fill: var(--st-water); }     /* จอฟ้าแบบ LCD หน้าตู้จริง ส่วนจอมิเตอร์ไฟเป็นสีพลังงาน */
.iso-inlet .p-flow { display: none; }

/* ปุ่มสลับแบบภาพในหัวการ์ด */
.iso-views { display: inline-flex; margin-left: 8px; padding: 2px; border-radius: 9px; background: var(--well);
    border: 1px solid var(--border); vertical-align: middle; }
.iso-vbtn { border: 0; background: transparent; color: var(--text2); font: inherit; font-size: 0.78rem; font-weight: 800;
    padding: 3px 10px; border-radius: 7px; cursor: pointer; min-height: 26px; }
.iso-vbtn.on { background: var(--card); color: var(--primary); box-shadow: var(--shadow); }

/* ===== แบบ 2D ด้านข้างเสมือนจริง (flatBuild) =====
   สีวัสดุ (ถังใส เหล็ก ดิน ใบไม้ จอ LCD ฟ้า) เป็นสีของจริงทั้งสองโหมดและอยู่ใน SVG เอง
   ที่นี่เหลือแค่ พื้น/ตัวหนังสือที่ตามธีม และสถานะที่สลับด้วยคลาส (on / load / run / wet / low / fault) */
/* จอแคบ script.js วาดเป็นหลายแถว (.fl-compact) พอดีความกว้างจอ ไม่ต้องเลื่อนซ้ายขวา
   ภาพถูกย่อราว 0.7 เท่า ตัวหนังสือในภาพจึงตั้งใหญ่กว่าแบบกว้าง ให้บนจอยังได้ราว 10-12 px
   (ทับกฎ .fl-svg width ของแบบ 2.5D บนจอแคบ - แบบ 2D ไม่ต้องกว้างคงที่อีกแล้ว) */
@media (max-width: 760px) {
    .fl-svg { width: 100%; max-width: 100%; }
}
.fl-compact .fl-title { font-size: 17px; }
.fl-compact .fl-name { font-size: 14px; }
.fl-compact .iso-lbl { font-size: 15px; stroke-width: 4px; }
.fl-compact .fl-num { font-size: 15px; }
.fl-grass { fill: var(--iso-ground-l); }
.fl-wire { fill: none; stroke: var(--muted); stroke-width: 1.1; }
.fl-title { fill: var(--text); font-size: 13.5px; font-weight: 800; }
.fl-chev { fill: none; stroke: var(--muted); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.fl-name { fill: var(--text2); font-size: 11px; font-weight: 700; }
.fl-num { fill: #ffffff; font-size: 12px; font-weight: 800; paint-order: stroke; stroke: rgba(0, 0, 0, .35); stroke-width: 2px; }
.fl-pipe .p-body { stroke-width: 9; }
.fl-pipe .p-hi { stroke-width: 2.2; transform: translateY(-2px); }
.fl-drop .p-body { stroke-width: 4; }
.fl-drop .p-hi { display: none; }
.fl-drop .p-flow { stroke-width: 2.2; }
.fl-tankbody { stroke: #7fa6cc; stroke-width: 1.4; }
.iso-tank.low .fl-tankbody, .iso-ftank.fault .fl-tankbody { stroke: var(--danger); stroke-width: 2.2; }
.iso-ftank.fault .fl-tankbody { stroke-dasharray: 3 2; }
.fl-water { animation: fl-wave 2.6s linear infinite; }
.fl-lcdbg { fill: #1631c8; }
.fl-lcdtxt { fill: #eaf0ff; font-family: "Consolas", "DejaVu Sans Mono", "Courier New", monospace; }
.fl-led { fill: #59636e; transition: fill .3s; }
.iso-ctl.on .fl-led.pw, .iso-meter.on .fl-led.pw, .fl-meter.on .fl-led.pw { fill: var(--ok); }
.iso-ctl.on .fl-led.net { fill: var(--ok); animation: fl-blink 1.6s steps(2, start) infinite; }
.iso-ctl.run .fl-led.run, .iso-pump.on .fl-led.run { fill: var(--ok); }
.iso-pump.dry .fl-led.run { fill: var(--danger); }
.iso-valve.on .fl-led.vl { fill: #39e07a; }
.fl-meter.load .fl-disc { animation: fl-disc .8s linear infinite; transform-box: fill-box; transform-origin: center; }
.iso-pump.on .fl-fan { animation: iso-spin .32s linear infinite; }
.iso-doser.on .fl-rollers { animation: iso-spin 1.1s linear infinite; }
.fl-drip { fill: none; stroke: #23282e; stroke-width: 1.6; stroke-linecap: round; }
.iso-bed.wet .fl-drip { stroke: var(--st-water); stroke-width: 2; stroke-dasharray: 2 6; animation: iso-flow .9s linear infinite; }

@keyframes fl-wave { to { transform: translateX(var(--wv)); } }
@keyframes fl-disc { 50% { transform: scaleX(.12); } }
@keyframes fl-blink { to { opacity: .35; } }

@keyframes iso-flow { to { stroke-dashoffset: -10; } }
@keyframes iso-spin { to { transform: rotate(360deg); } }
@keyframes iso-drop {
    0% { opacity: 0; transform: translateY(-4px); }
    25% { opacity: 1; }
    100% { opacity: 0; transform: translateY(16px); }
}
/* เครื่องที่ขอลดการเคลื่อนไหว: ภาพยังบอกสถานะครบด้วยสี เส้นน้ำ และป้าย แค่ไม่ขยับ */
@media (prefers-reduced-motion: reduce) {
    .iso-svg *, .iso-svg *::before { animation: none !important; }
    .iso-bed.wet .iso-drops circle { opacity: .8; }
}

/* ===== แท็บสถิติ =====
   ตัวกรองแถวเดียวบนสุด -> การ์ดตัวเลข -> กราฟแท่ง -> เส้นแนวโน้ม -> ตาราง
   กราฟเป็น SVG ที่ script.js วาดตามความกว้างจริงของกล่อง (statsColumns / statsLine)
   ผิวและตัวหนังสือใช้โทเคนของหน้า สีของค่าใช้ --st-* ซึ่งสลับตามโหมดอยู่แล้ว */
.stats-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; margin-bottom: 12px; }
.stats-ranges { display: inline-flex; padding: 3px; border-radius: 12px; background: var(--well);
    border: 1px solid var(--border); }
.stats-range { border: 0; background: transparent; color: var(--text2); font: inherit; font-weight: 700;
    font-size: 0.88rem; padding: 7px 14px; border-radius: 9px; cursor: pointer; min-height: 36px; }
.stats-range:hover { color: var(--text); background: var(--soft); }
.stats-range.on { background: var(--card); color: var(--primary); box-shadow: var(--shadow); }
.stats-nav { display: inline-flex; align-items: center; gap: 8px; }
.stats-nav button { min-width: 36px; min-height: 36px; padding: 6px 10px; }
.stats-nav button:disabled { opacity: .4; cursor: default; }
.stats-period { font-weight: 800; color: var(--text); min-width: 9em; text-align: center;
    font-variant-numeric: tabular-nums; }
.stats-dl { display: inline-flex; gap: 8px; margin-left: auto; }
.stats-dl button { padding: 8px 12px; font-size: 0.85rem; }
.stats-note { margin-bottom: 14px; }
#stats-tab.stats-loading .stats-tiles,
#stats-tab.stats-loading .stats-chart,
#stats-tab.stats-loading .stats-lines { opacity: .45; transition: opacity .2s; }

.stats-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 14px; margin-bottom: 18px; }
.stats-tile { background: var(--card); border: 1px solid var(--border); border-radius: 14px; padding: 14px 16px;
    box-shadow: var(--shadow); min-width: 0; }
.stats-tile-label { color: var(--text2); font-weight: 700; font-size: 0.88rem; }
.stats-tile-label i { margin-right: 4px; }
.stats-tile-value { color: var(--text); font-size: 1.75rem; font-weight: 800; line-height: 1.25; margin-top: 4px; }
.stats-tile-value small { font-size: 0.9rem; font-weight: 700; color: var(--muted); }
.stats-tile-sub { color: var(--muted); font-size: 0.8rem; margin-top: 4px; line-height: 1.5; }
.stats-rate { display: flex; align-items: center; gap: 6px; margin-top: 8px; color: var(--muted); font-size: 0.8rem; }
.stats-rate input { width: 5.5em; padding: 4px 6px; border-radius: 8px; border: 1px solid var(--border);
    background: var(--soft); color: var(--text); font: inherit; }

.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 18px; margin-bottom: 18px; }
.stats-card { min-width: 0; }
.stats-grid .stats-card { margin: 0; }
/* รายช่องยาวสิบกว่าแถว ถ้าอยู่ในกริดสามคอลัมน์จะตกไปเป็นแถวที่สองคนเดียว เต็มแถวไปเลย */
.stats-grid .stats-wide { grid-column: 1 / -1; }
#stats-tab > .stats-card { margin-bottom: 18px; }
.stats-unit { color: var(--muted); font-weight: 600; font-size: 0.85rem; }
.stats-chart { margin-top: 8px; min-height: 120px; }
.stats-empty { color: var(--muted); font-size: 0.88rem; padding: 34px 10px; text-align: center;
    border: 1px dashed var(--border); border-radius: 10px; }

.stats-lines { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 10px 22px; margin-top: 8px; }
.stats-line { min-width: 0; }
.stats-line-h { color: var(--text2); font-weight: 700; font-size: 0.85rem; }
.stats-line-h span { color: var(--muted); font-weight: 600; }

.st-svg { display: block; max-width: 100%; height: auto; overflow: visible; }
.st-grid { stroke: var(--border); stroke-width: 1; }
.st-base { stroke: var(--muted); stroke-width: 1; opacity: .6; }
.st-ax { fill: var(--muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.st-name { fill: var(--text2); font-size: 12px; }
.st-val { fill: var(--text); font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; }
.st-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.st-dot { stroke: var(--card); stroke-width: 2; }
.st-cross { stroke: var(--muted); stroke-width: 1; }
.st-hitbox { fill: transparent; }
.st-hit { cursor: default; outline: none; }
.st-hit:hover .st-mark, .st-hit:focus .st-mark { filter: brightness(1.12); }
.st-hit:focus-visible .st-hitbox, .st-over:focus-visible { stroke: var(--primary); stroke-width: 1.5; }
.st-over { outline: none; }

.stats-tip { position: fixed; z-index: 3000; display: none; pointer-events: none; background: var(--card);
    color: var(--text); border: 1px solid var(--border); border-radius: 10px; padding: 7px 10px;
    box-shadow: var(--shadow-lg); font-size: 0.8rem; max-width: 260px; }
.stats-tip-v { display: flex; align-items: center; gap: 7px; font-size: 0.95rem; font-weight: 800;
    font-variant-numeric: tabular-nums; }
.stats-tip-v i { display: inline-block; width: 12px; height: 3px; border-radius: 2px; flex: none; }
.stats-tip-h { color: var(--muted); margin-top: 2px; }

#stats-tab details summary { cursor: pointer; font-weight: 700; color: var(--text); list-style-position: inside; }
.stats-tbl { min-width: 520px; }
.stats-tbl td { font-variant-numeric: tabular-nums; }
.stats-tbl td:first-child, .stats-tbl th:first-child { text-align: left; }

@media (max-width: 640px) {
    .stats-dl { margin-left: 0; width: 100%; }
    .stats-dl button { flex: 1; }
    .stats-ranges { width: 100%; }
    .stats-range { flex: 1; padding: 7px 6px; }
    .stats-grid { grid-template-columns: minmax(0, 1fr); }
    .stats-tile-value { font-size: 1.5rem; }
}
